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-node": "^1.3.1",
|
||||
"@sveltejs/kit": "^1.20.4",
|
||||
"@types/marked": "^5.0.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.45.0",
|
||||
"@typescript-eslint/parser": "^5.45.0",
|
||||
"autoprefixer": "^10.4.14",
|
||||
|
@ -41,6 +42,9 @@
|
|||
"dependencies": {
|
||||
"class-variance-authority": "^0.7.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 = {
|
||||
webServer: {
|
||||
command: 'npm run build && npm run preview',
|
||||
command: 'pnpm build && pnpm preview',
|
||||
port: 4173
|
||||
},
|
||||
testDir: 'tests',
|
||||
|
|
30
pnpm-lock.yaml
generated
30
pnpm-lock.yaml
generated
|
@ -11,9 +11,18 @@ dependencies:
|
|||
clsx:
|
||||
specifier: ^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:
|
||||
specifier: ^1.14.0
|
||||
version: 1.14.0
|
||||
zod:
|
||||
specifier: ^3.21.4
|
||||
version: 3.21.4
|
||||
|
||||
devDependencies:
|
||||
'@playwright/test':
|
||||
|
@ -28,6 +37,9 @@ devDependencies:
|
|||
'@sveltejs/kit':
|
||||
specifier: ^1.20.4
|
||||
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':
|
||||
specifier: ^5.45.0
|
||||
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==}
|
||||
dev: true
|
||||
|
||||
/@types/marked@5.0.1:
|
||||
resolution: {integrity: sha512-Y3pAUzHKh605fN6fvASsz5FDSWbZcs/65Q6xYRmnIP9ZIYz27T4IOmXfH9gWJV1dpi7f1e7z7nBGUTx/a0ptpA==}
|
||||
dev: true
|
||||
|
||||
/@types/node@20.4.5:
|
||||
resolution: {integrity: sha512-rt40Nk13II9JwQBdeYqmbn2Q6IVTA5uPhvSO+JVqdXw/6/4glI6oR9ezty/A9Hg5u7JH4OmYmuQ+XvjKm0Datg==}
|
||||
dev: true
|
||||
|
@ -1718,6 +1734,12 @@ packages:
|
|||
'@jridgewell/sourcemap-codec': 1.4.15
|
||||
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:
|
||||
resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==}
|
||||
dev: true
|
||||
|
@ -2359,6 +2381,10 @@ packages:
|
|||
engines: {node: '>= 0.4'}
|
||||
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):
|
||||
resolution: {integrity: sha512-OBlY8866Zh1zHQTkBMPS6psPi7o2umTUyj6JWm4SacnIHXpWFm658pG32m3dKvKFL49V4ntAkfFHKo4ztH07og==}
|
||||
hasBin: true
|
||||
|
@ -2812,3 +2838,7 @@ packages:
|
|||
resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==}
|
||||
engines: {node: '>=12.20'}
|
||||
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">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
<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" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<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>
|
||||
</html>
|
||||
|
|
|
@ -2,6 +2,16 @@
|
|||
@tailwind components;
|
||||
@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 {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
|
|
|
@ -2,6 +2,8 @@ import type { Handle } from '@sveltejs/kit';
|
|||
|
||||
import { API_URL } from '$env/static/private';
|
||||
|
||||
import type { User } from '$lib/types';
|
||||
|
||||
export const handle = (async ({ event, resolve }) => {
|
||||
const session = event.cookies.get('session');
|
||||
|
||||
|
@ -13,7 +15,7 @@ export const handle = (async ({ event, resolve }) => {
|
|||
});
|
||||
|
||||
if (res.ok) {
|
||||
const user = await res.json();
|
||||
const user = (await res.json()) as User;
|
||||
event.locals.user = user;
|
||||
} else {
|
||||
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">
|
||||
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;
|
||||
$: segment = $page.url.pathname.slice(1).replaceAll('/', ' > ');
|
||||
$: segment = $page.url.pathname.slice(1).replaceAll('/', ' / ');
|
||||
|
||||
function handleToggle() {
|
||||
isOpen = !isOpen;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
|
@ -10,22 +19,22 @@
|
|||
>
|
||||
<div class="flex flex-row items-center space-x-2 sm:space-x-0">
|
||||
<div class="flex items-center">
|
||||
<button on:click={() => console.log('toggle')} class="block sm:hidden">
|
||||
<!-- {isOpen ? (
|
||||
<X size={20} class="text-muted" />
|
||||
) : (
|
||||
<AlignLeftIcon size={20} class="text-muted" />
|
||||
)} -->
|
||||
<span>menu</span>
|
||||
<button on:click={handleToggle} class="block sm:hidden">
|
||||
{#if isOpen}
|
||||
<X class="h-5 w-5 text-muted" />
|
||||
{:else}
|
||||
<AlignLeft class="h-5 w-5 text-muted" />
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
{#if segment}
|
||||
<div class="flex uppercase items-center justify-center text-highlight-secondary">
|
||||
{#if !isOpen && segment}
|
||||
<div class="flex items-center justify-center capitalize text-highlight-secondary">
|
||||
{segment}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex flex-row items-center space-x-4">
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
<Avatar />
|
||||
{user?.pseudo}
|
||||
<!-- {!isLoading && me ? (
|
||||
<Popover
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { cn } from '$lib/Utils';
|
||||
import type { Puzzle } from '$lib/types';
|
||||
import ChevronRight from './Icons/ChevronRight.svelte';
|
||||
|
||||
export let puzzle: Puzzle;
|
||||
|
||||
|
@ -9,7 +10,7 @@
|
|||
|
||||
<li
|
||||
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-yellow-600/30': puzzle.tags?.find((tag) => tag.name.toLowerCase() === 'medium'),
|
||||
|
@ -19,29 +20,30 @@
|
|||
}
|
||||
)}
|
||||
>
|
||||
<a
|
||||
class="flex h-full w-full items-center justify-between p-4"
|
||||
href={`/dashboard/puzzles/${puzzle.id}`}
|
||||
>
|
||||
<div class="flex w-10/12 flex-col gap-2 lg:w-full lg:flex-row">
|
||||
<span class="text-base font-semibold">
|
||||
{puzzle.name}{' '}
|
||||
<a class="flex h-full w-full items-center gap-4 p-4" href="/dashboard/puzzles/{puzzle.id}">
|
||||
<div class="flex w-full flex-col justify-between gap-2 sm:flex-row">
|
||||
<h2 class="text-base font-semibold">
|
||||
{puzzle.name}
|
||||
<span class="text-sm text-highlight-secondary">
|
||||
({puzzle.score ? `${puzzle.score}` : '?'}/{puzzle.scoreMax} points)
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</h2>
|
||||
<div class="flex items-center gap-x-6">
|
||||
{#if puzzle.tags?.length}
|
||||
<div class="flex gap-x-2 text-sm text-muted">
|
||||
<div class="flex gap-x-2 text-sm">
|
||||
{#each puzzle.tags as tag}
|
||||
<span class="inline-block rounded-md bg-primary-800 px-2 py-1">
|
||||
<span
|
||||
class="inline-block rounded-md bg-primary-800 px-2 py-1 text-highlight-secondary"
|
||||
>
|
||||
{tag.name}
|
||||
</span>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<!-- <ChevronRightIcon class="-translate-x-2 transform-gpu text-highlight-secondary duration-300 group-hover:translate-x-0 group-hover:text-brand" /> -->
|
||||
</div>
|
||||
</div>
|
||||
<span class="translate-x-0 transform-gpu duration-300 group-hover:translate-x-2">
|
||||
<ChevronRight />
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
|
|
|
@ -1,38 +1,43 @@
|
|||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
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 = [
|
||||
{
|
||||
name: 'Dashboard',
|
||||
slug: 'dashboard',
|
||||
icon: 'LayoutDashboard',
|
||||
disabled: false
|
||||
slug: '/dashboard',
|
||||
icon: Dashboard
|
||||
},
|
||||
{
|
||||
name: 'Classement',
|
||||
slug: 'dashboard/leaderboard',
|
||||
icon: 'BarChart2',
|
||||
disabled: false
|
||||
slug: '/dashboard/leaderboard',
|
||||
icon: Leaderboard
|
||||
},
|
||||
{
|
||||
name: 'Puzzles',
|
||||
slug: 'dashboard/puzzles',
|
||||
icon: 'Code',
|
||||
disabled: false
|
||||
slug: '/dashboard/puzzles',
|
||||
icon: Code
|
||||
},
|
||||
{
|
||||
name: 'Badges',
|
||||
slug: 'dashboard/badges',
|
||||
icon: 'Award',
|
||||
disabled: false
|
||||
slug: '/dashboard/badges',
|
||||
icon: Badge
|
||||
},
|
||||
{
|
||||
name: 'Paramètres',
|
||||
slug: 'dashboard/settings',
|
||||
icon: 'Settings2',
|
||||
disabled: false
|
||||
slug: '/dashboard/settings',
|
||||
icon: Settings
|
||||
}
|
||||
];
|
||||
</script>
|
||||
|
@ -47,35 +52,44 @@
|
|||
)}
|
||||
>
|
||||
<div class="flex h-full flex-col">
|
||||
<div class="flex w-full justify-center p-[9px]">
|
||||
<!-- <Image
|
||||
title="Logo"
|
||||
<div class="flex w-full justify-center p-[8.5px]">
|
||||
<img
|
||||
src="/assets/brand/peerat.png"
|
||||
alt="Peer-at"
|
||||
width={50}
|
||||
height={50}
|
||||
alt="Logo"
|
||||
width="50"
|
||||
height="50"
|
||||
loading="eager"
|
||||
priority
|
||||
/> -->
|
||||
<img src="/assets/brand/peerat.png" alt="Logo" width="50" height="50" loading="eager">
|
||||
draggable="false"
|
||||
/>
|
||||
</div>
|
||||
<div class="px-4">
|
||||
<hr class="border-highlight-primary" />
|
||||
</div>
|
||||
<div class="px-4 pt-4">
|
||||
<ul class="space-y-4">
|
||||
<ul class="flex flex-col gap-4">
|
||||
{#each navItems as item}
|
||||
<li>
|
||||
<a
|
||||
href="/{item.slug}"
|
||||
class="flex justify-center rounded-md px-3 py-3 text-sm transition-colors duration-150 lg:justify-start"
|
||||
on:click={() => {
|
||||
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">
|
||||
<!-- <item.icon /> -->
|
||||
<div class="flex items-center gap-2">
|
||||
<svelte:component this={item.icon} />
|
||||
<span
|
||||
class={cn('hidden lg:block', {
|
||||
'block sm:hidden': isOpen,
|
||||
hidden: !isOpen
|
||||
hidden: !isOpen,
|
||||
'text-highlight-secondary transition-colors duration-150 group-hover:text-primary':
|
||||
!isActive(item.slug)
|
||||
})}
|
||||
>
|
||||
{item.name}
|
||||
|
@ -90,7 +104,7 @@
|
|||
<hr class="border-highlight-primary" />
|
||||
</div>
|
||||
<div class="px-4 pt-4">
|
||||
<ul class="space-y-4">
|
||||
<ul class="flex flex-col gap-4">
|
||||
<li>
|
||||
<!-- <NavItem
|
||||
item={{
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
<script lang="ts">
|
||||
import '../global.css';
|
||||
|
||||
import { page } from '$app/stores';
|
||||
|
||||
$: origin = $page.url.origin;
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Peer-at Code</title>
|
||||
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||
|
||||
<meta name="title" content="Peer-at Code" />
|
||||
<meta name="description" content="Apprendre la programmation et la cybersécurité en s'amusant." />
|
||||
<meta name="theme-color" content="#110F15" />
|
||||
|
@ -16,7 +18,7 @@
|
|||
|
||||
<!-- Open Graph / Facebook -->
|
||||
<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:description"
|
||||
|
@ -25,7 +27,7 @@
|
|||
|
||||
<!-- Twitter -->
|
||||
<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:description"
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
<script class="ts">
|
||||
import Navbar from '$lib/components/Navbar.svelte';
|
||||
import Sidenav from '$lib/components/Sidenav.svelte';
|
||||
|
||||
let isOpen = false;
|
||||
</script>
|
||||
|
||||
<div class="flex h-screen w-full flex-col">
|
||||
<div class="flex flex-1 overflow-hidden">
|
||||
<Sidenav />
|
||||
<Sidenav bind:isOpen />
|
||||
<div class="flex flex-1 flex-col">
|
||||
<Navbar />
|
||||
<Navbar bind:isOpen />
|
||||
<div
|
||||
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';
|
||||
|
||||
export const load = (async ({ fetch, cookies }) => {
|
||||
export const load = (async ({ parent, fetch, cookies }) => {
|
||||
await parent();
|
||||
|
||||
const session = cookies.get('session');
|
||||
|
||||
// TODO: change this
|
||||
|
|
|
@ -2,7 +2,9 @@ import type { PageServerLoad } from './$types';
|
|||
import { API_URL } from '$env/static/private';
|
||||
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 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 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');
|
||||
|
||||
if (isNaN(parseInt(id))) {
|
||||
|
@ -20,14 +22,14 @@ export const load = (async ({ fetch, cookies, params: { id } }) => {
|
|||
throw error(404, 'Puzzle not found');
|
||||
}
|
||||
|
||||
const puzzle = (await res.json()) as Puzzle;
|
||||
const puzzle = await res.json();
|
||||
|
||||
if (!puzzle) {
|
||||
throw error(404, 'Puzzle not found');
|
||||
}
|
||||
|
||||
return {
|
||||
puzzle
|
||||
puzzle: puzzle as Puzzle
|
||||
};
|
||||
}) satisfies PageServerLoad;
|
||||
|
||||
|
@ -35,8 +37,6 @@ export const actions = {
|
|||
default: async (event) => {
|
||||
const { id } = event.params;
|
||||
|
||||
// TODO: Check id
|
||||
|
||||
const data = await event.request.formData();
|
||||
|
||||
const res = await fetch(`${API_URL}/puzzleResponse/${id}`, {
|
||||
|
@ -47,7 +47,11 @@ export const actions = {
|
|||
body: data
|
||||
});
|
||||
|
||||
throw redirect(303, `/dashboard/puzzles/${id}`);
|
||||
return {
|
||||
success: res.ok
|
||||
};
|
||||
|
||||
// throw redirect(303, `/dashboard/puzzles/${id}`);
|
||||
|
||||
// if (res.ok) {
|
||||
// const token = res.headers.get('Authorization')?.split(' ')[1];
|
||||
|
|
|
@ -1,23 +1,44 @@
|
|||
<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 Input from '$lib/components/ui/Input.svelte';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
$: 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>
|
||||
|
||||
<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">
|
||||
{puzzle.name}{' '}
|
||||
{puzzle.name}
|
||||
<span class="text-xl text-highlight-secondary">({puzzle.scoreMax} points)</span>
|
||||
</h1>
|
||||
<!-- <Separator /> -->
|
||||
<div class="flex h-screen w-full overflow-y-auto">
|
||||
<span class="font-code text-xs sm:text-base">
|
||||
{@html puzzle.content}
|
||||
</span>
|
||||
<div class="h-screen w-full overflow-y-auto font-fira">
|
||||
{@html marked(puzzle.content, options)}
|
||||
</div>
|
||||
{#if !puzzle.score}
|
||||
<!-- <InputForm {puzzle} /> -->
|
||||
|
@ -25,6 +46,7 @@
|
|||
class="flex w-full flex-col items-end justify-between gap-4 sm:flex-row"
|
||||
method="POST"
|
||||
enctype="multipart/form-data"
|
||||
use:enhance
|
||||
>
|
||||
<div class="flex w-full flex-col gap-2 sm:flex-row sm:gap-4">
|
||||
<div class="flex flex-col gap-y-2">
|
||||
|
@ -40,7 +62,7 @@
|
|||
</form>
|
||||
{:else}
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="items-center gap-x-2">
|
||||
<div class="items-center gap-2">
|
||||
<p>
|
||||
Tentative{puzzle.tries && puzzle.tries > 1 ? 's' : ''} :{' '}
|
||||
<span class="text-brand-accent">{puzzle.tries}</span>
|
||||
|
@ -52,7 +74,7 @@
|
|||
<!-- <Button type="button" onClick={() => router.push(getURL(`/dashboard/puzzles`))}>
|
||||
Retour aux puzzles
|
||||
</Button> -->
|
||||
<button>retour aux puzzles</button>
|
||||
<Button href="/puzzles" class="w-full sm:w-44" variant="brand">Retour aux puzzles</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</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">
|
||||
import { enhance } from '$app/forms';
|
||||
import { page } from '$app/stores';
|
||||
|
||||
import type { ActionData } from './$types';
|
||||
|
||||
import Button from '$lib/components/ui/Button.svelte';
|
||||
import Input from '$lib/components/ui/Input.svelte';
|
||||
|
||||
$: user = $page.data.user;
|
||||
|
||||
export let form: ActionData;
|
||||
|
||||
$: console.log(form);
|
||||
</script>
|
||||
|
||||
<form class="space-y-4" method="post">
|
||||
<form class="flex flex-col gap-4" method="POST" use:enhance>
|
||||
<label for="email">Email</label>
|
||||
<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 { API_URL } from '$env/static/private';
|
||||
|
||||
import { z } from 'zod';
|
||||
|
||||
export const load = (async ({ locals: { user } }) => {
|
||||
if (user) throw redirect(303, '/dashboard');
|
||||
}) satisfies PageServerLoad;
|
||||
|
||||
const schema = z.object({
|
||||
pseudo: z.string().trim(),
|
||||
passwd: z.string()
|
||||
});
|
||||
|
||||
export const actions = {
|
||||
default: async (event) => {
|
||||
const data = await event.request.formData();
|
||||
|
||||
const pseudo = data.get('pseudo') as string;
|
||||
const passwd = data.get('passwd') as string;
|
||||
const parse = schema.safeParse(Object.fromEntries(data.entries()));
|
||||
|
||||
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`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
pseudo,
|
||||
passwd
|
||||
...parse.data
|
||||
})
|
||||
});
|
||||
|
||||
|
@ -33,6 +46,8 @@ export const actions = {
|
|||
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;
|
||||
|
|
|
@ -1,26 +1,50 @@
|
|||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
|
||||
import type { ActionData } from './$types';
|
||||
|
||||
import Button from '$lib/components/ui/Button.svelte';
|
||||
import Input from '$lib/components/ui/Input.svelte';
|
||||
|
||||
export let form: ActionData;
|
||||
</script>
|
||||
|
||||
<div class="flex h-screen w-full">
|
||||
<div class="flex h-full w-full flex-col items-center justify-center">
|
||||
<div class="flex flex-col justify-start space-y-4">
|
||||
<h2 class="mx-auto text-xl font-bold">Connexion</h2>
|
||||
<form class="flex w-52 flex-col justify-center space-y-4 sm:w-72" method="POST" use:enhance>
|
||||
<div class="flex w-full flex-col items-center justify-center">
|
||||
<div class="flex w-full max-w-xs flex-col gap-4">
|
||||
<h1 class="mx-auto text-xl font-bold">Connexion</h1>
|
||||
<form class="flex flex-col justify-center gap-2" method="POST" use:enhance>
|
||||
<label for="pseudo"> Nom d'utilisateur </label>
|
||||
<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>
|
||||
<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" />} -->
|
||||
Se connecter
|
||||
</Button>
|
||||
</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>
|
||||
|
|
|
@ -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 { API_URL } from '$env/static/private';
|
||||
|
||||
import { z } from 'zod';
|
||||
|
||||
export const load = (async ({ locals: { user } }) => {
|
||||
if (user) throw redirect(303, '/dashboard');
|
||||
}) 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 = {
|
||||
default: async (event) => {
|
||||
const data = await event.request.formData();
|
||||
|
||||
const email = data.get('email') as string;
|
||||
const firstname = data.get('firstname') as string;
|
||||
const lastname = data.get('lastname') as string;
|
||||
const pseudo = data.get('pseudo') as string;
|
||||
const passwd = data.get('passwd') as string;
|
||||
const description = data.get('description') as string;
|
||||
const sgroup = data.get('sgroup') as string;
|
||||
const avatar = data.get('avatar') as string;
|
||||
const parse = schema.safeParse(Object.fromEntries(data.entries()));
|
||||
|
||||
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}/register`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
email,
|
||||
firstname,
|
||||
lastname,
|
||||
pseudo,
|
||||
passwd,
|
||||
description,
|
||||
sgroup,
|
||||
avatar
|
||||
...parse.data
|
||||
})
|
||||
});
|
||||
|
||||
|
@ -45,6 +57,30 @@ export const actions = {
|
|||
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;
|
||||
|
|
|
@ -1,39 +1,73 @@
|
|||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
|
||||
import type { ActionData } from './$types';
|
||||
|
||||
import Button from '$lib/components/ui/Button.svelte';
|
||||
import Input from '$lib/components/ui/Input.svelte';
|
||||
|
||||
export let form: ActionData;
|
||||
</script>
|
||||
|
||||
<div class="flex h-screen w-full">
|
||||
<div class="flex h-full w-full flex-col items-center justify-center">
|
||||
<div class="flex flex-col justify-start space-y-4">
|
||||
<div class="flex w-full flex-col items-center justify-center">
|
||||
<div class="flex w-full max-w-xs flex-col gap-4">
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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="description" />
|
||||
<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" />} -->
|
||||
S'inscrire
|
||||
</Button>
|
||||
</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>
|
||||
|
|
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} */
|
||||
export default {
|
||||
content: ['./src/**/*.{html,js,svelte,ts}'],
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: {
|
||||
sans: ['Karrik', ...fontFamily.sans],
|
||||
fira: ['Fira Code', ...fontFamily.sans]
|
||||
},
|
||||
colors: {
|
||||
border: 'hsl(var(--border))',
|
||||
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