Compare commits
3 commits
c1cd1d7eb5
...
b158d725d1
Author | SHA1 | Date | |
---|---|---|---|
|
b158d725d1 | ||
|
ae1fda71a8 | ||
|
926971aa91 |
37 changed files with 719 additions and 132 deletions
40
Dockerfile
Normal file
40
Dockerfile
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
|
||||||
|
FROM node:18-alpine AS base
|
||||||
|
|
||||||
|
RUN npm i -g pnpm
|
||||||
|
|
||||||
|
FROM base AS dependencies
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package.json pnpm-lock.yaml ./
|
||||||
|
|
||||||
|
RUN pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
FROM base AS build
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
COPY --from=dependencies /app/node_modules ./node_modules
|
||||||
|
|
||||||
|
RUN pnpm build
|
||||||
|
|
||||||
|
RUN pnpm prune --prod
|
||||||
|
|
||||||
|
FROM base AS deploy
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY --from=build /app/build ./build
|
||||||
|
COPY --from=build /app/node_modules ./node_modules
|
||||||
|
COPY --from=build /app/package.json ./package.json
|
||||||
|
|
||||||
|
ARG PORT=3000
|
||||||
|
|
||||||
|
ENV NODE_ENV=production PORT=$PORT
|
||||||
|
|
||||||
|
EXPOSE $PORT
|
||||||
|
|
||||||
|
CMD ["node", "build"]
|
|
@ -19,6 +19,7 @@
|
||||||
"@sveltejs/adapter-auto": "^2.0.0",
|
"@sveltejs/adapter-auto": "^2.0.0",
|
||||||
"@sveltejs/adapter-node": "^1.3.1",
|
"@sveltejs/adapter-node": "^1.3.1",
|
||||||
"@sveltejs/kit": "^1.20.4",
|
"@sveltejs/kit": "^1.20.4",
|
||||||
|
"@types/marked": "^5.0.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.45.0",
|
"@typescript-eslint/eslint-plugin": "^5.45.0",
|
||||||
"@typescript-eslint/parser": "^5.45.0",
|
"@typescript-eslint/parser": "^5.45.0",
|
||||||
"autoprefixer": "^10.4.14",
|
"autoprefixer": "^10.4.14",
|
||||||
|
@ -41,6 +42,9 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.0.0",
|
"clsx": "^2.0.0",
|
||||||
"tailwind-merge": "^1.14.0"
|
"marked": "^7.0.1",
|
||||||
|
"svelte-boring-avatars": "^1.2.4",
|
||||||
|
"tailwind-merge": "^1.14.0",
|
||||||
|
"zod": "^3.21.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ import type { PlaywrightTestConfig } from '@playwright/test';
|
||||||
|
|
||||||
const config: PlaywrightTestConfig = {
|
const config: PlaywrightTestConfig = {
|
||||||
webServer: {
|
webServer: {
|
||||||
command: 'npm run build && npm run preview',
|
command: 'pnpm build && pnpm preview',
|
||||||
port: 4173
|
port: 4173
|
||||||
},
|
},
|
||||||
testDir: 'tests',
|
testDir: 'tests',
|
||||||
|
|
30
pnpm-lock.yaml
generated
30
pnpm-lock.yaml
generated
|
@ -11,9 +11,18 @@ dependencies:
|
||||||
clsx:
|
clsx:
|
||||||
specifier: ^2.0.0
|
specifier: ^2.0.0
|
||||||
version: 2.0.0
|
version: 2.0.0
|
||||||
|
marked:
|
||||||
|
specifier: ^7.0.1
|
||||||
|
version: 7.0.1
|
||||||
|
svelte-boring-avatars:
|
||||||
|
specifier: ^1.2.4
|
||||||
|
version: 1.2.4
|
||||||
tailwind-merge:
|
tailwind-merge:
|
||||||
specifier: ^1.14.0
|
specifier: ^1.14.0
|
||||||
version: 1.14.0
|
version: 1.14.0
|
||||||
|
zod:
|
||||||
|
specifier: ^3.21.4
|
||||||
|
version: 3.21.4
|
||||||
|
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@playwright/test':
|
'@playwright/test':
|
||||||
|
@ -28,6 +37,9 @@ devDependencies:
|
||||||
'@sveltejs/kit':
|
'@sveltejs/kit':
|
||||||
specifier: ^1.20.4
|
specifier: ^1.20.4
|
||||||
version: 1.22.3(svelte@4.1.1)(vite@4.4.7)
|
version: 1.22.3(svelte@4.1.1)(vite@4.4.7)
|
||||||
|
'@types/marked':
|
||||||
|
specifier: ^5.0.1
|
||||||
|
version: 5.0.1
|
||||||
'@typescript-eslint/eslint-plugin':
|
'@typescript-eslint/eslint-plugin':
|
||||||
specifier: ^5.45.0
|
specifier: ^5.45.0
|
||||||
version: 5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.45.0)(typescript@5.1.6)
|
version: 5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.45.0)(typescript@5.1.6)
|
||||||
|
@ -606,6 +618,10 @@ packages:
|
||||||
resolution: {integrity: sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==}
|
resolution: {integrity: sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@types/marked@5.0.1:
|
||||||
|
resolution: {integrity: sha512-Y3pAUzHKh605fN6fvASsz5FDSWbZcs/65Q6xYRmnIP9ZIYz27T4IOmXfH9gWJV1dpi7f1e7z7nBGUTx/a0ptpA==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@types/node@20.4.5:
|
/@types/node@20.4.5:
|
||||||
resolution: {integrity: sha512-rt40Nk13II9JwQBdeYqmbn2Q6IVTA5uPhvSO+JVqdXw/6/4glI6oR9ezty/A9Hg5u7JH4OmYmuQ+XvjKm0Datg==}
|
resolution: {integrity: sha512-rt40Nk13II9JwQBdeYqmbn2Q6IVTA5uPhvSO+JVqdXw/6/4glI6oR9ezty/A9Hg5u7JH4OmYmuQ+XvjKm0Datg==}
|
||||||
dev: true
|
dev: true
|
||||||
|
@ -1718,6 +1734,12 @@ packages:
|
||||||
'@jridgewell/sourcemap-codec': 1.4.15
|
'@jridgewell/sourcemap-codec': 1.4.15
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/marked@7.0.1:
|
||||||
|
resolution: {integrity: sha512-m8Aze620Ts62yaciz2DghZGUkUfdgvSNRicS2/XtQkStMNoce3NWjOD2b/jWF32+XXK6udM6pRhv2dKNlneAFA==}
|
||||||
|
engines: {node: '>= 16'}
|
||||||
|
hasBin: true
|
||||||
|
dev: false
|
||||||
|
|
||||||
/mdn-data@2.0.30:
|
/mdn-data@2.0.30:
|
||||||
resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==}
|
resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==}
|
||||||
dev: true
|
dev: true
|
||||||
|
@ -2359,6 +2381,10 @@ packages:
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/svelte-boring-avatars@1.2.4:
|
||||||
|
resolution: {integrity: sha512-090ndMpf+FV1dlx723rdDf+t25hSjN12Vx1aSnyrexRxsqGy3XqKLLZ7qGEnSFTbtRi9SeycofjjFRzNIqmb2g==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/svelte-check@3.4.6(postcss@8.4.27)(svelte@4.1.1):
|
/svelte-check@3.4.6(postcss@8.4.27)(svelte@4.1.1):
|
||||||
resolution: {integrity: sha512-OBlY8866Zh1zHQTkBMPS6psPi7o2umTUyj6JWm4SacnIHXpWFm658pG32m3dKvKFL49V4ntAkfFHKo4ztH07og==}
|
resolution: {integrity: sha512-OBlY8866Zh1zHQTkBMPS6psPi7o2umTUyj6JWm4SacnIHXpWFm658pG32m3dKvKFL49V4ntAkfFHKo4ztH07og==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
@ -2812,3 +2838,7 @@ packages:
|
||||||
resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==}
|
resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==}
|
||||||
engines: {node: '>=12.20'}
|
engines: {node: '>=12.20'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/zod@3.21.4:
|
||||||
|
resolution: {integrity: sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==}
|
||||||
|
dev: false
|
||||||
|
|
21
src/app.html
21
src/app.html
|
@ -2,11 +2,30 @@
|
||||||
<html lang="fr" class="scroll-smooth bg-gradient-to-b from-primary-800 to-primary-900">
|
<html lang="fr" class="scroll-smooth bg-gradient-to-b from-primary-800 to-primary-900">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
|
|
||||||
<link rel="icon" type="image/x-icon" href="%sveltekit.assets%/favicon.ico" />
|
<link rel="icon" type="image/x-icon" href="%sveltekit.assets%/favicon.ico" />
|
||||||
|
<link
|
||||||
|
rel="apple-touch-icon"
|
||||||
|
sizes="180x180"
|
||||||
|
href="%sveltekit.assets%/assets/icons/apple-touch-icon.png"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="icon"
|
||||||
|
type="image/png"
|
||||||
|
sizes="32x32"
|
||||||
|
href="%sveltekit.assets%/assets/icons/favicon-32x32.png"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="icon"
|
||||||
|
type="image/png"
|
||||||
|
sizes="16x16"
|
||||||
|
href="%sveltekit.assets%/assets/icons/favicon-16x16.png"
|
||||||
|
/>
|
||||||
|
|
||||||
<meta name="viewport" content="width=device-width" />
|
<meta name="viewport" content="width=device-width" />
|
||||||
%sveltekit.head%
|
%sveltekit.head%
|
||||||
</head>
|
</head>
|
||||||
<body data-sveltekit-preload-data="hover" class="relative min-h-screen">
|
<body data-sveltekit-preload-data="hover" class="relative min-h-screen">
|
||||||
<div style="display: contents">%sveltekit.body%</div>
|
<main style="display: contents">%sveltekit.body%</main>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -2,6 +2,16 @@
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Karrik';
|
||||||
|
src: url('/fonts/Karrik.woff2');
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Fira Code';
|
||||||
|
src: url('/fonts/FiraCode.woff2');
|
||||||
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
:root {
|
:root {
|
||||||
--background: 0 0% 100%;
|
--background: 0 0% 100%;
|
||||||
|
|
|
@ -2,6 +2,8 @@ import type { Handle } from '@sveltejs/kit';
|
||||||
|
|
||||||
import { API_URL } from '$env/static/private';
|
import { API_URL } from '$env/static/private';
|
||||||
|
|
||||||
|
import type { User } from '$lib/types';
|
||||||
|
|
||||||
export const handle = (async ({ event, resolve }) => {
|
export const handle = (async ({ event, resolve }) => {
|
||||||
const session = event.cookies.get('session');
|
const session = event.cookies.get('session');
|
||||||
|
|
||||||
|
@ -13,7 +15,7 @@ export const handle = (async ({ event, resolve }) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const user = await res.json();
|
const user = (await res.json()) as User;
|
||||||
event.locals.user = user;
|
event.locals.user = user;
|
||||||
} else {
|
} else {
|
||||||
event.locals.user = undefined;
|
event.locals.user = undefined;
|
||||||
|
|
16
src/lib/components/Avatar.svelte
Normal file
16
src/lib/components/Avatar.svelte
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
import Avatar from 'svelte-boring-avatars';
|
||||||
|
|
||||||
|
$: user = $page.data.user;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if user?.avatar}
|
||||||
|
<img
|
||||||
|
src="data:image;base64,${user.avatar}"
|
||||||
|
alt="Avatar de {user.pseudo}"
|
||||||
|
class="h-9 w-9 rounded-full object-cover"
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<Avatar name={user?.pseudo} size={35} variant="beam" />
|
||||||
|
{/if}
|
26
src/lib/components/Icons/AlignLeft.svelte
Normal file
26
src/lib/components/Icons/AlignLeft.svelte
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { cn } from '$lib';
|
||||||
|
|
||||||
|
let className: string | undefined | null = undefined;
|
||||||
|
|
||||||
|
export { className as class };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svg
|
||||||
|
class={cn(className)}
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
><line x1="21" x2="3" y1="6" y2="6" /><line x1="15" x2="3" y1="12" y2="12" /><line
|
||||||
|
x1="17"
|
||||||
|
x2="3"
|
||||||
|
y1="18"
|
||||||
|
y2="18"
|
||||||
|
/></svg
|
||||||
|
>
|
21
src/lib/components/Icons/Badge.svelte
Normal file
21
src/lib/components/Icons/Badge.svelte
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { cn } from '$lib';
|
||||||
|
|
||||||
|
let className: string | undefined | null = undefined;
|
||||||
|
|
||||||
|
export { className as class };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svg
|
||||||
|
class={cn(className)}
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
><circle cx="12" cy="8" r="6" /><path d="M15.477 12.89 17 22l-5-3-5 3 1.523-9.11" /></svg
|
||||||
|
>
|
20
src/lib/components/Icons/ChevronRight.svelte
Normal file
20
src/lib/components/Icons/ChevronRight.svelte
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { cn } from '$lib';
|
||||||
|
|
||||||
|
let className: string | undefined | null = undefined;
|
||||||
|
|
||||||
|
export { className as class };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svg
|
||||||
|
class={cn(className)}
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"><path d="m9 18 6-6-6-6" /></svg
|
||||||
|
>
|
21
src/lib/components/Icons/Code.svelte
Normal file
21
src/lib/components/Icons/Code.svelte
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { cn } from '$lib';
|
||||||
|
|
||||||
|
let className: string | undefined | null = undefined;
|
||||||
|
|
||||||
|
export { className as class };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svg
|
||||||
|
class={cn(className)}
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
><polyline points="16 18 22 12 16 6" /><polyline points="8 6 2 12 8 18" /></svg
|
||||||
|
>
|
33
src/lib/components/Icons/Dashboard.svelte
Normal file
33
src/lib/components/Icons/Dashboard.svelte
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { cn } from '$lib';
|
||||||
|
|
||||||
|
let className: string | undefined | null = undefined;
|
||||||
|
|
||||||
|
export { className as class };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svg
|
||||||
|
class={cn(className)}
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
><rect width="7" height="9" x="3" y="3" rx="1" /><rect
|
||||||
|
width="7"
|
||||||
|
height="5"
|
||||||
|
x="14"
|
||||||
|
y="3"
|
||||||
|
rx="1"
|
||||||
|
/><rect width="7" height="9" x="14" y="12" rx="1" /><rect
|
||||||
|
width="7"
|
||||||
|
height="5"
|
||||||
|
x="3"
|
||||||
|
y="16"
|
||||||
|
rx="1"
|
||||||
|
/></svg
|
||||||
|
>
|
26
src/lib/components/Icons/Leaderboard.svelte
Normal file
26
src/lib/components/Icons/Leaderboard.svelte
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { cn } from '$lib';
|
||||||
|
|
||||||
|
let className: string | undefined | null = undefined;
|
||||||
|
|
||||||
|
export { className as class };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svg
|
||||||
|
class={cn(className)}
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
><line x1="18" x2="18" y1="20" y2="10" /><line x1="12" x2="12" y1="20" y2="4" /><line
|
||||||
|
x1="6"
|
||||||
|
x2="6"
|
||||||
|
y1="20"
|
||||||
|
y2="14"
|
||||||
|
/></svg
|
||||||
|
>
|
36
src/lib/components/Icons/Settings.svelte
Normal file
36
src/lib/components/Icons/Settings.svelte
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { cn } from '$lib';
|
||||||
|
|
||||||
|
let className: string | undefined | null = undefined;
|
||||||
|
|
||||||
|
export { className as class };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svg
|
||||||
|
class={cn(className)}
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
><line x1="21" x2="14" y1="4" y2="4" /><line x1="10" x2="3" y1="4" y2="4" /><line
|
||||||
|
x1="21"
|
||||||
|
x2="12"
|
||||||
|
y1="12"
|
||||||
|
y2="12"
|
||||||
|
/><line x1="8" x2="3" y1="12" y2="12" /><line x1="21" x2="16" y1="20" y2="20" /><line
|
||||||
|
x1="12"
|
||||||
|
x2="3"
|
||||||
|
y1="20"
|
||||||
|
y2="20"
|
||||||
|
/><line x1="14" x2="14" y1="2" y2="6" /><line x1="8" x2="8" y1="10" y2="14" /><line
|
||||||
|
x1="16"
|
||||||
|
x2="16"
|
||||||
|
y1="18"
|
||||||
|
y2="22"
|
||||||
|
/></svg
|
||||||
|
>
|
20
src/lib/components/Icons/X.svelte
Normal file
20
src/lib/components/Icons/X.svelte
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { cn } from '$lib';
|
||||||
|
|
||||||
|
let className: string | undefined | null = undefined;
|
||||||
|
|
||||||
|
export { className as class };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svg
|
||||||
|
class={cn(className)}
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"><path d="M18 6 6 18" /><path d="m6 6 12 12" /></svg
|
||||||
|
>
|
|
@ -1,8 +1,17 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
|
import Avatar from './Avatar.svelte';
|
||||||
|
import AlignLeft from './Icons/AlignLeft.svelte';
|
||||||
|
import X from './Icons/X.svelte';
|
||||||
|
|
||||||
|
export let isOpen: boolean;
|
||||||
|
|
||||||
$: user = $page.data.user;
|
$: user = $page.data.user;
|
||||||
$: segment = $page.url.pathname.slice(1).replaceAll('/', ' > ');
|
$: segment = $page.url.pathname.slice(1).replaceAll('/', ' / ');
|
||||||
|
|
||||||
|
function handleToggle() {
|
||||||
|
isOpen = !isOpen;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
@ -10,22 +19,22 @@
|
||||||
>
|
>
|
||||||
<div class="flex flex-row items-center space-x-2 sm:space-x-0">
|
<div class="flex flex-row items-center space-x-2 sm:space-x-0">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<button on:click={() => console.log('toggle')} class="block sm:hidden">
|
<button on:click={handleToggle} class="block sm:hidden">
|
||||||
<!-- {isOpen ? (
|
{#if isOpen}
|
||||||
<X size={20} class="text-muted" />
|
<X class="h-5 w-5 text-muted" />
|
||||||
) : (
|
{:else}
|
||||||
<AlignLeftIcon size={20} class="text-muted" />
|
<AlignLeft class="h-5 w-5 text-muted" />
|
||||||
)} -->
|
{/if}
|
||||||
<span>menu</span>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{#if segment}
|
{#if !isOpen && segment}
|
||||||
<div class="flex uppercase items-center justify-center text-highlight-secondary">
|
<div class="flex items-center justify-center capitalize text-highlight-secondary">
|
||||||
{segment}
|
{segment}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-row items-center space-x-4">
|
<div class="flex flex-row items-center gap-2">
|
||||||
|
<Avatar />
|
||||||
{user?.pseudo}
|
{user?.pseudo}
|
||||||
<!-- {!isLoading && me ? (
|
<!-- {!isLoading && me ? (
|
||||||
<Popover
|
<Popover
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { cn } from '$lib/Utils';
|
import { cn } from '$lib/Utils';
|
||||||
import type { Puzzle } from '$lib/types';
|
import type { Puzzle } from '$lib/types';
|
||||||
|
import ChevronRight from './Icons/ChevronRight.svelte';
|
||||||
|
|
||||||
export let puzzle: Puzzle;
|
export let puzzle: Puzzle;
|
||||||
|
|
||||||
|
@ -9,7 +10,7 @@
|
||||||
|
|
||||||
<li
|
<li
|
||||||
class={cn(
|
class={cn(
|
||||||
'group relative flex h-full w-full rounded-md border-2 bg-primary-700 font-code transition-colors duration-150 hover:bg-primary-600',
|
'font-code group relative flex h-full w-full rounded-md border-2 bg-primary-700 transition-colors duration-150 hover:bg-primary-600',
|
||||||
{
|
{
|
||||||
'border-green-600/30': puzzle.tags?.find((tag) => tag.name.toLowerCase() === 'easy'),
|
'border-green-600/30': puzzle.tags?.find((tag) => tag.name.toLowerCase() === 'easy'),
|
||||||
'border-yellow-600/30': puzzle.tags?.find((tag) => tag.name.toLowerCase() === 'medium'),
|
'border-yellow-600/30': puzzle.tags?.find((tag) => tag.name.toLowerCase() === 'medium'),
|
||||||
|
@ -19,29 +20,30 @@
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<a
|
<a class="flex h-full w-full items-center gap-4 p-4" href="/dashboard/puzzles/{puzzle.id}">
|
||||||
class="flex h-full w-full items-center justify-between p-4"
|
<div class="flex w-full flex-col justify-between gap-2 sm:flex-row">
|
||||||
href={`/dashboard/puzzles/${puzzle.id}`}
|
<h2 class="text-base font-semibold">
|
||||||
>
|
{puzzle.name}
|
||||||
<div class="flex w-10/12 flex-col gap-2 lg:w-full lg:flex-row">
|
|
||||||
<span class="text-base font-semibold">
|
|
||||||
{puzzle.name}{' '}
|
|
||||||
<span class="text-sm text-highlight-secondary">
|
<span class="text-sm text-highlight-secondary">
|
||||||
({puzzle.score ? `${puzzle.score}` : '?'}/{puzzle.scoreMax} points)
|
({puzzle.score ? `${puzzle.score}` : '?'}/{puzzle.scoreMax} points)
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</h2>
|
||||||
</div>
|
<div class="flex items-center gap-x-6">
|
||||||
<div class="flex items-center gap-x-6">
|
{#if puzzle.tags?.length}
|
||||||
{#if puzzle.tags?.length}
|
<div class="flex gap-x-2 text-sm">
|
||||||
<div class="flex gap-x-2 text-sm text-muted">
|
{#each puzzle.tags as tag}
|
||||||
{#each puzzle.tags as tag}
|
<span
|
||||||
<span class="inline-block rounded-md bg-primary-800 px-2 py-1">
|
class="inline-block rounded-md bg-primary-800 px-2 py-1 text-highlight-secondary"
|
||||||
{tag.name}
|
>
|
||||||
</span>
|
{tag.name}
|
||||||
{/each}
|
</span>
|
||||||
</div>
|
{/each}
|
||||||
{/if}
|
</div>
|
||||||
<!-- <ChevronRightIcon class="-translate-x-2 transform-gpu text-highlight-secondary duration-300 group-hover:translate-x-0 group-hover:text-brand" /> -->
|
{/if}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<span class="translate-x-0 transform-gpu duration-300 group-hover:translate-x-2">
|
||||||
|
<ChevronRight />
|
||||||
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -1,38 +1,43 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { page } from '$app/stores';
|
||||||
import { cn } from '$lib/Utils';
|
import { cn } from '$lib/Utils';
|
||||||
|
|
||||||
let isOpen = false;
|
import Badge from '$lib/components/Icons/Badge.svelte';
|
||||||
|
import Code from '$lib/components/Icons/Code.svelte';
|
||||||
|
import Dashboard from '$lib/components/Icons/Dashboard.svelte';
|
||||||
|
import Leaderboard from '$lib/components/Icons/Leaderboard.svelte';
|
||||||
|
import Settings from '$lib/components/Icons/Settings.svelte';
|
||||||
|
|
||||||
|
$: path = $page.url.pathname;
|
||||||
|
$: isActive = (slug: string) => path === slug;
|
||||||
|
|
||||||
|
export let isOpen: boolean;
|
||||||
|
|
||||||
const navItems = [
|
const navItems = [
|
||||||
{
|
{
|
||||||
name: 'Dashboard',
|
name: 'Dashboard',
|
||||||
slug: 'dashboard',
|
slug: '/dashboard',
|
||||||
icon: 'LayoutDashboard',
|
icon: Dashboard
|
||||||
disabled: false
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Classement',
|
name: 'Classement',
|
||||||
slug: 'dashboard/leaderboard',
|
slug: '/dashboard/leaderboard',
|
||||||
icon: 'BarChart2',
|
icon: Leaderboard
|
||||||
disabled: false
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Puzzles',
|
name: 'Puzzles',
|
||||||
slug: 'dashboard/puzzles',
|
slug: '/dashboard/puzzles',
|
||||||
icon: 'Code',
|
icon: Code
|
||||||
disabled: false
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Badges',
|
name: 'Badges',
|
||||||
slug: 'dashboard/badges',
|
slug: '/dashboard/badges',
|
||||||
icon: 'Award',
|
icon: Badge
|
||||||
disabled: false
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Paramètres',
|
name: 'Paramètres',
|
||||||
slug: 'dashboard/settings',
|
slug: '/dashboard/settings',
|
||||||
icon: 'Settings2',
|
icon: Settings
|
||||||
disabled: false
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
</script>
|
</script>
|
||||||
|
@ -47,35 +52,44 @@
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div class="flex h-full flex-col">
|
<div class="flex h-full flex-col">
|
||||||
<div class="flex w-full justify-center p-[9px]">
|
<div class="flex w-full justify-center p-[8.5px]">
|
||||||
<!-- <Image
|
<img
|
||||||
title="Logo"
|
src="/assets/brand/peerat.png"
|
||||||
src="/assets/brand/peerat.png"
|
alt="Logo"
|
||||||
alt="Peer-at"
|
width="50"
|
||||||
width={50}
|
height="50"
|
||||||
height={50}
|
loading="eager"
|
||||||
loading="eager"
|
draggable="false"
|
||||||
priority
|
/>
|
||||||
/> -->
|
|
||||||
<img src="/assets/brand/peerat.png" alt="Logo" width="50" height="50" loading="eager">
|
|
||||||
</div>
|
</div>
|
||||||
<div class="px-4">
|
<div class="px-4">
|
||||||
<hr class="border-highlight-primary" />
|
<hr class="border-highlight-primary" />
|
||||||
</div>
|
</div>
|
||||||
<div class="px-4 pt-4">
|
<div class="px-4 pt-4">
|
||||||
<ul class="space-y-4">
|
<ul class="flex flex-col gap-4">
|
||||||
{#each navItems as item}
|
{#each navItems as item}
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
href="/{item.slug}"
|
on:click={() => {
|
||||||
class="flex justify-center rounded-md px-3 py-3 text-sm transition-colors duration-150 lg:justify-start"
|
isOpen = false;
|
||||||
|
}}
|
||||||
|
href={item.slug}
|
||||||
|
class={cn(
|
||||||
|
'flex justify-center rounded-md px-3 py-3 text-sm transition-colors duration-150 lg:justify-start',
|
||||||
|
{
|
||||||
|
'bg-primary-700': isActive(item.slug),
|
||||||
|
'group hover:bg-primary-700': !isActive(item.slug)
|
||||||
|
}
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<div class="flex items-center space-x-2">
|
<div class="flex items-center gap-2">
|
||||||
<!-- <item.icon /> -->
|
<svelte:component this={item.icon} />
|
||||||
<span
|
<span
|
||||||
class={cn('hidden lg:block', {
|
class={cn('hidden lg:block', {
|
||||||
'block sm:hidden': isOpen,
|
'block sm:hidden': isOpen,
|
||||||
hidden: !isOpen
|
hidden: !isOpen,
|
||||||
|
'text-highlight-secondary transition-colors duration-150 group-hover:text-primary':
|
||||||
|
!isActive(item.slug)
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{item.name}
|
{item.name}
|
||||||
|
@ -90,7 +104,7 @@
|
||||||
<hr class="border-highlight-primary" />
|
<hr class="border-highlight-primary" />
|
||||||
</div>
|
</div>
|
||||||
<div class="px-4 pt-4">
|
<div class="px-4 pt-4">
|
||||||
<ul class="space-y-4">
|
<ul class="flex flex-col gap-4">
|
||||||
<li>
|
<li>
|
||||||
<!-- <NavItem
|
<!-- <NavItem
|
||||||
item={{
|
item={{
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import '../global.css';
|
import '../global.css';
|
||||||
|
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
|
||||||
|
$: origin = $page.url.origin;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>Peer-at Code</title>
|
<title>Peer-at Code</title>
|
||||||
|
|
||||||
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
|
||||||
|
|
||||||
<meta name="title" content="Peer-at Code" />
|
<meta name="title" content="Peer-at Code" />
|
||||||
<meta name="description" content="Apprendre la programmation et la cybersécurité en s'amusant." />
|
<meta name="description" content="Apprendre la programmation et la cybersécurité en s'amusant." />
|
||||||
<meta name="theme-color" content="#110F15" />
|
<meta name="theme-color" content="#110F15" />
|
||||||
|
@ -16,7 +18,7 @@
|
||||||
|
|
||||||
<!-- Open Graph / Facebook -->
|
<!-- Open Graph / Facebook -->
|
||||||
<meta property="og:type" content="website" />
|
<meta property="og:type" content="website" />
|
||||||
<meta property="og:url" content="https://app.peerat.dev/" />
|
<meta property="og:url" content={origin} />
|
||||||
<meta property="og:title" content="Peer-at Code" />
|
<meta property="og:title" content="Peer-at Code" />
|
||||||
<meta
|
<meta
|
||||||
property="og:description"
|
property="og:description"
|
||||||
|
@ -25,7 +27,7 @@
|
||||||
|
|
||||||
<!-- Twitter -->
|
<!-- Twitter -->
|
||||||
<meta property="twitter:card" content="summary_large_image" />
|
<meta property="twitter:card" content="summary_large_image" />
|
||||||
<meta property="twitter:url" content="https://app.peerat.dev/" />
|
<meta property="twitter:url" content={origin} />
|
||||||
<meta property="twitter:title" content="Peer-at Code" />
|
<meta property="twitter:title" content="Peer-at Code" />
|
||||||
<meta
|
<meta
|
||||||
property="twitter:description"
|
property="twitter:description"
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
<script class="ts">
|
<script class="ts">
|
||||||
import Navbar from '$lib/components/Navbar.svelte';
|
import Navbar from '$lib/components/Navbar.svelte';
|
||||||
import Sidenav from '$lib/components/Sidenav.svelte';
|
import Sidenav from '$lib/components/Sidenav.svelte';
|
||||||
|
|
||||||
|
let isOpen = false;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex h-screen w-full flex-col">
|
<div class="flex h-screen w-full flex-col">
|
||||||
<div class="flex flex-1 overflow-hidden">
|
<div class="flex flex-1 overflow-hidden">
|
||||||
<Sidenav />
|
<Sidenav bind:isOpen />
|
||||||
<div class="flex flex-1 flex-col">
|
<div class="flex flex-1 flex-col">
|
||||||
<Navbar />
|
<Navbar bind:isOpen />
|
||||||
<div
|
<div
|
||||||
class="flex w-full flex-1 transform flex-col overflow-y-scroll p-8 duration-300 ease-in-out"
|
class="flex w-full flex-1 transform flex-col overflow-y-scroll p-8 duration-300 ease-in-out"
|
||||||
>
|
>
|
||||||
|
|
5
src/routes/dashboard/badges/+page.server.ts
Normal file
5
src/routes/dashboard/badges/+page.server.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import type { PageServerLoad } from './$types';
|
||||||
|
|
||||||
|
export const load = (async ({ parent }) => {
|
||||||
|
await parent();
|
||||||
|
}) satisfies PageServerLoad;
|
|
@ -3,7 +3,9 @@ import type { LeaderboardEvent } from '$lib/types';
|
||||||
|
|
||||||
import type { PageServerLoad } from './$types';
|
import type { PageServerLoad } from './$types';
|
||||||
|
|
||||||
export const load = (async ({ fetch, cookies }) => {
|
export const load = (async ({ parent, fetch, cookies }) => {
|
||||||
|
await parent();
|
||||||
|
|
||||||
const session = cookies.get('session');
|
const session = cookies.get('session');
|
||||||
|
|
||||||
// TODO: change this
|
// TODO: change this
|
||||||
|
|
|
@ -2,7 +2,9 @@ import type { PageServerLoad } from './$types';
|
||||||
import { API_URL } from '$env/static/private';
|
import { API_URL } from '$env/static/private';
|
||||||
import type { Chapter, Puzzle } from '$lib/types';
|
import type { Chapter, Puzzle } from '$lib/types';
|
||||||
|
|
||||||
export const load = (async ({ fetch, cookies }) => {
|
export const load = (async ({ parent, fetch, cookies }) => {
|
||||||
|
await parent();
|
||||||
|
|
||||||
const session = cookies.get('session');
|
const session = cookies.get('session');
|
||||||
|
|
||||||
const res = await fetch(`${API_URL}/chapters`, {
|
const res = await fetch(`${API_URL}/chapters`, {
|
||||||
|
|
|
@ -3,7 +3,9 @@ import { API_URL } from '$env/static/private';
|
||||||
import { error, redirect, type Actions } from '@sveltejs/kit';
|
import { error, redirect, type Actions } from '@sveltejs/kit';
|
||||||
import type Puzzle from '$lib/components/Puzzle.svelte';
|
import type Puzzle from '$lib/components/Puzzle.svelte';
|
||||||
|
|
||||||
export const load = (async ({ fetch, cookies, params: { id } }) => {
|
export const load = (async ({ parent, fetch, cookies, params: { id } }) => {
|
||||||
|
await parent();
|
||||||
|
|
||||||
const session = cookies.get('session');
|
const session = cookies.get('session');
|
||||||
|
|
||||||
if (isNaN(parseInt(id))) {
|
if (isNaN(parseInt(id))) {
|
||||||
|
@ -20,14 +22,14 @@ export const load = (async ({ fetch, cookies, params: { id } }) => {
|
||||||
throw error(404, 'Puzzle not found');
|
throw error(404, 'Puzzle not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
const puzzle = (await res.json()) as Puzzle;
|
const puzzle = await res.json();
|
||||||
|
|
||||||
if (!puzzle) {
|
if (!puzzle) {
|
||||||
throw error(404, 'Puzzle not found');
|
throw error(404, 'Puzzle not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
puzzle
|
puzzle: puzzle as Puzzle
|
||||||
};
|
};
|
||||||
}) satisfies PageServerLoad;
|
}) satisfies PageServerLoad;
|
||||||
|
|
||||||
|
@ -35,8 +37,6 @@ export const actions = {
|
||||||
default: async (event) => {
|
default: async (event) => {
|
||||||
const { id } = event.params;
|
const { id } = event.params;
|
||||||
|
|
||||||
// TODO: Check id
|
|
||||||
|
|
||||||
const data = await event.request.formData();
|
const data = await event.request.formData();
|
||||||
|
|
||||||
const res = await fetch(`${API_URL}/puzzleResponse/${id}`, {
|
const res = await fetch(`${API_URL}/puzzleResponse/${id}`, {
|
||||||
|
@ -47,7 +47,11 @@ export const actions = {
|
||||||
body: data
|
body: data
|
||||||
});
|
});
|
||||||
|
|
||||||
throw redirect(303, `/dashboard/puzzles/${id}`);
|
return {
|
||||||
|
success: res.ok
|
||||||
|
};
|
||||||
|
|
||||||
|
// throw redirect(303, `/dashboard/puzzles/${id}`);
|
||||||
|
|
||||||
// if (res.ok) {
|
// if (res.ok) {
|
||||||
// const token = res.headers.get('Authorization')?.split(' ')[1];
|
// const token = res.headers.get('Authorization')?.split(' ')[1];
|
||||||
|
|
|
@ -1,23 +1,44 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import type { PageData } from './$types';
|
||||||
|
|
||||||
|
import { marked, type MarkedOptions } from 'marked';
|
||||||
|
|
||||||
|
import { enhance } from '$app/forms';
|
||||||
import Button from '$lib/components/ui/Button.svelte';
|
import Button from '$lib/components/ui/Button.svelte';
|
||||||
import Input from '$lib/components/ui/Input.svelte';
|
import Input from '$lib/components/ui/Input.svelte';
|
||||||
import type { PageData } from './$types';
|
|
||||||
|
|
||||||
export let data: PageData;
|
export let data: PageData;
|
||||||
|
|
||||||
$: puzzle = data.puzzle;
|
$: puzzle = data.puzzle;
|
||||||
|
|
||||||
|
const renderer = new marked.Renderer();
|
||||||
|
|
||||||
|
renderer.link = (href, title, text) => {
|
||||||
|
const html = marked.Renderer.prototype.link.call(renderer, href, title, text);
|
||||||
|
return html.replace(
|
||||||
|
/^<a /,
|
||||||
|
'<a target="_blank" rel="nofollow" class="text-brand hover:text-brand/90" '
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
renderer.codespan = (code) => {
|
||||||
|
return `<code class="cursor-default select-none text-transparent transition-colors delay-150 hover:text-highlight-secondary">${code}</code>`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const options: MarkedOptions = {
|
||||||
|
breaks: true,
|
||||||
|
renderer
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex h-full w-full flex-col justify-between space-y-4">
|
<div class="flex h-full w-full flex-col justify-between space-y-4">
|
||||||
<h1 class="text-2xl font-bold sm:text-3xl md:text-4xl">
|
<h1 class="text-2xl font-bold sm:text-3xl md:text-4xl">
|
||||||
{puzzle.name}{' '}
|
{puzzle.name}
|
||||||
<span class="text-xl text-highlight-secondary">({puzzle.scoreMax} points)</span>
|
<span class="text-xl text-highlight-secondary">({puzzle.scoreMax} points)</span>
|
||||||
</h1>
|
</h1>
|
||||||
<!-- <Separator /> -->
|
<!-- <Separator /> -->
|
||||||
<div class="flex h-screen w-full overflow-y-auto">
|
<div class="h-screen w-full overflow-y-auto font-fira">
|
||||||
<span class="font-code text-xs sm:text-base">
|
{@html marked(puzzle.content, options)}
|
||||||
{@html puzzle.content}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
{#if !puzzle.score}
|
{#if !puzzle.score}
|
||||||
<!-- <InputForm {puzzle} /> -->
|
<!-- <InputForm {puzzle} /> -->
|
||||||
|
@ -25,6 +46,7 @@
|
||||||
class="flex w-full flex-col items-end justify-between gap-4 sm:flex-row"
|
class="flex w-full flex-col items-end justify-between gap-4 sm:flex-row"
|
||||||
method="POST"
|
method="POST"
|
||||||
enctype="multipart/form-data"
|
enctype="multipart/form-data"
|
||||||
|
use:enhance
|
||||||
>
|
>
|
||||||
<div class="flex w-full flex-col gap-2 sm:flex-row sm:gap-4">
|
<div class="flex w-full flex-col gap-2 sm:flex-row sm:gap-4">
|
||||||
<div class="flex flex-col gap-y-2">
|
<div class="flex flex-col gap-y-2">
|
||||||
|
@ -40,7 +62,7 @@
|
||||||
</form>
|
</form>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div class="items-center gap-x-2">
|
<div class="items-center gap-2">
|
||||||
<p>
|
<p>
|
||||||
Tentative{puzzle.tries && puzzle.tries > 1 ? 's' : ''} :{' '}
|
Tentative{puzzle.tries && puzzle.tries > 1 ? 's' : ''} :{' '}
|
||||||
<span class="text-brand-accent">{puzzle.tries}</span>
|
<span class="text-brand-accent">{puzzle.tries}</span>
|
||||||
|
@ -52,7 +74,7 @@
|
||||||
<!-- <Button type="button" onClick={() => router.push(getURL(`/dashboard/puzzles`))}>
|
<!-- <Button type="button" onClick={() => router.push(getURL(`/dashboard/puzzles`))}>
|
||||||
Retour aux puzzles
|
Retour aux puzzles
|
||||||
</Button> -->
|
</Button> -->
|
||||||
<button>retour aux puzzles</button>
|
<Button href="/puzzles" class="w-full sm:w-44" variant="brand">Retour aux puzzles</Button>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
31
src/routes/dashboard/settings/+page.server.ts
Normal file
31
src/routes/dashboard/settings/+page.server.ts
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import type { Actions } from '@sveltejs/kit';
|
||||||
|
|
||||||
|
import type { PageServerLoad } from './$types';
|
||||||
|
|
||||||
|
export const load = (async ({ parent }) => {
|
||||||
|
await parent();
|
||||||
|
}) satisfies PageServerLoad;
|
||||||
|
|
||||||
|
export const actions = {
|
||||||
|
default: async (event) => {
|
||||||
|
return {
|
||||||
|
success: true
|
||||||
|
};
|
||||||
|
|
||||||
|
// throw redirect(303, `/dashboard/puzzles/${id}`);
|
||||||
|
|
||||||
|
// if (res.ok) {
|
||||||
|
// const token = res.headers.get('Authorization')?.split(' ')[1];
|
||||||
|
|
||||||
|
// if (!token) throw new Error('No token found');
|
||||||
|
|
||||||
|
// event.cookies.set('session', token, {
|
||||||
|
// path: '/'
|
||||||
|
// });
|
||||||
|
|
||||||
|
// throw redirect(303, '/dashboard');
|
||||||
|
// }
|
||||||
|
|
||||||
|
// throw redirect(303, '/sign-in');
|
||||||
|
}
|
||||||
|
} satisfies Actions;
|
|
@ -1,12 +1,20 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { enhance } from '$app/forms';
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
|
|
||||||
|
import type { ActionData } from './$types';
|
||||||
|
|
||||||
import Button from '$lib/components/ui/Button.svelte';
|
import Button from '$lib/components/ui/Button.svelte';
|
||||||
import Input from '$lib/components/ui/Input.svelte';
|
import Input from '$lib/components/ui/Input.svelte';
|
||||||
|
|
||||||
$: user = $page.data.user;
|
$: user = $page.data.user;
|
||||||
|
|
||||||
|
export let form: ActionData;
|
||||||
|
|
||||||
|
$: console.log(form);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<form class="space-y-4" method="post">
|
<form class="flex flex-col gap-4" method="POST" use:enhance>
|
||||||
<label for="email">Email</label>
|
<label for="email">Email</label>
|
||||||
<Input name="email" type="email" placeholder="philipzcwbarlow@peerat.dev" value={user?.email} />
|
<Input name="email" type="email" placeholder="philipzcwbarlow@peerat.dev" value={user?.email} />
|
||||||
|
|
||||||
|
|
|
@ -1,23 +1,36 @@
|
||||||
import { redirect, type Actions } from '@sveltejs/kit';
|
import { redirect, type Actions, fail } from '@sveltejs/kit';
|
||||||
import type { PageServerLoad } from './$types';
|
import type { PageServerLoad } from './$types';
|
||||||
import { API_URL } from '$env/static/private';
|
import { API_URL } from '$env/static/private';
|
||||||
|
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
export const load = (async ({ locals: { user } }) => {
|
export const load = (async ({ locals: { user } }) => {
|
||||||
if (user) throw redirect(303, '/dashboard');
|
if (user) throw redirect(303, '/dashboard');
|
||||||
}) satisfies PageServerLoad;
|
}) satisfies PageServerLoad;
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
pseudo: z.string().trim(),
|
||||||
|
passwd: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
export const actions = {
|
export const actions = {
|
||||||
default: async (event) => {
|
default: async (event) => {
|
||||||
const data = await event.request.formData();
|
const data = await event.request.formData();
|
||||||
|
|
||||||
const pseudo = data.get('pseudo') as string;
|
const parse = schema.safeParse(Object.fromEntries(data.entries()));
|
||||||
const passwd = data.get('passwd') as string;
|
|
||||||
|
if (!parse.success) {
|
||||||
|
const errors = parse.error.errors.map((error) => {
|
||||||
|
const { path, message } = error;
|
||||||
|
return { field: path[0], message };
|
||||||
|
});
|
||||||
|
return fail(400, { errors });
|
||||||
|
}
|
||||||
|
|
||||||
const res = await fetch(`${API_URL}/login`, {
|
const res = await fetch(`${API_URL}/login`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
pseudo,
|
...parse.data
|
||||||
passwd
|
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -33,6 +46,8 @@ export const actions = {
|
||||||
throw redirect(303, '/dashboard');
|
throw redirect(303, '/dashboard');
|
||||||
}
|
}
|
||||||
|
|
||||||
throw redirect(303, '/sign-in');
|
return fail(400, {
|
||||||
|
errors: [{ field: 'passwd', message: "Nom d'utilisateur ou mot de passe incorrect" }]
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} satisfies Actions;
|
} satisfies Actions;
|
||||||
|
|
|
@ -1,26 +1,50 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { enhance } from '$app/forms';
|
import { enhance } from '$app/forms';
|
||||||
|
|
||||||
|
import type { ActionData } from './$types';
|
||||||
|
|
||||||
import Button from '$lib/components/ui/Button.svelte';
|
import Button from '$lib/components/ui/Button.svelte';
|
||||||
import Input from '$lib/components/ui/Input.svelte';
|
import Input from '$lib/components/ui/Input.svelte';
|
||||||
|
|
||||||
|
export let form: ActionData;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex h-screen w-full">
|
<div class="flex h-screen w-full">
|
||||||
<div class="flex h-full w-full flex-col items-center justify-center">
|
<div class="flex w-full flex-col items-center justify-center">
|
||||||
<div class="flex flex-col justify-start space-y-4">
|
<div class="flex w-full max-w-xs flex-col gap-4">
|
||||||
<h2 class="mx-auto text-xl font-bold">Connexion</h2>
|
<h1 class="mx-auto text-xl font-bold">Connexion</h1>
|
||||||
<form class="flex w-52 flex-col justify-center space-y-4 sm:w-72" method="POST" use:enhance>
|
<form class="flex flex-col justify-center gap-2" method="POST" use:enhance>
|
||||||
<label for="pseudo"> Nom d'utilisateur </label>
|
<label for="pseudo"> Nom d'utilisateur </label>
|
||||||
<Input name="pseudo" placeholder="Barlow" type="text" required />
|
<Input name="pseudo" placeholder="Barlow" type="text" required />
|
||||||
|
{#if form?.errors.find((error) => error.field === 'pseudo')}
|
||||||
|
<p class="text-sm text-red-500">
|
||||||
|
{form?.errors.find((error) => error.field === 'pseudo')?.message}
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<label for="passwd"> Mot de passe </label>
|
<label for="passwd"> Mot de passe </label>
|
||||||
<Input name="passwd" placeholder="************" type="password" required />
|
<Input name="passwd" placeholder="************" type="password" required />
|
||||||
|
{#if form?.errors.find((error) => error.field === 'passwd')}
|
||||||
|
<p class="text-sm text-red-500">
|
||||||
|
{form?.errors.find((error) => error.field === 'passwd')?.message}
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<Button variant="brand">
|
<Button class="mt-2" variant="brand">
|
||||||
<!-- {isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />} -->
|
<!-- {isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />} -->
|
||||||
Se connecter
|
Se connecter
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
<ul class="flex justify-between">
|
||||||
|
<li>
|
||||||
|
<a class="text-highlight-secondary hover:text-brand" href="/sign-up">S'inscrire</a>
|
||||||
|
</li>
|
||||||
|
<!-- <li>
|
||||||
|
<a class="text-highlight-secondary hover:text-brand" href="/forgot-password"
|
||||||
|
>Mot de passe oublié</a
|
||||||
|
>
|
||||||
|
</li> -->
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,35 +1,47 @@
|
||||||
import { redirect, type Actions } from '@sveltejs/kit';
|
import { redirect, type Actions, fail } from '@sveltejs/kit';
|
||||||
import type { PageServerLoad } from './$types';
|
import type { PageServerLoad } from './$types';
|
||||||
import { API_URL } from '$env/static/private';
|
import { API_URL } from '$env/static/private';
|
||||||
|
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
export const load = (async ({ locals: { user } }) => {
|
export const load = (async ({ locals: { user } }) => {
|
||||||
if (user) throw redirect(303, '/dashboard');
|
if (user) throw redirect(303, '/dashboard');
|
||||||
}) satisfies PageServerLoad;
|
}) satisfies PageServerLoad;
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
email: z
|
||||||
|
.string()
|
||||||
|
.email({
|
||||||
|
message: 'Email invalide'
|
||||||
|
})
|
||||||
|
.trim(),
|
||||||
|
firstname: z.string().trim(),
|
||||||
|
lastname: z.string().trim(),
|
||||||
|
pseudo: z.string().trim(),
|
||||||
|
passwd: z.string(),
|
||||||
|
description: z.string().nullable(),
|
||||||
|
sgroup: z.string().nullable(),
|
||||||
|
avatar: z.string().nullable()
|
||||||
|
});
|
||||||
|
|
||||||
export const actions = {
|
export const actions = {
|
||||||
default: async (event) => {
|
default: async (event) => {
|
||||||
const data = await event.request.formData();
|
const data = await event.request.formData();
|
||||||
|
|
||||||
const email = data.get('email') as string;
|
const parse = schema.safeParse(Object.fromEntries(data.entries()));
|
||||||
const firstname = data.get('firstname') as string;
|
|
||||||
const lastname = data.get('lastname') as string;
|
if (!parse.success) {
|
||||||
const pseudo = data.get('pseudo') as string;
|
const errors = parse.error.errors.map((error) => {
|
||||||
const passwd = data.get('passwd') as string;
|
const { path, message } = error;
|
||||||
const description = data.get('description') as string;
|
return { field: path[0], message };
|
||||||
const sgroup = data.get('sgroup') as string;
|
});
|
||||||
const avatar = data.get('avatar') as string;
|
return fail(400, { errors });
|
||||||
|
}
|
||||||
|
|
||||||
const res = await fetch(`${API_URL}/register`, {
|
const res = await fetch(`${API_URL}/register`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
email,
|
...parse.data
|
||||||
firstname,
|
|
||||||
lastname,
|
|
||||||
pseudo,
|
|
||||||
passwd,
|
|
||||||
description,
|
|
||||||
sgroup,
|
|
||||||
avatar
|
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -45,6 +57,30 @@ export const actions = {
|
||||||
throw redirect(303, '/dashboard');
|
throw redirect(303, '/dashboard');
|
||||||
}
|
}
|
||||||
|
|
||||||
throw redirect(303, '/sign-in');
|
if (res.status === 400) {
|
||||||
|
const { username_valid, email_valid } = await res.json();
|
||||||
|
|
||||||
|
const errors = [];
|
||||||
|
|
||||||
|
if (!username_valid) {
|
||||||
|
errors.push({
|
||||||
|
field: 'pseudo',
|
||||||
|
message: 'Ce pseudo est déjà utilisé'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!email_valid) {
|
||||||
|
errors.push({
|
||||||
|
field: 'email',
|
||||||
|
message: 'Cet email est déjà utilisé'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return fail(400, { errors });
|
||||||
|
}
|
||||||
|
|
||||||
|
return fail(400, {
|
||||||
|
errors: [{ field: 'passwd', message: "Une erreur s'est produite" }]
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} satisfies Actions;
|
} satisfies Actions;
|
||||||
|
|
|
@ -1,39 +1,73 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { enhance } from '$app/forms';
|
import { enhance } from '$app/forms';
|
||||||
|
|
||||||
|
import type { ActionData } from './$types';
|
||||||
|
|
||||||
import Button from '$lib/components/ui/Button.svelte';
|
import Button from '$lib/components/ui/Button.svelte';
|
||||||
import Input from '$lib/components/ui/Input.svelte';
|
import Input from '$lib/components/ui/Input.svelte';
|
||||||
|
|
||||||
|
export let form: ActionData;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex h-screen w-full">
|
<div class="flex h-screen w-full">
|
||||||
<div class="flex h-full w-full flex-col items-center justify-center">
|
<div class="flex w-full flex-col items-center justify-center">
|
||||||
<div class="flex flex-col justify-start space-y-4">
|
<div class="flex w-full max-w-xs flex-col gap-4">
|
||||||
<h2 class="mx-auto text-xl font-bold">Inscription</h2>
|
<h2 class="mx-auto text-xl font-bold">Inscription</h2>
|
||||||
<form class="flex w-52 flex-col justify-center space-y-4 sm:w-72" method="POST" use:enhance>
|
<form class="flex flex-col justify-center gap-2" method="POST" use:enhance>
|
||||||
<label for="email">Email</label>
|
<label for="email">Email</label>
|
||||||
<Input name="email" type="email" placeholder="philipzcwbarlow@peerat.dev" />
|
<Input name="email" type="email" placeholder="philipzcwbarlow@peerat.dev" />
|
||||||
|
{#if form?.errors.find((error) => error.field === 'email')}
|
||||||
|
<p class="text-sm text-red-500">
|
||||||
|
{form?.errors.find((error) => error.field === 'email')?.message}
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<label for="firstname">Prénom</label>
|
<label for="firstname">Prénom</label>
|
||||||
<Input name="firstname" type="text" placeholder="Philip" required />
|
<Input name="firstname" type="text" placeholder="Philip" required />
|
||||||
|
{#if form?.errors.find((error) => error.field === 'firstname')}
|
||||||
|
<p class="text-sm text-red-500">
|
||||||
|
{form?.errors.find((error) => error.field === 'firstname')?.message}
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<label for="lastname">Nom</label>
|
<label for="lastname">Nom</label>
|
||||||
<Input name="lastname" type="text" placeholder="Barlow" required />
|
<Input name="lastname" type="text" placeholder="Barlow" required />
|
||||||
|
{#if form?.errors.find((error) => error.field === 'lastname')}
|
||||||
|
<p class="text-sm text-red-500">
|
||||||
|
{form?.errors.find((error) => error.field === 'lastname')?.message}
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<label for="pseudo"> Nom d'utilisateur </label>
|
<label for="pseudo"> Nom d'utilisateur </label>
|
||||||
<Input name="pseudo" type="text" placeholder="Cypher Wolf" required />
|
<Input name="pseudo" type="text" placeholder="Cypher Wolf" required />
|
||||||
|
{#if form?.errors.find((error) => error.field === 'pseudo')}
|
||||||
|
<p class="text-sm text-red-500">
|
||||||
|
{form?.errors.find((error) => error.field === 'pseudo')?.message}
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<label for="passwd"> Mot de passe </label>
|
<label for="passwd"> Mot de passe </label>
|
||||||
<Input name="passwd" placeholder="************" type="password" required />
|
<Input name="passwd" placeholder="************" type="password" required />
|
||||||
|
{#if form?.errors.find((error) => error.field === 'passwd')}
|
||||||
|
<p class="text-sm text-red-500">
|
||||||
|
{form?.errors.find((error) => error.field === 'passwd')?.message}
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<Input class="hidden" type="text" name="sgroup" />
|
<Input class="hidden" type="text" name="sgroup" />
|
||||||
<Input class="hidden" type="text" name="description" />
|
<Input class="hidden" type="text" name="description" />
|
||||||
<Input class="hidden" type="text" name="avatar" />
|
<Input class="hidden" type="text" name="avatar" />
|
||||||
|
|
||||||
<Button variant="brand">
|
<Button class="mt-2" variant="brand">
|
||||||
<!-- {isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />} -->
|
<!-- {isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />} -->
|
||||||
S'inscrire
|
S'inscrire
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
<ul class="flex justify-between">
|
||||||
|
<li>
|
||||||
|
<a class="text-highlight-secondary hover:text-brand" href="/sign-in">Se connecter</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
BIN
static/fonts/FiraCode.woff2
Normal file
BIN
static/fonts/FiraCode.woff2
Normal file
Binary file not shown.
BIN
static/fonts/Karrik.woff2
Normal file
BIN
static/fonts/Karrik.woff2
Normal file
Binary file not shown.
|
@ -1,8 +1,14 @@
|
||||||
|
import { fontFamily } from 'tailwindcss/defaultTheme';
|
||||||
|
|
||||||
/** @type {import('tailwindcss').Config} */
|
/** @type {import('tailwindcss').Config} */
|
||||||
export default {
|
export default {
|
||||||
content: ['./src/**/*.{html,js,svelte,ts}'],
|
content: ['./src/**/*.{html,js,svelte,ts}'],
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
|
fontFamily: {
|
||||||
|
sans: ['Karrik', ...fontFamily.sans],
|
||||||
|
fira: ['Fira Code', ...fontFamily.sans]
|
||||||
|
},
|
||||||
colors: {
|
colors: {
|
||||||
border: 'hsl(var(--border))',
|
border: 'hsl(var(--border))',
|
||||||
input: 'hsl(var(--input))',
|
input: 'hsl(var(--input))',
|
||||||
|
|
51
tests/index.test.ts
Normal file
51
tests/index.test.ts
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
import { expect, test } from '@playwright/test';
|
||||||
|
|
||||||
|
test('index page redirects to login page', async ({ page }) => {
|
||||||
|
await page.goto('/');
|
||||||
|
|
||||||
|
await page.waitForURL('/sign-in');
|
||||||
|
|
||||||
|
await expect(page.url()).toContain('/sign-in');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('sign-in page has a sign-up link that redirects to the sign-up page', async ({ page }) => {
|
||||||
|
await page.goto('/sign-in');
|
||||||
|
|
||||||
|
const link = await page.$('a[href*="/sign-up"]');
|
||||||
|
|
||||||
|
await link?.click();
|
||||||
|
|
||||||
|
await page.waitForURL('/sign-up');
|
||||||
|
|
||||||
|
await expect(page.url()).toContain('/sign-up');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('sign-up page has a sign-in link that redirects to the sign-in page', async ({ page }) => {
|
||||||
|
await page.goto('/sign-up');
|
||||||
|
|
||||||
|
const link = await page.$('a[href*="/sign-in"]');
|
||||||
|
|
||||||
|
await link?.click();
|
||||||
|
|
||||||
|
await page.waitForURL('/sign-in');
|
||||||
|
|
||||||
|
await expect(page.url()).toContain('/sign-in');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('dashboard page redirects to sign-in page if user is not logged in', async ({ page }) => {
|
||||||
|
await page.goto('/dashboard');
|
||||||
|
await expect(page.url()).toContain('/sign-in');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('login form accepts valid credentials', async ({ page }) => {
|
||||||
|
await page.context().clearCookies();
|
||||||
|
|
||||||
|
await page.goto('/sign-in');
|
||||||
|
|
||||||
|
await page.fill('input[name="pseudo"]', 'glazk0');
|
||||||
|
await page.fill('input[name="passwd"]', 'Cookies Are #Miam42');
|
||||||
|
|
||||||
|
await Promise.all([page.getByRole('button').click(), page.waitForURL('/dashboard')]);
|
||||||
|
|
||||||
|
await expect(page.url()).toContain('/dashboard');
|
||||||
|
});
|
|
@ -1,6 +0,0 @@
|
||||||
import { expect, test } from '@playwright/test';
|
|
||||||
|
|
||||||
test('index page has expected h1', async ({ page }) => {
|
|
||||||
await page.goto('/');
|
|
||||||
await expect(page.getByRole('heading', { name: 'Welcome to SvelteKit' })).toBeVisible();
|
|
||||||
});
|
|
Loading…
Add table
Reference in a new issue