chore: merge to sveltekit #5

Merged
glazk0 merged 15 commits from sveltekit into main 2023-09-04 13:38:33 +02:00
34 changed files with 644 additions and 131 deletions
Showing only changes of commit b158d725d1 - Show all commits

View file

@ -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',

View file

@ -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>

View file

@ -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%;

View file

@ -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;

View 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}

View 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
>

View 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
>

View 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
>

View 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
>

View 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
>

View 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
>

View 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
>

View 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
>

View file

@ -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

View file

@ -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>
<div class="flex items-center gap-x-6">
{#if puzzle.tags?.length}
<div class="flex gap-x-2 text-sm text-muted">
{#each puzzle.tags as tag}
<span class="inline-block rounded-md bg-primary-800 px-2 py-1">
{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" /> -->
</h2>
<div class="flex items-center gap-x-6">
{#if puzzle.tags?.length}
<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 text-highlight-secondary"
>
{tag.name}
</span>
{/each}
</div>
{/if}
</div>
</div>
<span class="translate-x-0 transform-gpu duration-300 group-hover:translate-x-2">
<ChevronRight />
</span>
</a>
</li>

View file

@ -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"
src="/assets/brand/peerat.png"
alt="Peer-at"
width={50}
height={50}
loading="eager"
priority
/> -->
<img src="/assets/brand/peerat.png" alt="Logo" width="50" height="50" loading="eager">
<div class="flex w-full justify-center p-[8.5px]">
<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={{

View file

@ -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"

View file

@ -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"
>

View file

@ -0,0 +1,5 @@
import type { PageServerLoad } from './$types';
export const load = (async ({ parent }) => {
await parent();
}) satisfies PageServerLoad;

View file

@ -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

View file

@ -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`, {

View file

@ -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];

View file

@ -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>

View 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;

View file

@ -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} />

View file

@ -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;

View file

@ -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>

View file

@ -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;

View file

@ -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

Binary file not shown.

BIN
static/fonts/Karrik.woff2 Normal file

Binary file not shown.

View file

@ -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
View 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');
});

View file

@ -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();
});