QoL stuff

This commit is contained in:
Arnav 2025-11-10 14:05:05 +00:00
parent 3ab58eec3e
commit 0f17308379
13 changed files with 709 additions and 110 deletions

View file

@ -3,7 +3,10 @@
import Auth from './lib/Auth.svelte';
import Dashboard from './lib/Dashboard.svelte';
import AdminPanel from './lib/AdminPanel.svelte';
import ThemeSwitcher from './lib/ThemeSwitcher.svelte';
import { API_BASE } from './config.js';
import { applyTheme, currentTheme } from './stores/theme.js';
import { get } from 'svelte/store';
let isAuthenticated = false;
let user = null;
@ -11,6 +14,8 @@
let showAdminPanel = false;
onMount(() => {
applyTheme(get(currentTheme));
const storedAuth = localStorage.getItem('auth_token');
const storedUser = localStorage.getItem('user_data');
@ -89,6 +94,7 @@
<main>
{#if isAuthenticated && user}
<ThemeSwitcher />
{#if showAdminPanel && user.is_admin}
<div class="admin-header">
<button on:click={() => showAdminPanel = false}>Back to Dashboard</button>
@ -120,20 +126,22 @@
.admin-header, .admin-link {
padding: 10px 20px;
background-color: #f8f9fa;
border-bottom: 1px solid #ddd;
background-color: var(--snow);
border-bottom: 1px solid var(--smoke);
}
.admin-header button, .admin-link button {
padding: 8px 16px;
background-color: #007bff;
color: white;
background-color: var(--blue);
color: var(--white);
border: none;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s ease;
}
.admin-header button:hover, .admin-link button:hover {
background-color: #0056b3;
background-color: var(--cyan);
transform: translateY(-1px);
}
</style>

View file

@ -96,3 +96,64 @@ button {
input, textarea, select {
font-family: 'Phantom Sans', sans-serif;
}
[data-theme="darkGradient"] body {
background: linear-gradient(135deg, #0a0f1a 0%, #0f1a2a 25%, #1a2a3a 50%, #0f1a2a 75%, #0a0f1a 100%);
background-size: 400% 400%;
animation: gradientShift 15s ease infinite;
}
[data-theme="lgbtq"] body {
position: relative;
background:
radial-gradient(circle at 20% 80%, rgba(228, 3, 3, 0.8) 0%, transparent 50%),
radial-gradient(circle at 80% 20%, rgba(255, 140, 0, 0.8) 0%, transparent 50%),
radial-gradient(circle at 40% 40%, rgba(255, 237, 0, 0.8) 0%, transparent 40%),
radial-gradient(circle at 80% 80%, rgba(0, 128, 38, 0.8) 0%, transparent 50%),
radial-gradient(circle at 20% 20%, rgba(36, 64, 142, 0.8) 0%, transparent 50%),
radial-gradient(circle at 60% 60%, rgba(115, 41, 130, 0.8) 0%, transparent 40%),
linear-gradient(135deg, #1a0616 0%, #2a0a26 100%);
background-size: 200% 200%;
animation: prideShift 30s ease infinite;
}
[data-theme="lgbtq"] .dashboard,
[data-theme="lgbtq"] .auth-container,
[data-theme="lgbtq"] .create-form,
[data-theme="lgbtq"] .space-card,
[data-theme="lgbtq"] .theme-menu,
[data-theme="lgbtq"] .theme-toggle {
background: rgba(26, 6, 22, 0.7) !important;
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
}
[data-theme="lgbtq"] .auth-form-panel {
background: rgba(26, 6, 22, 0.8) !important;
backdrop-filter: blur(30px);
-webkit-backdrop-filter: blur(30px);
}
@keyframes gradientShift {
0%, 100% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
}
@keyframes prideShift {
0%, 100% {
background-position: 0% 0%;
}
25% {
background-position: 100% 0%;
}
50% {
background-position: 100% 100%;
}
75% {
background-position: 0% 100%;
}
}

View file

@ -0,0 +1,20 @@
<svg
fill-rule="evenodd"
clip-rule="evenodd"
stroke-linejoin="round"
stroke-miterlimit="1.414"
xmlns="http://www.w3.org/2000/svg"
aria-label="flag"
viewBox="0 0 32 32"
preserveAspectRatio="xMidYMid meet"
fill="0"
width="256"
height="256"
>
<g>
<path d="M10.953 5.034a1 1 0 0 0-1.225.707L4.034 26.992a1 1 0 1 0 1.932.517l5.694-21.25a1 1 0 0 0-.707-1.225zm2.107 9.005c.425-1.703.798-3.036 1.225-4.079.429-1.058.766-1.43.912-1.532a.216.216 0 0 0 .022-.023l.017.003c.131-.022.133-.021.353.073l.065.028c.584.23 1.492.826 2.826 2.076 1.584 1.462 3.173 2.338 4.36 2.738a9.906 9.906 0 0 0 2.045.4c-.312 1.161-.627 2.297-1.028 3.334-.405 1.061-.756 1.774-1.284 2.307-.385.41-.719.542-1.131.527-.519-.018-1.447-.289-2.901-1.37-1.746-1.291-3.25-2.073-4.327-2.514a17.61 17.61 0 0 0-1.498-.524c.08-.375.193-.838.344-1.444zm12.104-1.615a.522.522 0 0 1 0 0zm-13.21 2.816l.017.008a.08.08 0 0 1-.017-.008zm-.834-1.685c1.727-6.93 3.174-9.634 8.727-4.43 2.833 2.655 4.933 2.646 6.14 2.641 1.16-.005 1.494-.007.86 2.359-1.294 4.83-3.053 10.796-9.5 6-2.638-1.962-4.392-2.486-5.449-2.801-1.526-.456-1.599-.478-.778-3.769z" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -35,7 +35,21 @@
message = 'Verification code sent to your email!';
mode = 'verify';
} else {
error = data.message || 'Failed to send verification code';
if (response.status === 404 && mode === 'login') {
error = "We couldn't find your account. Let's create one!";
setTimeout(() => {
error = '';
authIntent = 'signup';
setTimeout(() => {
mode = 'signup';
}, 400);
setTimeout(() => {
displayMode = 'signup';
}, 800);
}, 1500);
} else {
error = data.message || 'Failed to send verification code';
}
}
} catch (err) {
error = ERROR_MESSAGES.NETWORK_ERROR;
@ -128,19 +142,21 @@
<img class="flag-banner" src="https://assets.hackclub.com/flag-orpheus-top.svg" alt="Hack Club"/>
</a>
<div class="auth-container" class:signup-mode={mode === 'signup'}>
<div class="auth-container" class:signup-mode={authIntent === 'signup'} class:verify-mode={mode === 'verify' && authIntent === 'login'}>
<div class="auth-panel auth-form-panel">
<div class="auth-form-content">
<div class="auth-header">
<img class="auth-logo" src="https://icons.hackclub.com/api/icons/ec3750/clubs" alt="Hack Club" />
<img class="auth-logo" src="https://icons.hackclub.com/api/icons/ec3750/flag" alt="Hack Club" />
<h2 class="auth-title">{displayMode === 'signup' ? 'Join Hack Club Spaces' : 'Welcome Back'}</h2>
<p class="auth-subtitle">Lorem ipsum dolor sit amet</p>
<p class="auth-subtitle">Build amazing projects in the cloud</p>
</div>
{#if mode === 'login' || mode === 'signup'}
<form on:submit|preventDefault={sendVerificationCode}>
<div class="form-group">
<label class="form-label" for="email">Email</label>
<label class="form-label" for="email">
Email
</label>
<input
class="form-input"
id="email"
@ -152,7 +168,9 @@
</div>
<div class="form-group username-field" class:show={mode === 'signup'}>
<label class="form-label" for="username">Username</label>
<label class="form-label" for="username">
Username
</label>
<input
class="form-input"
id="username"
@ -172,7 +190,7 @@
<div class="success-message">{message}</div>
{/if}
<button class="primary-button" type="submit" disabled={loading || !email}>
<button class="primary-button" type="submit" disabled={loading || !email || (mode === 'signup' && !username)}>
{loading ? 'Sending...' : 'Send Verification Code'}
</button>
</form>
@ -184,12 +202,56 @@
Already have an account? <span class="auth-mode-link" on:click={() => switchMode('login')} on:keypress={(e) => e.key === 'Enter' && switchMode('login')} role="button" tabindex="0">Log in</span>
{/if}
</div>
{:else if mode === 'verify'}
<p class="info-message">Check your email for the verification code</p>
{/if}
<form on:submit|preventDefault={authIntent === 'login' ? handleLogin : handleSignup}>
{#if authIntent === 'signup'}
<div class="verification-code-section" class:show={mode === 'verify'}>
<div class="verification-notice">
<p class="verification-title">Check your email</p>
<p class="verification-text">We sent a verification code to <strong>{email}</strong></p>
</div>
<form on:submit|preventDefault={handleSignup}>
<div class="form-group">
<label class="form-label" for="code">
Verification Code
</label>
<input
class="form-input"
id="code"
type="text"
bind:value={verificationCode}
required
placeholder="Enter code from email"
/>
</div>
{#if error}
<div class="error-message">{error}</div>
{/if}
<button class="primary-button" type="submit" disabled={loading || !verificationCode}>
{loading ? 'Verifying...' : 'Complete Sign Up'}
</button>
<button class="secondary-button" type="button" on:click={sendVerificationCode}>
Resend Code
</button>
</form>
</div>
{/if}
{#if mode === 'verify' && authIntent === 'login'}
<div class="verification-notice">
<p class="verification-title">Check your email</p>
<p class="verification-text">We sent a verification code to <strong>{email}</strong></p>
</div>
<form on:submit|preventDefault={handleLogin}>
<div class="form-group">
<label class="form-label" for="code">Verification Code</label>
<label class="form-label" for="code">
Verification Code
</label>
<input
class="form-input"
id="code"
@ -205,10 +267,10 @@
{/if}
<button class="primary-button" type="submit" disabled={loading || !verificationCode}>
{loading ? 'Verifying...' : authIntent === 'signup' ? 'Complete Sign Up' : 'Login'}
{loading ? 'Verifying...' : 'Login'}
</button>
<button class="secondary-button" type="button" on:click={() => switchMode(mode)}>
<button class="secondary-button" type="button" on:click={() => switchMode('login')}>
Resend Code
</button>
</form>

View file

@ -1,7 +1,10 @@
<script>
import { createEventDispatcher } from 'svelte';
import { createEventDispatcher, onMount } from 'svelte';
import { API_BASE, ERROR_MESSAGES } from '../config.js';
import '../styles/dashboard.css';
import { currentTheme } from '../stores/theme.js';
import { themes } from '../themes.js';
import FlagIcon from '../assets/flag.svg?raw';
export let spaces = [];
export let authorization = '';
@ -237,6 +240,14 @@
$: selectedType = spaceTypes.find(type => type.value === newSpaceType) || spaceTypes[0];
$: logoColor = (() => {
const theme = themes[$currentTheme];
if (theme && theme.colors['--red']) {
return theme.colors['--red'];
}
return '#ec3750';
})();
function togglePasswordVisibility() {
showPassword = !showPassword;
}
@ -245,7 +256,9 @@
<div class="dashboard">
<header class="dashboard-header">
<div class="header-content">
<img class="dashboard-logo" src="https://icons.hackclub.com/api/icons/ec3750/clubs" alt="Hack Club" />
<div class="dashboard-logo" style="color: {logoColor}">
{@html FlagIcon}
</div>
<div>
<h1 class="dashboard-title">Hack Club Spaces</h1>
<p class="welcome-text">Welcome, {username}!</p>

View file

@ -0,0 +1,126 @@
<script>
import { currentTheme, setTheme } from '../stores/theme.js';
import { themes } from '../themes.js';
let isOpen = false;
function toggleMenu() {
isOpen = !isOpen;
}
function selectTheme(themeName) {
setTheme(themeName);
isOpen = false;
}
function handleClickOutside(event) {
if (isOpen && !event.target.closest('.theme-switcher')) {
isOpen = false;
}
}
</script>
<svelte:window on:click={handleClickOutside} />
<div class="theme-switcher">
<button class="theme-toggle" on:click|stopPropagation={toggleMenu} aria-label="Switch theme">
theme: {themes[$currentTheme]?.name || 'light'}
</button>
{#if isOpen}
<div class="theme-menu">
{#each Object.entries(themes) as [key, theme]}
<button
class="theme-option"
class:active={$currentTheme === key}
on:click={() => selectTheme(key)}
>
{theme.name}
</button>
{/each}
</div>
{/if}
</div>
<style>
.theme-switcher {
position: fixed;
bottom: 20px;
left: 20px;
z-index: 1000;
}
.theme-toggle {
padding: 8px 16px;
background: var(--white);
border: 2px solid var(--smoke);
border-radius: 4px;
cursor: pointer;
font-size: 14px;
font-weight: 600;
color: var(--black);
transition: all 0.15s ease;
font-family: 'Phantom Sans', sans-serif;
}
.theme-toggle:hover {
border-color: var(--blue);
background: var(--snow);
}
.theme-menu {
position: absolute;
bottom: 42px;
left: 0;
background: var(--white);
border: 2px solid var(--smoke);
border-radius: 4px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
overflow: hidden;
animation: slideUp 0.2s ease;
display: flex;
flex-direction: column;
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.theme-option {
padding: 8px 16px;
background: transparent;
border: none;
cursor: pointer;
font-size: 14px;
font-weight: 500;
color: var(--black);
text-align: left;
transition: all 0.1s ease;
font-family: 'Phantom Sans', sans-serif;
white-space: nowrap;
}
.theme-option:hover {
background: var(--snow);
}
.theme-option.active {
background: var(--blue);
color: var(--white);
font-weight: 700;
}
@media (max-width: 768px) {
.theme-switcher {
bottom: 16px;
left: 16px;
}
}
</style>

View file

@ -0,0 +1,37 @@
import { writable } from 'svelte/store';
import { themes, defaultTheme } from '../themes.js';
const getInitialTheme = () => {
if (typeof window !== 'undefined') {
const saved = localStorage.getItem('theme');
return saved && themes[saved] ? saved : defaultTheme;
}
return defaultTheme;
};
export const currentTheme = writable(getInitialTheme());
export const setTheme = (themeName) => {
if (themes[themeName]) {
currentTheme.set(themeName);
if (typeof window !== 'undefined') {
localStorage.setItem('theme', themeName);
applyTheme(themeName);
}
}
};
export const applyTheme = (themeName) => {
const theme = themes[themeName];
if (theme && typeof document !== 'undefined') {
const root = document.documentElement;
Object.entries(theme.colors).forEach(([key, value]) => {
root.style.setProperty(key, value);
});
root.setAttribute('data-theme', themeName);
}
};
if (typeof window !== 'undefined') {
applyTheme(getInitialTheme());
}

View file

@ -1,7 +1,7 @@
.auth-container {
display: flex;
min-height: 100vh;
background: #f9fafc;
background: var(--snow);
position: relative;
overflow: hidden;
}
@ -18,13 +18,13 @@
}
.auth-form-panel {
background: #ffffff;
background: var(--white);
left: 0;
padding: 64px;
}
.auth-image-panel {
background: #f9fafc;
background: var(--snow);
right: 0;
padding: 0;
}
@ -37,6 +37,15 @@
right: 50%;
}
.auth-container.verify-mode:not(.signup-mode) .auth-image-content {
opacity: 0.6;
}
.auth-container.verify-mode:not(.signup-mode) .auth-image-content::before {
background: linear-gradient(to bottom, rgba(51, 142, 218, 0.4) 0%, transparent 60%);
height: 200px;
}
.auth-form-content {
width: 100%;
max-width: 440px;
@ -84,13 +93,13 @@
left: 32px;
font-size: 16px;
font-weight: bold;
color: #ffffff;
color: var(--white);
z-index: 2;
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.5);
}
.image-caption a {
color: #ffffff;
color: var(--white);
text-decoration: underline;
transition: opacity 0.125s ease-in-out;
}
@ -121,7 +130,7 @@
.auth-title {
font-size: 36px;
font-weight: bold;
color: #1f2d3d;
color: var(--black);
margin: 0 0 8px 0;
line-height: 1.2;
transition: opacity 0.3s ease-in-out;
@ -129,7 +138,7 @@
.auth-subtitle {
font-size: 18px;
color: #8492a6;
color: var(--muted);
margin: 0;
line-height: 1.5;
transition: opacity 0.3s ease-in-out;
@ -139,11 +148,11 @@
text-align: center;
margin-top: 24px;
font-size: 16px;
color: #8492a6;
color: var(--muted);
}
.auth-mode-link {
color: #ec3750;
color: var(--red);
font-weight: bold;
cursor: pointer;
transition: color 0.125s ease-in-out;
@ -151,7 +160,7 @@
}
.auth-mode-link:hover {
color: #ff8c37;
color: var(--orange);
}
.form-group {
@ -173,20 +182,30 @@
}
.form-label {
display: block;
display: inline-flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
font-size: 16px;
font-weight: bold;
color: #1f2d3d;
color: var(--black);
}
.label-icon {
width: 18px;
height: 18px;
flex-shrink: 0;
display: inline-block;
vertical-align: middle;
}
.form-input {
width: 100%;
padding: 14px 16px;
border: 2px solid #e0e6ed;
border: 2px solid var(--smoke);
border-radius: 8px;
background: #ffffff;
color: #1f2d3d;
background: var(--white);
color: var(--black);
font-size: 16px;
font-family: Phantom Sans, sans-serif;
box-sizing: border-box;
@ -195,12 +214,12 @@
.form-input:focus {
outline: none;
border-color: #338eda;
border-color: var(--blue);
box-shadow: 0 0 0 3px rgba(51, 142, 218, 0.1);
}
.form-input::placeholder {
color: #8492a6;
color: var(--muted);
}
.primary-button {
@ -209,8 +228,8 @@
margin-top: 8px;
border: none;
border-radius: 99999px;
background: #ec3750;
color: #ffffff;
background: var(--red);
color: var(--white);
font-size: 18px;
font-weight: bold;
font-family: Phantom Sans, sans-serif;
@ -242,9 +261,9 @@
padding: 16px 32px;
margin-top: 16px;
background: transparent;
border: 2px solid #ec3750;
border: 2px solid var(--red);
border-radius: 99999px;
color: #ec3750;
color: var(--red);
font-size: 18px;
font-weight: bold;
font-family: Phantom Sans, sans-serif;
@ -269,9 +288,9 @@
padding: 12px 16px;
margin-bottom: 16px;
background: rgba(236, 55, 80, 0.1);
border: 2px solid #ec3750;
border: 2px solid var(--red);
border-radius: 8px;
color: #ec3750;
color: var(--red);
font-size: 16px;
}
@ -279,19 +298,64 @@
padding: 12px 16px;
margin-bottom: 16px;
background: rgba(51, 214, 166, 0.1);
border: 2px solid #33d6a6;
border: 2px solid var(--green);
border-radius: 8px;
color: #33d6a6;
color: var(--green);
font-size: 16px;
}
.info-message {
text-align: center;
margin-bottom: 24px;
color: #8492a6;
color: var(--muted);
font-size: 16px;
}
.verification-notice {
padding: 20px;
margin-bottom: 24px;
background: rgba(51, 142, 218, 0.05);
border: 2px solid rgba(51, 142, 218, 0.2);
border-radius: 12px;
}
.verification-title {
margin: 0 0 8px 0;
font-size: 18px;
font-weight: bold;
color: var(--black);
}
.verification-text {
margin: 0;
font-size: 16px;
color: var(--muted);
line-height: 1.5;
}
.verification-text strong {
color: var(--blue);
font-weight: bold;
}
.verification-code-section {
max-height: 0;
opacity: 0;
overflow: hidden;
margin-top: 0;
padding-top: 0;
border-top: 2px solid transparent;
transition: opacity 0.4s ease-in-out, max-height 0.6s ease-in-out, margin-top 0.6s ease-in-out, padding-top 0.6s ease-in-out, border-color 0.3s ease-in-out;
}
.verification-code-section.show {
max-height: 1000px;
opacity: 1;
margin-top: 32px;
padding-top: 32px;
border-top-color: var(--smoke);
}
@media (max-width: 768px) {
.auth-container {

View file

@ -1,7 +1,7 @@
.dashboard {
min-height: 100vh;
padding: 32px;
background: #f9fafc;
background: var(--snow);
}
.dashboard-header {
@ -10,7 +10,7 @@
align-items: center;
margin-bottom: 64px;
padding-bottom: 32px;
border-bottom: 2px solid #e0e6ed;
border-bottom: 2px solid var(--smoke);
}
.header-content {
@ -24,25 +24,31 @@
height: 48px;
}
.dashboard-logo svg {
width: 100%;
height: 100%;
fill: currentColor !important;
}
.dashboard-title {
font-size: 32px;
font-weight: bold;
color: #1f2d3d;
color: var(--black);
margin: 0;
}
.welcome-text {
margin: 8px 0 0 0;
font-size: 16px;
color: #8492a6;
color: var(--muted);
}
.signout-button {
padding: 12px 24px;
background: transparent;
border: 2px solid #ec3750;
border: 2px solid var(--red);
border-radius: 99999px;
color: #ec3750;
color: var(--red);
font-size: 16px;
font-weight: bold;
font-family: Phantom Sans, sans-serif;
@ -73,8 +79,8 @@
.btn-primary {
padding: 14px 32px;
background: #ec3750;
color: #ffffff;
background: var(--red);
color: var(--white);
border: none;
border-radius: 99999px;
font-size: 16px;
@ -103,9 +109,9 @@
.btn-secondary {
padding: 14px 32px;
background: transparent;
border: 2px solid #ec3750;
border: 2px solid var(--red);
border-radius: 99999px;
color: #ec3750;
color: var(--red);
font-size: 16px;
font-weight: bold;
font-family: Phantom Sans, sans-serif;
@ -124,7 +130,7 @@
}
.create-form {
background: #ffffff;
background: var(--white);
padding: 32px;
border-radius: 8px;
margin-bottom: 32px;
@ -135,7 +141,7 @@
margin: 0 0 24px 0;
font-size: 24px;
font-weight: bold;
color: #1f2d3d;
color: var(--black);
}
.form-group {
@ -147,16 +153,16 @@
margin-bottom: 8px;
font-size: 16px;
font-weight: bold;
color: #1f2d3d;
color: var(--black);
}
.form-input {
width: 100%;
padding: 14px 16px;
border: 2px solid #e0e6ed;
border: 2px solid var(--smoke);
border-radius: 8px;
background: #ffffff;
color: #1f2d3d;
background: var(--white);
color: var(--black);
font-size: 16px;
font-family: Phantom Sans, sans-serif;
box-sizing: border-box;
@ -165,10 +171,17 @@
.form-input:focus {
outline: none;
border-color: #338eda;
border-color: var(--blue);
box-shadow: 0 0 0 3px rgba(51, 142, 218, 0.1);
}
.password-info {
margin-bottom: 8px;
font-size: 14px;
color: var(--muted);
line-height: 1.5;
}
.password-input-wrapper {
position: relative;
width: 100%;
@ -217,10 +230,10 @@
width: 100%;
padding: 14px 16px;
padding-right: 48px;
border: 2px solid #e0e6ed;
border: 2px solid var(--smoke);
border-radius: 8px;
background: #ffffff;
color: #1f2d3d;
background: var(--white);
color: var(--black);
font-size: 16px;
font-family: Phantom Sans, sans-serif;
font-weight: bold;
@ -231,11 +244,11 @@
}
.select-trigger:hover {
border-color: #338eda;
border-color: var(--blue);
}
.select-trigger.open {
border-color: #338eda;
border-color: var(--blue);
box-shadow: 0 0 0 3px rgba(51, 142, 218, 0.1);
}
@ -248,7 +261,7 @@
height: 0;
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-top: 6px solid #3c4858;
border-top: 6px solid var(--slate);
transition: transform 0.125s ease-in-out;
pointer-events: none;
}
@ -262,8 +275,8 @@
top: calc(100% + 8px);
left: 0;
right: 0;
background: #ffffff;
border: 2px solid #338eda;
background: var(--white);
border: 2px solid var(--blue);
border-radius: 8px;
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
z-index: 100;
@ -283,7 +296,7 @@
padding: 14px 16px;
cursor: pointer;
transition: background-color 0.125s ease-in-out;
border-bottom: 1px solid #e0e6ed;
border-bottom: 1px solid var(--smoke);
}
.select-option:last-child {
@ -291,34 +304,34 @@
}
.select-option:hover {
background: #f9fafc;
background: var(--snow);
}
.select-option.selected {
background: rgba(51, 142, 218, 0.1);
color: #338eda;
color: var(--blue);
font-weight: bold;
}
.option-label {
font-size: 16px;
font-weight: bold;
color: #1f2d3d;
color: var(--black);
margin-bottom: 4px;
}
.option-description {
font-size: 14px;
color: #8492a6;
color: var(--muted);
}
.error-message {
padding: 12px 16px;
margin-bottom: 16px;
background: rgba(236, 55, 80, 0.1);
border: 2px solid #ec3750;
border: 2px solid var(--red);
border-radius: 8px;
color: #ec3750;
color: var(--red);
font-size: 16px;
}
@ -330,14 +343,14 @@
.section-title {
font-size: 24px;
font-weight: bold;
color: #1f2d3d;
color: var(--black);
margin: 0 0 24px 0;
}
.empty-state {
text-align: center;
padding: 64px 32px;
color: #8492a6;
color: var(--muted);
font-size: 16px;
}
@ -348,15 +361,15 @@
}
.space-card {
background: #ffffff;
background: var(--white);
padding: 24px;
border-radius: 8px;
border: 2px solid #e0e6ed;
border: 2px solid var(--smoke);
transition: border-color 0.125s ease-in-out, box-shadow 0.125s ease-in-out;
}
.space-card:hover {
border-color: #338eda;
border-color: var(--blue);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.125);
}
@ -370,7 +383,7 @@
.space-type {
font-size: 20px;
font-weight: bold;
color: #1f2d3d;
color: var(--black);
margin: 0;
text-transform: capitalize;
}
@ -380,23 +393,23 @@
border-radius: 99999px;
font-size: 14px;
font-weight: bold;
color: #ffffff;
color: var(--white);
}
.status-running {
background: #33d6a6;
background: var(--green);
}
.status-stopped {
background: #ec3750;
background: var(--red);
}
.status-created {
background: #f1c40f;
background: var(--yellow);
}
.status-unknown {
background: #8492a6;
background: var(--muted);
}
.space-info {
@ -406,21 +419,21 @@
.space-info p {
margin: 8px 0;
color: #8492a6;
color: var(--muted);
}
.space-info strong {
color: #3c4858;
color: var(--slate);
}
.space-info a {
color: #338eda;
color: var(--blue);
text-decoration: none;
transition: color 0.125s ease-in-out;
}
.space-info a:hover {
color: #ec3750;
color: var(--red);
text-decoration: underline;
}
@ -460,28 +473,33 @@
}
.action-btn.start {
background: #33d6a6;
color: #ffffff;
background: var(--green);
color: var(--white);
}
.action-btn.stop {
background: #ec3750;
color: #ffffff;
background: var(--red);
color: var(--white);
}
.action-btn.open {
background: #338eda;
color: #ffffff;
background: var(--blue);
color: var(--white);
}
.action-btn.refresh {
background: transparent;
border: 2px solid #338eda;
color: #338eda;
border: 2px solid var(--blue);
color: var(--blue);
flex: 0 0 auto;
min-width: 44px;
}
.action-btn.delete {
background: var(--orange);
color: var(--white);
}
@media (max-width: 768px) {
.dashboard {
padding: 16px;

188
client/src/themes.js Normal file
View file

@ -0,0 +1,188 @@
export const themes = {
light: {
name: 'Light',
icon: 'Default',
colors: {
'--red': '#ec3750',
'--orange': '#ff8c37',
'--yellow': '#f1c40f',
'--green': '#33d6a6',
'--cyan': '#5bc0de',
'--blue': '#338eda',
'--purple': '#a633d6',
'--muted': '#8492a6',
'--darker': '#121217',
'--dark': '#17171d',
'--darkless': '#252429',
'--black': '#1f2d3d',
'--steel': '#273444',
'--slate': '#3c4858',
'--smoke': '#e0e6ed',
'--snow': '#f9fafc',
'--white': '#ffffff'
}
},
dark: {
name: 'Dark',
icon: 'Moon',
colors: {
'--red': '#ff4060',
'--orange': '#ff9c47',
'--yellow': '#f9d423',
'--green': '#3ae6b6',
'--cyan': '#6bd0ee',
'--blue': '#5098ea',
'--purple': '#b643e6',
'--muted': '#9aa2b0',
'--darker': '#f9fafc',
'--dark': '#e0e6ed',
'--darkless': '#c0c6cd',
'--black': '#ffffff',
'--steel': '#e0e6ed',
'--slate': '#c0c6cd',
'--smoke': '#2a2a2f',
'--snow': '#1a1a1f',
'--white': '#0f0f14'
}
},
darkPurple: {
name: 'Dark Purple',
icon: 'Sparkle',
colors: {
'--red': '#ff6b9d',
'--orange': '#ffaa6b',
'--yellow': '#ffd966',
'--green': '#5de6c1',
'--cyan': '#7ddeff',
'--blue': '#7d9cff',
'--purple': '#c77dff',
'--muted': '#b4a5d6',
'--darker': '#f0e6ff',
'--dark': '#e0d1ff',
'--darkless': '#c9b3ff',
'--black': '#f5f0ff',
'--steel': '#d9ccff',
'--slate': '#c9b3ff',
'--smoke': '#2e1f47',
'--snow': '#1f0f3a',
'--white': '#15082b'
}
},
darkGradient: {
name: 'Dark Gradient',
icon: 'Gradient',
colors: {
'--red': '#ff4d6d',
'--orange': '#ff9057',
'--yellow': '#ffda77',
'--green': '#06ffa5',
'--cyan': '#00d9ff',
'--blue': '#4d8cff',
'--purple': '#b366ff',
'--muted': '#a0b0c0',
'--darker': '#e8f4f8',
'--dark': '#d0e4f0',
'--darkless': '#b0d4e8',
'--black': '#ffffff',
'--steel': '#c0d8e8',
'--slate': '#a0c0d8',
'--smoke': '#1a2a3a',
'--snow': '#0f1a2a',
'--white': '#0a0f1a'
}
},
lgbtq: {
name: 'Rainbow',
icon: 'Heart',
colors: {
'--red': '#E40303',
'--orange': '#FF8C00',
'--yellow': '#FFED00',
'--green': '#008026',
'--cyan': '#24408E',
'--blue': '#732982',
'--purple': '#732982',
'--muted': '#8b8b8b',
'--darker': '#ffffff',
'--dark': '#f5f5f5',
'--darkless': '#e8e8e8',
'--black': '#ffffff',
'--steel': '#e0e0e0',
'--slate': '#c0c0c0',
'--smoke': '#4a1942',
'--snow': '#2a0a26',
'--white': '#1a0616'
}
},
sunset: {
name: 'Sunset',
icon: 'Sunset',
colors: {
'--red': '#ff6b6b',
'--orange': '#ffa06b',
'--yellow': '#ffe66d',
'--green': '#95e1d3',
'--cyan': '#6bcfff',
'--blue': '#a29bfe',
'--purple': '#fd79a8',
'--muted': '#b8a89d',
'--darker': '#2d3436',
'--dark': '#636e72',
'--darkless': '#95a5a6',
'--black': '#2d3436',
'--steel': '#636e72',
'--slate': '#95a5a6',
'--smoke': '#dfe6e9',
'--snow': '#f8f9fa',
'--white': '#ffffff'
}
},
ocean: {
name: 'Ocean',
icon: 'Waves',
colors: {
'--red': '#ff6b9d',
'--orange': '#ff9f7f',
'--yellow': '#ffd97d',
'--green': '#69f0ae',
'--cyan': '#64d8cb',
'--blue': '#4d94ff',
'--purple': '#8c7ae6',
'--muted': '#7f8fa6',
'--darker': '#0f1c2e',
'--dark': '#1e3a5f',
'--darkless': '#2d5886',
'--black': '#e1f5fe',
'--steel': '#b3e5fc',
'--slate': '#81d4fa',
'--smoke': '#1e3a5f',
'--snow': '#0a1929',
'--white': '#021120'
}
},
forest: {
name: 'Forest',
icon: 'Tree',
colors: {
'--red': '#e74c3c',
'--orange': '#f39c12',
'--yellow': '#f1c40f',
'--green': '#27ae60',
'--cyan': '#16a085',
'--blue': '#3498db',
'--purple': '#9b59b6',
'--muted': '#95a5a6',
'--darker': '#0e2f1f',
'--dark': '#1a4731',
'--darkless': '#275f43',
'--black': '#ecf0f1',
'--steel': '#d5e8df',
'--slate': '#bdd4c7',
'--smoke': '#2c5f3f',
'--snow': '#1a4731',
'--white': '#0e2f1f'
}
}
};
export const defaultTheme = 'light';

View file

@ -1,11 +1,16 @@
import { defineConfig } from 'vite'
import { svelte } from '@sveltejs/vite-plugin-svelte'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [svelte()],
server: {
host: '0.0.0.0', // Binds to all network interfaces, accessible from other devices
port: 5173, // Default port, can be changed
server: {
host: '0.0.0.0',
port: 5173,
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true,
}
}
}
})

3
package-lock.json generated
View file

@ -776,7 +776,6 @@
"version": "4.18.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
"integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==",
"peer": true,
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
@ -818,7 +817,6 @@
"version": "8.2.1",
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.2.1.tgz",
"integrity": "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==",
"license": "MIT",
"dependencies": {
"ip-address": "10.0.1"
},
@ -2719,7 +2717,6 @@
"version": "4.18.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
"integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==",
"peer": true,
"requires": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",