feat: add styling to pages :D

This commit is contained in:
Arnav 2025-11-03 15:56:22 +00:00
parent 5829ccc6f0
commit 33fa885336
8 changed files with 1177 additions and 648 deletions

1
.gitignore vendored
View file

@ -5,3 +5,4 @@ build
.github .github
.vscode .vscode
.DS_Store .DS_Store
.claude

View file

@ -637,7 +637,6 @@
"resolved": "https://registry.npmjs.org/svelte/-/svelte-3.53.1.tgz", "resolved": "https://registry.npmjs.org/svelte/-/svelte-3.53.1.tgz",
"integrity": "sha512-Q4/hHkktZogGhN5iqxqSi9sjEVoe/NbIxX4hXEHoasTxj+TxEQVAq66LnDMdAZxjmsodkoI5F3slqsS68U7FNw==", "integrity": "sha512-Q4/hHkktZogGhN5iqxqSi9sjEVoe/NbIxX4hXEHoasTxj+TxEQVAq66LnDMdAZxjmsodkoI5F3slqsS68U7FNw==",
"dev": true, "dev": true,
"peer": true,
"engines": { "engines": {
"node": ">= 8" "node": ">= 8"
} }
@ -659,7 +658,6 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-3.2.3.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-3.2.3.tgz",
"integrity": "sha512-h8jl1TZ76eGs3o2dIBSsvXDLb1m/Ec1iej8ZMdz+PsaFUsftZeWe2CZOI3qogEsMNaywc17gu0q6cQDzh/weCQ==", "integrity": "sha512-h8jl1TZ76eGs3o2dIBSsvXDLb1m/Ec1iej8ZMdz+PsaFUsftZeWe2CZOI3qogEsMNaywc17gu0q6cQDzh/weCQ==",
"dev": true, "dev": true,
"peer": true,
"dependencies": { "dependencies": {
"esbuild": "^0.15.9", "esbuild": "^0.15.9",
"postcss": "^8.4.18", "postcss": "^8.4.18",
@ -1041,8 +1039,7 @@
"version": "3.53.1", "version": "3.53.1",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-3.53.1.tgz", "resolved": "https://registry.npmjs.org/svelte/-/svelte-3.53.1.tgz",
"integrity": "sha512-Q4/hHkktZogGhN5iqxqSi9sjEVoe/NbIxX4hXEHoasTxj+TxEQVAq66LnDMdAZxjmsodkoI5F3slqsS68U7FNw==", "integrity": "sha512-Q4/hHkktZogGhN5iqxqSi9sjEVoe/NbIxX4hXEHoasTxj+TxEQVAq66LnDMdAZxjmsodkoI5F3slqsS68U7FNw==",
"dev": true, "dev": true
"peer": true
}, },
"svelte-hmr": { "svelte-hmr": {
"version": "0.15.0", "version": "0.15.0",
@ -1056,7 +1053,6 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-3.2.3.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-3.2.3.tgz",
"integrity": "sha512-h8jl1TZ76eGs3o2dIBSsvXDLb1m/Ec1iej8ZMdz+PsaFUsftZeWe2CZOI3qogEsMNaywc17gu0q6cQDzh/weCQ==", "integrity": "sha512-h8jl1TZ76eGs3o2dIBSsvXDLb1m/Ec1iej8ZMdz+PsaFUsftZeWe2CZOI3qogEsMNaywc17gu0q6cQDzh/weCQ==",
"dev": true, "dev": true,
"peer": true,
"requires": { "requires": {
"esbuild": "^0.15.9", "esbuild": "^0.15.9",
"fsevents": "~2.3.2", "fsevents": "~2.3.2",

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

View file

@ -1,12 +1,56 @@
@font-face {
font-family: 'Phantom Sans';
src: url('https://assets.hackclub.com/fonts/Phantom_Sans_0.7/Regular.woff2') format('woff2'),
url('https://assets.hackclub.com/fonts/Phantom_Sans_0.7/Regular.woff') format('woff');
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Phantom Sans';
src: url('https://assets.hackclub.com/fonts/Phantom_Sans_0.7/Italic.woff2') format('woff2'),
url('https://assets.hackclub.com/fonts/Phantom_Sans_0.7/Italic.woff') format('woff');
font-weight: 400;
font-style: italic;
font-display: swap;
}
@font-face {
font-family: 'Phantom Sans';
src: url('https://assets.hackclub.com/fonts/Phantom_Sans_0.7/Bold.woff2') format('woff2'),
url('https://assets.hackclub.com/fonts/Phantom_Sans_0.7/Bold.woff') format('woff');
font-weight: 700;
font-style: normal;
font-display: swap;
}
:root { :root {
font-family: Inter, Avenir, Helvetica, Arial, sans-serif; --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;
font-family: 'Phantom Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 16px; font-size: 16px;
line-height: 24px; line-height: 1.5;
font-weight: 400; font-weight: 400;
color-scheme: light dark; color: var(--black);
color: rgba(255, 255, 255, 0.87); background-color: var(--snow);
background-color: #242424;
font-synthesis: none; font-synthesis: none;
text-rendering: optimizeLegibility; text-rendering: optimizeLegibility;
@ -15,67 +59,40 @@
-webkit-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;
} }
a { * {
font-weight: 500; box-sizing: border-box;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
} }
body { body {
margin: 0; margin: 0;
display: flex;
place-items: center;
min-width: 320px; min-width: 320px;
min-height: 100vh; min-height: 100vh;
} }
h1 {
font-size: 3.2em;
line-height: 1.1;
}
.card {
padding: 2em;
}
#app { #app {
max-width: 1280px; width: 100%;
margin: 0 auto; min-height: 100vh;
padding: 2rem; }
text-align: center;
a {
color: var(--blue);
text-decoration: none;
transition: color 0.125s ease-in-out;
}
a:hover {
color: var(--red);
}
h1, h2, h3, h4, h5, h6 {
font-weight: bold;
line-height: 1.2;
} }
button { button {
border-radius: 8px; font-family: 'Phantom Sans', sans-serif;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
} }
@media (prefers-color-scheme: light) { input, textarea, select {
:root { font-family: 'Phantom Sans', sans-serif;
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
} }

View file

@ -4,13 +4,14 @@
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
let mode = 'login'; // 'login', 'signup', or 'verify' let mode = 'login';
let email = ''; let email = '';
let username = ''; let username = '';
let verificationCode = ''; let verificationCode = '';
let error = ''; let error = '';
let loading = false; let loading = false;
let message = ''; let message = '';
let displayMode = 'login';
async function sendVerificationCode() { async function sendVerificationCode() {
error = ''; error = '';
@ -104,230 +105,120 @@
} }
function switchMode(newMode) { function switchMode(newMode) {
mode = newMode;
error = ''; error = '';
message = ''; message = '';
verificationCode = ''; verificationCode = '';
setTimeout(() => {
mode = newMode;
}, 400);
setTimeout(() => {
displayMode = newMode;
}, 800);
} }
</script> </script>
<div class="auth-container"> <svelte:head>
<div class="auth-card"> <link rel="stylesheet" href="/src/styles/auth.css" />
<h2>Hack Club Spaces</h2> </svelte:head>
{#if mode === 'login' || mode === 'signup'} <a href="https://hackclub.com/">
<div class="tabs"> <img class="flag-banner" src="https://assets.hackclub.com/flag-orpheus-top.svg" alt="Hack Club"/>
<button </a>
class:active={mode === 'login'}
on:click={() => switchMode('login')} <div class="auth-container" class:signup-mode={mode === 'signup'}>
> <div class="auth-panel auth-form-panel">
Login <div class="auth-form-content">
</button> <div class="auth-header">
<button <img class="auth-logo" src="https://icons.hackclub.com/api/icons/ec3750/clubs" alt="Hack Club" />
class:active={mode === 'signup'} <h2 class="auth-title">{displayMode === 'signup' ? 'Join Hack Club Spaces' : 'Welcome Back'}</h2>
on:click={() => switchMode('signup')} <p class="auth-subtitle">Lorem ipsum dolor sit amet</p>
>
Sign Up
</button>
</div> </div>
<form on:submit|preventDefault={sendVerificationCode}> {#if mode === 'login' || mode === 'signup'}
<div class="form-group"> <form on:submit|preventDefault={sendVerificationCode}>
<label for="email">Email</label>
<input
id="email"
type="email"
bind:value={email}
required
placeholder="your@email.com"
/>
</div>
{#if mode === 'signup'}
<div class="form-group"> <div class="form-group">
<label for="username">Username</label> <label class="form-label" for="email">Email</label>
<input <input
class="form-input"
id="email"
type="email"
bind:value={email}
required
placeholder="your@email.com"
/>
</div>
<div class="form-group username-field" class:show={mode === 'signup'}>
<label class="form-label" for="username">Username</label>
<input
class="form-input"
id="username" id="username"
type="text" type="text"
bind:value={username} bind:value={username}
required required={mode === 'signup'}
maxlength="100" maxlength="100"
placeholder="Choose a username" placeholder="Choose a username"
/> />
</div> </div>
{/if}
{#if error} {#if error}
<div class="error">{error}</div> <div class="error-message">{error}</div>
{/if} {/if}
{#if message} {#if message}
<div class="success">{message}</div> <div class="success-message">{message}</div>
{/if} {/if}
<button type="submit" disabled={loading || !email}> <button class="primary-button" type="submit" disabled={loading || !email}>
{loading ? 'Sending...' : 'Send Verification Code'} {loading ? 'Sending...' : 'Send Verification Code'}
</button> </button>
</form> </form>
{:else if mode === 'verify'}
<p class="info">Check your email for the verification code</p>
<form on:submit|preventDefault={mode === 'login' ? handleLogin : handleSignup}> <div class="auth-mode-switch">
<div class="form-group"> {#if mode === 'login'}
<label for="code">Verification Code</label> Don't have an account? <span class="auth-mode-link" on:click={() => switchMode('signup')} on:keypress={(e) => e.key === 'Enter' && switchMode('signup')} role="button" tabindex="0">Sign up</span>
<input {:else}
id="code" 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>
type="text" {/if}
bind:value={verificationCode}
required
placeholder="Enter code from email"
/>
</div> </div>
{:else if mode === 'verify'}
<p class="info-message">Check your email for the verification code</p>
{#if error} <form on:submit|preventDefault={mode === 'login' ? handleLogin : handleSignup}>
<div class="error">{error}</div> <div class="form-group">
{/if} <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>
<button type="submit" disabled={loading || !verificationCode}> {#if error}
{loading ? 'Verifying...' : mode === 'signup' ? 'Complete Sign Up' : 'Login'} <div class="error-message">{error}</div>
</button> {/if}
<button type="button" class="secondary" on:click={() => switchMode(mode)}> <button class="primary-button" type="submit" disabled={loading || !verificationCode}>
Resend Code {loading ? 'Verifying...' : mode === 'signup' ? 'Complete Sign Up' : 'Login'}
</button> </button>
</form>
{/if} <button class="secondary-button" type="button" on:click={() => switchMode(mode)}>
Resend Code
</button>
</form>
{/if}
</div>
</div>
<div class="auth-panel auth-image-panel">
<div class="auth-image-content">
<img class="auth-image" src="/group-photo.jpg" alt="Hack Club Shipwrecked" />
<div class="image-caption">
Hackers at <a href="https://shipwrecked.hackclub.com" target="_blank" rel="noopener noreferrer">Shipwrecked</a>, 2025
</div>
</div>
</div> </div>
</div> </div>
<style>
.auth-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
padding: 2rem;
}
.auth-card {
background: rgba(255, 255, 255, 0.05);
border-radius: 12px;
padding: 2rem;
width: 100%;
max-width: 400px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
h2 {
text-align: center;
margin-bottom: 2rem;
color: #ec3750;
}
.tabs {
display: flex;
gap: 0.5rem;
margin-bottom: 1.5rem;
}
.tabs button {
flex: 1;
padding: 0.75rem;
background: transparent;
border: 1px solid #646cff;
color: #646cff;
cursor: pointer;
transition: all 0.3s;
}
.tabs button.active {
background: #646cff;
color: white;
}
.form-group {
margin-bottom: 1.5rem;
}
label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
}
input {
width: 100%;
padding: 0.75rem;
border: 1px solid #444;
border-radius: 8px;
background: rgba(255, 255, 255, 0.05);
color: inherit;
font-size: 1rem;
box-sizing: border-box;
}
input:focus {
outline: none;
border-color: #646cff;
}
button[type="submit"], .secondary {
width: 100%;
padding: 0.75rem;
margin-top: 0.5rem;
border-radius: 8px;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: all 0.3s;
}
button[type="submit"] {
background: #646cff;
border: none;
color: white;
}
button[type="submit"]:hover:not(:disabled) {
background: #535bf2;
}
button[type="submit"]:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.secondary {
background: transparent;
border: 1px solid #646cff;
color: #646cff;
}
.secondary:hover {
background: rgba(100, 108, 255, 0.1);
}
.error {
padding: 0.75rem;
margin-bottom: 1rem;
background: rgba(239, 68, 68, 0.1);
border: 1px solid rgba(239, 68, 68, 0.3);
border-radius: 8px;
color: #ef4444;
}
.success {
padding: 0.75rem;
margin-bottom: 1rem;
background: rgba(34, 197, 94, 0.1);
border: 1px solid rgba(34, 197, 94, 0.3);
border-radius: 8px;
color: #22c55e;
}
.info {
text-align: center;
margin-bottom: 1.5rem;
color: #888;
}
</style>

View file

@ -15,6 +15,8 @@
let loading = false; let loading = false;
let actionLoading = {}; let actionLoading = {};
let actionError = {}; let actionError = {};
let dropdownOpen = false;
let showPassword = false;
const spaceTypes = [ const spaceTypes = [
{ value: 'code-server', label: 'VS Code Server', description: 'Web-based code editor' }, { value: 'code-server', label: 'VS Code Server', description: 'Web-based code editor' },
@ -79,7 +81,6 @@
actionLoading = actionLoading; actionLoading = actionLoading;
actionError = actionError; actionError = actionError;
// Find the space to get its access_url
const space = spaces.find(s => s.id === spaceId); const space = spaces.find(s => s.id === spaceId);
try { try {
@ -176,67 +177,139 @@
dispatch('signout'); dispatch('signout');
} }
function getStatusColor(status) { function getStatusClass(status) {
switch(status?.toLowerCase()) { switch(status?.toLowerCase()) {
case 'running': return '#22c55e'; case 'running': return 'status-running';
case 'stopped': return '#ef4444'; case 'stopped': return 'status-stopped';
case 'created': return '#eab308'; case 'created': return 'status-created';
default: return '#888'; default: return 'status-unknown';
} }
} }
function toggleDropdown() {
dropdownOpen = !dropdownOpen;
}
function selectSpaceType(value) {
newSpaceType = value;
dropdownOpen = false;
}
$: selectedType = spaceTypes.find(type => type.value === newSpaceType) || spaceTypes[0];
function togglePasswordVisibility() {
showPassword = !showPassword;
}
</script> </script>
<svelte:head>
<link rel="stylesheet" href="/src/styles/dashboard.css" />
</svelte:head>
<div class="dashboard"> <div class="dashboard">
<header> <header class="dashboard-header">
<div> <div class="header-content">
<h1>Hack Club Spaces</h1> <img class="dashboard-logo" src="https://icons.hackclub.com/api/icons/ec3750/clubs" alt="Hack Club" />
<p class="welcome">Welcome, {username}!</p> <div>
<h1 class="dashboard-title">Hack Club Spaces</h1>
<p class="welcome-text">Welcome, {username}!</p>
</div>
</div> </div>
<button class="signout" on:click={handleSignOut}>Sign Out</button> <button class="signout-button" on:click={handleSignOut}>Sign Out</button>
</header> </header>
<div class="content"> <div class="dashboard-content">
<div class="actions-bar"> <div class="actions-bar">
<button class="primary" on:click={() => showCreateForm = !showCreateForm}> <button class="btn-primary" on:click={() => showCreateForm = !showCreateForm}>
{showCreateForm ? 'Cancel' : '+ Create New Space'} {showCreateForm ? 'Cancel' : '+ Create New Space'}
</button> </button>
<button class="secondary" on:click={loadSpaces}> <button class="btn-secondary" on:click={loadSpaces}>
Refresh Refresh
</button> </button>
</div> </div>
{#if showCreateForm} {#if showCreateForm}
<div class="create-form"> <div class="create-form">
<h3>Create New Space</h3> <h3 class="create-form-title">Create New Space</h3>
<div class="form-group"> <div class="form-group">
<label for="type">Space Type</label> <label class="form-label" for="type">Space Type</label>
<select id="type" bind:value={newSpaceType}> <div class="custom-select">
{#each spaceTypes as spaceType} <button
<option value={spaceType.value}> type="button"
{spaceType.label} - {spaceType.description} class="select-trigger"
</option> class:open={dropdownOpen}
{/each} on:click={toggleDropdown}
</select> >
{selectedType.label}
</button>
<div class="select-arrow" class:open={dropdownOpen}></div>
<div class="select-dropdown" class:open={dropdownOpen}>
{#each spaceTypes as spaceType}
<div
class="select-option"
class:selected={spaceType.value === newSpaceType}
on:click={() => selectSpaceType(spaceType.value)}
on:keypress={(e) => e.key === 'Enter' && selectSpaceType(spaceType.value)}
role="option"
tabindex="0"
aria-selected={spaceType.value === newSpaceType}
>
<div class="option-label">{spaceType.label}</div>
<div class="option-description">{spaceType.description}</div>
</div>
{/each}
</div>
</div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="password">Password</label> <label class="form-label" for="password">Password</label>
<input <div class="password-input-wrapper">
id="password" {#if showPassword}
type="password" <input
bind:value={newSpacePassword} class="form-input password-input"
required id="password"
placeholder="Set a password for this space" type="text"
/> bind:value={newSpacePassword}
required
placeholder="Set a password for this space"
/>
{:else}
<input
class="form-input password-input"
id="password"
type="password"
bind:value={newSpacePassword}
required
placeholder="Set a password for this space"
/>
{/if}
<button
type="button"
class="password-toggle"
on:click={togglePasswordVisibility}
aria-label={showPassword ? 'Hide password' : 'Show password'}
>
{#if showPassword}
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 256 256">
<path d="M53.92,34.62A8,8,0,1,0,42.08,45.38L61.32,66.55C25,88.84,9.38,123.2,8.69,124.76a8,8,0,0,0,0,6.5c.35.79,8.82,19.57,27.65,38.4C61.43,194.74,93.12,208,128,208a127.11,127.11,0,0,0,52.07-10.83l22,24.21a8,8,0,1,0,11.84-10.76Zm47.33,75.84,41.67,45.85a32,32,0,0,1-41.67-45.85ZM128,192c-30.78,0-57.67-11.19-79.93-33.25A133.16,133.16,0,0,1,25,128c4.69-8.79,19.66-33.39,47.35-49.38l18,19.75a48,48,0,0,0,63.66,70l14.73,16.2A112,112,0,0,1,128,192Zm6-95.43a8,8,0,0,1,3-15.72,48.16,48.16,0,0,1,38.77,42.64,8,8,0,0,1-7.22,8.71,6.39,6.39,0,0,1-.75,0,8,8,0,0,1-8-7.26A32.09,32.09,0,0,0,134,96.57Zm113.28,34.69c-.42.94-10.55,23.37-33.36,43.8a8,8,0,1,1-10.67-11.92A132.77,132.77,0,0,0,231.05,128a133.15,133.15,0,0,0-23.12-30.77C185.67,75.19,158.78,64,128,64a118.37,118.37,0,0,0-19.36,1.57A8,8,0,1,1,106,49.79,134,134,0,0,1,128,48c34.88,0,66.57,13.26,91.66,38.35,18.83,18.83,27.3,37.62,27.65,38.41A8,8,0,0,1,247.31,131.26Z"></path>
</svg>
{:else}
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 256 256">
<path d="M247.31,124.76c-.35-.79-8.82-19.58-27.65-38.41C194.57,61.26,162.88,48,128,48S61.43,61.26,36.34,86.35C17.51,105.18,9,124,8.69,124.76a8,8,0,0,0,0,6.5c.35.79,8.82,19.57,27.65,38.4C61.43,194.74,93.12,208,128,208s66.57-13.26,91.66-38.34c18.83-18.83,27.3-37.61,27.65-38.4A8,8,0,0,0,247.31,124.76ZM128,192c-30.78,0-57.67-11.19-79.93-33.25A133.47,133.47,0,0,1,25,128,133.33,133.33,0,0,1,48.07,97.25C70.33,75.19,97.22,64,128,64s57.67,11.19,79.93,33.25A133.46,133.46,0,0,1,231.05,128C223.84,141.46,192.43,192,128,192Zm0-112a48,48,0,1,0,48,48A48.05,48.05,0,0,0,128,80Zm0,80a32,32,0,1,1,32-32A32,32,0,0,1,128,160Z"></path>
</svg>
{/if}
</button>
</div>
</div> </div>
{#if error} {#if error}
<div class="error">{error}</div> <div class="error-message">{error}</div>
{/if} {/if}
<button <button
class="primary" class="btn-primary"
on:click={createSpace} on:click={createSpace}
disabled={loading || !newSpacePassword} disabled={loading || !newSpacePassword}
> >
@ -246,7 +319,7 @@
{/if} {/if}
<div class="spaces-list"> <div class="spaces-list">
<h3>Your Spaces</h3> <h3 class="section-title">Your Spaces</h3>
{#if spaces.length === 0} {#if spaces.length === 0}
<div class="empty-state"> <div class="empty-state">
@ -257,11 +330,8 @@
{#each spaces as space} {#each spaces as space}
<div class="space-card"> <div class="space-card">
<div class="space-header"> <div class="space-header">
<h4>{space.type}</h4> <h4 class="space-type">{space.type}</h4>
<span <span class="status-badge {getStatusClass(space.status)}">
class="status-badge"
style="background-color: {getStatusColor(space.status)}"
>
{space.status || 'Unknown'} {space.status || 'Unknown'}
</span> </span>
</div> </div>
@ -279,7 +349,7 @@
</div> </div>
{#if actionError[space.id]} {#if actionError[space.id]}
<div class="error small">{actionError[space.id]}</div> <div class="error-message small">{actionError[space.id]}</div>
{/if} {/if}
<div class="space-actions"> <div class="space-actions">
@ -328,274 +398,3 @@
</div> </div>
</div> </div>
</div> </div>
<style>
.dashboard {
min-height: 100vh;
padding: 2rem;
}
header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
padding-bottom: 1rem;
border-bottom: 2px solid #333;
}
h1 {
margin: 0;
color: #ec3750;
}
.welcome {
margin: 0.5rem 0 0 0;
color: #888;
}
.signout {
padding: 0.5rem 1rem;
background: transparent;
border: 1px solid #ef4444;
color: #ef4444;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s;
}
.signout:hover {
background: rgba(239, 68, 68, 0.1);
}
.content {
max-width: 1200px;
margin: 0 auto;
}
.actions-bar {
display: flex;
gap: 1rem;
margin-bottom: 2rem;
}
.primary {
background: #646cff;
color: white;
border: none;
padding: 0.75rem 1.5rem;
border-radius: 8px;
cursor: pointer;
font-weight: 500;
transition: all 0.3s;
}
.primary:hover:not(:disabled) {
background: #535bf2;
}
.primary:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.secondary {
background: transparent;
border: 1px solid #646cff;
color: #646cff;
padding: 0.75rem 1.5rem;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s;
}
.secondary:hover {
background: rgba(100, 108, 255, 0.1);
}
.create-form {
background: rgba(255, 255, 255, 0.05);
padding: 2rem;
border-radius: 12px;
margin-bottom: 2rem;
}
.create-form h3 {
margin-top: 0;
}
.form-group {
margin-bottom: 1.5rem;
}
label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
}
input, select {
width: 100%;
padding: 0.75rem;
border: 1px solid #444;
border-radius: 8px;
background: rgba(255, 255, 255, 0.05);
color: inherit;
font-size: 1rem;
box-sizing: border-box;
}
input:focus, select:focus {
outline: none;
border-color: #646cff;
}
.error {
padding: 0.75rem;
margin-bottom: 1rem;
background: rgba(239, 68, 68, 0.1);
border: 1px solid rgba(239, 68, 68, 0.3);
border-radius: 8px;
color: #ef4444;
}
.error.small {
padding: 0.5rem;
font-size: 0.875rem;
}
.spaces-list h3 {
margin-bottom: 1.5rem;
}
.empty-state {
text-align: center;
padding: 3rem;
color: #888;
}
.spaces-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1.5rem;
}
.space-card {
background: rgba(255, 255, 255, 0.05);
padding: 1.5rem;
border-radius: 12px;
border: 1px solid #333;
transition: all 0.3s;
}
.space-card:hover {
border-color: #646cff;
box-shadow: 0 4px 12px rgba(100, 108, 255, 0.1);
}
.space-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.space-header h4 {
margin: 0;
text-transform: capitalize;
}
.status-badge {
padding: 0.25rem 0.75rem;
border-radius: 12px;
font-size: 0.875rem;
font-weight: 500;
color: white;
}
.space-info {
margin-bottom: 1rem;
font-size: 0.875rem;
}
.space-info p {
margin: 0.5rem 0;
color: #888;
}
.space-info strong {
color: #ccc;
}
.space-info a {
color: #646cff;
text-decoration: none;
}
.space-info a:hover {
text-decoration: underline;
}
.space-actions {
display: flex;
gap: 0.5rem;
}
.action-btn {
flex: 1;
padding: 0.5rem;
border-radius: 8px;
border: none;
cursor: pointer;
font-weight: 500;
transition: all 0.3s;
text-decoration: none;
display: flex;
align-items: center;
justify-content: center;
}
.action-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.action-btn.start {
background: #22c55e;
color: white;
}
.action-btn.start:hover:not(:disabled) {
background: #16a34a;
}
.action-btn.stop {
background: #ef4444;
color: white;
}
.action-btn.stop:hover:not(:disabled) {
background: #dc2626;
}
.action-btn.open {
background: #646cff;
color: white;
}
.action-btn.open:hover {
background: #535bf2;
}
.action-btn.refresh {
background: transparent;
border: 1px solid #646cff;
color: #646cff;
flex: 0 0 auto;
min-width: 40px;
}
.action-btn.refresh:hover:not(:disabled) {
background: rgba(100, 108, 255, 0.1);
}
</style>

322
client/src/styles/auth.css Normal file
View file

@ -0,0 +1,322 @@
.auth-container {
display: flex;
min-height: 100vh;
background: #f9fafc;
position: relative;
overflow: hidden;
}
.auth-panel {
width: 50%;
display: flex;
align-items: center;
justify-content: center;
position: absolute;
height: 100%;
top: 0;
transition: left 1s cubic-bezier(0.65, 0, 0.35, 1), right 1s cubic-bezier(0.65, 0, 0.35, 1);
}
.auth-form-panel {
background: #ffffff;
left: 0;
padding: 64px;
}
.auth-image-panel {
background: #f9fafc;
right: 0;
padding: 0;
}
.auth-container.signup-mode .auth-form-panel {
left: 50%;
}
.auth-container.signup-mode .auth-image-panel {
right: 50%;
}
.auth-form-content {
width: 100%;
max-width: 440px;
}
.auth-image-content {
width: 100%;
height: 100%;
position: relative;
}
.auth-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.auth-image-content::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 120px;
background: linear-gradient(to bottom, rgba(249, 250, 252, 0.3) 0%, transparent 100%);
pointer-events: none;
z-index: 1;
}
.auth-image-content::after {
content: '';
position: absolute;
top: 0;
bottom: 0;
left: 0;
width: 60px;
background: linear-gradient(to right, rgba(249, 250, 252, 0.2) 0%, transparent 100%);
pointer-events: none;
z-index: 1;
}
.image-caption {
position: absolute;
bottom: 32px;
left: 32px;
font-size: 16px;
font-weight: bold;
color: #ffffff;
z-index: 2;
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.5);
}
.image-caption a {
color: #ffffff;
text-decoration: underline;
transition: opacity 0.125s ease-in-out;
}
.image-caption a:hover {
opacity: 0.8;
}
.flag-banner {
position: absolute;
top: 0;
left: 10px;
width: 256px;
z-index: 999;
border: 0;
}
.auth-header {
margin-bottom: 48px;
}
.auth-logo {
width: 56px;
height: 56px;
margin-bottom: 16px;
}
.auth-title {
font-size: 36px;
font-weight: bold;
color: #1f2d3d;
margin: 0 0 8px 0;
line-height: 1.2;
transition: opacity 0.3s ease-in-out;
}
.auth-subtitle {
font-size: 18px;
color: #8492a6;
margin: 0;
line-height: 1.5;
transition: opacity 0.3s ease-in-out;
}
.auth-mode-switch {
text-align: center;
margin-top: 24px;
font-size: 16px;
color: #8492a6;
}
.auth-mode-link {
color: #ec3750;
font-weight: bold;
cursor: pointer;
transition: color 0.125s ease-in-out;
text-decoration: underline;
}
.auth-mode-link:hover {
color: #ff8c37;
}
.form-group {
margin-bottom: 24px;
transition: opacity 0.6s ease-in-out, max-height 0.8s ease-in-out, margin 0.8s ease-in-out;
}
.username-field {
max-height: 0;
opacity: 0;
margin-bottom: 0;
overflow: hidden;
}
.username-field.show {
max-height: 200px;
opacity: 1;
margin-bottom: 24px;
}
.form-label {
display: block;
margin-bottom: 8px;
font-size: 16px;
font-weight: bold;
color: #1f2d3d;
}
.form-input {
width: 100%;
padding: 14px 16px;
border: 2px solid #e0e6ed;
border-radius: 8px;
background: #ffffff;
color: #1f2d3d;
font-size: 16px;
font-family: Phantom Sans, sans-serif;
box-sizing: border-box;
transition: border-color 0.125s ease-in-out, box-shadow 0.125s ease-in-out;
}
.form-input:focus {
outline: none;
border-color: #338eda;
box-shadow: 0 0 0 3px rgba(51, 142, 218, 0.1);
}
.form-input::placeholder {
color: #8492a6;
}
.primary-button {
width: 100%;
padding: 16px 32px;
margin-top: 8px;
border: none;
border-radius: 99999px;
background: #ec3750;
color: #ffffff;
font-size: 18px;
font-weight: bold;
font-family: Phantom Sans, sans-serif;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.125);
transition: transform 0.125s ease-in-out, box-shadow 0.125s ease-in-out;
-webkit-tap-highlight-color: transparent;
}
.primary-button:hover:not(:disabled) {
transform: scale(1.0625);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2);
}
.primary-button:active:not(:disabled) {
transform: scale(1);
}
.primary-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.secondary-button {
width: 100%;
padding: 16px 32px;
margin-top: 16px;
background: transparent;
border: 2px solid #ec3750;
border-radius: 99999px;
color: #ec3750;
font-size: 18px;
font-weight: bold;
font-family: Phantom Sans, sans-serif;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
transition: transform 0.125s ease-in-out, box-shadow 0.125s ease-in-out;
-webkit-tap-highlight-color: transparent;
}
.secondary-button:hover {
transform: scale(1.0625);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.125);
}
.secondary-button:active {
transform: scale(1);
}
.error-message {
padding: 12px 16px;
margin-bottom: 16px;
background: rgba(236, 55, 80, 0.1);
border: 2px solid #ec3750;
border-radius: 8px;
color: #ec3750;
font-size: 16px;
}
.success-message {
padding: 12px 16px;
margin-bottom: 16px;
background: rgba(51, 214, 166, 0.1);
border: 2px solid #33d6a6;
border-radius: 8px;
color: #33d6a6;
font-size: 16px;
}
.info-message {
text-align: center;
margin-bottom: 24px;
color: #8492a6;
font-size: 16px;
}
@media (max-width: 768px) {
.auth-container {
flex-direction: column;
}
.auth-panel {
padding: 32px;
}
.auth-form-panel {
order: 1 !important;
min-height: auto;
}
.auth-image-panel {
order: 2 !important;
min-height: 300px;
}
.flag-banner {
width: 128px;
}
.auth-title {
font-size: 28px;
}
}

View file

@ -0,0 +1,503 @@
.dashboard {
min-height: 100vh;
padding: 32px;
background: #f9fafc;
}
.dashboard-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 64px;
padding-bottom: 32px;
border-bottom: 2px solid #e0e6ed;
}
.header-content {
display: flex;
align-items: center;
gap: 16px;
}
.dashboard-logo {
width: 48px;
height: 48px;
}
.dashboard-title {
font-size: 32px;
font-weight: bold;
color: #1f2d3d;
margin: 0;
}
.welcome-text {
margin: 8px 0 0 0;
font-size: 16px;
color: #8492a6;
}
.signout-button {
padding: 12px 24px;
background: transparent;
border: 2px solid #ec3750;
border-radius: 99999px;
color: #ec3750;
font-size: 16px;
font-weight: bold;
font-family: Phantom Sans, sans-serif;
cursor: pointer;
transition: transform 0.125s ease-in-out, box-shadow 0.125s ease-in-out;
-webkit-tap-highlight-color: transparent;
}
.signout-button:hover {
transform: scale(1.0625);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.125);
}
.signout-button:active {
transform: scale(1);
}
.dashboard-content {
max-width: 1200px;
margin: 0 auto;
}
.actions-bar {
display: flex;
gap: 16px;
margin-bottom: 32px;
}
.btn-primary {
padding: 14px 32px;
background: #ec3750;
color: #ffffff;
border: none;
border-radius: 99999px;
font-size: 16px;
font-weight: bold;
font-family: Phantom Sans, sans-serif;
cursor: pointer;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.125);
transition: transform 0.125s ease-in-out, box-shadow 0.125s ease-in-out;
-webkit-tap-highlight-color: transparent;
}
.btn-primary:hover:not(:disabled) {
transform: scale(1.0625);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2);
}
.btn-primary:active:not(:disabled) {
transform: scale(1);
}
.btn-primary:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.btn-secondary {
padding: 14px 32px;
background: transparent;
border: 2px solid #ec3750;
border-radius: 99999px;
color: #ec3750;
font-size: 16px;
font-weight: bold;
font-family: Phantom Sans, sans-serif;
cursor: pointer;
transition: transform 0.125s ease-in-out, box-shadow 0.125s ease-in-out;
-webkit-tap-highlight-color: transparent;
}
.btn-secondary:hover {
transform: scale(1.0625);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.125);
}
.btn-secondary:active {
transform: scale(1);
}
.create-form {
background: #ffffff;
padding: 32px;
border-radius: 8px;
margin-bottom: 32px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.125);
}
.create-form-title {
margin: 0 0 24px 0;
font-size: 24px;
font-weight: bold;
color: #1f2d3d;
}
.form-group {
margin-bottom: 24px;
}
.form-label {
display: block;
margin-bottom: 8px;
font-size: 16px;
font-weight: bold;
color: #1f2d3d;
}
.form-input {
width: 100%;
padding: 14px 16px;
border: 2px solid #e0e6ed;
border-radius: 8px;
background: #ffffff;
color: #1f2d3d;
font-size: 16px;
font-family: Phantom Sans, sans-serif;
box-sizing: border-box;
transition: border-color 0.125s ease-in-out, box-shadow 0.125s ease-in-out;
}
.form-input:focus {
outline: none;
border-color: #338eda;
box-shadow: 0 0 0 3px rgba(51, 142, 218, 0.1);
}
.password-input-wrapper {
position: relative;
width: 100%;
}
.password-input {
padding-right: 48px;
}
.password-toggle {
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
background: transparent;
border: none;
cursor: pointer;
padding: 4px;
display: flex;
align-items: center;
justify-content: center;
transition: opacity 0.125s ease-in-out;
}
.password-toggle:hover {
opacity: 0.7;
}
.password-toggle img {
width: 20px;
height: 20px;
filter: opacity(0.6);
transition: filter 0.125s ease-in-out;
}
.password-toggle:hover img {
filter: opacity(1);
}
.custom-select {
position: relative;
width: 100%;
}
.select-trigger {
width: 100%;
padding: 14px 16px;
padding-right: 48px;
border: 2px solid #e0e6ed;
border-radius: 8px;
background: #ffffff;
color: #1f2d3d;
font-size: 16px;
font-family: Phantom Sans, sans-serif;
font-weight: bold;
cursor: pointer;
box-sizing: border-box;
transition: border-color 0.125s ease-in-out, box-shadow 0.125s ease-in-out;
text-align: left;
}
.select-trigger:hover {
border-color: #338eda;
}
.select-trigger.open {
border-color: #338eda;
box-shadow: 0 0 0 3px rgba(51, 142, 218, 0.1);
}
.select-arrow {
position: absolute;
right: 16px;
top: 50%;
transform: translateY(-50%);
width: 0;
height: 0;
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-top: 6px solid #3c4858;
transition: transform 0.125s ease-in-out;
pointer-events: none;
}
.select-arrow.open {
transform: translateY(-50%) rotate(180deg);
}
.select-dropdown {
position: absolute;
top: calc(100% + 8px);
left: 0;
right: 0;
background: #ffffff;
border: 2px solid #338eda;
border-radius: 8px;
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
z-index: 100;
max-height: 0;
overflow: hidden;
opacity: 0;
transition: max-height 0.25s ease-in-out, opacity 0.25s ease-in-out;
}
.select-dropdown.open {
max-height: 300px;
opacity: 1;
overflow-y: auto;
}
.select-option {
padding: 14px 16px;
cursor: pointer;
transition: background-color 0.125s ease-in-out;
border-bottom: 1px solid #e0e6ed;
}
.select-option:last-child {
border-bottom: none;
}
.select-option:hover {
background: #f9fafc;
}
.select-option.selected {
background: rgba(51, 142, 218, 0.1);
color: #338eda;
font-weight: bold;
}
.option-label {
font-size: 16px;
font-weight: bold;
color: #1f2d3d;
margin-bottom: 4px;
}
.option-description {
font-size: 14px;
color: #8492a6;
}
.error-message {
padding: 12px 16px;
margin-bottom: 16px;
background: rgba(236, 55, 80, 0.1);
border: 2px solid #ec3750;
border-radius: 8px;
color: #ec3750;
font-size: 16px;
}
.error-message.small {
padding: 8px 12px;
font-size: 14px;
}
.section-title {
font-size: 24px;
font-weight: bold;
color: #1f2d3d;
margin: 0 0 24px 0;
}
.empty-state {
text-align: center;
padding: 64px 32px;
color: #8492a6;
font-size: 16px;
}
.spaces-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 24px;
}
.space-card {
background: #ffffff;
padding: 24px;
border-radius: 8px;
border: 2px solid #e0e6ed;
transition: border-color 0.125s ease-in-out, box-shadow 0.125s ease-in-out;
}
.space-card:hover {
border-color: #338eda;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.125);
}
.space-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.space-type {
font-size: 20px;
font-weight: bold;
color: #1f2d3d;
margin: 0;
text-transform: capitalize;
}
.status-badge {
padding: 4px 12px;
border-radius: 99999px;
font-size: 14px;
font-weight: bold;
color: #ffffff;
}
.status-running {
background: #33d6a6;
}
.status-stopped {
background: #ec3750;
}
.status-created {
background: #f1c40f;
}
.status-unknown {
background: #8492a6;
}
.space-info {
margin-bottom: 16px;
font-size: 14px;
}
.space-info p {
margin: 8px 0;
color: #8492a6;
}
.space-info strong {
color: #3c4858;
}
.space-info a {
color: #338eda;
text-decoration: none;
transition: color 0.125s ease-in-out;
}
.space-info a:hover {
color: #ec3750;
text-decoration: underline;
}
.space-actions {
display: flex;
gap: 8px;
}
.action-btn {
flex: 1;
padding: 10px 16px;
border-radius: 99999px;
border: none;
font-size: 14px;
font-weight: bold;
font-family: Phantom Sans, sans-serif;
cursor: pointer;
transition: transform 0.125s ease-in-out;
text-decoration: none;
display: flex;
align-items: center;
justify-content: center;
-webkit-tap-highlight-color: transparent;
}
.action-btn:hover:not(:disabled) {
transform: scale(1.0625);
}
.action-btn:active:not(:disabled) {
transform: scale(1);
}
.action-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.action-btn.start {
background: #33d6a6;
color: #ffffff;
}
.action-btn.stop {
background: #ec3750;
color: #ffffff;
}
.action-btn.open {
background: #338eda;
color: #ffffff;
}
.action-btn.refresh {
background: transparent;
border: 2px solid #338eda;
color: #338eda;
flex: 0 0 auto;
min-width: 44px;
}
@media (max-width: 768px) {
.dashboard {
padding: 16px;
}
.dashboard-header {
flex-direction: column;
align-items: flex-start;
gap: 16px;
}
.actions-bar {
flex-direction: column;
}
.spaces-grid {
grid-template-columns: 1fr;
}
}