super-svelte-skeleton 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +17 -0
- package/.github/workflows/ninja_i18n.yml +23 -0
- package/.prettierignore +9 -0
- package/.prettierrc +12 -0
- package/.vscode/extensions.json +5 -0
- package/.vscode/launch.json +15 -0
- package/.vscode/settings.json +30 -0
- package/README.md +237 -0
- package/_gitignore +27 -0
- package/eslint.config.js +40 -0
- package/messages/ar.json +70 -0
- package/messages/en.json +70 -0
- package/messages/es.json +70 -0
- package/package.json +54 -0
- package/project.inlang/settings.json +15 -0
- package/src/app.css +8 -0
- package/src/app.d.ts +20 -0
- package/src/app.html +40 -0
- package/src/auto-imports.d.ts +35 -0
- package/src/hooks.client.ts +17 -0
- package/src/hooks.server.ts +73 -0
- package/src/hooks.ts +15 -0
- package/src/lib/entities/auth/api/endpoints.ts +11 -0
- package/src/lib/entities/auth/api/service.ts +35 -0
- package/src/lib/entities/auth/index.ts +9 -0
- package/src/lib/entities/auth/store.svelte.ts +50 -0
- package/src/lib/entities/auth/types.ts +33 -0
- package/src/lib/entities/user/api/endpoints.ts +6 -0
- package/src/lib/entities/user/api/service.ts +10 -0
- package/src/lib/entities/user/index.ts +2 -0
- package/src/lib/entities/user/types.ts +18 -0
- package/src/lib/features/theme-editor/constants.ts +33 -0
- package/src/lib/features/theme-editor/index.ts +3 -0
- package/src/lib/features/theme-editor/types.ts +10 -0
- package/src/lib/features/theme-editor/ui/CSSOutput.svelte +17 -0
- package/src/lib/features/theme-editor/ui/ColorCard.svelte +66 -0
- package/src/lib/features/theme-editor/ui/ThemeEditorWidget.svelte +319 -0
- package/src/lib/features/theme-editor/ui/ThemePreview.svelte +121 -0
- package/src/lib/features/theme-editor/ui/TypographySettings.svelte +73 -0
- package/src/lib/features/theme-editor/utils.ts +10 -0
- package/src/lib/shared/api/client.ts +47 -0
- package/src/lib/shared/api/index.ts +3 -0
- package/src/lib/shared/api/types.ts +25 -0
- package/src/lib/shared/config/api.ts +1 -0
- package/src/lib/shared/config/index.ts +2 -0
- package/src/lib/shared/config/routes.ts +18 -0
- package/src/lib/shared/i18n/index.ts +1 -0
- package/src/lib/shared/index.ts +2 -0
- package/src/lib/tailwind.config.ts +28 -0
- package/src/lib/widgets/topbar/Topbar.svelte +122 -0
- package/src/lib/widgets/topbar/constants.ts +16 -0
- package/src/lib/widgets/topbar/index.ts +2 -0
- package/src/params/integer.ts +5 -0
- package/src/routes/(app)/(admin)/+layout.server.ts +14 -0
- package/src/routes/(app)/(admin)/admin/+page.svelte +101 -0
- package/src/routes/(app)/+layout.server.ts +9 -0
- package/src/routes/(app)/+layout.svelte +12 -0
- package/src/routes/(app)/settings/+page.svelte +48 -0
- package/src/routes/(app)/theme/+page.svelte +5 -0
- package/src/routes/(auth)/forgot-password/+page.svelte +83 -0
- package/src/routes/(auth)/login/+page.server.ts +66 -0
- package/src/routes/(auth)/login/+page.svelte +156 -0
- package/src/routes/(auth)/logout/+page.server.ts +16 -0
- package/src/routes/(auth)/register/+page.svelte +167 -0
- package/src/routes/(auth)/reset-password/+page.svelte +127 -0
- package/src/routes/+error.svelte +95 -0
- package/src/routes/+layout.svelte +36 -0
- package/src/routes/+layout.ts +24 -0
- package/src/routes/+page.svelte +192 -0
- package/src/routes/+page.ts +3 -0
- package/static/config/config.local.json +3 -0
- package/static/config/config.prod.json +3 -0
- package/static/favicon.svg +1 -0
- package/static/logo.svg +7 -0
- package/static/profile.avif +0 -0
- package/static/smile.jpg +0 -0
- package/static/styles/theme-dark.css +30 -0
- package/static/styles/theme-light.css +28 -0
- package/stats.html +4950 -0
- package/svelte.config.js +78 -0
- package/tsconfig.json +46 -0
- package/vite.config.ts +51 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Actions } from '@sveltejs/kit';
|
|
2
|
+
import { redirect } from '@sveltejs/kit';
|
|
3
|
+
import { AUTH_COOKIE_NAME, LOGIN_ROUTE } from '$shared/config';
|
|
4
|
+
|
|
5
|
+
export const actions: Actions = {
|
|
6
|
+
default: async ({ cookies }) => {
|
|
7
|
+
cookies.delete(AUTH_COOKIE_NAME, { path: '/' });
|
|
8
|
+
throw redirect(302, LOGIN_ROUTE);
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// GET /logout — also works for simple <a href="/logout"> links
|
|
13
|
+
export async function load({ cookies }) {
|
|
14
|
+
cookies.delete(AUTH_COOKIE_NAME, { path: '/' });
|
|
15
|
+
throw redirect(302, LOGIN_ROUTE);
|
|
16
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import * as m from '$lib/paraglide/messages';
|
|
3
|
+
import { Eye, EyeSlash } from 'svelte-bootstrap-icons';
|
|
4
|
+
import { resolve } from '$app/paths';
|
|
5
|
+
|
|
6
|
+
let name = $state('');
|
|
7
|
+
let email = $state('');
|
|
8
|
+
let password = $state('');
|
|
9
|
+
let confirm = $state('');
|
|
10
|
+
let showPassword = $state(false);
|
|
11
|
+
let showConfirm = $state(false);
|
|
12
|
+
|
|
13
|
+
const handleSubmit = (e: Event) => {
|
|
14
|
+
e.preventDefault();
|
|
15
|
+
// TODO: wire up registration logic
|
|
16
|
+
};
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<svelte:head>
|
|
20
|
+
<title>{m.register_title()}</title>
|
|
21
|
+
</svelte:head>
|
|
22
|
+
|
|
23
|
+
<div class="min-h-screen bg-surface-tertiary flex items-center justify-center px-4 py-12">
|
|
24
|
+
|
|
25
|
+
<!-- Ambient blobs -->
|
|
26
|
+
<div class="fixed inset-0 -z-10 pointer-events-none overflow-hidden" aria-hidden="true">
|
|
27
|
+
<div class="absolute w-96 h-96 rounded-full bg-accent/5 -top-32 -right-32 blur-3xl"></div>
|
|
28
|
+
<div class="absolute w-80 h-80 rounded-full bg-warning/5 -bottom-24 -left-24 blur-3xl"></div>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<div class="w-full max-w-md">
|
|
32
|
+
|
|
33
|
+
<!-- Logo -->
|
|
34
|
+
<a href={resolve("/",{})} class="flex items-center justify-center gap-2 mb-8">
|
|
35
|
+
<span class="text-accent text-xl leading-none">✦</span>
|
|
36
|
+
<span class="font-semibold text-content-primary tracking-wide text-lg">{m.app_name()}</span>
|
|
37
|
+
</a>
|
|
38
|
+
|
|
39
|
+
<!-- Card -->
|
|
40
|
+
<div class="bg-surface-primary rounded-2xl shadow-xl border p-8">
|
|
41
|
+
|
|
42
|
+
<!-- Heading -->
|
|
43
|
+
<div class="mb-8 text-center">
|
|
44
|
+
<h1 class="text-2xl font-bold text-content-primary mb-1">{m.register_title()}</h1>
|
|
45
|
+
<p class="text-sm text-content-secondary">{m.register_subtitle()}</p>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<form onsubmit={handleSubmit} class="flex flex-col gap-5">
|
|
49
|
+
|
|
50
|
+
<!-- Full Name -->
|
|
51
|
+
<div class="flex flex-col gap-1.5">
|
|
52
|
+
<label for="name" class="text-sm font-medium text-content-primary">
|
|
53
|
+
{m.register_name_label()}
|
|
54
|
+
</label>
|
|
55
|
+
<input
|
|
56
|
+
id="name"
|
|
57
|
+
type="text"
|
|
58
|
+
bind:value={name}
|
|
59
|
+
placeholder={m.register_name_placeholder()}
|
|
60
|
+
required
|
|
61
|
+
autocomplete="name"
|
|
62
|
+
class="w-full px-4 py-2.5 rounded-lg border bg-surface-secondary text-content-primary placeholder:text-content-tertiary focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent transition"
|
|
63
|
+
/>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<!-- Email -->
|
|
67
|
+
<div class="flex flex-col gap-1.5">
|
|
68
|
+
<label for="email" class="text-sm font-medium text-content-primary">
|
|
69
|
+
{m.register_email_label()}
|
|
70
|
+
</label>
|
|
71
|
+
<input
|
|
72
|
+
id="email"
|
|
73
|
+
type="email"
|
|
74
|
+
bind:value={email}
|
|
75
|
+
placeholder={m.register_email_placeholder()}
|
|
76
|
+
required
|
|
77
|
+
autocomplete="email"
|
|
78
|
+
class="w-full px-4 py-2.5 rounded-lg border bg-surface-secondary text-content-primary placeholder:text-content-tertiary focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent transition"
|
|
79
|
+
/>
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
<!-- Password -->
|
|
83
|
+
<div class="flex flex-col gap-1.5">
|
|
84
|
+
<label for="password" class="text-sm font-medium text-content-primary">
|
|
85
|
+
{m.register_password_label()}
|
|
86
|
+
</label>
|
|
87
|
+
<div class="relative">
|
|
88
|
+
<input
|
|
89
|
+
id="password"
|
|
90
|
+
type={showPassword ? 'text' : 'password'}
|
|
91
|
+
bind:value={password}
|
|
92
|
+
placeholder={m.register_password_placeholder()}
|
|
93
|
+
required
|
|
94
|
+
autocomplete="new-password"
|
|
95
|
+
class="w-full px-4 py-2.5 pr-11 rounded-lg border bg-surface-secondary text-content-primary placeholder:text-content-tertiary focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent transition"
|
|
96
|
+
/>
|
|
97
|
+
<button
|
|
98
|
+
type="button"
|
|
99
|
+
onclick={() => showPassword = !showPassword}
|
|
100
|
+
class="absolute right-3 top-1/2 -translate-y-1/2 text-content-tertiary hover:text-content-primary transition"
|
|
101
|
+
aria-label="Toggle password visibility"
|
|
102
|
+
>
|
|
103
|
+
{#if showPassword}
|
|
104
|
+
<EyeSlash width={16} height={16} />
|
|
105
|
+
{:else}
|
|
106
|
+
<Eye width={16} height={16} />
|
|
107
|
+
{/if}
|
|
108
|
+
</button>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
<!-- Confirm Password -->
|
|
113
|
+
<div class="flex flex-col gap-1.5">
|
|
114
|
+
<label for="confirm" class="text-sm font-medium text-content-primary">
|
|
115
|
+
{m.register_confirm_label()}
|
|
116
|
+
</label>
|
|
117
|
+
<div class="relative">
|
|
118
|
+
<input
|
|
119
|
+
id="confirm"
|
|
120
|
+
type={showConfirm ? 'text' : 'password'}
|
|
121
|
+
bind:value={confirm}
|
|
122
|
+
placeholder={m.register_confirm_placeholder()}
|
|
123
|
+
required
|
|
124
|
+
autocomplete="new-password"
|
|
125
|
+
class="w-full px-4 py-2.5 pr-11 rounded-lg border bg-surface-secondary text-content-primary placeholder:text-content-tertiary focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent transition"
|
|
126
|
+
/>
|
|
127
|
+
<button
|
|
128
|
+
type="button"
|
|
129
|
+
onclick={() => showConfirm = !showConfirm}
|
|
130
|
+
class="absolute right-3 top-1/2 -translate-y-1/2 text-content-tertiary hover:text-content-primary transition"
|
|
131
|
+
aria-label="Toggle confirm password visibility"
|
|
132
|
+
>
|
|
133
|
+
{#if showConfirm}
|
|
134
|
+
<EyeSlash width={16} height={16} />
|
|
135
|
+
{:else}
|
|
136
|
+
<Eye width={16} height={16} />
|
|
137
|
+
{/if}
|
|
138
|
+
</button>
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
|
|
142
|
+
<!-- Submit -->
|
|
143
|
+
<button type="submit" class="btn btn-primary w-full py-3 text-base font-semibold mt-1">
|
|
144
|
+
{m.register_submit()}
|
|
145
|
+
</button>
|
|
146
|
+
|
|
147
|
+
</form>
|
|
148
|
+
|
|
149
|
+
<!-- Divider -->
|
|
150
|
+
<div class="relative my-6">
|
|
151
|
+
<div class="absolute inset-0 flex items-center">
|
|
152
|
+
<div class="w-full border-t border-border-primary"></div>
|
|
153
|
+
</div>
|
|
154
|
+
<div class="relative flex justify-center text-xs">
|
|
155
|
+
<span class="px-3 bg-surface-primary text-content-tertiary">{m.divider_text()}</span>
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
|
|
159
|
+
<!-- Sign in link -->
|
|
160
|
+
<p class="text-center text-sm text-content-secondary">
|
|
161
|
+
{m.register_have_account()}
|
|
162
|
+
<a href={resolve("/login",{})} class="text-accent font-medium hover:underline ml-1">{m.register_sign_in()}</a>
|
|
163
|
+
</p>
|
|
164
|
+
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import * as m from '$lib/paraglide/messages';
|
|
3
|
+
import { page } from '$app/state';
|
|
4
|
+
import { Eye, EyeSlash, ExclamationCircleFill } from 'svelte-bootstrap-icons';
|
|
5
|
+
import { resolve } from '$app/paths';
|
|
6
|
+
|
|
7
|
+
// Token comes from ?token=... query param (emailed link)
|
|
8
|
+
const token = $derived(page.url.searchParams.get('token') ?? '');
|
|
9
|
+
|
|
10
|
+
let password = $state('');
|
|
11
|
+
let confirm = $state('');
|
|
12
|
+
let showPassword = $state(false);
|
|
13
|
+
let showConfirm = $state(false);
|
|
14
|
+
let error = $state('');
|
|
15
|
+
|
|
16
|
+
const handleSubmit = async (e: Event) => {
|
|
17
|
+
e.preventDefault();
|
|
18
|
+
error = '';
|
|
19
|
+
if (!token) {
|
|
20
|
+
error = 'Invalid or missing reset link.';
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (password !== confirm) {
|
|
24
|
+
error = 'Passwords do not match.';
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
// TODO: call authApi.resetPassword({ token, password, confirmPassword: confirm })
|
|
28
|
+
};
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<svelte:head>
|
|
32
|
+
<title>{m.reset_title()}</title>
|
|
33
|
+
</svelte:head>
|
|
34
|
+
|
|
35
|
+
<div class="min-h-screen bg-surface-tertiary flex items-center justify-center px-4 py-12">
|
|
36
|
+
|
|
37
|
+
<div class="fixed inset-0 -z-10 pointer-events-none overflow-hidden" aria-hidden="true">
|
|
38
|
+
<div class="absolute w-80 h-80 rounded-full bg-accent/5 -bottom-24 -left-24 blur-3xl"></div>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<div class="w-full max-w-md">
|
|
42
|
+
|
|
43
|
+
<a href={resolve("/",{})} class="flex items-center justify-center gap-2 mb-8">
|
|
44
|
+
<span class="text-accent text-xl leading-none">✦</span>
|
|
45
|
+
<span class="font-semibold text-content-primary tracking-wide text-lg">{m.app_name()}</span>
|
|
46
|
+
</a>
|
|
47
|
+
|
|
48
|
+
<div class="bg-surface-primary rounded-2xl shadow-xl border p-8">
|
|
49
|
+
|
|
50
|
+
<div class="mb-8 text-center">
|
|
51
|
+
<h1 class="text-2xl font-bold text-content-primary mb-1">{m.reset_title()}</h1>
|
|
52
|
+
<p class="text-sm text-content-secondary">{m.reset_subtitle()}</p>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<form onsubmit={handleSubmit} class="flex flex-col gap-5">
|
|
56
|
+
|
|
57
|
+
<!-- New password -->
|
|
58
|
+
<div class="flex flex-col gap-1.5">
|
|
59
|
+
<label for="password" class="text-sm font-medium text-content-primary">
|
|
60
|
+
{m.reset_new_label()}
|
|
61
|
+
</label>
|
|
62
|
+
<div class="relative">
|
|
63
|
+
<input
|
|
64
|
+
id="password"
|
|
65
|
+
type={showPassword ? 'text' : 'password'}
|
|
66
|
+
bind:value={password}
|
|
67
|
+
placeholder={m.reset_new_placeholder()}
|
|
68
|
+
required
|
|
69
|
+
autocomplete="new-password"
|
|
70
|
+
class="w-full px-4 py-2.5 pr-11 rounded-lg border bg-surface-secondary text-content-primary placeholder:text-content-tertiary focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent transition"
|
|
71
|
+
/>
|
|
72
|
+
<button type="button" onclick={() => showPassword = !showPassword}
|
|
73
|
+
class="absolute right-3 top-1/2 -translate-y-1/2 text-content-tertiary hover:text-content-primary transition"
|
|
74
|
+
aria-label="Toggle visibility">
|
|
75
|
+
{#if showPassword}
|
|
76
|
+
<EyeSlash width={16} height={16} />
|
|
77
|
+
{:else}
|
|
78
|
+
<Eye width={16} height={16} />
|
|
79
|
+
{/if}
|
|
80
|
+
</button>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
<!-- Confirm password -->
|
|
85
|
+
<div class="flex flex-col gap-1.5">
|
|
86
|
+
<label for="confirm" class="text-sm font-medium text-content-primary">
|
|
87
|
+
{m.reset_confirm_label()}
|
|
88
|
+
</label>
|
|
89
|
+
<div class="relative">
|
|
90
|
+
<input
|
|
91
|
+
id="confirm"
|
|
92
|
+
type={showConfirm ? 'text' : 'password'}
|
|
93
|
+
bind:value={confirm}
|
|
94
|
+
placeholder={m.reset_confirm_placeholder()}
|
|
95
|
+
required
|
|
96
|
+
autocomplete="new-password"
|
|
97
|
+
class="w-full px-4 py-2.5 pr-11 rounded-lg border bg-surface-secondary text-content-primary placeholder:text-content-tertiary focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent transition"
|
|
98
|
+
/>
|
|
99
|
+
<button type="button" onclick={() => showConfirm = !showConfirm}
|
|
100
|
+
class="absolute right-3 top-1/2 -translate-y-1/2 text-content-tertiary hover:text-content-primary transition"
|
|
101
|
+
aria-label="Toggle visibility">
|
|
102
|
+
{#if showConfirm}
|
|
103
|
+
<EyeSlash width={16} height={16} />
|
|
104
|
+
{:else}
|
|
105
|
+
<Eye width={16} height={16} />
|
|
106
|
+
{/if}
|
|
107
|
+
</button>
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
<!-- Inline error -->
|
|
112
|
+
{#if error}
|
|
113
|
+
<p class="text-xs text-error flex items-center gap-1.5">
|
|
114
|
+
<ExclamationCircleFill width={12} height={12} />
|
|
115
|
+
{error}
|
|
116
|
+
</p>
|
|
117
|
+
{/if}
|
|
118
|
+
|
|
119
|
+
<button type="submit" class="btn btn-primary w-full py-3 text-base font-semibold mt-1">
|
|
120
|
+
{m.reset_submit()}
|
|
121
|
+
</button>
|
|
122
|
+
|
|
123
|
+
</form>
|
|
124
|
+
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { page } from '$app/state';
|
|
3
|
+
import { HttpStatus } from '$shared/api';
|
|
4
|
+
import { resolve } from '$app/paths';
|
|
5
|
+
import { LightbulbOff } from 'svelte-bootstrap-icons';
|
|
6
|
+
|
|
7
|
+
const status = $derived(page.status);
|
|
8
|
+
const message = $derived(page.error?.message ?? 'An unexpected error occurred');
|
|
9
|
+
|
|
10
|
+
const title = $derived(
|
|
11
|
+
status === HttpStatus.NOT_FOUND
|
|
12
|
+
? "LOOKS LIKE YOU'RE LOST"
|
|
13
|
+
: status === HttpStatus.FORBIDDEN
|
|
14
|
+
? "ACCESS DENIED"
|
|
15
|
+
: status === HttpStatus.INTERNAL_SERVER_ERROR
|
|
16
|
+
? "SOMETHING BROKE"
|
|
17
|
+
: "SOMETHING WENT WRONG"
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
const hint = $derived(
|
|
21
|
+
status === HttpStatus.NOT_FOUND
|
|
22
|
+
? "The page you are looking for is not available!"
|
|
23
|
+
: status === HttpStatus.FORBIDDEN
|
|
24
|
+
? "You don't have permission to view this resource."
|
|
25
|
+
: status === HttpStatus.INTERNAL_SERVER_ERROR
|
|
26
|
+
? "Our servers ran into an issue. Please try again in a moment."
|
|
27
|
+
: message
|
|
28
|
+
);
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<svelte:head>
|
|
32
|
+
<title>Error {status}</title>
|
|
33
|
+
</svelte:head>
|
|
34
|
+
|
|
35
|
+
<main class="flex min-h-screen flex-col bg-primary">
|
|
36
|
+
<!-- Main content -->
|
|
37
|
+
<div class="flex flex-1 items-center justify-center px-8 pb-16">
|
|
38
|
+
<div class="flex flex-col items-center gap-16 md:flex-row md:gap-24">
|
|
39
|
+
|
|
40
|
+
<!-- Left: Icon in circle -->
|
|
41
|
+
<div class="shrink-0">
|
|
42
|
+
<div class="flex h-72 w-72 items-center justify-center rounded-full bg-error">
|
|
43
|
+
<div class="text-error opacity-90 animate-bounce duration-300" >
|
|
44
|
+
<LightbulbOff width={140} height={140} />
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<!-- Right: Text content -->
|
|
50
|
+
<div class="flex flex-col gap-4 items-center ">
|
|
51
|
+
<!-- Big status number -->
|
|
52
|
+
<p
|
|
53
|
+
class="leading-none text-error select-none text-[clamp(6rem,15vw,11rem)] "
|
|
54
|
+
>
|
|
55
|
+
{status}
|
|
56
|
+
</p>
|
|
57
|
+
|
|
58
|
+
<!-- Title -->
|
|
59
|
+
<h1 class="text-lg font-bold uppercase tracking-widest text-content-secondary">
|
|
60
|
+
{title}
|
|
61
|
+
</h1>
|
|
62
|
+
|
|
63
|
+
<!-- Hint -->
|
|
64
|
+
<p class="font-light italic" >
|
|
65
|
+
{hint}
|
|
66
|
+
</p>
|
|
67
|
+
|
|
68
|
+
<!-- CTA -->
|
|
69
|
+
<div class="mt-10 flex items-center gap-8">
|
|
70
|
+
<a
|
|
71
|
+
href={resolve("/", {})}
|
|
72
|
+
class="btn btn-primary"
|
|
73
|
+
>
|
|
74
|
+
GO TO HOME
|
|
75
|
+
<span class="inline-block transition-transform group-hover:translate-x-1">→</span>
|
|
76
|
+
</a>
|
|
77
|
+
|
|
78
|
+
<button
|
|
79
|
+
onclick={() => history.back()}
|
|
80
|
+
class="btn btn-secondary"
|
|
81
|
+
>
|
|
82
|
+
Go back
|
|
83
|
+
</button>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
</main>
|
|
89
|
+
|
|
90
|
+
<style>
|
|
91
|
+
@keyframes float {
|
|
92
|
+
0%, 100% { transform: translateY(0px); }
|
|
93
|
+
50% { transform: translateY(-12px); }
|
|
94
|
+
}
|
|
95
|
+
</style>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import '../app.css';
|
|
3
|
+
import { updated } from '$app/state';
|
|
4
|
+
import { beforeNavigate } from '$app/navigation';
|
|
5
|
+
import { snackStore, loaderStore, configStore } from '@aryagg/ui-kit';
|
|
6
|
+
import { SnackBar, Loader } from '@aryagg/ui-kit';
|
|
7
|
+
import { ESnackType } from '@aryagg/types';
|
|
8
|
+
|
|
9
|
+
let { children, data } = $props();
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
$effect(() => {
|
|
13
|
+
// Set synchronously so children have config available on first render (SSR + CSR)
|
|
14
|
+
if (data.configError) {
|
|
15
|
+
snackStore.show({ type: ESnackType.DANGER, message: data.configError });
|
|
16
|
+
}
|
|
17
|
+
if (data.config) {
|
|
18
|
+
configStore.set(data.config);
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// When a new build is deployed, force a full-page reload on the next navigation
|
|
23
|
+
// so the user always runs the latest code without a manual refresh.
|
|
24
|
+
beforeNavigate(({ willUnload, to }) => {
|
|
25
|
+
if (updated.current && !willUnload && to?.url) {
|
|
26
|
+
location.href = to.url.href;
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
{#if snackStore.current}<SnackBar />{/if}
|
|
32
|
+
{#if loaderStore.isVisible}<Loader />{/if}
|
|
33
|
+
|
|
34
|
+
<div class="h-screen w-screen">
|
|
35
|
+
{@render children()}
|
|
36
|
+
</div>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { PUBLIC_CONFIG_ENV, PUBLIC_BASE_PATH } from "$env/static/public";
|
|
2
|
+
import type { LayoutLoad } from "./$types";
|
|
3
|
+
|
|
4
|
+
export const load: LayoutLoad = async ({ fetch }) => {
|
|
5
|
+
if (!PUBLIC_CONFIG_ENV) {
|
|
6
|
+
console.error("PUBLIC_CONFIG_ENV is not set");
|
|
7
|
+
return { config: null, configError: "Configuration environment missing" };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
const res = await fetch(`${PUBLIC_BASE_PATH}/config/config.${PUBLIC_CONFIG_ENV}.json`);
|
|
12
|
+
|
|
13
|
+
if (!res.ok) {
|
|
14
|
+
throw new Error(`Failed to load config.${PUBLIC_CONFIG_ENV}.json`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const config = await res.json();
|
|
18
|
+
return { config, configError: null };
|
|
19
|
+
|
|
20
|
+
} catch (err) {
|
|
21
|
+
console.error(err);
|
|
22
|
+
return { config: null, configError: "Failed to load configuration" };
|
|
23
|
+
}
|
|
24
|
+
};
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import * as m from '$lib/paraglide/messages';
|
|
3
|
+
import { Topbar } from '$widgets/topbar';
|
|
4
|
+
import {
|
|
5
|
+
ArrowRight,
|
|
6
|
+
LightningCharge,
|
|
7
|
+
ShieldLock,
|
|
8
|
+
Sun,
|
|
9
|
+
Globe2,
|
|
10
|
+
Grid3x3Gap,
|
|
11
|
+
CodeSlash,
|
|
12
|
+
} from 'svelte-bootstrap-icons';
|
|
13
|
+
import { resolve } from '$app/paths';
|
|
14
|
+
|
|
15
|
+
const features = [
|
|
16
|
+
{
|
|
17
|
+
title: 'Authentication',
|
|
18
|
+
desc: 'Login, register, forgot & reset password flows with server-side session management.',
|
|
19
|
+
tag: 'Ready',
|
|
20
|
+
href: resolve('/register', {}),
|
|
21
|
+
icon: ShieldLock,
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
title: 'Theming System',
|
|
25
|
+
desc: 'Light and dark themes using CSS variables with persistent user preference storage.',
|
|
26
|
+
tag: 'Built-in',
|
|
27
|
+
href: null,
|
|
28
|
+
icon: Sun,
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
title: 'Internationalization',
|
|
32
|
+
desc: '8 languages out of the box with Paraglide. RTL-aware layout support included.',
|
|
33
|
+
tag: '8 Languages',
|
|
34
|
+
href: null,
|
|
35
|
+
icon: Globe2,
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
title: 'Component Library',
|
|
39
|
+
desc: 'Over 20 pre-built UI components ready to use and customise across your app.',
|
|
40
|
+
tag: '20+ Items',
|
|
41
|
+
href: null,
|
|
42
|
+
icon: Grid3x3Gap,
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
title: 'TypeScript',
|
|
46
|
+
desc: 'Strict TypeScript throughout — full type safety from routes to components.',
|
|
47
|
+
tag: 'Strict Mode',
|
|
48
|
+
href: null,
|
|
49
|
+
icon: CodeSlash,
|
|
50
|
+
},
|
|
51
|
+
];
|
|
52
|
+
</script>
|
|
53
|
+
|
|
54
|
+
<svelte:head>
|
|
55
|
+
<title>{m.home_title()}</title>
|
|
56
|
+
<meta name="description" content={m.home_description()} />
|
|
57
|
+
</svelte:head>
|
|
58
|
+
|
|
59
|
+
<div class="min-h-screen flex flex-col bg-surface-primary text-primary overflow-x-hidden">
|
|
60
|
+
|
|
61
|
+
<Topbar />
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
<!-- HERO -->
|
|
65
|
+
<section class="flex-1 flex flex-col items-center justify-center px-6 py-20 text-center">
|
|
66
|
+
|
|
67
|
+
<!-- Logo mark -->
|
|
68
|
+
<div class="relative mb-10">
|
|
69
|
+
<div class="absolute inset-0 rounded-full bg-accent/15 blur-3xl scale-[2.5] pointer-events-none"></div>
|
|
70
|
+
<div class="relative w-20 h-20 rounded-3xl bg-accent flex items-center justify-center shadow-2xl rotate-12">
|
|
71
|
+
<span class="-rotate-12 text-on-accent">
|
|
72
|
+
<LightningCharge width={36} height={36} />
|
|
73
|
+
</span>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<!-- Eyebrow badge -->
|
|
78
|
+
<div class="inline-flex items-center gap-2 px-3.5 py-1.5 rounded-full bg-accent/10 border border-accent/20 text-accent text-xs font-semibold tracking-[0.15em] uppercase mb-7">
|
|
79
|
+
<span class="w-1.5 h-1.5 rounded-full bg-accent animate-pulse"></span>
|
|
80
|
+
{m.hero_eyebrow()}
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
<!-- Heading -->
|
|
84
|
+
<h1 class="text-5xl md:text-7xl font-bold leading-[1.06] tracking-tight text-primary mb-5 max-w-2xl">
|
|
85
|
+
{m.home_welcome()}
|
|
86
|
+
</h1>
|
|
87
|
+
|
|
88
|
+
<!-- Subtitle -->
|
|
89
|
+
<p class="text-secondary text-lg md:text-xl leading-relaxed max-w-md mb-10">
|
|
90
|
+
{m.home_subtitle()}
|
|
91
|
+
</p>
|
|
92
|
+
|
|
93
|
+
<!-- CTAs -->
|
|
94
|
+
<div class="flex flex-wrap items-center justify-center gap-3 mb-14">
|
|
95
|
+
<a
|
|
96
|
+
href={resolve('/register', {})}
|
|
97
|
+
class="no-underline inline-flex items-center gap-2 px-7 py-3.5 rounded-xl bg-accent text-on-accent font-semibold text-base transition-all duration-200 shadow-lg hover:opacity-90 hover:-translate-y-0.5"
|
|
98
|
+
>
|
|
99
|
+
{m.home_register_btn()}
|
|
100
|
+
<ArrowRight width={15} height={15} />
|
|
101
|
+
</a>
|
|
102
|
+
<a
|
|
103
|
+
href={resolve('/login', {})}
|
|
104
|
+
class="no-underline inline-flex items-center gap-2 px-7 py-3.5 rounded-xl border border-accent/40 text-accent hover:bg-accent/10 font-semibold text-base transition-all duration-200 hover:-translate-y-0.5"
|
|
105
|
+
>
|
|
106
|
+
{m.home_login_btn()}
|
|
107
|
+
</a>
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
<!-- Stats -->
|
|
111
|
+
<div class="flex items-center gap-6 sm:gap-10">
|
|
112
|
+
<div>
|
|
113
|
+
<p class="text-2xl font-bold text-primary">{m.hero_stat1_value()}</p>
|
|
114
|
+
<p class="text-[11px] text-tertiary uppercase tracking-widest mt-1">{m.hero_stat1_label()}</p>
|
|
115
|
+
</div>
|
|
116
|
+
<div class="w-px h-8 bg-border-primary"></div>
|
|
117
|
+
<div>
|
|
118
|
+
<p class="text-2xl font-bold text-primary">{m.hero_stat2_value()}</p>
|
|
119
|
+
<p class="text-[11px] text-tertiary uppercase tracking-widest mt-1">{m.hero_stat2_label()}</p>
|
|
120
|
+
</div>
|
|
121
|
+
<div class="w-px h-8 bg-border-primary"></div>
|
|
122
|
+
<div>
|
|
123
|
+
<p class="text-2xl font-bold text-primary">{m.hero_stat3_value()}</p>
|
|
124
|
+
<p class="text-[11px] text-tertiary uppercase tracking-widest mt-1">{m.hero_stat3_label()}</p>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
</section>
|
|
128
|
+
|
|
129
|
+
<!-- FEATURES GRID -->
|
|
130
|
+
<section class="w-full max-w-5xl mx-auto px-6 pb-24">
|
|
131
|
+
|
|
132
|
+
<!-- Section divider label -->
|
|
133
|
+
<div class="flex items-center gap-4 mb-8">
|
|
134
|
+
<span class="h-px flex-1 bg-linear-to-r from-transparent to-accent/30"></span>
|
|
135
|
+
<span class="section-label text-accent">What's Included</span>
|
|
136
|
+
<span class="h-px flex-1 bg-linear-to-l from-transparent to-accent/30"></span>
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
140
|
+
{#each features as f, i (i)}
|
|
141
|
+
<div class="group flex items-start gap-4 p-5 rounded-2xl bg-surface-secondary border border-border-primary hover:border-accent/40 hover:bg-accent/3 transition-all duration-200 hover:-translate-y-0.5 hover:shadow-lg">
|
|
142
|
+
|
|
143
|
+
<!-- Icon bubble -->
|
|
144
|
+
<div class="flex-none w-10 h-10 rounded-xl bg-accent/10 group-hover:bg-accent/20 flex items-center justify-center transition-colors shrink-0 text-accent">
|
|
145
|
+
<svelte:component this={f.icon} width={20} height={20} />
|
|
146
|
+
</div>
|
|
147
|
+
|
|
148
|
+
<!-- Text -->
|
|
149
|
+
<div class="flex-1 min-w-0">
|
|
150
|
+
<div class="flex items-center justify-between gap-2 mb-1.5">
|
|
151
|
+
<h3 class="text-sm font-semibold text-primary">{f.title}</h3>
|
|
152
|
+
<span class="flex-none text-[10px] font-semibold px-1.5 py-0.5 rounded-md bg-accent/10 text-accent">{f.tag}</span>
|
|
153
|
+
</div>
|
|
154
|
+
<p class="text-xs text-secondary leading-relaxed">{f.desc}</p>
|
|
155
|
+
{#if f.href}
|
|
156
|
+
<a href={f.href} class="no-underline inline-flex items-center gap-1 mt-3 text-xs font-medium text-accent hover:gap-2 transition-all">
|
|
157
|
+
Get started <ArrowRight width={11} height={11} />
|
|
158
|
+
</a>
|
|
159
|
+
{/if}
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
{/each}
|
|
163
|
+
</div>
|
|
164
|
+
</section>
|
|
165
|
+
|
|
166
|
+
<!-- MARQUEE STRIP — unchanged -->
|
|
167
|
+
<div class="sticky flex-none bottom-0 z-40 border-t border-border-primary bg-surface-primary/80 backdrop-blur-sm overflow-hidden py-3">
|
|
168
|
+
<div class="flex gap-10 w-max marquee-track items-center">
|
|
169
|
+
{#each [0,1,2,3,4,5,6,7] as i (i)}
|
|
170
|
+
<span class="text-tertiary text-xs tracking-[0.18em] uppercase whitespace-nowrap">{m.marquee_1()}</span>
|
|
171
|
+
<span class="text-accent text-xs">✦</span>
|
|
172
|
+
<span class="text-tertiary text-xs tracking-[0.18em] uppercase whitespace-nowrap">{m.marquee_2()}</span>
|
|
173
|
+
<span class="text-accent text-xs">◈</span>
|
|
174
|
+
<span class="text-tertiary text-xs tracking-[0.18em] uppercase whitespace-nowrap">{m.marquee_3()}</span>
|
|
175
|
+
<span class="text-accent text-xs">✦</span>
|
|
176
|
+
<span class="text-tertiary text-xs tracking-[0.18em] uppercase whitespace-nowrap">{m.marquee_4()}</span>
|
|
177
|
+
<span class="text-accent text-xs">◈</span>
|
|
178
|
+
{/each}
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
|
|
182
|
+
</div>
|
|
183
|
+
|
|
184
|
+
<style>
|
|
185
|
+
.marquee-track {
|
|
186
|
+
animation: marquee 28s linear infinite;
|
|
187
|
+
}
|
|
188
|
+
@keyframes marquee {
|
|
189
|
+
from { transform: translateX(0); }
|
|
190
|
+
to { transform: translateX(-50%); }
|
|
191
|
+
}
|
|
192
|
+
</style>
|