BLOG-136 Dashboard route and frontend authentication (#137)
All checks were successful
Frontend CI / build (push) Successful in 1m23s
All checks were successful
Frontend CI / build (push) Successful in 1m23s
### Description - Create a dashboard layout (with a navigation sidebar) - Only logged in users are able to access (others 404) ### Package Changes > shadcn-svelte ```json { "@lucide/svelte": "^0.545.0", "clsx": "^2.1.1", "tailwind-merge": "^3.3.1", "tailwind-variants": "^3.1.1", "tw-animate-css": "^1.4.0" } ``` ### Screenshots |Status|Screenshot| |-|-| |Logged in|| |Logged out|| ### Reference Resolves #136. ### Checklist - [x] A milestone is set - [x] The related issuse has been linked to this branch Reviewed-on: #137 Co-authored-by: SquidSpirit <squid@squidspirit.com> Co-committed-by: SquidSpirit <squid@squidspirit.com>
This commit is contained in:
parent
1ae104cd56
commit
e8c5e678d5
@ -1 +1 @@
|
|||||||
PUBLIC_API_BASE_URL=http://127.0.0.1:5173/api/
|
PUBLIC_API_BASE_URL=http://localhost:5173/api/
|
||||||
|
16
frontend/components.json
Normal file
16
frontend/components.json
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://shadcn-svelte.com/schema.json",
|
||||||
|
"tailwind": {
|
||||||
|
"css": "src/app.css",
|
||||||
|
"baseColor": "gray"
|
||||||
|
},
|
||||||
|
"aliases": {
|
||||||
|
"components": "$lib/common/framework/components",
|
||||||
|
"utils": "$lib/common/framework/components/utils",
|
||||||
|
"ui": "$lib/common/framework/components/ui",
|
||||||
|
"hooks": "$lib/common/framework/components/hooks",
|
||||||
|
"lib": "$lib"
|
||||||
|
},
|
||||||
|
"typescript": true,
|
||||||
|
"registry": "https://shadcn-svelte.com/registry"
|
||||||
|
}
|
@ -13,10 +13,14 @@
|
|||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
"lint": "prettier --check . && eslint ."
|
"lint": "prettier --check . && eslint ."
|
||||||
},
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@sentry/sveltekit": "^10.1.0"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/compat": "^1.2.5",
|
"@eslint/compat": "^1.2.5",
|
||||||
"@eslint/js": "^9.18.0",
|
"@eslint/js": "^9.18.0",
|
||||||
"@fortawesome/fontawesome-free": "^7.0.0",
|
"@fortawesome/fontawesome-free": "^7.0.0",
|
||||||
|
"@lucide/svelte": "^0.545.0",
|
||||||
"@sveltejs/adapter-auto": "^6.0.0",
|
"@sveltejs/adapter-auto": "^6.0.0",
|
||||||
"@sveltejs/adapter-node": "^5.2.13",
|
"@sveltejs/adapter-node": "^5.2.13",
|
||||||
"@sveltejs/kit": "^2.22.0",
|
"@sveltejs/kit": "^2.22.0",
|
||||||
@ -25,6 +29,7 @@
|
|||||||
"@tailwindcss/vite": "^4.0.0",
|
"@tailwindcss/vite": "^4.0.0",
|
||||||
"@types/markdown-it": "^14.1.2",
|
"@types/markdown-it": "^14.1.2",
|
||||||
"@types/sanitize-html": "^2.16.0",
|
"@types/sanitize-html": "^2.16.0",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
"eslint": "^9.18.0",
|
"eslint": "^9.18.0",
|
||||||
"eslint-config-prettier": "^10.0.1",
|
"eslint-config-prettier": "^10.0.1",
|
||||||
"eslint-plugin-svelte": "^3.0.0",
|
"eslint-plugin-svelte": "^3.0.0",
|
||||||
@ -36,7 +41,10 @@
|
|||||||
"sanitize-html": "^2.17.0",
|
"sanitize-html": "^2.17.0",
|
||||||
"svelte": "^5.0.0",
|
"svelte": "^5.0.0",
|
||||||
"svelte-check": "^4.0.0",
|
"svelte-check": "^4.0.0",
|
||||||
|
"tailwind-merge": "^3.3.1",
|
||||||
|
"tailwind-variants": "^3.1.1",
|
||||||
"tailwindcss": "^4.0.0",
|
"tailwindcss": "^4.0.0",
|
||||||
|
"tw-animate-css": "^1.4.0",
|
||||||
"typescript": "^5.0.0",
|
"typescript": "^5.0.0",
|
||||||
"typescript-eslint": "^8.20.0",
|
"typescript-eslint": "^8.20.0",
|
||||||
"vite": "^7.0.4",
|
"vite": "^7.0.4",
|
||||||
@ -47,8 +55,5 @@
|
|||||||
"esbuild"
|
"esbuild"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.12.4",
|
"packageManager": "pnpm@10.17.1"
|
||||||
"dependencies": {
|
|
||||||
"@sentry/sveltekit": "^10.1.0"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
50
frontend/pnpm-lock.yaml
generated
50
frontend/pnpm-lock.yaml
generated
@ -21,6 +21,9 @@ importers:
|
|||||||
'@fortawesome/fontawesome-free':
|
'@fortawesome/fontawesome-free':
|
||||||
specifier: ^7.0.0
|
specifier: ^7.0.0
|
||||||
version: 7.0.0
|
version: 7.0.0
|
||||||
|
'@lucide/svelte':
|
||||||
|
specifier: ^0.545.0
|
||||||
|
version: 0.545.0(svelte@5.36.13)
|
||||||
'@sveltejs/adapter-auto':
|
'@sveltejs/adapter-auto':
|
||||||
specifier: ^6.0.0
|
specifier: ^6.0.0
|
||||||
version: 6.0.1(@sveltejs/kit@2.25.1(@sveltejs/vite-plugin-svelte@6.1.0(svelte@5.36.13)(vite@7.0.5(@types/node@24.2.0)(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.36.13)(vite@7.0.5(@types/node@24.2.0)(jiti@2.4.2)(lightningcss@1.30.1)))
|
version: 6.0.1(@sveltejs/kit@2.25.1(@sveltejs/vite-plugin-svelte@6.1.0(svelte@5.36.13)(vite@7.0.5(@types/node@24.2.0)(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.36.13)(vite@7.0.5(@types/node@24.2.0)(jiti@2.4.2)(lightningcss@1.30.1)))
|
||||||
@ -45,6 +48,9 @@ importers:
|
|||||||
'@types/sanitize-html':
|
'@types/sanitize-html':
|
||||||
specifier: ^2.16.0
|
specifier: ^2.16.0
|
||||||
version: 2.16.0
|
version: 2.16.0
|
||||||
|
clsx:
|
||||||
|
specifier: ^2.1.1
|
||||||
|
version: 2.1.1
|
||||||
eslint:
|
eslint:
|
||||||
specifier: ^9.18.0
|
specifier: ^9.18.0
|
||||||
version: 9.31.0(jiti@2.4.2)
|
version: 9.31.0(jiti@2.4.2)
|
||||||
@ -78,9 +84,18 @@ importers:
|
|||||||
svelte-check:
|
svelte-check:
|
||||||
specifier: ^4.0.0
|
specifier: ^4.0.0
|
||||||
version: 4.3.0(picomatch@4.0.3)(svelte@5.36.13)(typescript@5.8.3)
|
version: 4.3.0(picomatch@4.0.3)(svelte@5.36.13)(typescript@5.8.3)
|
||||||
|
tailwind-merge:
|
||||||
|
specifier: ^3.3.1
|
||||||
|
version: 3.3.1
|
||||||
|
tailwind-variants:
|
||||||
|
specifier: ^3.1.1
|
||||||
|
version: 3.1.1(tailwind-merge@3.3.1)(tailwindcss@4.1.11)
|
||||||
tailwindcss:
|
tailwindcss:
|
||||||
specifier: ^4.0.0
|
specifier: ^4.0.0
|
||||||
version: 4.1.11
|
version: 4.1.11
|
||||||
|
tw-animate-css:
|
||||||
|
specifier: ^1.4.0
|
||||||
|
version: 1.4.0
|
||||||
typescript:
|
typescript:
|
||||||
specifier: ^5.0.0
|
specifier: ^5.0.0
|
||||||
version: 5.8.3
|
version: 5.8.3
|
||||||
@ -416,6 +431,11 @@ packages:
|
|||||||
'@jridgewell/trace-mapping@0.3.29':
|
'@jridgewell/trace-mapping@0.3.29':
|
||||||
resolution: {integrity: sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==}
|
resolution: {integrity: sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==}
|
||||||
|
|
||||||
|
'@lucide/svelte@0.545.0':
|
||||||
|
resolution: {integrity: sha512-RlAtWefx9MdpXaOMbx3Qv3/NqpeZKOIPxN2D0RBN2+op0opKly8VgYEEWZTT6Ow/zf7UwyTg6/0ExJlsVLK+8g==}
|
||||||
|
peerDependencies:
|
||||||
|
svelte: ^5
|
||||||
|
|
||||||
'@nodelib/fs.scandir@2.1.5':
|
'@nodelib/fs.scandir@2.1.5':
|
||||||
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
|
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
@ -2118,6 +2138,19 @@ packages:
|
|||||||
resolution: {integrity: sha512-LnSywHHQM/nJekC65d84T1Yo85IeCYN4AryWYPhTokSvcEAFdYFCfbMhX1mc0zHizT736QQj0nalUk+SXaWrEQ==}
|
resolution: {integrity: sha512-LnSywHHQM/nJekC65d84T1Yo85IeCYN4AryWYPhTokSvcEAFdYFCfbMhX1mc0zHizT736QQj0nalUk+SXaWrEQ==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
tailwind-merge@3.3.1:
|
||||||
|
resolution: {integrity: sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==}
|
||||||
|
|
||||||
|
tailwind-variants@3.1.1:
|
||||||
|
resolution: {integrity: sha512-ftLXe3krnqkMHsuBTEmaVUXYovXtPyTK7ckEfDRXS8PBZx0bAUas+A0jYxuKA5b8qg++wvQ3d2MQ7l/xeZxbZQ==}
|
||||||
|
engines: {node: '>=16.x', pnpm: '>=7.x'}
|
||||||
|
peerDependencies:
|
||||||
|
tailwind-merge: '>=3.0.0'
|
||||||
|
tailwindcss: '*'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
tailwind-merge:
|
||||||
|
optional: true
|
||||||
|
|
||||||
tailwindcss@4.1.11:
|
tailwindcss@4.1.11:
|
||||||
resolution: {integrity: sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==}
|
resolution: {integrity: sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==}
|
||||||
|
|
||||||
@ -2159,6 +2192,9 @@ packages:
|
|||||||
tslib@2.8.1:
|
tslib@2.8.1:
|
||||||
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||||
|
|
||||||
|
tw-animate-css@1.4.0:
|
||||||
|
resolution: {integrity: sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==}
|
||||||
|
|
||||||
type-check@0.4.0:
|
type-check@0.4.0:
|
||||||
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
|
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
@ -2561,6 +2597,10 @@ snapshots:
|
|||||||
'@jridgewell/resolve-uri': 3.1.2
|
'@jridgewell/resolve-uri': 3.1.2
|
||||||
'@jridgewell/sourcemap-codec': 1.5.4
|
'@jridgewell/sourcemap-codec': 1.5.4
|
||||||
|
|
||||||
|
'@lucide/svelte@0.545.0(svelte@5.36.13)':
|
||||||
|
dependencies:
|
||||||
|
svelte: 5.36.13
|
||||||
|
|
||||||
'@nodelib/fs.scandir@2.1.5':
|
'@nodelib/fs.scandir@2.1.5':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@nodelib/fs.stat': 2.0.5
|
'@nodelib/fs.stat': 2.0.5
|
||||||
@ -4284,6 +4324,14 @@ snapshots:
|
|||||||
magic-string: 0.30.17
|
magic-string: 0.30.17
|
||||||
zimmerframe: 1.1.2
|
zimmerframe: 1.1.2
|
||||||
|
|
||||||
|
tailwind-merge@3.3.1: {}
|
||||||
|
|
||||||
|
tailwind-variants@3.1.1(tailwind-merge@3.3.1)(tailwindcss@4.1.11):
|
||||||
|
dependencies:
|
||||||
|
tailwindcss: 4.1.11
|
||||||
|
optionalDependencies:
|
||||||
|
tailwind-merge: 3.3.1
|
||||||
|
|
||||||
tailwindcss@4.1.11: {}
|
tailwindcss@4.1.11: {}
|
||||||
|
|
||||||
tapable@2.2.2: {}
|
tapable@2.2.2: {}
|
||||||
@ -4323,6 +4371,8 @@ snapshots:
|
|||||||
|
|
||||||
tslib@2.8.1: {}
|
tslib@2.8.1: {}
|
||||||
|
|
||||||
|
tw-animate-css@1.4.0: {}
|
||||||
|
|
||||||
type-check@0.4.0:
|
type-check@0.4.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
prelude-ls: 1.2.1
|
prelude-ls: 1.2.1
|
||||||
|
@ -1,7 +1,126 @@
|
|||||||
@import 'tailwindcss';
|
@import 'tailwindcss';
|
||||||
|
@import 'tw-animate-css';
|
||||||
@plugin '@tailwindcss/typography';
|
@plugin '@tailwindcss/typography';
|
||||||
@config "../tailwind.config.js";
|
@config "../tailwind.config.js";
|
||||||
|
|
||||||
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--radius: 0.625rem;
|
||||||
|
--background: oklch(1 0 0);
|
||||||
|
--foreground: oklch(0.13 0.028 261.692);
|
||||||
|
--card: oklch(1 0 0);
|
||||||
|
--card-foreground: oklch(0.13 0.028 261.692);
|
||||||
|
--popover: oklch(1 0 0);
|
||||||
|
--popover-foreground: oklch(0.13 0.028 261.692);
|
||||||
|
--primary: oklch(0.21 0.034 264.665);
|
||||||
|
--primary-foreground: oklch(0.985 0.002 247.839);
|
||||||
|
--secondary: oklch(0.967 0.003 264.542);
|
||||||
|
--secondary-foreground: oklch(0.21 0.034 264.665);
|
||||||
|
--muted: oklch(0.967 0.003 264.542);
|
||||||
|
--muted-foreground: oklch(0.551 0.027 264.364);
|
||||||
|
--accent: oklch(0.967 0.003 264.542);
|
||||||
|
--accent-foreground: oklch(0.21 0.034 264.665);
|
||||||
|
--destructive: oklch(0.577 0.245 27.325);
|
||||||
|
--border: oklch(0.928 0.006 264.531);
|
||||||
|
--input: oklch(0.928 0.006 264.531);
|
||||||
|
--ring: oklch(0.707 0.022 261.325);
|
||||||
|
--chart-1: oklch(0.646 0.222 41.116);
|
||||||
|
--chart-2: oklch(0.6 0.118 184.704);
|
||||||
|
--chart-3: oklch(0.398 0.07 227.392);
|
||||||
|
--chart-4: oklch(0.828 0.189 84.429);
|
||||||
|
--chart-5: oklch(0.769 0.188 70.08);
|
||||||
|
--sidebar: oklch(0.985 0.002 247.839);
|
||||||
|
--sidebar-foreground: oklch(0.13 0.028 261.692);
|
||||||
|
--sidebar-primary: oklch(0.21 0.034 264.665);
|
||||||
|
--sidebar-primary-foreground: oklch(0.985 0.002 247.839);
|
||||||
|
--sidebar-accent: oklch(0.967 0.003 264.542);
|
||||||
|
--sidebar-accent-foreground: oklch(0.21 0.034 264.665);
|
||||||
|
--sidebar-border: oklch(0.928 0.006 264.531);
|
||||||
|
--sidebar-ring: oklch(0.707 0.022 261.325);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: oklch(0.13 0.028 261.692);
|
||||||
|
--foreground: oklch(0.985 0.002 247.839);
|
||||||
|
--card: oklch(0.21 0.034 264.665);
|
||||||
|
--card-foreground: oklch(0.985 0.002 247.839);
|
||||||
|
--popover: oklch(0.21 0.034 264.665);
|
||||||
|
--popover-foreground: oklch(0.985 0.002 247.839);
|
||||||
|
--primary: oklch(0.928 0.006 264.531);
|
||||||
|
--primary-foreground: oklch(0.21 0.034 264.665);
|
||||||
|
--secondary: oklch(0.278 0.033 256.848);
|
||||||
|
--secondary-foreground: oklch(0.985 0.002 247.839);
|
||||||
|
--muted: oklch(0.278 0.033 256.848);
|
||||||
|
--muted-foreground: oklch(0.707 0.022 261.325);
|
||||||
|
--accent: oklch(0.278 0.033 256.848);
|
||||||
|
--accent-foreground: oklch(0.985 0.002 247.839);
|
||||||
|
--destructive: oklch(0.704 0.191 22.216);
|
||||||
|
--border: oklch(1 0 0 / 10%);
|
||||||
|
--input: oklch(1 0 0 / 15%);
|
||||||
|
--ring: oklch(0.551 0.027 264.364);
|
||||||
|
--chart-1: oklch(0.488 0.243 264.376);
|
||||||
|
--chart-2: oklch(0.696 0.17 162.48);
|
||||||
|
--chart-3: oklch(0.769 0.188 70.08);
|
||||||
|
--chart-4: oklch(0.627 0.265 303.9);
|
||||||
|
--chart-5: oklch(0.645 0.246 16.439);
|
||||||
|
--sidebar: oklch(0.21 0.034 264.665);
|
||||||
|
--sidebar-foreground: oklch(0.985 0.002 247.839);
|
||||||
|
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||||
|
--sidebar-primary-foreground: oklch(0.985 0.002 247.839);
|
||||||
|
--sidebar-accent: oklch(0.278 0.033 256.848);
|
||||||
|
--sidebar-accent-foreground: oklch(0.985 0.002 247.839);
|
||||||
|
--sidebar-border: oklch(1 0 0 / 10%);
|
||||||
|
--sidebar-ring: oklch(0.551 0.027 264.364);
|
||||||
|
}
|
||||||
|
|
||||||
|
@theme inline {
|
||||||
|
--radius-sm: calc(var(--radius) - 4px);
|
||||||
|
--radius-md: calc(var(--radius) - 2px);
|
||||||
|
--radius-lg: var(--radius);
|
||||||
|
--radius-xl: calc(var(--radius) + 4px);
|
||||||
|
--color-background: var(--background);
|
||||||
|
--color-foreground: var(--foreground);
|
||||||
|
--color-card: var(--card);
|
||||||
|
--color-card-foreground: var(--card-foreground);
|
||||||
|
--color-popover: var(--popover);
|
||||||
|
--color-popover-foreground: var(--popover-foreground);
|
||||||
|
--color-primary: var(--primary);
|
||||||
|
--color-primary-foreground: var(--primary-foreground);
|
||||||
|
--color-secondary: var(--secondary);
|
||||||
|
--color-secondary-foreground: var(--secondary-foreground);
|
||||||
|
--color-muted: var(--muted);
|
||||||
|
--color-muted-foreground: var(--muted-foreground);
|
||||||
|
--color-accent: var(--accent);
|
||||||
|
--color-accent-foreground: var(--accent-foreground);
|
||||||
|
--color-destructive: var(--destructive);
|
||||||
|
--color-border: var(--border);
|
||||||
|
--color-input: var(--input);
|
||||||
|
--color-ring: var(--ring);
|
||||||
|
--color-chart-1: var(--chart-1);
|
||||||
|
--color-chart-2: var(--chart-2);
|
||||||
|
--color-chart-3: var(--chart-3);
|
||||||
|
--color-chart-4: var(--chart-4);
|
||||||
|
--color-chart-5: var(--chart-5);
|
||||||
|
--color-sidebar: var(--sidebar);
|
||||||
|
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||||
|
--color-sidebar-primary: var(--sidebar-primary);
|
||||||
|
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||||
|
--color-sidebar-accent: var(--sidebar-accent);
|
||||||
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||||
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border outline-ring/50;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'HackNerdMono';
|
font-family: 'HackNerdMono';
|
||||||
src: url('/font/HackNerdMono.woff2') format('woff2');
|
src: url('/font/HackNerdMono.woff2') format('woff2');
|
||||||
@ -30,6 +149,10 @@ body {
|
|||||||
@apply bg-white font-sans text-base font-normal text-gray-700;
|
@apply bg-white font-sans text-base font-normal text-gray-700;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
@apply cursor-pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
@apply mx-auto max-w-screen-xl px-4 md:px-6;
|
@apply mx-auto max-w-screen-xl px-4 md:px-6;
|
||||||
}
|
}
|
||||||
|
1
frontend/src/app.d.ts
vendored
1
frontend/src/app.d.ts
vendored
@ -5,6 +5,7 @@ declare global {
|
|||||||
// interface Error {}
|
// interface Error {}
|
||||||
|
|
||||||
interface Locals {
|
interface Locals {
|
||||||
|
authBloc: import('$lib/auth/adapter/presenter/authBloc').AuthBloc;
|
||||||
postListBloc: import('$lib/post/adapter/presenter/postListBloc').PostListBloc;
|
postListBloc: import('$lib/post/adapter/presenter/postListBloc').PostListBloc;
|
||||||
postBloc: import('$lib/post/adapter/presenter/postBloc').PostBloc;
|
postBloc: import('$lib/post/adapter/presenter/postBloc').PostBloc;
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,14 @@ import { GetPostUseCase } from '$lib/post/application/useCase/getPostUseCase';
|
|||||||
import { PostApiServiceImpl } from '$lib/post/framework/api/postApiServiceImpl';
|
import { PostApiServiceImpl } from '$lib/post/framework/api/postApiServiceImpl';
|
||||||
import type { Handle } from '@sveltejs/kit';
|
import type { Handle } from '@sveltejs/kit';
|
||||||
import { Environment } from '$lib/environment';
|
import { Environment } from '$lib/environment';
|
||||||
|
import { AuthApiServiceImpl } from '$lib/auth/framework/api/authApiServiceImpl';
|
||||||
|
import { AuthRepositoryImpl } from '$lib/auth/adapter/gateway/authRepositoryImpl';
|
||||||
|
import type { AuthApiService } from '$lib/auth/adapter/gateway/authApiService';
|
||||||
|
import type { AuthRepository } from '$lib/auth/application/gateway/authRepository';
|
||||||
|
import type { PostApiService } from '$lib/post/adapter/gateway/postApiService';
|
||||||
|
import type { PostRepository } from '$lib/post/application/gateway/postRepository';
|
||||||
|
import { AuthBloc } from '$lib/auth/adapter/presenter/authBloc';
|
||||||
|
import { GetCurrentUserUseCase } from '$lib/auth/application/useCase/getCurrentUserUseCase';
|
||||||
|
|
||||||
Sentry.init({
|
Sentry.init({
|
||||||
dsn: Environment.SENTRY_DSN,
|
dsn: Environment.SENTRY_DSN,
|
||||||
@ -16,11 +24,16 @@ Sentry.init({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const handle: Handle = sequence(Sentry.sentryHandle(), ({ event, resolve }) => {
|
export const handle: Handle = sequence(Sentry.sentryHandle(), ({ event, resolve }) => {
|
||||||
const postApiService = new PostApiServiceImpl(event.fetch);
|
const authApiService: AuthApiService = new AuthApiServiceImpl(event.fetch);
|
||||||
const postRepository = new PostRepositoryImpl(postApiService);
|
const authRepository: AuthRepository = new AuthRepositoryImpl(authApiService);
|
||||||
|
const getCurrentUserUseCase = new GetCurrentUserUseCase(authRepository);
|
||||||
|
|
||||||
|
const postApiService: PostApiService = new PostApiServiceImpl(event.fetch);
|
||||||
|
const postRepository: PostRepository = new PostRepositoryImpl(postApiService);
|
||||||
const getAllPostsUseCase = new GetAllPostsUseCase(postRepository);
|
const getAllPostsUseCase = new GetAllPostsUseCase(postRepository);
|
||||||
const getPostUseCase = new GetPostUseCase(postRepository);
|
const getPostUseCase = new GetPostUseCase(postRepository);
|
||||||
|
|
||||||
|
event.locals.authBloc = new AuthBloc(getCurrentUserUseCase);
|
||||||
event.locals.postListBloc = new PostListBloc(getAllPostsUseCase);
|
event.locals.postListBloc = new PostListBloc(getAllPostsUseCase);
|
||||||
event.locals.postBloc = new PostBloc(getPostUseCase);
|
event.locals.postBloc = new PostBloc(getPostUseCase);
|
||||||
|
|
||||||
|
5
frontend/src/lib/auth/adapter/gateway/authApiService.ts
Normal file
5
frontend/src/lib/auth/adapter/gateway/authApiService.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import type { UserResponseDto } from '$lib/auth/adapter/gateway/userResponseDto';
|
||||||
|
|
||||||
|
export interface AuthApiService {
|
||||||
|
getCurrentUser(): Promise<UserResponseDto | null>;
|
||||||
|
}
|
12
frontend/src/lib/auth/adapter/gateway/authRepositoryImpl.ts
Normal file
12
frontend/src/lib/auth/adapter/gateway/authRepositoryImpl.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import type { AuthApiService } from '$lib/auth/adapter/gateway/authApiService';
|
||||||
|
import type { AuthRepository } from '$lib/auth/application/gateway/authRepository';
|
||||||
|
import type { User } from '$lib/auth/domain/entity/user';
|
||||||
|
|
||||||
|
export class AuthRepositoryImpl implements AuthRepository {
|
||||||
|
constructor(private readonly authApiService: AuthApiService) {}
|
||||||
|
|
||||||
|
async getCurrentUser(): Promise<User | null> {
|
||||||
|
const result = await this.authApiService.getCurrentUser();
|
||||||
|
return result?.toEntity() ?? null;
|
||||||
|
}
|
||||||
|
}
|
37
frontend/src/lib/auth/adapter/gateway/userResponseDto.ts
Normal file
37
frontend/src/lib/auth/adapter/gateway/userResponseDto.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { User } from '$lib/auth/domain/entity/user';
|
||||||
|
import z from 'zod';
|
||||||
|
|
||||||
|
export const UserResponseSchema = z.object({
|
||||||
|
id: z.int32(),
|
||||||
|
displayed_name: z.string(),
|
||||||
|
email: z.email(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export class UserResponseDto {
|
||||||
|
readonly id: number;
|
||||||
|
readonly displayedName: string;
|
||||||
|
readonly email: string;
|
||||||
|
|
||||||
|
private constructor(props: { id: number; displayedName: string; email: string }) {
|
||||||
|
this.id = props.id;
|
||||||
|
this.displayedName = props.displayedName;
|
||||||
|
this.email = props.email;
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromJson(json: unknown): UserResponseDto {
|
||||||
|
const parsedJson = UserResponseSchema.parse(json);
|
||||||
|
return new UserResponseDto({
|
||||||
|
id: parsedJson.id,
|
||||||
|
displayedName: parsedJson.displayed_name,
|
||||||
|
email: parsedJson.email,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
toEntity(): User {
|
||||||
|
return new User({
|
||||||
|
id: this.id,
|
||||||
|
name: this.displayedName,
|
||||||
|
email: this.email,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
59
frontend/src/lib/auth/adapter/presenter/authBloc.ts
Normal file
59
frontend/src/lib/auth/adapter/presenter/authBloc.ts
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import { AuthViewModel } from '$lib/auth/adapter/presenter/authViewModel';
|
||||||
|
import { UserViewModel } from '$lib/auth/adapter/presenter/userViewModel';
|
||||||
|
import type { GetCurrentUserUseCase } from '$lib/auth/application/useCase/getCurrentUserUseCase';
|
||||||
|
import { StatusType, type AsyncState } from '$lib/common/adapter/presenter/asyncState';
|
||||||
|
import { get, writable } from 'svelte/store';
|
||||||
|
|
||||||
|
export type AuthState = AsyncState<AuthViewModel>;
|
||||||
|
export type AuthEvent = CurrentUserLoadedEvent;
|
||||||
|
|
||||||
|
export class AuthBloc {
|
||||||
|
private readonly state = writable<AuthState>({
|
||||||
|
status: StatusType.Idle,
|
||||||
|
});
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly getCurrentUserUseCase: GetCurrentUserUseCase,
|
||||||
|
initialData?: AuthViewModel
|
||||||
|
) {
|
||||||
|
this.state.set({
|
||||||
|
status: StatusType.Idle,
|
||||||
|
data: initialData,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get subscribe() {
|
||||||
|
return this.state.subscribe;
|
||||||
|
}
|
||||||
|
|
||||||
|
async dispatch(event: AuthEvent): Promise<AuthState> {
|
||||||
|
switch (event.event) {
|
||||||
|
case AuthEventType.CurrentUserLoadedEvent:
|
||||||
|
return this.loadCurrentUser();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async loadCurrentUser(): Promise<AuthState> {
|
||||||
|
this.state.set({ status: StatusType.Loading, data: get(this.state).data });
|
||||||
|
|
||||||
|
const user = await this.getCurrentUserUseCase.execute();
|
||||||
|
|
||||||
|
const userViewModel = user ? UserViewModel.fromEntity(user) : null;
|
||||||
|
const authViewModel = AuthViewModel.fromEntity(userViewModel);
|
||||||
|
const result: AuthState = {
|
||||||
|
status: StatusType.Success,
|
||||||
|
data: authViewModel,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.state.set(result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum AuthEventType {
|
||||||
|
CurrentUserLoadedEvent,
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CurrentUserLoadedEvent {
|
||||||
|
event: AuthEventType.CurrentUserLoadedEvent;
|
||||||
|
}
|
33
frontend/src/lib/auth/adapter/presenter/authViewModel.ts
Normal file
33
frontend/src/lib/auth/adapter/presenter/authViewModel.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { UserViewModel, type DehydratedUserProps } from '$lib/auth/adapter/presenter/userViewModel';
|
||||||
|
|
||||||
|
export class AuthViewModel {
|
||||||
|
readonly user: UserViewModel | null;
|
||||||
|
|
||||||
|
constructor(params: { user: UserViewModel | null }) {
|
||||||
|
this.user = params.user;
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromEntity(user: UserViewModel | null): AuthViewModel {
|
||||||
|
return new AuthViewModel({ user });
|
||||||
|
}
|
||||||
|
|
||||||
|
static rehydrate(props: DehydratedAuthProps): AuthViewModel {
|
||||||
|
return new AuthViewModel({
|
||||||
|
user: props.user ? UserViewModel.rehydrate(props.user) : null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
dehydrate(): DehydratedAuthProps {
|
||||||
|
return {
|
||||||
|
user: this.user ? this.user.dehydrate() : null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
get isAuthenticated(): boolean {
|
||||||
|
return this.user !== null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DehydratedAuthProps {
|
||||||
|
user: DehydratedUserProps | null;
|
||||||
|
}
|
43
frontend/src/lib/auth/adapter/presenter/userViewModel.ts
Normal file
43
frontend/src/lib/auth/adapter/presenter/userViewModel.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import type { User } from '$lib/auth/domain/entity/user';
|
||||||
|
|
||||||
|
export class UserViewModel {
|
||||||
|
readonly id: number;
|
||||||
|
readonly name: string;
|
||||||
|
readonly email: string;
|
||||||
|
|
||||||
|
private constructor(props: { id: number; name: string; email: string }) {
|
||||||
|
this.id = props.id;
|
||||||
|
this.name = props.name;
|
||||||
|
this.email = props.email;
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromEntity(user: User): UserViewModel {
|
||||||
|
return new UserViewModel({
|
||||||
|
id: user.id,
|
||||||
|
name: user.name,
|
||||||
|
email: user.email,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static rehydrate(props: DehydratedUserProps): UserViewModel {
|
||||||
|
return new UserViewModel({
|
||||||
|
id: props.id,
|
||||||
|
name: props.name,
|
||||||
|
email: props.email,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
dehydrate(): DehydratedUserProps {
|
||||||
|
return {
|
||||||
|
id: this.id,
|
||||||
|
name: this.name,
|
||||||
|
email: this.email,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DehydratedUserProps {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
}
|
@ -0,0 +1,5 @@
|
|||||||
|
import type { User } from '$lib/auth/domain/entity/user';
|
||||||
|
|
||||||
|
export interface AuthRepository {
|
||||||
|
getCurrentUser(): Promise<User | null>;
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
import type { AuthRepository } from '$lib/auth/application/gateway/authRepository';
|
||||||
|
import type { User } from '$lib/auth/domain/entity/user';
|
||||||
|
|
||||||
|
export class GetCurrentUserUseCase {
|
||||||
|
constructor(private readonly authRepository: AuthRepository) {}
|
||||||
|
|
||||||
|
async execute(): Promise<User | null> {
|
||||||
|
return await this.authRepository.getCurrentUser();
|
||||||
|
}
|
||||||
|
}
|
11
frontend/src/lib/auth/domain/entity/user.ts
Normal file
11
frontend/src/lib/auth/domain/entity/user.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
export class User {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
|
||||||
|
constructor(props: { id: number; name: string; email: string }) {
|
||||||
|
this.id = props.id;
|
||||||
|
this.name = props.name;
|
||||||
|
this.email = props.email;
|
||||||
|
}
|
||||||
|
}
|
20
frontend/src/lib/auth/framework/api/authApiServiceImpl.ts
Normal file
20
frontend/src/lib/auth/framework/api/authApiServiceImpl.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import type { AuthApiService } from '$lib/auth/adapter/gateway/authApiService';
|
||||||
|
import { UserResponseDto } from '$lib/auth/adapter/gateway/userResponseDto';
|
||||||
|
import { Environment } from '$lib/environment';
|
||||||
|
|
||||||
|
export class AuthApiServiceImpl implements AuthApiService {
|
||||||
|
constructor(private readonly fetchFn: typeof fetch) {}
|
||||||
|
|
||||||
|
async getCurrentUser(): Promise<UserResponseDto | null> {
|
||||||
|
const url = new URL('me', Environment.API_BASE_URL);
|
||||||
|
|
||||||
|
const response = await this.fetchFn(url);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const json = await response.json();
|
||||||
|
return UserResponseDto.fromJson(json);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,82 @@
|
|||||||
|
<script lang="ts" module>
|
||||||
|
import { cn, type WithElementRef } from '$lib/common/framework/components/utils.js';
|
||||||
|
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from 'svelte/elements';
|
||||||
|
import { type VariantProps, tv } from 'tailwind-variants';
|
||||||
|
|
||||||
|
export const buttonVariants = tv({
|
||||||
|
base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium outline-none transition-all focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: 'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90',
|
||||||
|
destructive:
|
||||||
|
'bg-destructive shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60 text-white',
|
||||||
|
outline:
|
||||||
|
'bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 border',
|
||||||
|
secondary: 'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80',
|
||||||
|
ghost: 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
|
||||||
|
link: 'text-primary underline-offset-4 hover:underline',
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: 'h-9 px-4 py-2 has-[>svg]:px-3',
|
||||||
|
sm: 'h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5',
|
||||||
|
lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
|
||||||
|
icon: 'size-9',
|
||||||
|
'icon-sm': 'size-8',
|
||||||
|
'icon-lg': 'size-10',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: 'default',
|
||||||
|
size: 'default',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ButtonVariant = VariantProps<typeof buttonVariants>['variant'];
|
||||||
|
export type ButtonSize = VariantProps<typeof buttonVariants>['size'];
|
||||||
|
|
||||||
|
export type ButtonProps = WithElementRef<HTMLButtonAttributes> &
|
||||||
|
WithElementRef<HTMLAnchorAttributes> & {
|
||||||
|
variant?: ButtonVariant;
|
||||||
|
size?: ButtonSize;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
let {
|
||||||
|
class: className,
|
||||||
|
variant = 'default',
|
||||||
|
size = 'default',
|
||||||
|
ref = $bindable(null),
|
||||||
|
href = undefined,
|
||||||
|
type = 'button',
|
||||||
|
disabled,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: ButtonProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if href}
|
||||||
|
<a
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="button"
|
||||||
|
class={cn(buttonVariants({ variant, size }), className)}
|
||||||
|
href={disabled ? undefined : href}
|
||||||
|
aria-disabled={disabled}
|
||||||
|
role={disabled ? 'link' : undefined}
|
||||||
|
tabindex={disabled ? -1 : undefined}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</a>
|
||||||
|
{:else}
|
||||||
|
<button
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="button"
|
||||||
|
class={cn(buttonVariants({ variant, size }), className)}
|
||||||
|
{type}
|
||||||
|
{disabled}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</button>
|
||||||
|
{/if}
|
@ -0,0 +1,17 @@
|
|||||||
|
import Root, {
|
||||||
|
type ButtonProps,
|
||||||
|
type ButtonSize,
|
||||||
|
type ButtonVariant,
|
||||||
|
buttonVariants,
|
||||||
|
} from './button.svelte';
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
type ButtonProps as Props,
|
||||||
|
//
|
||||||
|
Root as Button,
|
||||||
|
buttonVariants,
|
||||||
|
type ButtonProps,
|
||||||
|
type ButtonSize,
|
||||||
|
type ButtonVariant,
|
||||||
|
};
|
13
frontend/src/lib/common/framework/components/utils.ts
Normal file
13
frontend/src/lib/common/framework/components/utils.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { clsx, type ClassValue } from 'clsx';
|
||||||
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs));
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
export type WithoutChild<T> = T extends { child?: any } ? Omit<T, 'child'> : T;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
export type WithoutChildren<T> = T extends { children?: any } ? Omit<T, 'children'> : T;
|
||||||
|
export type WithoutChildrenOrChild<T> = WithoutChildren<WithoutChild<T>>;
|
||||||
|
export type WithElementRef<T, U extends HTMLElement = HTMLElement> = T & { ref?: U | null };
|
14
frontend/src/lib/common/framework/ui/ErrorPage.svelte
Normal file
14
frontend/src/lib/common/framework/ui/ErrorPage.svelte
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<div
|
||||||
|
class="mx-auto flex min-h-content-height max-w-screen-xl flex-col items-center justify-center px-4 md:px-6"
|
||||||
|
>
|
||||||
|
<div class="flex flex-row items-end gap-x-4 md:gap-x-6">
|
||||||
|
<h1 class="text-5xl font-extrabold text-gray-800 underline md:text-7xl">404</h1>
|
||||||
|
<h2 class="flex flex-row items-center gap-x-2 text-2xl font-bold md:gap-x-2.5 md:text-3xl">
|
||||||
|
<div class="h-7 w-1.5 bg-gray-800 md:h-9 md:w-2"></div>
|
||||||
|
<div class="rounded-md bg-blue-600 px-1 py-px md:px-1.5 md:py-0.5">
|
||||||
|
<span class="text-white">Not</span>
|
||||||
|
</div>
|
||||||
|
<span class="text-gray-800">Found.</span>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -3,7 +3,7 @@
|
|||||||
import NavbarAction from '$lib/common/framework/ui/NavbarAction.svelte';
|
import NavbarAction from '$lib/common/framework/ui/NavbarAction.svelte';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="border-b border-gray-300">
|
<nav class="border-b border-gray-300">
|
||||||
<div
|
<div
|
||||||
class="mx-auto flex h-toolbar-height max-w-screen-xl flex-row items-center justify-between px-4 md:px-6"
|
class="mx-auto flex h-toolbar-height max-w-screen-xl flex-row items-center justify-between px-4 md:px-6"
|
||||||
>
|
>
|
||||||
@ -20,4 +20,4 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</nav>
|
||||||
|
@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { page } from '$app/state';
|
||||||
|
import { Button } from '$lib/common/framework/components/ui/button/index';
|
||||||
|
import type { DashboardLink } from '$lib/dashboard/framework/ui/dashboardLink';
|
||||||
|
|
||||||
|
const { link }: { link: DashboardLink } = $props();
|
||||||
|
|
||||||
|
const isSelected = $derived.by(() => {
|
||||||
|
const pathname = page.url.pathname;
|
||||||
|
return pathname === link.href || pathname.startsWith(link.href + '/');
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<a href={link.href}>
|
||||||
|
<Button variant={isSelected ? 'outline' : 'ghost'} class="w-full">
|
||||||
|
<span class="w-full text-left">
|
||||||
|
{link.label}
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
</a>
|
@ -0,0 +1,15 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { DashboardLink } from '$lib/dashboard/framework/ui/dashboardLink';
|
||||||
|
import DashboardLinkButton from '$lib/dashboard/framework/ui/DashboardLinkButton.svelte';
|
||||||
|
|
||||||
|
const { links }: { links: DashboardLink[] } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="w-3xs border-e border-true-gray-300 p-8">
|
||||||
|
<div class="mb-3 font-bold">Dashboard</div>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
{#each links as link (link.href)}
|
||||||
|
<DashboardLinkButton {link} />
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
4
frontend/src/lib/dashboard/framework/ui/dashboardLink.ts
Normal file
4
frontend/src/lib/dashboard/framework/ui/dashboardLink.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export interface DashboardLink {
|
||||||
|
label: string;
|
||||||
|
href: string;
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
import type { PostApiService } from '$lib/post/adapter/gateway/postApiService';
|
import type { PostApiService } from '$lib/post/adapter/gateway/postApiService';
|
||||||
import type { PostRepository } from '$lib/post/application/repository/postRepository';
|
import type { PostRepository } from '$lib/post/application/gateway/postRepository';
|
||||||
import type { Post } from '$lib/post/domain/entity/post';
|
import type { Post } from '$lib/post/domain/entity/post';
|
||||||
import type { PostInfo } from '$lib/post/domain/entity/postInfo';
|
import type { PostInfo } from '$lib/post/domain/entity/postInfo';
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import type { PostRepository } from '$lib/post/application/repository/postRepository';
|
import type { PostRepository } from '$lib/post/application/gateway/postRepository';
|
||||||
import type { PostInfo } from '$lib/post/domain/entity/postInfo';
|
import type { PostInfo } from '$lib/post/domain/entity/postInfo';
|
||||||
|
|
||||||
export class GetAllPostsUseCase {
|
export class GetAllPostsUseCase {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import type { PostRepository } from '$lib/post/application/repository/postRepository';
|
import type { PostRepository } from '$lib/post/application/gateway/postRepository';
|
||||||
import type { Post } from '$lib/post/domain/entity/post';
|
import type { Post } from '$lib/post/domain/entity/post';
|
||||||
|
|
||||||
export class GetPostUseCase {
|
export class GetPostUseCase {
|
||||||
|
@ -4,12 +4,12 @@ import { PostInfoResponseDto } from '$lib/post/adapter/gateway/postInfoResponseD
|
|||||||
import { PostResponseDto } from '$lib/post/adapter/gateway/postResponseDto';
|
import { PostResponseDto } from '$lib/post/adapter/gateway/postResponseDto';
|
||||||
|
|
||||||
export class PostApiServiceImpl implements PostApiService {
|
export class PostApiServiceImpl implements PostApiService {
|
||||||
constructor(private fetchFn: typeof fetch) {}
|
constructor(private readonly fetchFn: typeof fetch) {}
|
||||||
|
|
||||||
async getAllPosts(): Promise<PostInfoResponseDto[]> {
|
async getAllPosts(): Promise<PostInfoResponseDto[]> {
|
||||||
const url = new URL('post', Environment.API_BASE_URL);
|
const url = new URL('post', Environment.API_BASE_URL);
|
||||||
|
|
||||||
const response = await this.fetchFn(url.href);
|
const response = await this.fetchFn(url);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
return [];
|
return [];
|
||||||
@ -22,7 +22,7 @@ export class PostApiServiceImpl implements PostApiService {
|
|||||||
async getPost(id: string): Promise<PostResponseDto | null> {
|
async getPost(id: string): Promise<PostResponseDto | null> {
|
||||||
const url = new URL(`post/${id}`, Environment.API_BASE_URL);
|
const url = new URL(`post/${id}`, Environment.API_BASE_URL);
|
||||||
|
|
||||||
const response = await this.fetchFn(url.href);
|
const response = await this.fetchFn(url);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -1,14 +1,5 @@
|
|||||||
<div
|
<script>
|
||||||
class="mx-auto flex min-h-content-height max-w-screen-xl flex-col items-center justify-center px-4 md:px-6"
|
import ErrorPage from '$lib/common/framework/ui/ErrorPage.svelte';
|
||||||
>
|
</script>
|
||||||
<div class="flex flex-row items-end gap-x-4 md:gap-x-6">
|
|
||||||
<h1 class="text-5xl font-extrabold text-gray-800 underline md:text-7xl">404</h1>
|
<ErrorPage />
|
||||||
<h2 class="flex flex-row items-center gap-x-2 text-2xl font-bold md:gap-x-2.5 md:text-3xl">
|
|
||||||
<div class="h-7 w-1.5 bg-gray-800 md:h-9 md:w-2"></div>
|
|
||||||
<div class="rounded-md bg-blue-600 px-1 py-px md:px-1.5 md:py-0.5">
|
|
||||||
<span class="text-white">Not</span>
|
|
||||||
</div>
|
|
||||||
<span class="text-gray-800">Found.</span>
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
@ -12,6 +12,8 @@
|
|||||||
</svelte:head>
|
</svelte:head>
|
||||||
<div class="min-h-screen">
|
<div class="min-h-screen">
|
||||||
<Navbar />
|
<Navbar />
|
||||||
|
<main>
|
||||||
<slot />
|
<slot />
|
||||||
|
</main>
|
||||||
</div>
|
</div>
|
||||||
<Footer />
|
<Footer />
|
||||||
|
56
frontend/src/routes/dashboard/+layout.svelte
Normal file
56
frontend/src/routes/dashboard/+layout.svelte
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { AuthApiService } from '$lib/auth/adapter/gateway/authApiService';
|
||||||
|
import { AuthRepositoryImpl } from '$lib/auth/adapter/gateway/authRepositoryImpl';
|
||||||
|
import { AuthBloc, AuthEventType } from '$lib/auth/adapter/presenter/authBloc';
|
||||||
|
import type { AuthRepository } from '$lib/auth/application/gateway/authRepository';
|
||||||
|
import { GetCurrentUserUseCase } from '$lib/auth/application/useCase/getCurrentUserUseCase';
|
||||||
|
import { AuthApiServiceImpl } from '$lib/auth/framework/api/authApiServiceImpl';
|
||||||
|
import { onMount, setContext } from 'svelte';
|
||||||
|
import type { LayoutProps } from './$types';
|
||||||
|
import { StatusType } from '$lib/common/adapter/presenter/asyncState';
|
||||||
|
import ErrorPage from '$lib/common/framework/ui/ErrorPage.svelte';
|
||||||
|
import DashboardNavbar from '$lib/dashboard/framework/ui/DashboardNavbar.svelte';
|
||||||
|
import type { DashboardLink } from '$lib/dashboard/framework/ui/dashboardLink';
|
||||||
|
|
||||||
|
const { children }: LayoutProps = $props();
|
||||||
|
|
||||||
|
const authApiService: AuthApiService = new AuthApiServiceImpl(fetch);
|
||||||
|
const authRepository: AuthRepository = new AuthRepositoryImpl(authApiService);
|
||||||
|
const getcurrentUserUseCase = new GetCurrentUserUseCase(authRepository);
|
||||||
|
const authBloc = new AuthBloc(getcurrentUserUseCase);
|
||||||
|
|
||||||
|
setContext(AuthBloc.name, authBloc);
|
||||||
|
|
||||||
|
onMount(() => authBloc.dispatch({ event: AuthEventType.CurrentUserLoadedEvent }));
|
||||||
|
|
||||||
|
const authState = $derived($authBloc);
|
||||||
|
const isLoading = $derived.by(
|
||||||
|
() => authState.status === StatusType.Loading || authState.status === StatusType.Idle
|
||||||
|
);
|
||||||
|
const hasError = $derived.by(() => {
|
||||||
|
if (authState.status === StatusType.Error) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (authState.status === StatusType.Success && !authState.data.isAuthenticated) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
const links: DashboardLink[] = [
|
||||||
|
{ label: 'Post', href: '/dashboard/post' },
|
||||||
|
{ label: 'Label', href: '/dashboard/label' },
|
||||||
|
{ label: 'Image', href: '/dashboard/image' },
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if isLoading}
|
||||||
|
<div></div>
|
||||||
|
{:else if hasError}
|
||||||
|
<ErrorPage />
|
||||||
|
{:else}
|
||||||
|
<div class="grid min-h-content-height grid-cols-[auto_1fr]">
|
||||||
|
<DashboardNavbar {links} />
|
||||||
|
{@render children()}
|
||||||
|
</div>
|
||||||
|
{/if}
|
11
frontend/src/routes/dashboard/+page.svelte
Normal file
11
frontend/src/routes/dashboard/+page.svelte
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
import { page } from '$app/state';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (page.url.pathname === '/dashboard') {
|
||||||
|
goto('/dashboard/post');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
1
frontend/src/routes/dashboard/image/+page.svelte
Normal file
1
frontend/src/routes/dashboard/image/+page.svelte
Normal file
@ -0,0 +1 @@
|
|||||||
|
<div>Image</div>
|
1
frontend/src/routes/dashboard/label/+page.svelte
Normal file
1
frontend/src/routes/dashboard/label/+page.svelte
Normal file
@ -0,0 +1 @@
|
|||||||
|
<div>Label</div>
|
1
frontend/src/routes/dashboard/post/+page.svelte
Normal file
1
frontend/src/routes/dashboard/post/+page.svelte
Normal file
@ -0,0 +1 @@
|
|||||||
|
<div>Post</div>
|
@ -7,13 +7,15 @@
|
|||||||
import type { PageProps } from './$types';
|
import type { PageProps } from './$types';
|
||||||
import { PostInfoViewModel } from '$lib/post/adapter/presenter/postInfoViewModel';
|
import { PostInfoViewModel } from '$lib/post/adapter/presenter/postInfoViewModel';
|
||||||
import PostOverallPage from '$lib/post/framework/ui/PostOverallPage.svelte';
|
import PostOverallPage from '$lib/post/framework/ui/PostOverallPage.svelte';
|
||||||
|
import type { PostApiService } from '$lib/post/adapter/gateway/postApiService';
|
||||||
|
import type { PostRepository } from '$lib/post/application/gateway/postRepository';
|
||||||
|
|
||||||
let { data }: PageProps = $props();
|
let { data }: PageProps = $props();
|
||||||
|
|
||||||
const initialData = data.dehydratedData?.map((post) => PostInfoViewModel.rehydrate(post));
|
const initialData = data.dehydratedData?.map((post) => PostInfoViewModel.rehydrate(post));
|
||||||
|
|
||||||
const postApiService = new PostApiServiceImpl(fetch);
|
const postApiService: PostApiService = new PostApiServiceImpl(fetch);
|
||||||
const postRepository = new PostRepositoryImpl(postApiService);
|
const postRepository: PostRepository = new PostRepositoryImpl(postApiService);
|
||||||
const getAllPostsUseCase = new GetAllPostsUseCase(postRepository);
|
const getAllPostsUseCase = new GetAllPostsUseCase(postRepository);
|
||||||
const postListBloc = new PostListBloc(getAllPostsUseCase, initialData);
|
const postListBloc = new PostListBloc(getAllPostsUseCase, initialData);
|
||||||
|
|
||||||
|
@ -7,14 +7,16 @@
|
|||||||
import { setContext } from 'svelte';
|
import { setContext } from 'svelte';
|
||||||
import type { PageProps } from './$types';
|
import type { PageProps } from './$types';
|
||||||
import PostContentPage from '$lib/post/framework/ui/PostContentPage.svelte';
|
import PostContentPage from '$lib/post/framework/ui/PostContentPage.svelte';
|
||||||
|
import type { PostApiService } from '$lib/post/adapter/gateway/postApiService';
|
||||||
|
import type { PostRepository } from '$lib/post/application/gateway/postRepository';
|
||||||
|
|
||||||
const { data, params }: PageProps = $props();
|
const { data, params }: PageProps = $props();
|
||||||
const { id } = params;
|
const { id } = params;
|
||||||
|
|
||||||
const initialData = PostViewModel.rehydrate(data.dehydratedData!);
|
const initialData = PostViewModel.rehydrate(data.dehydratedData!);
|
||||||
|
|
||||||
const postApiService = new PostApiServiceImpl(fetch);
|
const postApiService: PostApiService = new PostApiServiceImpl(fetch);
|
||||||
const postRepository = new PostRepositoryImpl(postApiService);
|
const postRepository: PostRepository = new PostRepositoryImpl(postApiService);
|
||||||
const getPostUseCase = new GetPostUseCase(postRepository);
|
const getPostUseCase = new GetPostUseCase(postRepository);
|
||||||
const postBloc = new PostBloc(getPostUseCase, initialData);
|
const postBloc = new PostBloc(getPostUseCase, initialData);
|
||||||
|
|
||||||
|
@ -1,2 +1,4 @@
|
|||||||
User-agent: *
|
User-agent: *
|
||||||
Allow: /
|
Allow: /
|
||||||
|
Disallow: /dashboard/
|
||||||
|
Disallow: /api/
|
||||||
|
Loading…
x
Reference in New Issue
Block a user