mirror of
https://github.com/System-End/spaces.git
synced 2026-04-19 16:38:24 +00:00
feat: add styling to pages :D
This commit is contained in:
parent
5829ccc6f0
commit
33fa885336
8 changed files with 1177 additions and 648 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -4,4 +4,5 @@ dist
|
||||||
build
|
build
|
||||||
.github
|
.github
|
||||||
.vscode
|
.vscode
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
.claude
|
||||||
6
client/package-lock.json
generated
6
client/package-lock.json
generated
|
|
@ -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",
|
||||||
|
|
|
||||||
BIN
client/public/group-photo.jpg
Normal file
BIN
client/public/group-photo.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.6 MiB |
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,23 @@
|
||||||
<script>
|
<script>
|
||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from 'svelte';
|
||||||
import { API_BASE, ERROR_MESSAGES } from '../config.js';
|
import { API_BASE, ERROR_MESSAGES } from '../config.js';
|
||||||
|
|
||||||
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 = '';
|
||||||
message = '';
|
message = '';
|
||||||
loading = true;
|
loading = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE}/users/send`, {
|
const response = await fetch(`${API_BASE}/users/send`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|
@ -25,9 +26,9 @@
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ email }),
|
body: JSON.stringify({ email }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
message = 'Verification code sent to your email!';
|
message = 'Verification code sent to your email!';
|
||||||
mode = 'verify';
|
mode = 'verify';
|
||||||
|
|
@ -40,11 +41,11 @@
|
||||||
loading = false;
|
loading = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleLogin() {
|
async function handleLogin() {
|
||||||
error = '';
|
error = '';
|
||||||
loading = true;
|
loading = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE}/users/login`, {
|
const response = await fetch(`${API_BASE}/users/login`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|
@ -53,9 +54,9 @@
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ email, verificationCode: parseInt(verificationCode) }),
|
body: JSON.stringify({ email, verificationCode: parseInt(verificationCode) }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
dispatch('authenticated', {
|
dispatch('authenticated', {
|
||||||
authorization: data.data.authorization,
|
authorization: data.data.authorization,
|
||||||
|
|
@ -71,11 +72,11 @@
|
||||||
loading = false;
|
loading = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleSignup() {
|
async function handleSignup() {
|
||||||
error = '';
|
error = '';
|
||||||
loading = true;
|
loading = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE}/users/signup`, {
|
const response = await fetch(`${API_BASE}/users/signup`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|
@ -84,9 +85,9 @@
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ email, username, verificationCode: parseInt(verificationCode) }),
|
body: JSON.stringify({ email, username, verificationCode: parseInt(verificationCode) }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
dispatch('authenticated', {
|
dispatch('authenticated', {
|
||||||
authorization: data.data.authorization,
|
authorization: data.data.authorization,
|
||||||
|
|
@ -102,232 +103,122 @@
|
||||||
loading = false;
|
loading = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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-message">{error}</div>
|
||||||
<div class="error">{error}</div>
|
{/if}
|
||||||
{/if}
|
|
||||||
|
{#if message}
|
||||||
{#if message}
|
<div class="success-message">{message}</div>
|
||||||
<div class="success">{message}</div>
|
{/if}
|
||||||
{/if}
|
|
||||||
|
<button class="primary-button" type="submit" disabled={loading || !email}>
|
||||||
<button type="submit" disabled={loading || !email}>
|
{loading ? 'Sending...' : 'Send Verification Code'}
|
||||||
{loading ? 'Sending...' : 'Send Verification Code'}
|
</button>
|
||||||
</button>
|
</form>
|
||||||
</form>
|
|
||||||
{:else if mode === 'verify'}
|
<div class="auth-mode-switch">
|
||||||
<p class="info">Check your email for the verification code</p>
|
{#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>
|
||||||
<form on:submit|preventDefault={mode === 'login' ? handleLogin : handleSignup}>
|
{:else}
|
||||||
<div class="form-group">
|
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>
|
||||||
<label for="code">Verification Code</label>
|
{/if}
|
||||||
<input
|
|
||||||
id="code"
|
|
||||||
type="text"
|
|
||||||
bind:value={verificationCode}
|
|
||||||
required
|
|
||||||
placeholder="Enter code from email"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
{:else if mode === 'verify'}
|
||||||
{#if error}
|
<p class="info-message">Check your email for the verification code</p>
|
||||||
<div class="error">{error}</div>
|
|
||||||
{/if}
|
<form on:submit|preventDefault={mode === 'login' ? handleLogin : handleSignup}>
|
||||||
|
<div class="form-group">
|
||||||
<button type="submit" disabled={loading || !verificationCode}>
|
<label class="form-label" for="code">Verification Code</label>
|
||||||
{loading ? 'Verifying...' : mode === 'signup' ? 'Complete Sign Up' : 'Login'}
|
<input
|
||||||
</button>
|
class="form-input"
|
||||||
|
id="code"
|
||||||
<button type="button" class="secondary" on:click={() => switchMode(mode)}>
|
type="text"
|
||||||
Resend Code
|
bind:value={verificationCode}
|
||||||
</button>
|
required
|
||||||
</form>
|
placeholder="Enter code from email"
|
||||||
{/if}
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if error}
|
||||||
|
<div class="error-message">{error}</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<button class="primary-button" type="submit" disabled={loading || !verificationCode}>
|
||||||
|
{loading ? 'Verifying...' : mode === 'signup' ? 'Complete Sign Up' : 'Login'}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<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>
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
<script>
|
<script>
|
||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from 'svelte';
|
||||||
import { API_BASE, ERROR_MESSAGES } from '../config.js';
|
import { API_BASE, ERROR_MESSAGES } from '../config.js';
|
||||||
|
|
||||||
export let spaces = [];
|
export let spaces = [];
|
||||||
export let authorization = '';
|
export let authorization = '';
|
||||||
export let username = '';
|
export let username = '';
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
let showCreateForm = false;
|
let showCreateForm = false;
|
||||||
let newSpaceType = 'code-server';
|
let newSpaceType = 'code-server';
|
||||||
let newSpacePassword = '';
|
let newSpacePassword = '';
|
||||||
|
|
@ -15,17 +15,19 @@
|
||||||
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' },
|
||||||
{ value: 'blender', label: 'Blender 3D', description: '3D modeling and animation' },
|
{ value: 'blender', label: 'Blender 3D', description: '3D modeling and animation' },
|
||||||
{ value: 'kicad', label: 'KiCad', description: 'PCB design software' }
|
{ value: 'kicad', label: 'KiCad', description: 'PCB design software' }
|
||||||
];
|
];
|
||||||
|
|
||||||
async function createSpace() {
|
async function createSpace() {
|
||||||
error = '';
|
error = '';
|
||||||
loading = true;
|
loading = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE}/spaces/create`, {
|
const response = await fetch(`${API_BASE}/spaces/create`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|
@ -33,14 +35,14 @@
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Authorization': authorization,
|
'Authorization': authorization,
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
password: newSpacePassword,
|
password: newSpacePassword,
|
||||||
type: newSpaceType
|
type: newSpaceType
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
showCreateForm = false;
|
showCreateForm = false;
|
||||||
newSpacePassword = '';
|
newSpacePassword = '';
|
||||||
|
|
@ -54,7 +56,7 @@
|
||||||
loading = false;
|
loading = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadSpaces() {
|
async function loadSpaces() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE}/spaces/list`, {
|
const response = await fetch(`${API_BASE}/spaces/list`, {
|
||||||
|
|
@ -62,9 +64,9 @@
|
||||||
'Authorization': authorization,
|
'Authorization': authorization,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
spaces = data.spaces;
|
spaces = data.spaces;
|
||||||
}
|
}
|
||||||
|
|
@ -72,16 +74,15 @@
|
||||||
console.error('Failed to load spaces:', err);
|
console.error('Failed to load spaces:', err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function startSpace(spaceId) {
|
async function startSpace(spaceId) {
|
||||||
actionLoading[spaceId] = 'starting';
|
actionLoading[spaceId] = 'starting';
|
||||||
actionError[spaceId] = '';
|
actionError[spaceId] = '';
|
||||||
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 {
|
||||||
const response = await fetch(`${API_BASE}/spaces/start/${spaceId}`, {
|
const response = await fetch(`${API_BASE}/spaces/start/${spaceId}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|
@ -89,9 +90,9 @@
|
||||||
'Authorization': authorization,
|
'Authorization': authorization,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
await loadSpaces();
|
await loadSpaces();
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -104,19 +105,19 @@
|
||||||
} finally {
|
} finally {
|
||||||
delete actionLoading[spaceId];
|
delete actionLoading[spaceId];
|
||||||
actionLoading = actionLoading;
|
actionLoading = actionLoading;
|
||||||
|
|
||||||
if (space && space.access_url) {
|
if (space && space.access_url) {
|
||||||
window.location.href = space.access_url;
|
window.location.href = space.access_url;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function stopSpace(spaceId) {
|
async function stopSpace(spaceId) {
|
||||||
actionLoading[spaceId] = 'stopping';
|
actionLoading[spaceId] = 'stopping';
|
||||||
actionError[spaceId] = '';
|
actionError[spaceId] = '';
|
||||||
actionLoading = actionLoading;
|
actionLoading = actionLoading;
|
||||||
actionError = actionError;
|
actionError = actionError;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE}/spaces/stop/${spaceId}`, {
|
const response = await fetch(`${API_BASE}/spaces/stop/${spaceId}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|
@ -124,9 +125,9 @@
|
||||||
'Authorization': authorization,
|
'Authorization': authorization,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
await loadSpaces();
|
await loadSpaces();
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -141,22 +142,22 @@
|
||||||
actionLoading = actionLoading;
|
actionLoading = actionLoading;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshStatus(spaceId) {
|
async function refreshStatus(spaceId) {
|
||||||
actionLoading[spaceId] = 'refreshing';
|
actionLoading[spaceId] = 'refreshing';
|
||||||
actionError[spaceId] = '';
|
actionError[spaceId] = '';
|
||||||
actionLoading = actionLoading;
|
actionLoading = actionLoading;
|
||||||
actionError = actionError;
|
actionError = actionError;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE}/spaces/status/${spaceId}`, {
|
const response = await fetch(`${API_BASE}/spaces/status/${spaceId}`, {
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': authorization,
|
'Authorization': authorization,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
await loadSpaces();
|
await loadSpaces();
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -171,83 +172,155 @@
|
||||||
actionLoading = actionLoading;
|
actionLoading = actionLoading;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSignOut() {
|
function handleSignOut() {
|
||||||
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}
|
||||||
>
|
>
|
||||||
{loading ? 'Creating...' : 'Create Space'}
|
{loading ? 'Creating...' : 'Create Space'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{/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">
|
||||||
<p>No spaces yet. Create your first space to get started!</p>
|
<p>No spaces yet. Create your first space to get started!</p>
|
||||||
|
|
@ -257,19 +330,16 @@
|
||||||
{#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>
|
||||||
|
|
||||||
<div class="space-info">
|
<div class="space-info">
|
||||||
<p><strong>Space ID:</strong> {space.id}</p>
|
<p><strong>Space ID:</strong> {space.id}</p>
|
||||||
{#if space.url}
|
{#if space.url}
|
||||||
<p><strong>URL:</strong>
|
<p><strong>URL:</strong>
|
||||||
<a href={space.url} target="_blank" rel="noopener noreferrer">
|
<a href={space.url} target="_blank" rel="noopener noreferrer">
|
||||||
{space.url}
|
{space.url}
|
||||||
</a>
|
</a>
|
||||||
|
|
@ -277,11 +347,11 @@
|
||||||
{/if}
|
{/if}
|
||||||
<p><strong>Created:</strong> {new Date(space.created_at).toLocaleString()}</p>
|
<p><strong>Created:</strong> {new Date(space.created_at).toLocaleString()}</p>
|
||||||
</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">
|
||||||
{#if actionLoading[space.id]}
|
{#if actionLoading[space.id]}
|
||||||
<button disabled class="action-btn">
|
<button disabled class="action-btn">
|
||||||
|
|
@ -289,16 +359,16 @@
|
||||||
</button>
|
</button>
|
||||||
{:else}
|
{:else}
|
||||||
{#if space.status?.toLowerCase() === 'running'}
|
{#if space.status?.toLowerCase() === 'running'}
|
||||||
<button
|
<button
|
||||||
class="action-btn stop"
|
class="action-btn stop"
|
||||||
on:click={() => stopSpace(space.id)}
|
on:click={() => stopSpace(space.id)}
|
||||||
>
|
>
|
||||||
Stop
|
Stop
|
||||||
</button>
|
</button>
|
||||||
{#if space.url}
|
{#if space.url}
|
||||||
<a
|
<a
|
||||||
href={space.url}
|
href={space.url}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
class="action-btn open"
|
class="action-btn open"
|
||||||
>
|
>
|
||||||
|
|
@ -306,15 +376,15 @@
|
||||||
</a>
|
</a>
|
||||||
{/if}
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
<button
|
<button
|
||||||
class="action-btn start"
|
class="action-btn start"
|
||||||
on:click={() => startSpace(space.id)}
|
on:click={() => startSpace(space.id)}
|
||||||
>
|
>
|
||||||
Start
|
Start
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
<button
|
<button
|
||||||
class="action-btn refresh"
|
class="action-btn refresh"
|
||||||
on:click={() => refreshStatus(space.id)}
|
on:click={() => refreshStatus(space.id)}
|
||||||
>
|
>
|
||||||
↻
|
↻
|
||||||
|
|
@ -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
322
client/src/styles/auth.css
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
503
client/src/styles/dashboard.css
Normal file
503
client/src/styles/dashboard.css
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue