Skip to content

Commit 0d98d48

Browse files
committed
even more SSR + style tweaks + full logo
1 parent 54c9d2d commit 0d98d48

File tree

10 files changed

+266
-50
lines changed

10 files changed

+266
-50
lines changed

frontend/src/app.d.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,26 @@
22
// for information about these interfaces
33
declare global {
44
namespace App {
5+
interface AuthUser {
6+
id: number;
7+
github_id: number;
8+
name: string | null;
9+
avatar_url: string | null;
10+
is_admin: boolean;
11+
}
12+
13+
interface Locals {
14+
auth: {
15+
isAuthenticated: boolean;
16+
sessionId: string | null;
17+
user: AuthUser | null;
18+
};
19+
}
20+
21+
interface PageData {
22+
auth: Locals['auth'];
23+
}
524
// interface Error {}
6-
// interface Locals {}
7-
// interface PageData {}
825
// interface PageState {}
926
// interface Platform {}
1027
}

frontend/src/hooks.server.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import type { Handle } from '@sveltejs/kit';
2+
import { PUBLIC_BACKEND_API_URL } from '$env/static/public';
3+
4+
const BACKEND_API_URL = PUBLIC_BACKEND_API_URL || 'http://localhost:3000';
5+
6+
interface RawVerifyResponse {
7+
valid: boolean;
8+
user?: {
9+
id: number;
10+
github_id: number;
11+
username?: string | null;
12+
name?: string | null;
13+
avatar_url: string | null;
14+
is_admin: boolean;
15+
};
16+
}
17+
18+
const DEFAULT_AUTH_STATE: App.Locals['auth'] = {
19+
isAuthenticated: false,
20+
sessionId: null,
21+
user: null
22+
};
23+
24+
export const handle: Handle = async ({ event, resolve }) => {
25+
const sessionId = event.cookies.get('rustytime_session');
26+
27+
event.locals.auth = {
28+
...DEFAULT_AUTH_STATE
29+
};
30+
31+
if (sessionId) {
32+
try {
33+
const response = await event.fetch(
34+
`${BACKEND_API_URL}/auth/github/verify?session_id=${sessionId}`
35+
);
36+
37+
if (response.ok) {
38+
const data = (await response.json()) as RawVerifyResponse;
39+
40+
if (data.valid && data.user) {
41+
const name = data.user.username ?? data.user.name ?? null;
42+
43+
event.locals.auth = {
44+
isAuthenticated: true,
45+
sessionId,
46+
user: {
47+
id: data.user.id,
48+
github_id: data.user.github_id,
49+
name,
50+
avatar_url: data.user.avatar_url,
51+
is_admin: data.user.is_admin
52+
}
53+
};
54+
}
55+
} // else if (response.status === 401 || response.status === 403) {
56+
// // ignore if unauthorized
57+
// }
58+
} catch (error) {
59+
console.error('SSR auth verification failed', error);
60+
}
61+
}
62+
63+
return resolve(event);
64+
};
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<script lang="ts">
2+
export let size: number | string | null = null;
3+
export let title: string | undefined = 'rustytime logo';
4+
export let className = '';
5+
export let color: string | undefined = undefined;
6+
7+
const dimension = size ?? undefined;
8+
const computedStyle = color ? `color: ${color};` : undefined;
9+
</script>
10+
11+
<svg
12+
xmlns="http://www.w3.org/2000/svg"
13+
viewBox="0 0 512 512"
14+
width={dimension}
15+
height={dimension}
16+
class={className}
17+
style={computedStyle}
18+
role={title ? 'img' : undefined}
19+
aria-label={title}
20+
aria-hidden={title ? undefined : true}
21+
>
22+
{#if title}
23+
<title>{title}</title>
24+
{/if}
25+
<path
26+
fill="currentColor"
27+
d="M237.382 0a4.644 4.644 0 0 0-4.655 4.655v31.481a221.09 221.09 0 0 0-43.054 8.957L177.118 16.22a4.644 4.644 0 0 0-6.125-2.41l-34.148 14.847a4.644 4.644 0 0 0-2.41 6.125l12.551 28.87a221.09 221.09 0 0 0-29.995 20.425L94.727 61.816a4.644 4.644 0 0 0-6.582 0l-26.33 26.33a4.644 4.644 0 0 0 0 6.581l22.262 22.262A221.09 221.09 0 0 0 61.78 150.36l-29.087-12.047a4.644 4.644 0 0 0-6.082 2.518l-14.25 34.402a4.645 4.645 0 0 0 2.52 6.082l29.087 12.048a221.09 221.09 0 0 0-7.832 39.363H4.655A4.644 4.644 0 0 0 0 237.382v37.236a4.644 4.644 0 0 0 4.655 4.655h31.481a221.09 221.09 0 0 0 7.832 39.363l-29.086 12.048a4.645 4.645 0 0 0-2.52 6.082l14.25 34.402a4.644 4.644 0 0 0 6.081 2.518L61.78 361.64a221.09 221.09 0 0 0 22.297 33.372l-22.261 22.262a4.644 4.644 0 0 0 0 6.582l26.33 26.33a4.644 4.644 0 0 0 6.581 0l22.262-22.262a221.09 221.09 0 0 0 29.997 20.425l-12.552 28.87a4.644 4.644 0 0 0 2.411 6.125l34.148 14.848a4.644 4.644 0 0 0 6.125-2.411l12.555-28.873a221.09 221.09 0 0 0 43.054 8.957v31.481a4.644 4.644 0 0 0 4.655 4.655h37.236a4.644 4.644 0 0 0 4.655-4.655v-31.481a221.09 221.09 0 0 0 43.054-8.957l12.555 28.873a4.644 4.644 0 0 0 6.125 2.41l34.148-14.847a4.644 4.644 0 0 0 2.41-6.125l-12.554-28.87a221.09 221.09 0 0 0 29.998-20.425l22.264 22.261a4.644 4.644 0 0 0 6.582 0l26.33-26.33a4.644 4.644 0 0 0 0-6.581l-22.262-22.264a221.09 221.09 0 0 0 22.297-33.37l29.087 12.047a4.644 4.644 0 0 0 6.082-2.518l14.25-34.402a4.645 4.645 0 0 0-2.52-6.082l-29.087-12.048a221.09 221.09 0 0 0 7.832-39.363h31.481a4.644 4.644 0 0 0 4.655-4.655v-37.236a4.644 4.644 0 0 0-4.655-4.655h-31.481a221.09 221.09 0 0 0-7.832-39.363l29.086-12.048a4.645 4.645 0 0 0 2.52-6.082l-14.25-34.402a4.644 4.644 0 0 0-6.081-2.518L450.22 150.36a221.09 221.09 0 0 0-22.297-33.372l22.261-22.262a4.644 4.644 0 0 0 0-6.582l-26.33-26.33a4.644 4.644 0 0 0-6.581 0l-22.264 22.262a221.09 221.09 0 0 0-29.995-20.425l12.552-28.87a4.644 4.644 0 0 0-2.411-6.125l-34.148-14.848a4.644 4.644 0 0 0-6.125 2.411l-12.555 28.873a221.09 221.09 0 0 0-43.054-8.957V4.655A4.644 4.644 0 0 0 274.618 0zM256 69.818A186.182 186.182 0 0 1 442.182 256 186.182 186.182 0 0 1 256 442.182 186.182 186.182 0 0 1 69.818 256 186.182 186.182 0 0 1 256 69.818z"
28+
/>
29+
<circle cx="256" cy="256" r="34.909" fill="currentColor" />
30+
<rect
31+
width="11.636"
32+
height="162.611"
33+
x="250.182"
34+
y="256"
35+
rx="4.655"
36+
ry="1.91"
37+
fill="currentColor"
38+
/>
39+
<rect
40+
width="33.307"
41+
height="132.325"
42+
x="-16.916"
43+
y="237.834"
44+
rx="4.441"
45+
ry="1.554"
46+
transform="scale(1.00375 .99624) rotate(-45)"
47+
fill="currentColor"
48+
/>
49+
<rect
50+
width="23.273"
51+
height="138.69"
52+
x="-374.317"
53+
y="9.645"
54+
rx="4.655"
55+
ry="1.629"
56+
transform="rotate(-135)"
57+
fill="currentColor"
58+
/>
59+
</svg>

frontend/src/lib/components/SideBar.svelte

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import LucideLogOut from '~icons/lucide/log-out';
1313
import LucideChevronsRight from '~icons/lucide/chevrons-right';
1414
import { onMount } from 'svelte';
15+
import UserTag from '$lib/components/UserTag.svelte';
1516
1617
let currentTheme: 'light' | 'dark' = 'light';
1718
let collapsed: boolean = false;
@@ -85,14 +86,7 @@
8586
{/if}
8687
<div class={collapsed ? 'hidden' : ''}>
8788
<div class="flex flex-row items-center gap-1 align-middle">
88-
<span
89-
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full {$auth.user
90-
.is_admin
91-
? 'bg-ctp-red-400 text-ctp-crust'
92-
: 'bg-ctp-overlay2 text-ctp-crust'} items-center h-6"
93-
>
94-
{$auth.user.is_admin ? 'Admin' : 'User'}
95-
</span>
89+
<UserTag is_admin={$auth.user.is_admin} />
9690
</div>
9791
<h2 class="{getNameSizeClass($auth.user.name)} text-subtext1 font-bold">
9892
{$auth.user.name || 'User'}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<script lang="ts">
2+
export let is_admin;
3+
</script>
4+
5+
<span
6+
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full {is_admin
7+
? 'bg-ctp-red-400 text-ctp-crust'
8+
: 'bg-ctp-overlay2 text-ctp-crust'} items-center h-6"
9+
>
10+
{is_admin ? 'Admin' : 'User'}
11+
</span>
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import type { ServerLoadEvent } from '@sveltejs/kit';
2+
3+
export const load = async ({ locals }: ServerLoadEvent) => {
4+
return {
5+
auth: locals.auth
6+
} satisfies App.PageData;
7+
};

frontend/src/routes/+layout.svelte

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,61 @@
22
import '../app.css';
33
import favicon from '$lib/assets/rustytime.svg';
44
import { page } from '$app/state';
5+
import { browser } from '$app/environment';
6+
import { auth } from '$lib/stores/auth';
57
import AuthErrorWarning from '$lib/components/AuthErrorWarning.svelte';
68
import SideBar from '$lib/components/SideBar.svelte';
79
8-
let { children } = $props();
10+
const props = $props();
11+
let { children, data } = props;
12+
13+
type AuthData = App.PageData['auth'];
14+
const DEFAULT_AUTH: AuthData = {
15+
isAuthenticated: false,
16+
sessionId: null,
17+
user: null
18+
};
19+
20+
let lastAuthSnapshot = '';
21+
22+
const applyAuthState = (authData: AuthData) => {
23+
auth.set({
24+
user: authData.user,
25+
sessionId: authData.sessionId,
26+
isAuthenticated: authData.isAuthenticated,
27+
isLoading: false,
28+
error: null
29+
});
30+
};
31+
32+
const syncClientStorage = (authData: AuthData) => {
33+
if (!browser) return;
34+
35+
if (authData.sessionId) {
36+
localStorage.setItem('rustytime_session_id', authData.sessionId);
37+
} else {
38+
localStorage.removeItem('rustytime_session_id');
39+
}
40+
};
41+
42+
const hydrateAuth = (incoming: AuthData | undefined) => {
43+
const authData = incoming ?? DEFAULT_AUTH;
44+
const serialized = JSON.stringify(authData);
45+
46+
if (serialized === lastAuthSnapshot) {
47+
return;
48+
}
49+
50+
lastAuthSnapshot = serialized;
51+
applyAuthState(authData);
52+
syncClientStorage(authData);
53+
};
54+
55+
hydrateAuth(data?.auth);
56+
57+
$effect(() => {
58+
hydrateAuth(data?.auth);
59+
});
960
</script>
1061

1162
<svelte:head>

frontend/src/routes/+page.svelte

Lines changed: 46 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,42 @@
33
import { resolve } from '$app/paths';
44
import { page } from '$app/state';
55
import { browser } from '$app/environment';
6+
import { onMount } from 'svelte';
7+
import Logo from '$lib/components/Logo.svelte';
8+
import UserTag from '$lib/components/UserTag.svelte';
69
710
import LucideGithub from '~icons/lucide/github';
811
12+
const props = $props();
13+
let { data } = props;
14+
15+
type AuthSnapshot = App.PageData['auth'];
16+
const DEFAULT_AUTH: AuthSnapshot = {
17+
isAuthenticated: false,
18+
sessionId: null,
19+
user: null
20+
};
21+
22+
let authState: AuthSnapshot = $state(data?.auth ?? DEFAULT_AUTH);
23+
24+
$effect(() => {
25+
if (data?.auth) {
26+
authState = data.auth;
27+
}
28+
});
29+
30+
onMount(() => {
31+
const unsubscribe = auth.subscribe((state) => {
32+
authState = {
33+
isAuthenticated: state.isAuthenticated,
34+
sessionId: state.sessionId,
35+
user: state.user
36+
};
37+
});
38+
39+
return () => unsubscribe();
40+
});
41+
942
// Handle url changes
1043
$effect(() => {
1144
if (browser) {
@@ -42,9 +75,12 @@
4275
}
4376
</script>
4477

45-
<div class="min-h-screen p-8 bg-mantle">
78+
<div class="bg-mantle">
4679
<!-- Header -->
4780
<header class="text-center mb-4 mt-[10vh]">
81+
<Logo
82+
className="w-32 h-32 mx-auto mb-4 text-ctp-subtext0 dark:text-ctp-lavender-300 drop-shadow-[0_10px_30px_rgba(108,111,133,0.35)] dark:drop-shadow-[0_10px_30px_rgba(198,160,246,0.35)] transition-colors"
83+
/>
4884
<div class="flex text-text items-center justify-center gap-3 mb-4">
4985
<h1 class="text-5xl font-bold">rustytime</h1>
5086
</div>
@@ -53,53 +89,40 @@
5389

5490
<!-- Main Content -->
5591
<div class="rounded-xl p-8 mb-12">
56-
{#if $auth.isLoading}
57-
<!-- Loading State -->
58-
<div class="text-center">
59-
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-ctp-text mx-auto"></div>
60-
<p class="mt-4 text-subtext0">Loading...</p>
61-
</div>
62-
{:else if $auth.isAuthenticated && $auth.user}
92+
{#if authState.isAuthenticated && authState.user}
6393
<!-- Authenticated User -->
6494
<div class="text-center">
6595
<div class="flex items-center justify-center gap-4 mb-6">
66-
{#if $auth.user.avatar_url}
96+
{#if authState.user.avatar_url}
6797
<img
68-
src={$auth.user.avatar_url}
98+
src={authState.user.avatar_url}
6999
alt="Profile"
70100
class="w-16 h-16 rounded-full border-2 border-ctp-green-500"
71101
/>
72102
{/if}
73103
<div>
74104
<h2 class="text-2xl text-subtext1 font-bold">
75-
Welcome, {$auth.user.name || 'User'}!
105+
Welcome, {authState.user.name || 'User'}!
76106
</h2>
77107
<div class="flex flex-row items-center gap-1 align-middle">
78-
<span
79-
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full {$auth.user
80-
.is_admin
81-
? 'bg-ctp-red-400 text-ctp-crust'
82-
: 'bg-ctp-overlay2 text-ctp-crust'} items-center h-6"
83-
>
84-
{$auth.user.is_admin ? 'Admin' : 'User'}
85-
</span>
86-
<p class="text-subtext0">User ID: {$auth.user.id}</p>
108+
<UserTag is_admin={authState.user.is_admin} />
109+
<p class="text-subtext0">User ID: {authState.user.id}</p>
87110
</div>
88111
</div>
89112
</div>
90113

91114
<div class="space-y-4">
92115
<a
93116
href={resolve('/dashboard')}
94-
class="inline-block bg-ctp-peach-500 hover:bg-ctp-peach-600 text-ctp-base font-semibold py-3 px-6 rounded-lg"
117+
class="inline-block bg-ctp-mauve-400 hover:bg-ctp-mauve-500 text-ctp-base font-semibold py-3 px-6 rounded-lg"
95118
>
96119
Go to Dashboard
97120
</a>
98121

99-
{#if $auth.user.is_admin}
122+
{#if authState.user.is_admin}
100123
<a
101124
href={resolve('/admin')}
102-
class="inline-block bg-ctp-red-600 hover:bg-ctp-red-700 text-ctp-base font-semibold py-3 px-6 rounded-lg ml-4"
125+
class="inline-block bg-ctp-red-400 hover:bg-ctp-red-500 text-ctp-base font-semibold py-3 px-6 rounded-lg ml-4"
103126
>
104127
Admin Panel
105128
</a>

0 commit comments

Comments
 (0)