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
.vscode
.DS_Store
.claude

View file

@ -637,7 +637,6 @@
"resolved": "https://registry.npmjs.org/svelte/-/svelte-3.53.1.tgz",
"integrity": "sha512-Q4/hHkktZogGhN5iqxqSi9sjEVoe/NbIxX4hXEHoasTxj+TxEQVAq66LnDMdAZxjmsodkoI5F3slqsS68U7FNw==",
"dev": true,
"peer": true,
"engines": {
"node": ">= 8"
}
@ -659,7 +658,6 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-3.2.3.tgz",
"integrity": "sha512-h8jl1TZ76eGs3o2dIBSsvXDLb1m/Ec1iej8ZMdz+PsaFUsftZeWe2CZOI3qogEsMNaywc17gu0q6cQDzh/weCQ==",
"dev": true,
"peer": true,
"dependencies": {
"esbuild": "^0.15.9",
"postcss": "^8.4.18",
@ -1041,8 +1039,7 @@
"version": "3.53.1",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-3.53.1.tgz",
"integrity": "sha512-Q4/hHkktZogGhN5iqxqSi9sjEVoe/NbIxX4hXEHoasTxj+TxEQVAq66LnDMdAZxjmsodkoI5F3slqsS68U7FNw==",
"dev": true,
"peer": true
"dev": true
},
"svelte-hmr": {
"version": "0.15.0",
@ -1056,7 +1053,6 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-3.2.3.tgz",
"integrity": "sha512-h8jl1TZ76eGs3o2dIBSsvXDLb1m/Ec1iej8ZMdz+PsaFUsftZeWe2CZOI3qogEsMNaywc17gu0q6cQDzh/weCQ==",
"dev": true,
"peer": true,
"requires": {
"esbuild": "^0.15.9",
"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 {
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;
line-height: 24px;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
color: var(--black);
background-color: var(--snow);
font-synthesis: none;
text-rendering: optimizeLegibility;
@ -15,67 +59,40 @@
-webkit-text-size-adjust: 100%;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
* {
box-sizing: border-box;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
.card {
padding: 2em;
}
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
width: 100%;
min-height: 100vh;
}
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 {
border-radius: 8px;
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;
font-family: 'Phantom Sans', sans-serif;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
input, textarea, select {
font-family: 'Phantom Sans', sans-serif;
}

View file

@ -4,13 +4,14 @@
const dispatch = createEventDispatcher();
let mode = 'login'; // 'login', 'signup', or 'verify'
let mode = 'login';
let email = '';
let username = '';
let verificationCode = '';
let error = '';
let loading = false;
let message = '';
let displayMode = 'login';
async function sendVerificationCode() {
error = '';
@ -104,37 +105,41 @@
}
function switchMode(newMode) {
mode = newMode;
error = '';
message = '';
verificationCode = '';
setTimeout(() => {
mode = newMode;
}, 400);
setTimeout(() => {
displayMode = newMode;
}, 800);
}
</script>
<div class="auth-container">
<div class="auth-card">
<h2>Hack Club Spaces</h2>
<svelte:head>
<link rel="stylesheet" href="/src/styles/auth.css" />
</svelte:head>
{#if mode === 'login' || mode === 'signup'}
<div class="tabs">
<button
class:active={mode === 'login'}
on:click={() => switchMode('login')}
>
Login
</button>
<button
class:active={mode === 'signup'}
on:click={() => switchMode('signup')}
>
Sign Up
</button>
<a href="https://hackclub.com/">
<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-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" />
<h2 class="auth-title">{displayMode === 'signup' ? 'Join Hack Club Spaces' : 'Welcome Back'}</h2>
<p class="auth-subtitle">Lorem ipsum dolor sit amet</p>
</div>
{#if mode === 'login' || mode === 'signup'}
<form on:submit|preventDefault={sendVerificationCode}>
<div class="form-group">
<label for="email">Email</label>
<label class="form-label" for="email">Email</label>
<input
class="form-input"
id="email"
type="email"
bind:value={email}
@ -143,39 +148,47 @@
/>
</div>
{#if mode === 'signup'}
<div class="form-group">
<label for="username">Username</label>
<div class="form-group username-field" class:show={mode === 'signup'}>
<label class="form-label" for="username">Username</label>
<input
class="form-input"
id="username"
type="text"
bind:value={username}
required
required={mode === 'signup'}
maxlength="100"
placeholder="Choose a username"
/>
</div>
{/if}
{#if error}
<div class="error">{error}</div>
<div class="error-message">{error}</div>
{/if}
{#if message}
<div class="success">{message}</div>
<div class="success-message">{message}</div>
{/if}
<button type="submit" disabled={loading || !email}>
<button class="primary-button" type="submit" disabled={loading || !email}>
{loading ? 'Sending...' : 'Send Verification Code'}
</button>
</form>
<div class="auth-mode-switch">
{#if mode === 'login'}
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>
{:else}
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">Check your email for the verification code</p>
<p class="info-message">Check your email for the verification code</p>
<form on:submit|preventDefault={mode === 'login' ? handleLogin : handleSignup}>
<div class="form-group">
<label for="code">Verification Code</label>
<label class="form-label" for="code">Verification Code</label>
<input
class="form-input"
id="code"
type="text"
bind:value={verificationCode}
@ -185,149 +198,27 @@
</div>
{#if error}
<div class="error">{error}</div>
<div class="error-message">{error}</div>
{/if}
<button type="submit" disabled={loading || !verificationCode}>
<button class="primary-button" type="submit" disabled={loading || !verificationCode}>
{loading ? 'Verifying...' : mode === 'signup' ? 'Complete Sign Up' : 'Login'}
</button>
<button type="button" class="secondary" on:click={() => switchMode(mode)}>
<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>
<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 actionLoading = {};
let actionError = {};
let dropdownOpen = false;
let showPassword = false;
const spaceTypes = [
{ value: 'code-server', label: 'VS Code Server', description: 'Web-based code editor' },
@ -79,7 +81,6 @@
actionLoading = actionLoading;
actionError = actionError;
// Find the space to get its access_url
const space = spaces.find(s => s.id === spaceId);
try {
@ -176,67 +177,139 @@
dispatch('signout');
}
function getStatusColor(status) {
function getStatusClass(status) {
switch(status?.toLowerCase()) {
case 'running': return '#22c55e';
case 'stopped': return '#ef4444';
case 'created': return '#eab308';
default: return '#888';
case 'running': return 'status-running';
case 'stopped': return 'status-stopped';
case 'created': return 'status-created';
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>
<svelte:head>
<link rel="stylesheet" href="/src/styles/dashboard.css" />
</svelte:head>
<div class="dashboard">
<header>
<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>
<h1>Hack Club Spaces</h1>
<p class="welcome">Welcome, {username}!</p>
<h1 class="dashboard-title">Hack Club Spaces</h1>
<p class="welcome-text">Welcome, {username}!</p>
</div>
<button class="signout" on:click={handleSignOut}>Sign Out</button>
</div>
<button class="signout-button" on:click={handleSignOut}>Sign Out</button>
</header>
<div class="content">
<div class="dashboard-content">
<div class="actions-bar">
<button class="primary" on:click={() => showCreateForm = !showCreateForm}>
<button class="btn-primary" on:click={() => showCreateForm = !showCreateForm}>
{showCreateForm ? 'Cancel' : '+ Create New Space'}
</button>
<button class="secondary" on:click={loadSpaces}>
<button class="btn-secondary" on:click={loadSpaces}>
Refresh
</button>
</div>
{#if showCreateForm}
<div class="create-form">
<h3>Create New Space</h3>
<h3 class="create-form-title">Create New Space</h3>
<div class="form-group">
<label for="type">Space Type</label>
<select id="type" bind:value={newSpaceType}>
<label class="form-label" for="type">Space Type</label>
<div class="custom-select">
<button
type="button"
class="select-trigger"
class:open={dropdownOpen}
on:click={toggleDropdown}
>
{selectedType.label}
</button>
<div class="select-arrow" class:open={dropdownOpen}></div>
<div class="select-dropdown" class:open={dropdownOpen}>
{#each spaceTypes as spaceType}
<option value={spaceType.value}>
{spaceType.label} - {spaceType.description}
</option>
<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}
</select>
</div>
</div>
</div>
<div class="form-group">
<label for="password">Password</label>
<label class="form-label" for="password">Password</label>
<div class="password-input-wrapper">
{#if showPassword}
<input
class="form-input password-input"
id="password"
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>
{#if error}
<div class="error">{error}</div>
<div class="error-message">{error}</div>
{/if}
<button
class="primary"
class="btn-primary"
on:click={createSpace}
disabled={loading || !newSpacePassword}
>
@ -246,7 +319,7 @@
{/if}
<div class="spaces-list">
<h3>Your Spaces</h3>
<h3 class="section-title">Your Spaces</h3>
{#if spaces.length === 0}
<div class="empty-state">
@ -257,11 +330,8 @@
{#each spaces as space}
<div class="space-card">
<div class="space-header">
<h4>{space.type}</h4>
<span
class="status-badge"
style="background-color: {getStatusColor(space.status)}"
>
<h4 class="space-type">{space.type}</h4>
<span class="status-badge {getStatusClass(space.status)}">
{space.status || 'Unknown'}
</span>
</div>
@ -279,7 +349,7 @@
</div>
{#if actionError[space.id]}
<div class="error small">{actionError[space.id]}</div>
<div class="error-message small">{actionError[space.id]}</div>
{/if}
<div class="space-actions">
@ -328,274 +398,3 @@
</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;
}
}