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
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "super-svelte-skeleton",
|
|
3
|
+
"version": "0.0.3",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "vite dev",
|
|
7
|
+
"build": "vite build",
|
|
8
|
+
"preview": "vite preview",
|
|
9
|
+
"prepare": "svelte-kit sync || echo ''",
|
|
10
|
+
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
|
11
|
+
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
|
12
|
+
"lint": "prettier --check . && eslint .",
|
|
13
|
+
"format": "prettier --write .",
|
|
14
|
+
"machine-translate": "inlang machine translate --project project.inlang"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@aryagg/theme": "^0.0.1",
|
|
18
|
+
"@aryagg/types": "^1.1.0",
|
|
19
|
+
"@eslint/compat": "^2.0.2",
|
|
20
|
+
"@eslint/js": "^10.0.1",
|
|
21
|
+
"@fontsource/fira-mono": "^5.2.7",
|
|
22
|
+
"@inlang/cli": "^3.1.9",
|
|
23
|
+
"@inlang/paraglide-js": "^2.16.0",
|
|
24
|
+
"@inlang/plugin-m-function-matcher": "^2.2.6",
|
|
25
|
+
"@inlang/plugin-message-format": "^4.4.0",
|
|
26
|
+
"@neoconfetti/svelte": "^2.2.2",
|
|
27
|
+
"@sveltejs/kit": "^2.53.4",
|
|
28
|
+
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
|
29
|
+
"@tailwindcss/vite": "^4.2.1",
|
|
30
|
+
"@types/node": "^25",
|
|
31
|
+
"eslint": "^10.0.2",
|
|
32
|
+
"eslint-config-prettier": "^10.1.8",
|
|
33
|
+
"eslint-plugin-svelte": "^3.15.0",
|
|
34
|
+
"globals": "^17.4.0",
|
|
35
|
+
"prettier": "^3.8.3",
|
|
36
|
+
"prettier-plugin-svelte": "^3.5.2",
|
|
37
|
+
"prettier-plugin-tailwindcss": "^0.7.4",
|
|
38
|
+
"svelte": "^5.53.7",
|
|
39
|
+
"svelte-check": "^4.4.4",
|
|
40
|
+
"tailwindcss": "^4.2.1",
|
|
41
|
+
"typescript": "^5.9.3",
|
|
42
|
+
"typescript-eslint": "^8.56.1",
|
|
43
|
+
"vite": "^7.3.1"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@aryagg/ui-kit": "^0.2.0",
|
|
47
|
+
"@aryagg/utils": "^1.1.0",
|
|
48
|
+
"@inlang/paraglide-sveltekit": "^0.16.1",
|
|
49
|
+
"@sveltejs/adapter-node": "^5.5.4",
|
|
50
|
+
"axios": "^1.13.6",
|
|
51
|
+
"svelte-bootstrap-icons": "^3.3.0",
|
|
52
|
+
"unplugin-auto-import": "^21.0.0"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"modules": [
|
|
3
|
+
"node_modules/@inlang/plugin-message-format/dist/index.js",
|
|
4
|
+
"node_modules/@inlang/plugin-m-function-matcher/dist/index.js"
|
|
5
|
+
],
|
|
6
|
+
"plugin.inlang.messageFormat": {
|
|
7
|
+
"pathPattern": "./messages/{locale}.json"
|
|
8
|
+
},
|
|
9
|
+
"baseLocale": "en",
|
|
10
|
+
"locales": [
|
|
11
|
+
"en",
|
|
12
|
+
"es",
|
|
13
|
+
"ar"
|
|
14
|
+
]
|
|
15
|
+
}
|
package/src/app.css
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/* 1. Shared design system — imports Tailwind base + theme colors + global styles */
|
|
2
|
+
@import "@aryagg/theme";
|
|
3
|
+
|
|
4
|
+
/* 2. Scan ui-kit for Tailwind classes (node_modules are skipped by default in v4) */
|
|
5
|
+
@source "../node_modules/@aryagg/ui-kit/dist";
|
|
6
|
+
|
|
7
|
+
/* 3. Project config — adds your custom colors/plugins on top of the theme */
|
|
8
|
+
@config "./lib/tailwind.config.ts";
|
package/src/app.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// See https://svelte.dev/docs/kit/types#app.d.ts
|
|
2
|
+
// for information about these interfaces
|
|
3
|
+
/// <reference path="../.svelte-kit/ambient.d.ts" />
|
|
4
|
+
declare global {
|
|
5
|
+
namespace App {
|
|
6
|
+
/** Error shape returned to error pages */
|
|
7
|
+
interface Error {
|
|
8
|
+
message: string;
|
|
9
|
+
code?: string;
|
|
10
|
+
}
|
|
11
|
+
/** Server-side locals set in hooks.server.ts */
|
|
12
|
+
interface Locals {
|
|
13
|
+
user: IAuthUser | null;
|
|
14
|
+
isAuthenticated: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export { };
|
package/src/app.html
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
|
|
3
|
+
<html lang="%paraglide.lang%" dir="%paraglide.dir%">
|
|
4
|
+
|
|
5
|
+
<head>
|
|
6
|
+
<meta charset="utf-8" />
|
|
7
|
+
<link rel="icon" href="%sveltekit.assets%/favicon.svg?v=2" />
|
|
8
|
+
|
|
9
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
10
|
+
|
|
11
|
+
<title>%sveltekit.env.PUBLIC_SITE_NAME%</title>
|
|
12
|
+
|
|
13
|
+
<meta name="description" content="%sveltekit.env.PUBLIC_SITE_DESCRIPTION%" />
|
|
14
|
+
|
|
15
|
+
<meta name="theme-color" content="#3b82f6" />
|
|
16
|
+
<meta name="color-scheme" content="light dark" />
|
|
17
|
+
|
|
18
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
19
|
+
|
|
20
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="anonymous" />
|
|
21
|
+
|
|
22
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap"
|
|
23
|
+
rel="stylesheet" />
|
|
24
|
+
|
|
25
|
+
%sveltekit.head%
|
|
26
|
+
|
|
27
|
+
<link id="theme-style" rel="stylesheet" href="styles/theme-light.css" />
|
|
28
|
+
|
|
29
|
+
<script>
|
|
30
|
+
if (localStorage.getItem('theme') === 'dark') {
|
|
31
|
+
document.getElementById('theme-style').href = 'styles/theme-dark.css';
|
|
32
|
+
}
|
|
33
|
+
</script>
|
|
34
|
+
</head>
|
|
35
|
+
|
|
36
|
+
<body data-sveltekit-preload-data="hover">
|
|
37
|
+
<div style="display: contents">%sveltekit.body%</div>
|
|
38
|
+
</body>
|
|
39
|
+
|
|
40
|
+
</html>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
/* prettier-ignore */
|
|
3
|
+
// @ts-nocheck
|
|
4
|
+
// noinspection JSUnusedGlobalSymbols
|
|
5
|
+
// Generated by unplugin-auto-import
|
|
6
|
+
// biome-ignore lint: disable
|
|
7
|
+
export {}
|
|
8
|
+
declare global {
|
|
9
|
+
const afterUpdate: typeof import('svelte').afterUpdate
|
|
10
|
+
const beforeUpdate: typeof import('svelte').beforeUpdate
|
|
11
|
+
const blur: typeof import('svelte/transition').blur
|
|
12
|
+
const createEventDispatcher: typeof import('svelte').createEventDispatcher
|
|
13
|
+
const crossfade: typeof import('svelte/transition').crossfade
|
|
14
|
+
const derived: typeof import('svelte/store').derived
|
|
15
|
+
const draw: typeof import('svelte/transition').draw
|
|
16
|
+
const fade: typeof import('svelte/transition').fade
|
|
17
|
+
const fly: typeof import('svelte/transition').fly
|
|
18
|
+
const get: typeof import('svelte/store').get
|
|
19
|
+
const getAllContexts: typeof import('svelte').getAllContexts
|
|
20
|
+
const getContext: typeof import('svelte').getContext
|
|
21
|
+
const goto: typeof import('$app/navigation').goto
|
|
22
|
+
const hasContext: typeof import('svelte').hasContext
|
|
23
|
+
const invalidate: typeof import('$app/navigation').invalidate
|
|
24
|
+
const invalidateAll: typeof import('$app/navigation').invalidateAll
|
|
25
|
+
const m: typeof import('$lib/paraglide/messages')
|
|
26
|
+
const onDestroy: typeof import('svelte').onDestroy
|
|
27
|
+
const onMount: typeof import('svelte').onMount
|
|
28
|
+
const readable: typeof import('svelte/store').readable
|
|
29
|
+
const redirect: typeof import('@sveltejs/kit').redirect
|
|
30
|
+
const scale: typeof import('svelte/transition').scale
|
|
31
|
+
const setContext: typeof import('svelte').setContext
|
|
32
|
+
const slide: typeof import('svelte/transition').slide
|
|
33
|
+
const tick: typeof import('svelte').tick
|
|
34
|
+
const writable: typeof import('svelte/store').writable
|
|
35
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// SvelteKit client hooks - run in the browser
|
|
2
|
+
// Handle client-side errors and navigation events
|
|
3
|
+
|
|
4
|
+
import type { HandleClientError } from '@sveltejs/kit';
|
|
5
|
+
/**
|
|
6
|
+
* Global client-side error handler
|
|
7
|
+
* Catches unhandled errors in load functions and components
|
|
8
|
+
*/
|
|
9
|
+
export const handleError: HandleClientError = ({ error, event }) => {
|
|
10
|
+
// In production, send to error tracking
|
|
11
|
+
console.error('Client error:', error, 'URL:', event.url.pathname);
|
|
12
|
+
|
|
13
|
+
return {
|
|
14
|
+
message: 'Something went wrong. Please refresh and try again.',
|
|
15
|
+
code: 'CLIENT_HOOK_ERROR'
|
|
16
|
+
};
|
|
17
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// src/hooks.server.ts
|
|
2
|
+
import type { Handle, HandleFetch, HandleServerError } from '@sveltejs/kit';
|
|
3
|
+
import { paraglideMiddleware } from '$lib/paraglide/server';
|
|
4
|
+
import { getTextDirection } from '$lib/paraglide/runtime';
|
|
5
|
+
import { sequence } from '@sveltejs/kit/hooks';
|
|
6
|
+
import { AUTH_COOKIE_NAME } from '$shared/config';
|
|
7
|
+
import { env } from '$env/dynamic/private';
|
|
8
|
+
|
|
9
|
+
/* Paraglide: inject locale + direction into HTML */
|
|
10
|
+
const handleParaglide: Handle = ({ event, resolve }) =>
|
|
11
|
+
paraglideMiddleware(event.request, ({ request, locale }) => {
|
|
12
|
+
event.request = request;
|
|
13
|
+
return resolve(event, {
|
|
14
|
+
transformPageChunk: ({ html }) =>
|
|
15
|
+
html
|
|
16
|
+
.replace('%paraglide.lang%', locale)
|
|
17
|
+
.replace('%paraglide.dir%', getTextDirection(locale))
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
/* Auth: block access if no session cookie */
|
|
22
|
+
const authHandle: Handle = async ({ event, resolve }) => {
|
|
23
|
+
const sessionCookie = event.cookies.get(AUTH_COOKIE_NAME);
|
|
24
|
+
if(sessionCookie) {
|
|
25
|
+
try {
|
|
26
|
+
const session = JSON.parse(atob(sessionCookie));
|
|
27
|
+
if(session?.user) {
|
|
28
|
+
event.locals.user = session.user;
|
|
29
|
+
event.locals.isAuthenticated = true;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
} catch(err) {
|
|
33
|
+
console.error(err);
|
|
34
|
+
event.cookies.delete(AUTH_COOKIE_NAME, { path: '/' });
|
|
35
|
+
}
|
|
36
|
+
} else {
|
|
37
|
+
event.locals.user = null;
|
|
38
|
+
event.locals.isAuthenticated = false;
|
|
39
|
+
}
|
|
40
|
+
return resolve(event);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/* Combine middleware in order */
|
|
44
|
+
export const handle = sequence(handleParaglide, authHandle);
|
|
45
|
+
|
|
46
|
+
/* Add headers to all server-side fetch calls, runs before every server‑side fetch */
|
|
47
|
+
export const handleFetch: HandleFetch = async ({ request, fetch }) => {
|
|
48
|
+
request.headers.set('X-API-Key', env.API_KEY ?? '');
|
|
49
|
+
return fetch(request);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/* Log all server errors */
|
|
53
|
+
export const handleError: HandleServerError = ({ error, event }) => {
|
|
54
|
+
console.error('🔥 SERVER ERROR:', error, 'URL:', event.url.pathname);
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
message: 'Something went wrong on the server.'
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
// import { redirect } from '@sveltejs/kit';
|
|
63
|
+
|
|
64
|
+
// export function handle({ event, resolve }) {
|
|
65
|
+
// // Force all routes to lowercase so routing becomes case‑insensitive
|
|
66
|
+
// const lower = event.url.pathname.toLowerCase();
|
|
67
|
+
|
|
68
|
+
// if (event.url.pathname !== lower) {
|
|
69
|
+
// return redirect(301, lower + event.url.search);
|
|
70
|
+
// }
|
|
71
|
+
|
|
72
|
+
// return resolve(event);
|
|
73
|
+
// }
|
package/src/hooks.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { RequestEvent } from '@sveltejs/kit';
|
|
2
|
+
import { deLocalizeUrl } from '$lib/paraglide/runtime';
|
|
3
|
+
|
|
4
|
+
export const reroute = (request: RequestEvent) => deLocalizeUrl(request.url).pathname;
|
|
5
|
+
// If you export a function named reroute, SvelteKit will automatically run it on every request before routing.
|
|
6
|
+
// Rerouting helper for Paraglide i18n.
|
|
7
|
+
// This removes the locale prefix from the URL (e.g. /en/about → /about)
|
|
8
|
+
// so SvelteKit always receives a clean, non‑localized path.
|
|
9
|
+
// Paraglide adds this to ensure routing works normally even when URLs contain language codes.
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
// 1. handle - Runs before every request; used for middleware logic like auth, i18n, headers, etc.
|
|
13
|
+
// 2. reroute - Runs before routing; lets you rewrite URLs (e.g., remove /en from /en/about).
|
|
14
|
+
// 3. handleFetch - Runs on every fetch() call; lets you modify outgoing requests (add tokens, headers).
|
|
15
|
+
// 4. handleError - Runs whenever an error occurs; used for logging or sending errors to monitoring tools.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const BASE = '/auth/';
|
|
2
|
+
|
|
3
|
+
export const AUTH = {
|
|
4
|
+
LOGIN: `${BASE}login`,
|
|
5
|
+
LOGOUT: `${BASE}logout`,
|
|
6
|
+
REGISTER: `${BASE}register`,
|
|
7
|
+
REFRESH: `${BASE}refresh`,
|
|
8
|
+
ME: `${BASE}me`,
|
|
9
|
+
FORGOT_PASSWORD: `${BASE}forgot-password`,
|
|
10
|
+
RESET_PASSWORD: `${BASE}reset-password`,
|
|
11
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Auth service — domain layer that owns the HTTP + UI side-effect decision.
|
|
2
|
+
// snackStore lives here (not in the infra layer below it).
|
|
3
|
+
|
|
4
|
+
import { snackStore } from '@aryagg/ui-kit';
|
|
5
|
+
import { AUTH } from './endpoints';
|
|
6
|
+
import type { LoginPayload, RegisterPayload, ResetPayload, AuthToken, IAuthUser } from '../types';
|
|
7
|
+
import { handleApiResponse } from '@aryagg/utils';
|
|
8
|
+
import { httpClient } from '$shared';
|
|
9
|
+
|
|
10
|
+
async function callWithToast<T>(
|
|
11
|
+
promise: ReturnType<typeof handleApiResponse<T>>,
|
|
12
|
+
): Promise<Awaited<ReturnType<typeof handleApiResponse<T>>>> {
|
|
13
|
+
const result = await promise;
|
|
14
|
+
if (!result.isSuccess) snackStore.showError(result.message!);
|
|
15
|
+
return result;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const authApi = {
|
|
19
|
+
login: (body: LoginPayload) =>
|
|
20
|
+
callWithToast(handleApiResponse<AuthToken>(httpClient.post(AUTH.LOGIN, body), 'Login failed.')),
|
|
21
|
+
|
|
22
|
+
register: (body: RegisterPayload) =>
|
|
23
|
+
callWithToast(handleApiResponse<AuthToken>(httpClient.post(AUTH.REGISTER, body), 'Registration failed.')),
|
|
24
|
+
|
|
25
|
+
forgotPassword: (email: string) =>
|
|
26
|
+
callWithToast(handleApiResponse<void>(httpClient.post(AUTH.FORGOT_PASSWORD, { email }), 'Request failed.')),
|
|
27
|
+
|
|
28
|
+
resetPassword: (body: ResetPayload) =>
|
|
29
|
+
callWithToast(handleApiResponse<void>(httpClient.post(AUTH.RESET_PASSWORD, body), 'Reset failed.')),
|
|
30
|
+
|
|
31
|
+
me: () => handleApiResponse<IAuthUser>(httpClient.get(AUTH.ME), 'Could not load profile.'),
|
|
32
|
+
logout: () => handleApiResponse<void>(httpClient.post(AUTH.LOGOUT, {}), 'Logout failed.'),
|
|
33
|
+
refresh: (body: { refreshToken: string }) =>
|
|
34
|
+
handleApiResponse<AuthToken>(httpClient.post(AUTH.REFRESH, body), 'Token refresh failed.'),
|
|
35
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { EUserRole } from '@aryagg/types';
|
|
2
|
+
import type { IAuthUser } from './types';
|
|
3
|
+
|
|
4
|
+
let _authState = $state<IAuthUser | null>(null);
|
|
5
|
+
|
|
6
|
+
const isAdmin = $derived(
|
|
7
|
+
_authState?.role === EUserRole.ADMIN ||
|
|
8
|
+
_authState?.role === EUserRole.SUPER_ADMIN,
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
const isSuperAdmin = $derived(_authState?.role === EUserRole.SUPER_ADMIN);
|
|
12
|
+
|
|
13
|
+
const displayName = $derived(_authState?.name ?? _authState?.email ?? '');
|
|
14
|
+
|
|
15
|
+
function setUser(user: IAuthUser) {
|
|
16
|
+
_authState = user;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function clearUser() {
|
|
20
|
+
_authState = null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function hasRole(role: EUserRole): boolean {
|
|
24
|
+
if (!_authState) return false;
|
|
25
|
+
const hierarchy = [
|
|
26
|
+
EUserRole.GUEST,
|
|
27
|
+
EUserRole.USER,
|
|
28
|
+
EUserRole.MANAGER,
|
|
29
|
+
EUserRole.ADMIN,
|
|
30
|
+
EUserRole.SUPER_ADMIN,
|
|
31
|
+
];
|
|
32
|
+
return hierarchy.indexOf(_authState.role as EUserRole) >= hierarchy.indexOf(role);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function hasPermission(roles: EUserRole[]): boolean {
|
|
36
|
+
return roles.some((role) => hasRole(role));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const authStore = {
|
|
40
|
+
get state() { return _authState; },
|
|
41
|
+
get user() { return _authState; },
|
|
42
|
+
get isAuthenticated() { return !!_authState; },
|
|
43
|
+
get isAdmin() { return isAdmin; },
|
|
44
|
+
get isSuperAdmin() { return isSuperAdmin; },
|
|
45
|
+
get displayName() { return displayName; },
|
|
46
|
+
setUser,
|
|
47
|
+
clearUser,
|
|
48
|
+
hasRole,
|
|
49
|
+
hasPermission,
|
|
50
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// ── Request payloads ──────────────────────────────────────────────────────────
|
|
2
|
+
|
|
3
|
+
export interface LoginPayload {
|
|
4
|
+
email: string;
|
|
5
|
+
password: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface RegisterPayload {
|
|
9
|
+
name: string;
|
|
10
|
+
email: string;
|
|
11
|
+
password: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ResetPayload {
|
|
15
|
+
token: string;
|
|
16
|
+
password: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// ── Response shapes ───────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
export interface AuthToken {
|
|
22
|
+
accessToken: string;
|
|
23
|
+
refreshToken: string;
|
|
24
|
+
expiresIn: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface IAuthUser {
|
|
28
|
+
id: string;
|
|
29
|
+
name: string;
|
|
30
|
+
email: string;
|
|
31
|
+
role: string;
|
|
32
|
+
avatar?: string;
|
|
33
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { httpClient, handleApiResponse, buildUrl } from '$shared/api';
|
|
2
|
+
import { USER } from './endpoints';
|
|
3
|
+
import type { User, UpdateUserPayload } from '../types';
|
|
4
|
+
|
|
5
|
+
export const userApi = {
|
|
6
|
+
list: () => handleApiResponse<User[]>(httpClient.get(USER.LIST), 'Failed to load users.'),
|
|
7
|
+
get: (id: string) => handleApiResponse<User>(httpClient.get(buildUrl(USER.BY_ID, { id })), 'Failed to load user.'),
|
|
8
|
+
update: (id: string, body: UpdateUserPayload) => handleApiResponse<User>(httpClient.put(buildUrl(USER.BY_ID, { id }), body), 'Failed to update user.'),
|
|
9
|
+
remove: (id: string) => handleApiResponse<void>(httpClient.delete(buildUrl(USER.BY_ID, { id })), 'Failed to delete user.'),
|
|
10
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// ── Request payloads ──────────────────────────────────────────────────────────
|
|
2
|
+
|
|
3
|
+
export interface UpdateUserPayload {
|
|
4
|
+
name?: string;
|
|
5
|
+
email?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// ── Response shapes ───────────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
export interface User {
|
|
11
|
+
id: string;
|
|
12
|
+
name: string;
|
|
13
|
+
email: string;
|
|
14
|
+
role: string;
|
|
15
|
+
avatar?: string;
|
|
16
|
+
createdAt: string;
|
|
17
|
+
updatedAt: string;
|
|
18
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { PUBLIC_BASE_PATH } from '$env/static/public';
|
|
2
|
+
import { ETheme, EInputType, type IFormField } from '@aryagg/types';
|
|
3
|
+
|
|
4
|
+
export const THEME_FILES = [
|
|
5
|
+
{ name: 'Light', href: `${PUBLIC_BASE_PATH}/theme-light.css`, value: ETheme.LIGHT },
|
|
6
|
+
{ name: 'Dark', href: `${PUBLIC_BASE_PATH}/theme-dark.css`, value: ETheme.DARK },
|
|
7
|
+
];
|
|
8
|
+
|
|
9
|
+
export const THEME_INPUTS: IFormField[] = [
|
|
10
|
+
{
|
|
11
|
+
id: 'site-title', key: 'title', label: 'Site title',
|
|
12
|
+
placeholder: 'Theme Studio', type: EInputType.TEXT, value: 'Theme Studio',
|
|
13
|
+
required: true, helperText: 'Shown in the browser tab and shared links',
|
|
14
|
+
attributes: { maxlength: 60 },
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
id: 'meta-description', key: 'metaDescription', label: 'Meta description',
|
|
18
|
+
placeholder: 'Design tokens · CSS variables · Live preview',
|
|
19
|
+
type: EInputType.TEXTAREA, value: 'Design tokens · CSS variables · Live preview',
|
|
20
|
+
required: true, helperText: 'Used for SEO and link previews',
|
|
21
|
+
attributes: { rows: 3, maxlength: 160 },
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: 'logo', key: 'logo', label: 'Logo', type: EInputType.FILE,
|
|
25
|
+
required: false, helperText: 'Recommended 256×256 PNG', placeholder: 'Upload logo',
|
|
26
|
+
attributes: { accept: '.png,.jpg,.jpeg,.svg,.webp' }, multiple: false,
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
id: 'favicon', key: 'favicon', label: 'Favicon', type: EInputType.FILE,
|
|
30
|
+
required: false, helperText: '32×32 PNG or ICO', placeholder: 'Upload favicon',
|
|
31
|
+
attributes: { accept: '.png,.ico' }, multiple: false,
|
|
32
|
+
},
|
|
33
|
+
];
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { ThemeEntry } from '../types';
|
|
3
|
+
|
|
4
|
+
let { currentTheme }: { currentTheme: ThemeEntry | undefined } = $props();
|
|
5
|
+
</script>
|
|
6
|
+
|
|
7
|
+
<div class="mt-4 overflow-auto rounded-xl bg-surface-2 p-4 font-mono text-[11px] leading-relaxed text-primary">
|
|
8
|
+
<code>
|
|
9
|
+
{#if currentTheme}
|
|
10
|
+
<span class="text-accent font-semibold">:root</span> {<br />
|
|
11
|
+
{#each Object.entries(currentTheme.colors) as [k, v] (k)}
|
|
12
|
+
<span class="text-info pl-4">{k}</span><span class="text-tertiary">: </span><span class="text-success">{v}</span><span class="text-tertiary">;</span><br />
|
|
13
|
+
{/each}
|
|
14
|
+
}
|
|
15
|
+
{/if}
|
|
16
|
+
</code>
|
|
17
|
+
</div>
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { fade } from 'svelte/transition';
|
|
3
|
+
import { Copy } from 'svelte-bootstrap-icons';
|
|
4
|
+
|
|
5
|
+
let {
|
|
6
|
+
varName,
|
|
7
|
+
hex,
|
|
8
|
+
copied = false,
|
|
9
|
+
onColorChange,
|
|
10
|
+
onHexInput,
|
|
11
|
+
onCopy,
|
|
12
|
+
}: {
|
|
13
|
+
varName: string;
|
|
14
|
+
hex: string;
|
|
15
|
+
copied?: boolean;
|
|
16
|
+
onColorChange: (hex: string) => void;
|
|
17
|
+
onHexInput: (e: Event) => void;
|
|
18
|
+
onCopy: () => void;
|
|
19
|
+
} = $props();
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<div
|
|
23
|
+
class="group border-border-primary/60 bg-surface-secondary hover:border-accent/30 flex flex-col overflow-hidden rounded-xl border transition-all duration-200 hover:shadow-md"
|
|
24
|
+
in:fade={{ duration: 80 }}
|
|
25
|
+
>
|
|
26
|
+
<!-- Swatch -->
|
|
27
|
+
<div class="relative h-16 w-full cursor-pointer overflow-hidden">
|
|
28
|
+
<div class="absolute inset-0" style="background:{hex}"></div>
|
|
29
|
+
<input
|
|
30
|
+
type="color"
|
|
31
|
+
class="absolute inset-0 h-full w-full cursor-pointer opacity-0"
|
|
32
|
+
value={hex.startsWith('#') && hex.length <= 7 ? hex : '#000000'}
|
|
33
|
+
oninput={(e) => onColorChange(e.currentTarget.value)}
|
|
34
|
+
/>
|
|
35
|
+
<div
|
|
36
|
+
class="pointer-events-none absolute inset-0 flex items-center justify-center opacity-0 transition-all duration-150 group-hover:opacity-100"
|
|
37
|
+
style="background:rgba(0,0,0,0.18)"
|
|
38
|
+
>
|
|
39
|
+
<span class="rounded-full px-2 py-0.5 text-[9px] font-semibold text-white" style="background:rgba(0,0,0,0.45);backdrop-filter:blur(4px)">Edit</span>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<!-- Info -->
|
|
44
|
+
<div class="flex flex-col gap-1.5 p-2">
|
|
45
|
+
<p class="text-secondary truncate font-mono text-[9.5px] leading-none">{varName.replace('--', '')}</p>
|
|
46
|
+
<div class="flex items-center gap-1">
|
|
47
|
+
<input
|
|
48
|
+
type="text"
|
|
49
|
+
class="text-tertiary bg-surface-secondary/60 border-border-primary/40 focus:border-accent/60 focus:text-primary min-w-0 flex-1 rounded-lg border px-1.5 py-0.5 font-mono text-[10px] transition-colors focus:outline-none"
|
|
50
|
+
value={hex}
|
|
51
|
+
onchange={onHexInput}
|
|
52
|
+
/>
|
|
53
|
+
<button
|
|
54
|
+
class="shrink-0 rounded-lg border p-1 transition-all
|
|
55
|
+
{copied ? 'border-success/30 bg-success/10 text-success' : 'border-border-primary/60 text-tertiary hover:border-primary/30 hover:text-primary'}"
|
|
56
|
+
onclick={onCopy}
|
|
57
|
+
>
|
|
58
|
+
{#if copied}
|
|
59
|
+
<span class="px-0.5 text-[9px] font-bold">✓</span>
|
|
60
|
+
{:else}
|
|
61
|
+
<Copy width="10" />
|
|
62
|
+
{/if}
|
|
63
|
+
</button>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|