UI Refactor and group support #13

Merged
glazk0 merged 17 commits from dev into main 2024-03-28 20:01:37 +01:00
170 changed files with 5436 additions and 3190 deletions

View file

@ -1,12 +1,37 @@
.git
# build folder
dist/
build/
# logs
logs/
.cache
.DS_Store
npm-debug.log
yarn-debug.log
yarn-error.log
# process data
.pid
pids
.pid-lock
# Coverage & Test
coverage
.nyc_output
# Dependency folder
node_modules
.eslint*
.prettier*
.git*
bower_components
jspm_packages
# Environment files
.dot
# Editor and other files
.idea
typing
internal
.vscode
README.md
Dockerfile*
docker-compose.yml
public
*.tgz
.eslintcache
.svelte-kit
build

View file

@ -1,35 +1,37 @@
# Base Stage
FROM node:21-slim AS base
FROM node:18-alpine AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN npm i -g pnpm
FROM base AS dependencies
RUN corepack enable
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
# Dependencies Stage for Production
FROM base AS deps
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --frozen-lockfile
# Build Stage
FROM base AS build
WORKDIR /app
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
COPY . .
COPY --from=dependencies /app/node_modules ./node_modules
RUN pnpm build
RUN pnpm prune --prod
RUN pnpm run build
# Deploy Stage
FROM base AS deploy
WORKDIR /app
COPY --from=deps /app/node_modules /app/node_modules
COPY --from=build /app/build /app/build
COPY --from=build /app/build ./build
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/package.json ./package.json
# Run as non-root user
# USER node
ARG PORT=3000

14
components.json Normal file
View file

@ -0,0 +1,14 @@
{
"$schema": "https://shadcn-svelte.com/schema.json",
"style": "default",
"tailwind": {
"config": "tailwind.config.js",
"css": "src/app.css",
"baseColor": "slate"
},
"aliases": {
"components": "$lib/components",
"utils": "$lib/utils"
},
"typescript": true
}

View file

@ -1,6 +1,5 @@
{
"name": "peer-at-code",
"version": "0.1.0",
"type": "module",
"private": true,
"scripts": {
@ -16,39 +15,45 @@
"test:unit": "vitest"
},
"dependencies": {
"bits-ui": "^0.20.1",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"lucide-svelte": "^0.279.0",
"marked": "^7.0.5",
"svelte-boring-avatars": "^1.2.4",
"tailwind-merge": "^1.14.0"
"clsx": "^2.1.0",
"formsnap": "^0.5.1",
"lucide-svelte": "^0.363.0",
"mode-watcher": "^0.3.0",
"svelte-boring-avatars": "^1.2.6",
"svelte-sonner": "^0.3.19",
"tailwind-merge": "^2.2.2",
"tailwind-variants": "^0.2.1",
"vaul-svelte": "^0.3.0"
},
"devDependencies": {
"@melt-ui/pp": "^0.1.4",
"@melt-ui/svelte": "^0.50.1",
"@playwright/test": "^1.40.0",
"@sveltejs/adapter-node": "^1.3.1",
"@sveltejs/kit": "^1.27.6",
"@types/marked": "^5.0.2",
"@typescript-eslint/eslint-plugin": "^5.62.0",
"@typescript-eslint/parser": "^5.62.0",
"autoprefixer": "^10.4.16",
"eslint": "^8.54.0",
"eslint-config-prettier": "^8.10.0",
"@playwright/test": "^1.42.1",
"@sveltejs/adapter-node": "^5.0.1",
"@sveltejs/kit": "^2.5.4",
"@sveltejs/vite-plugin-svelte": "^3.0.2",
"@tailwindcss/forms": "^0.5.7",
"@tailwindcss/typography": "^0.5.10",
"@typescript-eslint/eslint-plugin": "^7.3.1",
"@typescript-eslint/parser": "^7.3.1",
"autoprefixer": "^10.4.19",
"boring-avatars": "^1.10.1",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.35.1",
"postcss": "^8.4.31",
"prettier": "^2.8.8",
"prettier-plugin-svelte": "^2.10.1",
"prettier-plugin-tailwindcss": "^0.4.1",
"svelte": "^4.2.7",
"svelte-check": "^3.6.1",
"svelte-sequential-preprocessor": "^2.0.1",
"sveltekit-superforms": "^1.10.2",
"tailwindcss": "^3.3.5",
"mdsvex": "^0.11.0",
"postcss": "^8.4.38",
"prettier": "^3.2.5",
"prettier-plugin-svelte": "^3.2.2",
"prettier-plugin-tailwindcss": "^0.5.12",
"svelte": "^4.2.12",
"svelte-check": "^3.6.8",
"sveltekit-superforms": "^2.11.0",
"tailwindcss": "^3.4.1",
"tslib": "^2.6.2",
"typescript": "^5.3.2",
"vite": "^4.5.0",
"vitest": "^0.32.4",
"typescript": "^5.4.3",
"vite": "^5.2.6",
"vitest": "^1.4.0",
"zod": "^3.22.4"
}
}

2212
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -1,103 +1,105 @@
@import 'tailwindcss/base';
@import 'tailwindcss/components';
@import 'tailwindcss/utilities';
@tailwind base;
@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%;
--foreground: 222.2 84% 4.9%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--ring: 215 20.2% 65.1%;
--radius: 0.5rem;
@font-face {
font-family: 'Karrik';
src: url('/fonts/Karrik.woff2');
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
@font-face {
font-family: 'Fira Code';
src: url('/fonts/FiraCode.woff2');
}
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
:root {
--gradient: linear-gradient(to top left, hsl(258 85% 67%), hsl(258 60% 40%));
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--background: 255 14.81% 10.59%;
--foreground: 284 4.7% 97.05%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--muted: 258 15% 17%;
--muted-foreground: 284 4.7% 54.1%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--popover: 256.36 14.29% 15.1%;
--popover-foreground: 284 4.7% 97.05%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--card: 256.36 14.29% 15.1%;
--card-foreground: 284 4.7% 97.05%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--border: 258 15% 17%;
--input: 258 15% 17%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--primary: 258 85% 67%;
--primary-foreground: 284 4.7% 97.05%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 85.7% 97.3%;
--secondary: 258 60% 40%;
--secondary-foreground: 284 4.7% 97.05%;
--ring: 217.2 32.6% 17.5%;
--accent: 258 60% 40%;
--accent-foreground: 284 4.7% 97.05%;
--destructive: 7 100% 67%;
--destructive-foreground: 284 4.7% 97.05%;
--ring: 258 85% 67%;
--radius: 0.5rem;
}
}
@layer base {
* {
@apply border-border text-white;
@apply border-border;
}
body {
@apply text-foreground;
@apply bg-background text-foreground;
}
::selection {
background-color: hsl(var(--primary));
color: hsl(var(--primary-foreground));
}
::-webkit-scrollbar {
@apply w-2;
@apply h-2;
}
::-webkit-scrollbar-track {
@apply !bg-muted;
}
::-webkit-scrollbar-thumb {
@apply rounded-sm !bg-muted-foreground/30;
}
/* Firefox */
/* https://developer.mozilla.org/en-US/docs/Web/CSS/scrollbar-color#browser_compatibility */
html {
scrollbar-color: hsl(215.4 16.3% 46.9% / 0.3);
}
.hide-scrollbar::-webkit-scrollbar {
display: none;
}
.hide-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
}
.antialised {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
}
@layer components {
.console {
@apply relative top-0.5 inline-block;
}
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
textarea:-webkit-autofill,
textarea:-webkit-autofill:hover,
textarea:-webkit-autofill:focus {
-webkit-box-shadow: 0 0 0px 1000px hsl(258deg 15% 17%) inset;
transition: background-color 5000s ease-in-out 0s;
.bg-gradient {
background: var(--gradient);
}
}

4
src/app.d.ts vendored
View file

@ -7,10 +7,10 @@ declare global {
namespace App {
// interface Error {}
interface Locals {
user?: User;
user: User | null;
}
interface PageData {
user?: User;
user: User | null;
}
// interface Platform {}
}

View file

@ -1,31 +1,15 @@
<!DOCTYPE html>
<html lang="fr" class="scroll-smooth bg-gradient-to-b from-primary-800 to-primary-900">
<head>
<html lang="fr" style="color-scheme: dark;">
<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" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" />
<script data-domain="app.peerat.dev" src="https://plosibl.peerat.dev/js/script.js" defer></script>
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover" class="relative min-h-screen">
<main style="display: contents">%sveltekit.body%</main>
</body>
</head>
<body data-sveltekit-preload-data="hover" class="min-h-screen antialiased font-sans text-base">
<div style="display: contents" class="relative flex min-h-screen flex-col">%sveltekit.body%</div>
</body>
</html>

View file

@ -4,24 +4,29 @@ import { API_URL } from '$env/static/private';
import type { User } from '$lib/types';
export const handle = (async ({ event, resolve }) => {
export const handle: Handle = async ({ event, resolve }) => {
const session = event.cookies.get('session');
if (session) {
if (!session) {
event.locals.user = null;
return resolve(event);
}
const res = await fetch(`${API_URL}/player/`, {
headers: {
Authorization: `Bearer ${session}`
}
});
if (res.ok) {
const user = (await res.json()) as User;
event.locals.user = user;
} else {
event.locals.user = undefined;
event.cookies.delete('session');
}
if (!res.ok) {
event.locals.user = null;
event.cookies.delete('session', { path: '/' });
return resolve(event);
}
return await resolve(event);
}) satisfies Handle;
const user: User = await res.json();
event.locals.user = user;
return resolve(event);
};

View file

@ -1,6 +0,0 @@
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

View file

@ -1,16 +0,0 @@
<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

@ -1,16 +0,0 @@
<script lang="ts">
export let title: string;
export let data: any;
</script>
<div
class="flex w-full items-center space-x-4 rounded-lg border-2 border-highlight-primary bg-primary-700 p-4 shadow-md"
>
<!-- <Icon class="text-muted" size={30} /> -->
<div class="flex w-full items-center justify-between">
<div class="flex-col">
<h2 class="text-xl font-semibold">{data}</h2>
<p class="text-muted">{title}</p>
</div>
</div>
</div>

View file

@ -1,47 +0,0 @@
<script lang="ts">
import { cn } from '$lib/Utils';
import type { Chapter } from '$lib/types';
import { Trophy } from 'lucide-svelte';
import ChevronRight from './Icons/ChevronRight.svelte';
import Button from './ui/Button.svelte';
export let chapter: Chapter;
</script>
<li
class={cn(
'group relative flex h-full w-full flex-col rounded-md bg-primary-700 transition-colors duration-150',
{
'hover:bg-primary-600': chapter.show,
'opacity-50': !chapter.show
}
)}
>
{#if chapter.show}
<a
class="flex h-full w-full items-center justify-between gap-4 p-4"
href={chapter.show ? `/dashboard/chapters/${chapter.id}` : '#'}
>
<div class="flex items-center gap-2">
<span class="text-base font-semibold">
{chapter.name}
</span>
{#if chapter.id === 1}
<Trophy class="stroke-highlight-secondary" />
{/if}
</div>
<span class="translate-x-0 transform-gpu duration-300 group-hover:translate-x-2">
<ChevronRight />
</span>
</a>
{:else}
<span class="flex h-full w-full items-center gap-4 p-4">
<div class="flex w-full flex-col justify-between gap-2 sm:flex-row">
<h2 class="text-base font-semibold">
{chapter.name}
</h2>
</div>
</span>
{/if}
</li>

View file

@ -1,26 +0,0 @@
<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

@ -1,21 +0,0 @@
<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

@ -1,20 +0,0 @@
<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

@ -1,21 +0,0 @@
<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

@ -1,33 +0,0 @@
<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

@ -1,19 +0,0 @@
<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"
height="24"
width="24"
viewBox="0 0 640 512"
fill="currentColor"
><path
d="M524.531,69.836a1.5,1.5,0,0,0-.764-.7A485.065,485.065,0,0,0,404.081,32.03a1.816,1.816,0,0,0-1.923.91,337.461,337.461,0,0,0-14.9,30.6,447.848,447.848,0,0,0-134.426,0,309.541,309.541,0,0,0-15.135-30.6,1.89,1.89,0,0,0-1.924-.91A483.689,483.689,0,0,0,116.085,69.137a1.712,1.712,0,0,0-.788.676C39.068,183.651,18.186,294.69,28.43,404.354a2.016,2.016,0,0,0,.765,1.375A487.666,487.666,0,0,0,176.02,479.918a1.9,1.9,0,0,0,2.063-.676A348.2,348.2,0,0,0,208.12,430.4a1.86,1.86,0,0,0-1.019-2.588,321.173,321.173,0,0,1-45.868-21.853,1.885,1.885,0,0,1-.185-3.126c3.082-2.309,6.166-4.711,9.109-7.137a1.819,1.819,0,0,1,1.9-.256c96.229,43.917,200.41,43.917,295.5,0a1.812,1.812,0,0,1,1.924.233c2.944,2.426,6.027,4.851,9.132,7.16a1.884,1.884,0,0,1-.162,3.126,301.407,301.407,0,0,1-45.89,21.83,1.875,1.875,0,0,0-1,2.611,391.055,391.055,0,0,0,30.014,48.815,1.864,1.864,0,0,0,2.063.7A486.048,486.048,0,0,0,610.7,405.729a1.882,1.882,0,0,0,.765-1.352C623.729,277.594,590.933,167.465,524.531,69.836ZM222.491,337.58c-28.972,0-52.844-26.587-52.844-59.239S193.056,219.1,222.491,219.1c29.665,0,53.306,26.82,52.843,59.239C275.334,310.993,251.924,337.58,222.491,337.58Zm195.38,0c-28.971,0-52.843-26.587-52.843-59.239S388.437,219.1,417.871,219.1c29.667,0,53.307,26.82,52.844,59.239C470.715,310.993,447.538,337.58,417.871,337.58Z"
/></svg
>

View file

@ -1,23 +0,0 @@
<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="M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4"
/><path d="M9 18c-4.51 2-5-2-7-2" /></svg
>

View file

@ -1,23 +0,0 @@
<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="12" r="10" /><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" /><path
d="M12 17h.01"
/></svg
>

View file

@ -1,25 +0,0 @@
<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"
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

@ -1,25 +0,0 @@
<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="M22 10.5V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v12c0 1.1.9 2 2 2h12.5" /><path
d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"
/><path d="M18 15.28c.2-.4.5-.8.9-1a2.1 2.1 0 0 1 2.6.4c.3.4.5.8.5 1.3 0 1.3-2 2-2 2" /><path
d="M20 22v.01"
/></svg
>

View file

@ -1,36 +0,0 @@
<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

@ -1,20 +0,0 @@
<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,95 +0,0 @@
<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;
$: segments = $page.url.pathname.slice(1).split('/');
$: breadcrumb = segments.map((segment, index) => {
return { name: segment, href: '/' + segments.slice(0, index + 1).join('/') };
}) as { name: string; href: string }[];
function handleToggle() {
isOpen = !isOpen;
}
</script>
<div
class="z-50 flex w-full flex-row items-center justify-between border-b border-solid border-highlight-primary px-4 py-4 sm:px-8"
>
<div class="flex flex-row items-center space-x-2 sm:space-x-0">
<div class="flex items-center">
<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 !isOpen && segments.length}
<div class="hidden items-center justify-center capitalize text-highlight-secondary sm:flex">
{#each breadcrumb as segment}
{@const last = segment === breadcrumb[breadcrumb.length - 1]}
<a class="hover:text-primary hover:underline" href={segment.href}>
{segment.name}
</a>
{#if !last}
<span class="mx-1 text-highlight-secondary">/</span>
{/if}
{/each}
</div>
{/if}
<!-- {#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 gap-2">
{#if !isOpen}
<a href="/logout">
<button
class="flex items-center gap-1 rounded-md p-2 text-destructive hover:bg-destructive/10"
>
Se déconnecter
</button>
</a>
{/if}
<div class="flex flex-row items-center gap-2">
<Avatar />
{user?.pseudo}
</div>
<!-- {!isLoading && me ? (
<Popover
open={isMenuOpen}
onOpenChange={setIsMenuOpen}
trigger={
<button class="mx-auto flex items-center gap-2">
<AvatarComponent name={me.pseudo} src={me.avatar} class="h-9 w-9" />
<span>{me?.pseudo}</span>
</button>
}
>
<nav class="flex w-32 flex-col gap-2">
<button
class="flex items-center gap-1 p-2 text-destructive hover:bg-destructive/10"
onClick={() => router.push('/logout')}
>
Se déconnecter
</button>
</nav>
</Popover>
) : (
<div class="animate-pulse">
<div class="flex items-center gap-2">
<div class="h-9 w-9 rounded-full bg-highlight-primary" />
<div class="h-4 w-14 rounded-full bg-highlight-primary" />
</div>
</div>
)} -->
</div>
</div>

View file

@ -1,211 +0,0 @@
<script lang="ts">
import { page } from '$app/stores';
import { cn } from '$lib/Utils';
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';
import Discord from './Icons/Discord.svelte';
import Git from './Icons/Git.svelte';
import Help from './Icons/Help.svelte';
import Mail from './Icons/Mail.svelte';
$: path = $page.url.pathname;
$: isActive = (slug: string) => path === slug;
export let isOpen: boolean;
const navItems = [
{
name: 'Dashboard',
slug: '/dashboard',
icon: Dashboard
},
{
name: 'Classement',
slug: '/dashboard/leaderboard',
icon: Leaderboard
},
{
name: 'Chapitres',
slug: '/dashboard/chapters',
icon: Code
},
{
name: 'Badges',
slug: '/dashboard/badges',
icon: Badge
},
{
name: 'Paramètres',
slug: '/dashboard/settings',
icon: Settings
}
];
</script>
<aside
class={cn(
'absolute z-10 h-screen w-28 border-r border-highlight-primary bg-gradient-to-b from-primary-800 to-primary-900 shadow-md transition-all duration-300 ease-in-out sm:relative sm:flex sm:flex-col lg:w-60',
{
'bottom-0 -translate-x-full sm:translate-x-0': !isOpen,
'bottom-0 w-full sm:w-28': isOpen
}
)}
>
<div class="flex h-full flex-col">
<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="flex flex-col gap-4">
{#each navItems as item}
<li>
<a
on:click={() => {
isOpen = false;
}}
href={item.slug}
class={cn(
'flex items-center justify-center gap-2 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)
}
)}
>
<svelte:component
this={item.icon}
class={cn({
'stroke-highlight-secondary transition-colors duration-150 group-hover:stroke-primary-0':
!isActive(item.slug)
})}
/>
<span
class={cn('hidden lg:block', {
'block sm:hidden': isOpen,
hidden: !isOpen,
'text-highlight-secondary transition-colors duration-150 group-hover:text-primary':
!isActive(item.slug)
})}
>
{item.name}
</span>
</a>
</li>
{/each}
</ul>
</div>
<div class="px-4 pt-4">
<hr class="border-highlight-primary" />
</div>
<div class="px-4 pt-4">
<ul class="flex flex-col gap-4">
<li>
<span
class="group pointer-events-none flex items-center justify-center gap-2 rounded-md px-3 py-3 text-sm opacity-50 transition-colors duration-150 lg:justify-start"
>
<Help class="stroke-highlight-secondary transition-colors duration-150" />
<span
class={cn('hidden text-highlight-secondary transition-colors duration-150 lg:block', {
'block sm:hidden': isOpen,
hidden: !isOpen
})}
>
Aide
</span>
</span>
</li>
<li>
<a
on:click={() => {
isOpen = false;
}}
href="mailto:cyberbottle@peerat.dev"
class="group flex items-center justify-center gap-2 rounded-md px-3 py-3 text-sm transition-colors duration-150 hover:bg-primary-700 lg:justify-start"
>
<Mail
class="stroke-highlight-secondary transition-colors duration-150 group-hover:stroke-primary-0"
/>
<span
class={cn(
'hidden text-highlight-secondary transition-colors duration-150 group-hover:text-primary lg:block',
{
'block sm:hidden': isOpen,
hidden: !isOpen
}
)}
>
Mail
</span>
</a>
</li>
<li>
<a
on:click={() => {
isOpen = false;
}}
href="//discord.gg/72vuHcwUkE"
target="_blank"
rel="noopener"
class="group flex items-center justify-center gap-2 rounded-md px-3 py-3 text-sm transition-colors duration-150 hover:bg-primary-700 lg:justify-start"
>
<Discord
class="fill-highlight-secondary transition-colors duration-150 group-hover:fill-primary-0"
/>
<span
class={cn(
'hidden text-highlight-secondary transition-colors duration-150 group-hover:text-primary lg:block',
{
'block sm:hidden': isOpen,
hidden: !isOpen
}
)}
>
Discord
</span>
</a>
</li>
<li>
<a
on:click={() => {
isOpen = false;
}}
href="//git.peerat.dev"
target="_blank"
rel="noopener"
class="group flex items-center justify-center gap-2 rounded-md px-3 py-3 text-sm transition-colors duration-150 hover:bg-primary-700 lg:justify-start"
>
<Git
class="stroke-highlight-secondary transition-colors duration-150 group-hover:stroke-primary-0"
/>
<span
class={cn(
'hidden text-highlight-secondary transition-colors duration-150 group-hover:text-primary lg:block',
{
'block sm:hidden': isOpen,
hidden: !isOpen
}
)}
>
Git
</span>
</a>
</li>
</ul>
</div>
</div>
</aside>

View file

@ -1,55 +0,0 @@
<script lang="ts" context="module">
export type ToastData = {
title: string;
description: string;
};
const {
elements: { content, title, description, close },
helpers,
states: { toasts },
actions: { portal }
} = createToaster<ToastData>();
export const addToast = helpers.addToast;
</script>
<script lang="ts">
import { createToaster, melt } from '@melt-ui/svelte';
import { flip } from 'svelte/animate';
import { fly } from 'svelte/transition';
import { X } from 'lucide-svelte';
</script>
<div class="fixed right-0 top-0 z-50 m-4 flex flex-col items-end gap-2 sm:top-20" use:portal>
{#each $toasts as { id, data } (id)}
<div
use:melt={$content(id)}
animate:flip={{ duration: 500 }}
in:fly={{ duration: 150, x: '100%' }}
out:fly={{ duration: 150, x: '100%' }}
class="rounded-lg border border-primary-600 bg-highlight-primary text-white shadow-md"
>
<div
class="relative flex w-[24rem] max-w-[calc(100vw-2rem)] items-center justify-between gap-4 p-5"
>
<div>
<h3 use:melt={$title(id)} class="flex items-center gap-2 font-semibold">
{data.title}
<span class="square-1.5 rounded-full" />
</h3>
<div use:melt={$description(id)}>
{data.description}
</div>
</div>
<button
use:melt={$close(id)}
class="text-magnum-500 square-6 hover:bg-magnum-900/50 absolute right-4 top-4 grid place-items-center
rounded-full"
>
<X class="square-4" />
</button>
</div>
</div>
{/each}
</div>

View file

@ -1,5 +1,5 @@
<script lang="ts">
import { cn } from '$lib/Utils';
import { cn } from '$lib/utils';
export let name: string;
export let src: string;

View file

@ -0,0 +1,28 @@
<script lang="ts">
import { page } from '$app/stores';
import * as Breadcrumb from '$lib/components/ui/breadcrumb';
$: segments = $page.url.pathname.slice(1).split('/');
$: breadcrumb = segments.map((segment, index) => {
return {
name: segment.charAt(0).toUpperCase() + segment.slice(1),
href: '/' + segments.slice(0, index + 1).join('/')
};
}) as { name: string; href: string }[];
</script>
<Breadcrumb.Root>
<Breadcrumb.List>
{#each breadcrumb as { name, href }, index}
<Breadcrumb.Item>
<Breadcrumb.Link {href}>
{name}
</Breadcrumb.Link>
</Breadcrumb.Item>
{#if index < breadcrumb.length - 1}
<Breadcrumb.Separator />
{/if}
{/each}
</Breadcrumb.List>
</Breadcrumb.Root>

View file

@ -0,0 +1,16 @@
<script lang="ts">
export let title: string;
export let data: any;
</script>
<div
class="flex w-full items-center space-x-4 border rounded border-border bg-card p-4 shadow-md"
>
<!-- <Icon class="text-muted-foreground" size={30} /> -->
<div class="flex w-full items-center justify-between">
<div class="">
<h2 class="text-xl font-semibold">{data}</h2>
<p class="text-muted-foreground">{title}</p>
</div>
</div>
</div>

View file

@ -0,0 +1,46 @@
<script lang="ts">
import Swords from 'lucide-svelte/icons/swords';
import ChevronRight from 'lucide-svelte/icons/chevron-right';
import type { Chapter } from '$lib/types';
import { cn } from '$lib/utils';
export let chapter: Chapter;
</script>
<li
class={cn(
'group relative flex h-full w-full flex-col rounded border border-border bg-card transition-colors duration-150',
{
'hover:bg-card/80': chapter.show || (chapter.start && chapter.end),
'opacity-50': !chapter.show && !(chapter.start && chapter.end)
}
)}
>
{#if chapter.show || (chapter.start && chapter.end)}
<a
class="flex h-full w-full items-center justify-between gap-4 p-4"
href={chapter.show || (chapter.start && chapter.end) ? `/chapters/${chapter.id}` : null}
>
<div class="flex items-center gap-2">
<span class="font-semibold">
{chapter.name}
</span>
{#if chapter.start && chapter.end}
<Swords class="stroke-muted-foreground" />
{/if}
</div>
<span class="translate-x-0 transform-gpu duration-300 group-hover:translate-x-2">
<ChevronRight />
</span>
</a>
{:else}
<span class="flex h-full w-full items-center gap-4 p-4">
<div class="flex w-full flex-col justify-between gap-2 sm:flex-row">
<h2 class="font-semibold">
{chapter.name}
</h2>
</div>
</span>
{/if}
</li>

View file

@ -0,0 +1,31 @@
<script lang="ts">
import { Check, Copy } from 'lucide-svelte';
let element: HTMLButtonElement;
let copying = false;
const copy = () => {
const sibling = element.nextElementSibling as HTMLElement;
const code = sibling.innerText;
navigator.clipboard.writeText(code);
copying = true;
setTimeout(() => (copying = false), 1000);
};
</script>
<button
class="absolute right-2 top-2 rounded-md p-2 text-white"
bind:this={element}
on:click={copy}
{...$$restProps}
>
{#if copying}
<Check color="green" />
{:else}
<Copy />
{/if}
</button>

View file

@ -0,0 +1,47 @@
<script lang="ts">
import { onMount } from 'svelte';
import CopyCodeButton from './copy-code-button.svelte';
onMount(() => {
const preTags: HTMLCollectionOf<HTMLPreElement> = document.getElementsByTagName('pre');
for (let preTag of preTags) {
const classList = Array.from(preTag.classList);
const isCodeBlock = classList.some((className) => className.startsWith('language-'));
if (isCodeBlock) {
const preTagParent = preTag.parentNode;
const newCodeBlockWrapper = document.createElement('div');
newCodeBlockWrapper.className = 'relative';
new CopyCodeButton({
target: newCodeBlockWrapper
});
if (preTagParent) {
preTagParent.replaceChild(newCodeBlockWrapper, preTag);
newCodeBlockWrapper.appendChild(preTag);
}
}
}
return () => {
for (let preTag of preTags) {
const preTagParent = preTag.parentNode;
if (preTagParent) {
const newCodeBlockWrapper = preTagParent.parentNode;
if (newCodeBlockWrapper) {
preTagParent.replaceChild(preTag, newCodeBlockWrapper);
}
}
}
};
});
</script>
<slot />

View file

@ -0,0 +1,4 @@
// TODO: Add more components here
export { default as CopyCodeInjector } from './copy-code-injector.svelte';
export { default as Metadata } from './metadata.svelte';

View file

@ -0,0 +1,3 @@
export { default as Loader } from './loader/loader.svelte';
export { default as Navbar } from './navbar/navbar.svelte';
export { default as Sidenav } from './sidenav/sidenav.svelte';

View file

@ -0,0 +1,42 @@
<div class="loading-bar">
<div class="progress-bar">
<div class="progress-bar-value" />
</div>
</div>
<style>
.loading-bar {
width: 100%;
position: fixed;
top: 0;
left: 0;
z-index: 100000;
}
.progress-bar {
@apply bg-muted;
height: 2px;
width: 100%;
overflow: hidden;
}
.progress-bar-value {
@apply bg-primary;
width: 100%;
height: 100%;
animation: loadingBarAnimation 2s infinite linear;
transform-origin: 0% 50%;
}
@keyframes loadingBarAnimation {
0% {
transform: translateX(0) scaleX(0);
}
40% {
transform: translateX(0) scaleX(0.4);
}
100% {
transform: translateX(100%) scaleX(0.5);
}
}
</style>

View file

@ -0,0 +1,63 @@
<script lang="ts">
import { page } from '$app/stores';
import type { NavItemWithChildren } from '$lib/config';
import { cn } from '$lib/utils';
import MobileNavLink from './mobile-nav-link.svelte';
export let navItem: NavItemWithChildren;
</script>
{#if navItem.children?.length}
<h5 class="mb-2 font-medium">{navItem.name}</h5>
<ul class="space-y-2">
{#each navItem.children as item}
{@const isActive = $page.url.pathname === item.href}
<li>
<MobileNavLink
class={cn(
'group flex items-center gap-2 rounded border border-border border-opacity-0 p-2 font-semibold text-muted-foreground lg:leading-6',
{
'border border-opacity-100 bg-card text-foreground': isActive,
'hover:border-opacity-100 hover:bg-card hover:text-foreground': !isActive
}
)}
href={item.href}
external={item.external}
>
<svelte:component
this={item.icon}
class={cn('stroke-muted-foreground', {
'stroke-foreground': $page.url.pathname === navItem.href,
'group-hover:stroke-foreground': $page.url.pathname !== navItem.href
})}
/>
<span>{item.name}</span>
</MobileNavLink>
</li>
{/each}
</ul>
{:else}
<MobileNavLink
class={cn(
'group flex items-center gap-2 rounded border border-border border-opacity-0 p-2 font-semibold text-muted-foreground lg:leading-6',
{
'border border-opacity-100 bg-card text-foreground': $page.url.pathname === navItem.href,
'hover:border-opacity-100 hover:bg-card hover:text-foreground':
$page.url.pathname !== navItem.href
}
)}
href={navItem.href}
external={navItem.external}
>
<svelte:component
this={navItem.icon}
class={cn('stroke-muted-foreground', {
'stroke-foreground': $page.url.pathname === navItem.href,
'group-hover:stroke-foreground': $page.url.pathname !== navItem.href
})}
/>
<span>{navItem.name}</span>
</MobileNavLink>
{/if}

View file

@ -0,0 +1,32 @@
<script lang="ts">
import { ArrowUpRight } from 'lucide-svelte';
import type { HTMLAnchorAttributes } from 'svelte/elements';
import { Drawer } from 'vaul-svelte';
import { cn } from '$lib/utils';
type $$Props = HTMLAnchorAttributes & {
external?: boolean;
};
let className: string | undefined | null = undefined;
export { className as class };
export let href: $$Props['href'] = '';
export let external = false;
</script>
<Drawer.Close asChild let:builder>
<a
use:builder.action
{href}
target={external ? '_blank' : undefined}
class={cn(external && 'flex items-center gap-0.5', className)}
{...$$restProps}
>
<slot />
{#if external}
<ArrowUpRight class="h-3 w-3" />
{/if}
</a>
</Drawer.Close>

View file

@ -0,0 +1,32 @@
<script lang="ts">
import { Menu } from 'lucide-svelte';
import { navigation } from '$lib/config';
import MobileNavItem from './mobile-nav-item.svelte';
import * as Drawer from '$lib/components/ui/drawer';
import Button from '$lib/components/ui/button/button.svelte';
</script>
<Drawer.Root>
<Drawer.Trigger asChild let:builder aria-label="open mobile menu">
<Button class="sm:hidden" builders={[builder]} variant="outline" size="icon"
><Menu class="h-4 w-4" /></Button
>
</Drawer.Trigger>
<Drawer.Content>
<div class="container mx-auto">
<Drawer.Title class="sr-only mb-4 font-medium">Navigation</Drawer.Title>
<nav class="py-4">
<ul class="flex w-full flex-col justify-center gap-4">
{#each navigation as navItem}
<li>
<MobileNavItem {navItem} />
</li>
{/each}
</ul>
</nav>
</div>
</Drawer.Content>
</Drawer.Root>

View file

@ -0,0 +1,91 @@
<script lang="ts">
import { page } from '$app/stores';
import Boring from 'svelte-boring-avatars';
import Award from 'lucide-svelte/icons/award';
import Github from 'lucide-svelte/icons/github';
import LifeBuoy from 'lucide-svelte/icons/life-buoy';
import LogOut from 'lucide-svelte/icons/log-out';
import Settings from 'lucide-svelte/icons/settings';
import Users from 'lucide-svelte/icons/users';
import Shield from 'lucide-svelte/icons/shield';
import ScrollText from 'lucide-svelte/icons/scroll-text';
import Code from 'lucide-svelte/icons/code';
import * as Avatar from '$lib/components/ui/avatar';
import { Button } from '$lib/components/ui/button';
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
</script>
<DropdownMenu.Root>
<DropdownMenu.Trigger asChild let:builder>
<Button builders={[builder]} variant="plain" size="icon">
<Avatar.Root>
<Avatar.Image
src="data:image;base64,{$page.data.user?.avatar}"
alt={$page.data.user?.pseudo}
/>
<Avatar.Fallback>
<Boring name={$page.data.user?.pseudo} variant="beam" />
</Avatar.Fallback>
</Avatar.Root>
</Button>
</DropdownMenu.Trigger>
<DropdownMenu.Content class="w-56">
<DropdownMenu.Label
>Salutation, <span class="text-primary">{$page.data.user?.pseudo}</span></DropdownMenu.Label
>
{#if $page.data.user?.email.endsWith('@peerat.dev')}
<DropdownMenu.Separator />
<DropdownMenu.Sub>
<DropdownMenu.SubTrigger>
<Shield class="mr-2 h-4 w-4" />
<span>Administration</span>
</DropdownMenu.SubTrigger>
<DropdownMenu.SubContent>
<DropdownMenu.Item href="/admin/logs">
<ScrollText class="mr-2 h-4 w-4" />
<span>Logs</span>
</DropdownMenu.Item>
<DropdownMenu.Item href="/admin/chapters">
<Code class="mr-2 h-4 w-4" />
<span>Chapitres</span>
</DropdownMenu.Item>
<DropdownMenu.Item href="/admin/puzzles">
<Code class="mr-2 h-4 w-4" />
<span>Puzzles</span>
</DropdownMenu.Item>
</DropdownMenu.SubContent>
</DropdownMenu.Sub>
{/if}
<DropdownMenu.Separator />
<DropdownMenu.Item href="/settings">
<Settings class="mr-2 h-4 w-4" />
<span>Paramètres</span>
</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item href="/badges">
<Award class="mr-2 h-4 w-4" />
<span>Mes badges</span>
</DropdownMenu.Item>
<!-- <DropdownMenu.Item href="/groups">
<Users class="mr-2 h-4 w-4" />
<span>Mes équipes</span>
</DropdownMenu.Item> -->
<DropdownMenu.Separator />
<DropdownMenu.Item href="/git" target="_blank">
<Github class="mr-2 h-4 w-4" />
<span>GitHub</span>
</DropdownMenu.Item>
<DropdownMenu.Item href="/discord" target="_blank">
<LifeBuoy class="mr-2 h-4 w-4" />
<span>Discord</span>
</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item href="/logout" class="text-destructive data-[highlighted]:bg-destructive">
<LogOut class="mr-2 h-4 w-4" />
Se déconnecter
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>

View file

@ -0,0 +1,17 @@
<script lang="ts">
import Breadcrumb from '$lib/components/breadcrumb.svelte';
import MobileNav from '../mobile-nav/mobile-nav.svelte';
import NavbarUser from './navbar-user.svelte';
</script>
<nav class="w-full border-b border-muted p-4">
<div class="flex items-center justify-between">
<div class="flex items-center gap-2">
<MobileNav />
<Breadcrumb />
</div>
<div class="flex items-center">
<NavbarUser />
</div>
</div>
</nav>

View file

@ -0,0 +1,63 @@
<script lang="ts">
import { page } from '$app/stores';
import type { NavItemWithChildren } from '$lib/config';
import { cn } from '$lib/utils';
import SidenavLink from './sidenav-link.svelte';
export let navItem: NavItemWithChildren;
</script>
{#if navItem.children?.length}
<h5 class="mb-2 font-medium">{navItem.name}</h5>
<ul class="space-y-2">
{#each navItem.children as item}
{@const isActive = $page.url.pathname === item.href}
<li>
<SidenavLink
class={cn(
'group flex items-center gap-2 rounded border border-border border-opacity-0 p-2 font-semibold text-muted-foreground lg:leading-6',
{
'border border-opacity-100 bg-card text-foreground': isActive,
'hover:border-opacity-100 hover:bg-card hover:text-foreground': !isActive
}
)}
href={item.href}
external={item.external}
>
<svelte:component
this={item.icon}
class={cn('stroke-muted-foreground', {
'stroke-foreground': $page.url.pathname === navItem.href,
'group-hover:stroke-foreground': $page.url.pathname !== navItem.href
})}
/>
<span>{item.name}</span>
</SidenavLink>
</li>
{/each}
</ul>
{:else}
<SidenavLink
class={cn(
'group flex items-center gap-2 rounded border border-border border-opacity-0 p-2 font-semibold text-muted-foreground lg:leading-6',
{
'border border-opacity-100 bg-card text-foreground': $page.url.pathname === navItem.href,
'hover:border-opacity-100 hover:bg-card hover:text-foreground':
$page.url.pathname !== navItem.href
}
)}
href={navItem.href}
external={navItem.external}
>
<svelte:component
this={navItem.icon}
class={cn('stroke-muted-foreground', {
'stroke-foreground': $page.url.pathname === navItem.href,
'group-hover:stroke-foreground': $page.url.pathname !== navItem.href
})}
/>
<span>{navItem.name}</span>
</SidenavLink>
{/if}

View file

@ -0,0 +1,28 @@
<script lang="ts">
import { ArrowUpRight } from 'lucide-svelte';
import type { HTMLAnchorAttributes } from 'svelte/elements';
import { cn } from '$lib/utils';
type $$Props = HTMLAnchorAttributes & {
external?: boolean;
};
let className: string | undefined | null = undefined;
export { className as class };
export let href: $$Props['href'] = '';
export let external = false;
</script>
<a
{href}
target={external ? '_blank' : undefined}
class={cn(external && 'flex items-center gap-0.5', className)}
{...$$restProps}
>
<slot />
{#if external}
<ArrowUpRight class="h-3 w-3" />
{/if}
</a>

View file

@ -0,0 +1,27 @@
<script lang="ts">
import { navigation } from '$lib/config';
import SidenavItem from './sidenav-item.svelte';
</script>
<aside
class="hidden min-w-60 overflow-hidden border-r border-border transition-all duration-300 ease-in-out sm:flex sm:flex-col"
>
<div class="flex flex-col p-4">
<img
src="/assets/brand/peerat.webp"
alt="Logo"
width="50"
height="50"
loading="eager"
draggable="false"
/>
<ul class="flex flex-col gap-2 pt-4">
{#each navigation as navItem}
<li>
<SidenavItem {navItem} />
</li>
{/each}
</ul>
</div>
</aside>

View file

@ -0,0 +1,50 @@
<script lang="ts">
import { page } from '$app/stores';
import { siteConfig } from '$lib/config/site';
export let title = siteConfig.name;
$: title = $page.data?.title ? `${$page.data.title} | ${siteConfig.name}` : siteConfig.name;
</script>
<svelte:head>
<title>{title}</title>
<meta name="title" content={title} />
<meta name="description" content={siteConfig.description} />
<meta name="keywords" content={siteConfig.keywords.join(',')} />
<meta name="author" content={siteConfig.author} />
<meta name="theme-color" content={siteConfig.themeColor} />
<meta name="robots" content="noindex,nofollow" />
<meta itemprop="name" content={title} />
<meta itemprop="description" content={siteConfig.description} />
<meta itemprop="image" content={siteConfig.imageUrl} />
<meta property="og:site_name" content={siteConfig.name} />
<meta property="og:title" content={title} />
<meta property="og:description" content={siteConfig.description} />
<meta property="og:type" content="website" />
<meta property="og:url" content={siteConfig.url + $page.url.pathname} />
<meta property="og:image" content={siteConfig.imageUrl} />
<meta property="og:image:alt" content={title} />
<meta property="og:locale" content="fr" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content={siteConfig.url} />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={siteConfig.description} />
<meta name="twitter:image" content={siteConfig.imageUrl} />
<meta name="twitter:image:alt" content={title} />
<meta name="twitter:creator" content={`@${siteConfig.author}`} />
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="/assets/icons/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/assets/icons/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/assets/icons/favicon-16x16.png" />
</svelte:head>

View file

@ -1,25 +1,24 @@
<script lang="ts">
import { page } from '$app/stores';
import { cn } from '$lib/Utils';
import ChevronRight from 'lucide-svelte/icons/chevron-right';
import type { Puzzle } from '$lib/types';
import ChevronRight from './Icons/ChevronRight.svelte';
import { cn } from '$lib/utils';
export let puzzle: Puzzle;
const chapterId = $page.params.chapterId;
$: tags = puzzle.tags?.filter((tag) => !['easy', 'medium', 'hard'].includes(tag.name));
</script>
<li
class={cn(
'group relative flex h-full w-full rounded-md border-2 bg-primary-700 transition-colors duration-150',
'group relative flex h-full w-full rounded border border-border bg-card transition-colors duration-150',
{
'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-red-600/30': puzzle.tags?.find((tag) => tag.name.toLowerCase() === 'hard'),
'border-highlight-primary': !puzzle.tags?.length,
'hover:bg-primary-600': puzzle.show,
'border-green-500/30': puzzle.tags?.find((tag) => tag.name.toLowerCase() === 'easy'),
'border-yellow-500/30': puzzle.tags?.find((tag) => tag.name.toLowerCase() === 'medium'),
'border-red-500/30': puzzle.tags?.find((tag) => tag.name.toLowerCase() === 'hard'),
'hover:bg-card/80': puzzle.show,
'opacity-50': !puzzle.show
}
)}
@ -27,12 +26,12 @@
{#if puzzle.show}
<a
class="flex h-full w-full items-center gap-4 p-4"
href="/dashboard/chapters/{chapterId}/puzzle/{puzzle.id}"
href="/chapters/{chapterId}/puzzle/{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">
<span class="text-highlight-secondary text-sm">
({puzzle.score ? `${puzzle.score}` : '?'}/{puzzle.scoreMax} points)
</span>
</h2>
@ -40,9 +39,7 @@
<div class="flex items-center gap-x-6">
<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"
>
<span class="inline-block rounded bg-muted px-2 py-1 text-muted-foreground">
{tag.name}
</span>
{/each}
@ -59,7 +56,7 @@
<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">
<span class="text-highlight-secondary text-sm">
({puzzle.score ? `${puzzle.score}` : '?'}/{puzzle.scoreMax} points)
</span>
</h2>
@ -67,9 +64,7 @@
{#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"
>
<span class="inline-block rounded bg-muted px-2 py-1 text-muted-foreground">
{tag.name}
</span>
{/each}

View file

@ -1,78 +0,0 @@
<script lang="ts">
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from 'svelte/elements';
import { type VariantProps, cva } from 'class-variance-authority';
import { cn } from '$lib';
// TODO: Remove this
export const buttonVariants = cva(
'inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
outline: 'border border-input hover:bg-accent hover:text-accent-foreground',
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'underline-offset-4 hover:underline text-primary',
brand: 'bg-gradient-to-tl from-brand to-brand-accent transition-opacity hover:opacity-90'
},
size: {
default: 'h-10 py-2 px-4',
sm: 'h-9 px-3 rounded-md',
lg: 'h-11 px-8 rounded-md'
}
},
defaultVariants: {
variant: 'default',
size: 'default'
}
}
);
let className: string | undefined | null = undefined;
export { className as class };
export let href: HTMLAnchorAttributes['href'] = undefined;
export let type: HTMLButtonAttributes['type'] = undefined;
export let variant: VariantProps<typeof buttonVariants>['variant'] = 'default';
export let size: VariantProps<typeof buttonVariants>['size'] = 'default';
type Props = {
class?: string | null;
variant?: VariantProps<typeof buttonVariants>['variant'];
size?: VariantProps<typeof buttonVariants>['size'];
};
interface AnchorElement extends Props, HTMLAnchorAttributes {
href?: HTMLAnchorAttributes['href'];
type?: never;
}
interface ButtonElement extends Props, HTMLButtonAttributes {
type?: HTMLButtonAttributes['type'];
href?: never;
}
type $$Props = AnchorElement | ButtonElement;
</script>
<svelte:element
this={href ? 'a' : 'button'}
type={href ? undefined : type}
{href}
class={cn(buttonVariants({ variant, size, className }))}
{...$$restProps}
on:click
on:change
on:keydown
on:keyup
on:mouseenter
on:mouseleave
role="button"
tabindex="0"
>
<slot />
</svelte:element>

View file

@ -1,52 +0,0 @@
<script lang="ts">
import type { HTMLInputAttributes } from 'svelte/elements';
import { cn } from '$lib';
type FormInputEvent<T extends Event = Event> = T & {
currentTarget: EventTarget & HTMLInputElement;
};
type InputEvents = {
blur: FormInputEvent<FocusEvent>;
change: FormInputEvent<Event>;
click: FormInputEvent<MouseEvent>;
focus: FormInputEvent<FocusEvent>;
keydown: FormInputEvent<KeyboardEvent>;
keypress: FormInputEvent<KeyboardEvent>;
keyup: FormInputEvent<KeyboardEvent>;
mouseover: FormInputEvent<MouseEvent>;
mouseenter: FormInputEvent<MouseEvent>;
mouseleave: FormInputEvent<MouseEvent>;
paste: FormInputEvent<ClipboardEvent>;
input: FormInputEvent<InputEvent>;
};
type $$Props = HTMLInputAttributes;
type $$Events = InputEvents;
let className: $$Props['class'] = undefined;
export let value: $$Props['value'] = undefined;
export { className as class };
</script>
<input
class={cn(
'flex h-10 w-full rounded-md border border-primary-600 bg-highlight-primary px-3 py-2 text-sm ring-offset-highlight-primary file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted focus:bg-primary-800 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
className
)}
bind:value
on:blur
on:change
on:click
on:focus
on:keydown
on:keypress
on:keyup
on:mouseover
on:mouseenter
on:mouseleave
on:paste
on:input
{...$$restProps}
/>

View file

@ -0,0 +1,16 @@
<script lang="ts">
import { Avatar as AvatarPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
type $$Props = AvatarPrimitive.FallbackProps;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<AvatarPrimitive.Fallback
class={cn("flex h-full w-full items-center justify-center rounded-full bg-muted", className)}
{...$$restProps}
>
<slot />
</AvatarPrimitive.Fallback>

View file

@ -0,0 +1,18 @@
<script lang="ts">
import { Avatar as AvatarPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
type $$Props = AvatarPrimitive.ImageProps;
let className: $$Props["class"] = undefined;
export let src: $$Props["src"] = undefined;
export let alt: $$Props["alt"] = undefined;
export { className as class };
</script>
<AvatarPrimitive.Image
{src}
{alt}
class={cn("aspect-square h-full w-full", className)}
{...$$restProps}
/>

View file

@ -0,0 +1,18 @@
<script lang="ts">
import { Avatar as AvatarPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
type $$Props = AvatarPrimitive.Props;
let className: $$Props["class"] = undefined;
export let delayMs: $$Props["delayMs"] = undefined;
export { className as class };
</script>
<AvatarPrimitive.Root
{delayMs}
class={cn("relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full", className)}
{...$$restProps}
>
<slot />
</AvatarPrimitive.Root>

View file

@ -0,0 +1,13 @@
import Root from "./avatar.svelte";
import Image from "./avatar-image.svelte";
import Fallback from "./avatar-fallback.svelte";
export {
Root,
Image,
Fallback,
//
Root as Avatar,
Image as AvatarImage,
Fallback as AvatarFallback,
};

View file

@ -0,0 +1,24 @@
<script lang="ts">
import { cn } from "$lib/utils.js";
import Ellipsis from "lucide-svelte/icons/ellipsis";
import type { HTMLAttributes } from "svelte/elements";
type $$Props = HTMLAttributes<HTMLSpanElement> & {
el?: HTMLSpanElement;
};
export let el: $$Props["el"] = undefined;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<span
bind:this={el}
role="presentation"
aria-hidden="true"
class={cn("flex h-9 w-9 items-center justify-center", className)}
{...$$restProps}
>
<Ellipsis class="h-4 w-4" />
<span class="sr-only">More</span>
</span>

View file

@ -0,0 +1,16 @@
<script lang="ts">
import type { HTMLLiAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
type $$Props = HTMLLiAttributes & {
el?: HTMLLIElement;
};
export let el: $$Props["el"] = undefined;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<li bind:this={el} class={cn("inline-flex items-center gap-1.5", className)}>
<slot />
</li>

View file

@ -0,0 +1,31 @@
<script lang="ts">
import type { HTMLAnchorAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
type $$Props = HTMLAnchorAttributes & {
el?: HTMLAnchorElement;
asChild?: boolean;
};
export let href: $$Props["href"] = undefined;
export let el: $$Props["el"] = undefined;
export let asChild: $$Props["asChild"] = false;
let className: $$Props["class"] = undefined;
export { className as class };
let attrs: Record<string, unknown>;
$: attrs = {
class: cn("transition-colors hover:text-foreground", className),
href,
...$$restProps,
};
</script>
{#if asChild}
<slot {attrs} />
{:else}
<a bind:this={el} {...attrs} {href}>
<slot {attrs} />
</a>
{/if}

View file

@ -0,0 +1,23 @@
<script lang="ts">
import type { HTMLOlAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
type $$Props = HTMLOlAttributes & {
el?: HTMLOListElement;
};
export let el: $$Props["el"] = undefined;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<ol
bind:this={el}
class={cn(
"flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
className
)}
{...$$restProps}
>
<slot />
</ol>

View file

@ -0,0 +1,23 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
type $$Props = HTMLAttributes<HTMLSpanElement> & {
el?: HTMLSpanElement;
};
export let el: $$Props["el"] = undefined;
export let className: $$Props["class"] = undefined;
export { className as class };
</script>
<span
bind:this={el}
role="link"
aria-disabled="true"
aria-current="page"
class={cn("font-normal text-foreground", className)}
{...$$restProps}
>
<slot />
</span>

View file

@ -0,0 +1,25 @@
<script lang="ts">
import type { HTMLLiAttributes } from "svelte/elements";
import ChevronRight from "lucide-svelte/icons/chevron-right";
import { cn } from "$lib/utils.js";
type $$Props = HTMLLiAttributes & {
el?: HTMLLIElement;
};
export let el: $$Props["el"] = undefined;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<li
role="presentation"
aria-hidden="true"
class={cn("[&>svg]:size-3.5", className)}
bind:this={el}
{...$$restProps}
>
<slot>
<ChevronRight />
</slot>
</li>

View file

@ -0,0 +1,15 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
type $$Props = HTMLAttributes<HTMLElement> & {
el?: HTMLElement;
};
export let el: $$Props["el"] = undefined;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<nav class={className} bind:this={el} aria-label="breadcrumb" {...$$restProps}>
<slot />
</nav>

View file

@ -0,0 +1,25 @@
import Root from "./breadcrumb.svelte";
import Ellipsis from "./breadcrumb-ellipsis.svelte";
import Item from "./breadcrumb-item.svelte";
import Separator from "./breadcrumb-separator.svelte";
import Link from "./breadcrumb-link.svelte";
import List from "./breadcrumb-list.svelte";
import Page from "./breadcrumb-page.svelte";
export {
Root,
Ellipsis,
Item,
Separator,
Link,
List,
Page,
//
Root as Breadcrumb,
Ellipsis as BreadcrumbEllipsis,
Item as BreadcrumbItem,
Separator as BreadcrumbSeparator,
Link as BreadcrumbLink,
List as BreadcrumbList,
Page as BreadcrumbPage,
};

View file

@ -0,0 +1,25 @@
<script lang="ts">
import { Button as ButtonPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
import { buttonVariants, type Props, type Events } from "./index.js";
type $$Props = Props;
type $$Events = Events;
let className: $$Props["class"] = undefined;
export let variant: $$Props["variant"] = "default";
export let size: $$Props["size"] = "default";
export let builders: $$Props["builders"] = [];
export { className as class };
</script>
<ButtonPrimitive.Root
{builders}
class={cn(buttonVariants({ variant, size, className }))}
type="button"
{...$$restProps}
on:click
on:keydown
>
<slot />
</ButtonPrimitive.Root>

View file

@ -0,0 +1,50 @@
import Root from "./button.svelte";
import { tv, type VariantProps } from "tailwind-variants";
import type { Button as ButtonPrimitive } from "bits-ui";
const buttonVariants = tv({
base: "inline-flex items-center justify-center rounded-md text-sm font-medium whitespace-nowrap ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
plain: "hover:bg-transparent hover:text-primary",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
});
type Variant = VariantProps<typeof buttonVariants>["variant"];
type Size = VariantProps<typeof buttonVariants>["size"];
type Props = ButtonPrimitive.Props & {
variant?: Variant;
size?: Size;
};
type Events = ButtonPrimitive.Events;
export {
Root,
type Props,
type Events,
//
Root as Button,
type Props as ButtonProps,
type Events as ButtonEvents,
buttonVariants,
};

View file

@ -0,0 +1,24 @@
<script lang="ts">
import { Drawer as DrawerPrimitive } from "vaul-svelte";
import DrawerOverlay from "./drawer-overlay.svelte";
import { cn } from "$lib/utils.js";
type $$Props = DrawerPrimitive.ContentProps;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<DrawerPrimitive.Portal>
<DrawerOverlay />
<DrawerPrimitive.Content
class={cn(
"fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background",
className
)}
{...$$restProps}
>
<div class="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
<slot />
</DrawerPrimitive.Content>
</DrawerPrimitive.Portal>

View file

@ -0,0 +1,18 @@
<script lang="ts">
import { Drawer as DrawerPrimitive } from "vaul-svelte";
import { cn } from "$lib/utils.js";
type $$Props = DrawerPrimitive.DescriptionProps;
export let el: $$Props["el"] = undefined;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<DrawerPrimitive.Description
bind:el
class={cn("text-sm text-muted-foreground", className)}
{...$$restProps}
>
<slot />
</DrawerPrimitive.Description>

View file

@ -0,0 +1,16 @@
<script lang="ts">
import { cn } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
type $$Props = HTMLAttributes<HTMLDivElement> & {
el?: HTMLDivElement;
};
export let el: $$Props["el"] = undefined;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<div bind:this={el} class={cn("mt-auto flex flex-col gap-2 p-4", className)} {...$$restProps}>
<slot />
</div>

View file

@ -0,0 +1,19 @@
<script lang="ts">
import { cn } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
type $$Props = HTMLAttributes<HTMLDivElement> & {
el?: HTMLDivElement;
};
export let el: $$Props["el"] = undefined;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<div
bind:this={el}
class={cn("grid gap-1.5 p-4 text-center sm:text-left", className)}
{...$$restProps}
>
<slot />
</div>

View file

@ -0,0 +1,12 @@
<script lang="ts">
import { Drawer as DrawerPrimitive } from "vaul-svelte";
type $$Props = DrawerPrimitive.Props;
export let shouldScaleBackground: $$Props["shouldScaleBackground"] = true;
export let open: $$Props["open"] = false;
export let activeSnapPoint: $$Props["activeSnapPoint"] = undefined;
</script>
<DrawerPrimitive.NestedRoot {shouldScaleBackground} bind:open bind:activeSnapPoint {...$$restProps}>
<slot />
</DrawerPrimitive.NestedRoot>

View file

@ -0,0 +1,18 @@
<script lang="ts">
import { Drawer as DrawerPrimitive } from "vaul-svelte";
import { cn } from "$lib/utils.js";
type $$Props = DrawerPrimitive.OverlayProps;
export let el: $$Props["el"] = undefined;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<DrawerPrimitive.Overlay
bind:el
class={cn("fixed inset-0 z-50 bg-black/80", className)}
{...$$restProps}
>
<slot />
</DrawerPrimitive.Overlay>

View file

@ -0,0 +1,18 @@
<script lang="ts">
import { Drawer as DrawerPrimitive } from "vaul-svelte";
import { cn } from "$lib/utils.js";
type $$Props = DrawerPrimitive.TitleProps;
export let el: $$Props["el"] = undefined;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<DrawerPrimitive.Title
bind:el
class={cn("text-lg font-semibold leading-none tracking-tight", className)}
{...$$restProps}
>
<slot />
</DrawerPrimitive.Title>

View file

@ -0,0 +1,12 @@
<script lang="ts">
import { Drawer as DrawerPrimitive } from "vaul-svelte";
type $$Props = DrawerPrimitive.Props;
export let shouldScaleBackground: $$Props["shouldScaleBackground"] = true;
export let open: $$Props["open"] = false;
export let activeSnapPoint: $$Props["activeSnapPoint"] = undefined;
</script>
<DrawerPrimitive.Root {shouldScaleBackground} bind:open bind:activeSnapPoint {...$$restProps}>
<slot />
</DrawerPrimitive.Root>

View file

@ -0,0 +1,41 @@
import { Drawer as DrawerPrimitive } from "vaul-svelte";
import Root from "./drawer.svelte";
import Content from "./drawer-content.svelte";
import Description from "./drawer-description.svelte";
import Overlay from "./drawer-overlay.svelte";
import Footer from "./drawer-footer.svelte";
import Header from "./drawer-header.svelte";
import Title from "./drawer-title.svelte";
import NestedRoot from "./drawer-nested.svelte";
const Trigger = DrawerPrimitive.Trigger;
const Portal = DrawerPrimitive.Portal;
const Close = DrawerPrimitive.Close;
export {
Root,
NestedRoot,
Content,
Description,
Overlay,
Footer,
Header,
Title,
Trigger,
Portal,
Close,
//
Root as Drawer,
NestedRoot as DrawerNestedRoot,
Content as DrawerContent,
Description as DrawerDescription,
Overlay as DrawerOverlay,
Footer as DrawerFooter,
Header as DrawerHeader,
Title as DrawerTitle,
Trigger as DrawerTrigger,
Portal as DrawerPortal,
Close as DrawerClose,
};

View file

@ -0,0 +1,35 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import Check from "lucide-svelte/icons/check";
import { cn } from "$lib/utils.js";
type $$Props = DropdownMenuPrimitive.CheckboxItemProps;
type $$Events = DropdownMenuPrimitive.CheckboxItemEvents;
let className: $$Props["class"] = undefined;
export let checked: $$Props["checked"] = undefined;
export { className as class };
</script>
<DropdownMenuPrimitive.CheckboxItem
bind:checked
class={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50",
className
)}
{...$$restProps}
on:click
on:keydown
on:focusin
on:focusout
on:pointerdown
on:pointerleave
on:pointermove
>
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.CheckboxIndicator>
<Check class="h-4 w-4" />
</DropdownMenuPrimitive.CheckboxIndicator>
</span>
<slot />
</DropdownMenuPrimitive.CheckboxItem>

View file

@ -0,0 +1,27 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn, flyAndScale } from "$lib/utils.js";
type $$Props = DropdownMenuPrimitive.ContentProps;
type $$Events = DropdownMenuPrimitive.ContentEvents;
let className: $$Props["class"] = undefined;
export let sideOffset: $$Props["sideOffset"] = 4;
export let transition: $$Props["transition"] = flyAndScale;
export let transitionConfig: $$Props["transitionConfig"] = undefined;
export { className as class };
</script>
<DropdownMenuPrimitive.Content
{transition}
{transitionConfig}
{sideOffset}
class={cn(
"z-50 min-w-[8rem] rounded-md border bg-popover p-1 text-popover-foreground shadow-md focus:outline-none",
className
)}
{...$$restProps}
on:keydown
>
<slot />
</DropdownMenuPrimitive.Content>

View file

@ -0,0 +1,31 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
type $$Props = DropdownMenuPrimitive.ItemProps & {
inset?: boolean;
};
type $$Events = DropdownMenuPrimitive.ItemEvents;
let className: $$Props["class"] = undefined;
export let inset: $$Props["inset"] = undefined;
export { className as class };
</script>
<DropdownMenuPrimitive.Item
class={cn(
"relative flex select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50",
inset && "pl-8",
className
)}
{...$$restProps}
on:click
on:keydown
on:focusin
on:focusout
on:pointerdown
on:pointerleave
on:pointermove
>
<slot />
</DropdownMenuPrimitive.Item>

View file

@ -0,0 +1,19 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
type $$Props = DropdownMenuPrimitive.LabelProps & {
inset?: boolean;
};
let className: $$Props["class"] = undefined;
export let inset: $$Props["inset"] = undefined;
export { className as class };
</script>
<DropdownMenuPrimitive.Label
class={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
{...$$restProps}
>
<slot />
</DropdownMenuPrimitive.Label>

View file

@ -0,0 +1,11 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
type $$Props = DropdownMenuPrimitive.RadioGroupProps;
export let value: $$Props["value"] = undefined;
</script>
<DropdownMenuPrimitive.RadioGroup {...$$restProps} bind:value>
<slot />
</DropdownMenuPrimitive.RadioGroup>

View file

@ -0,0 +1,35 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import Circle from "lucide-svelte/icons/circle";
import { cn } from "$lib/utils.js";
type $$Props = DropdownMenuPrimitive.RadioItemProps;
type $$Events = DropdownMenuPrimitive.RadioItemEvents;
let className: $$Props["class"] = undefined;
export let value: $$Props["value"];
export { className as class };
</script>
<DropdownMenuPrimitive.RadioItem
class={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50",
className
)}
{value}
{...$$restProps}
on:click
on:keydown
on:focusin
on:focusout
on:pointerdown
on:pointerleave
on:pointermove
>
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.RadioIndicator>
<Circle class="h-2 w-2 fill-current" />
</DropdownMenuPrimitive.RadioIndicator>
</span>
<slot />
</DropdownMenuPrimitive.RadioItem>

View file

@ -0,0 +1,14 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
type $$Props = DropdownMenuPrimitive.SeparatorProps;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<DropdownMenuPrimitive.Separator
class={cn("-mx-1 my-1 h-px bg-muted", className)}
{...$$restProps}
/>

View file

@ -0,0 +1,13 @@
<script lang="ts">
import { cn } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
type $$Props = HTMLAttributes<HTMLSpanElement>;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<span class={cn("ml-auto text-xs tracking-widest opacity-60", className)} {...$$restProps}>
<slot />
</span>

View file

@ -0,0 +1,30 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn, flyAndScale } from "$lib/utils.js";
type $$Props = DropdownMenuPrimitive.SubContentProps;
type $$Events = DropdownMenuPrimitive.SubContentEvents;
let className: $$Props["class"] = undefined;
export let transition: $$Props["transition"] = flyAndScale;
export let transitionConfig: $$Props["transitionConfig"] = {
x: -10,
y: 0,
};
export { className as class };
</script>
<DropdownMenuPrimitive.SubContent
{transition}
{transitionConfig}
class={cn(
"z-50 min-w-[8rem] rounded-md border bg-popover p-1 text-popover-foreground shadow-lg focus:outline-none",
className
)}
{...$$restProps}
on:keydown
on:focusout
on:pointermove
>
<slot />
</DropdownMenuPrimitive.SubContent>

View file

@ -0,0 +1,32 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import ChevronRight from "lucide-svelte/icons/chevron-right";
import { cn } from "$lib/utils.js";
type $$Props = DropdownMenuPrimitive.SubTriggerProps & {
inset?: boolean;
};
type $$Events = DropdownMenuPrimitive.SubTriggerEvents;
let className: $$Props["class"] = undefined;
export let inset: $$Props["inset"] = undefined;
export { className as class };
</script>
<DropdownMenuPrimitive.SubTrigger
class={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[highlighted]:bg-accent data-[state=open]:bg-accent data-[highlighted]:text-accent-foreground data-[state=open]:text-accent-foreground",
inset && "pl-8",
className
)}
{...$$restProps}
on:click
on:keydown
on:focusin
on:focusout
on:pointerleave
on:pointermove
>
<slot />
<ChevronRight class="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger>

View file

@ -0,0 +1,48 @@
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import Item from "./dropdown-menu-item.svelte";
import Label from "./dropdown-menu-label.svelte";
import Content from "./dropdown-menu-content.svelte";
import Shortcut from "./dropdown-menu-shortcut.svelte";
import RadioItem from "./dropdown-menu-radio-item.svelte";
import Separator from "./dropdown-menu-separator.svelte";
import RadioGroup from "./dropdown-menu-radio-group.svelte";
import SubContent from "./dropdown-menu-sub-content.svelte";
import SubTrigger from "./dropdown-menu-sub-trigger.svelte";
import CheckboxItem from "./dropdown-menu-checkbox-item.svelte";
const Sub = DropdownMenuPrimitive.Sub;
const Root = DropdownMenuPrimitive.Root;
const Trigger = DropdownMenuPrimitive.Trigger;
const Group = DropdownMenuPrimitive.Group;
export {
Sub,
Root,
Item,
Label,
Group,
Trigger,
Content,
Shortcut,
Separator,
RadioItem,
SubContent,
SubTrigger,
RadioGroup,
CheckboxItem,
//
Root as DropdownMenu,
Sub as DropdownMenuSub,
Item as DropdownMenuItem,
Label as DropdownMenuLabel,
Group as DropdownMenuGroup,
Content as DropdownMenuContent,
Trigger as DropdownMenuTrigger,
Shortcut as DropdownMenuShortcut,
RadioItem as DropdownMenuRadioItem,
Separator as DropdownMenuSeparator,
RadioGroup as DropdownMenuRadioGroup,
SubContent as DropdownMenuSubContent,
SubTrigger as DropdownMenuSubTrigger,
CheckboxItem as DropdownMenuCheckboxItem,
};

View file

@ -0,0 +1,10 @@
<script lang="ts">
import * as Button from "$lib/components/ui/button/index.js";
type $$Props = Button.Props;
type $$Events = Button.Events;
</script>
<Button.Root type="submit" on:click on:keydown {...$$restProps}>
<slot />
</Button.Root>

View file

@ -0,0 +1,17 @@
<script lang="ts">
import * as FormPrimitive from "formsnap";
import { cn } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
type $$Props = HTMLAttributes<HTMLSpanElement>;
let className: string | undefined | null = undefined;
export { className as class };
</script>
<FormPrimitive.Description
class={cn("text-sm text-muted-foreground", className)}
{...$$restProps}
let:descriptionAttrs
>
<slot {descriptionAttrs} />
</FormPrimitive.Description>

View file

@ -0,0 +1,26 @@
<script lang="ts" context="module">
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import type { FormPathLeaves, SuperForm } from "sveltekit-superforms";
type T = Record<string, unknown>;
type U = unknown;
</script>
<script lang="ts" generics="T extends Record<string, unknown>, U extends FormPathLeaves<T>">
import type { HTMLAttributes } from "svelte/elements";
import * as FormPrimitive from "formsnap";
import { cn } from "$lib/utils.js";
type $$Props = FormPrimitive.ElementFieldProps<T, U> & HTMLAttributes<HTMLElement>;
export let form: SuperForm<T>;
export let name: U;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<FormPrimitive.ElementField {form} {name} let:constraints let:errors let:tainted let:value>
<div class={cn("space-y-2", className)}>
<slot {constraints} {errors} {tainted} {value} />
</div>
</FormPrimitive.ElementField>

View file

@ -0,0 +1,26 @@
<script lang="ts">
import * as FormPrimitive from "formsnap";
import { cn } from "$lib/utils.js";
type $$Props = FormPrimitive.FieldErrorsProps & {
errorClasses?: string | undefined | null;
};
let className: $$Props["class"] = undefined;
export { className as class };
export let errorClasses: $$Props["class"] = undefined;
</script>
<FormPrimitive.FieldErrors
class={cn("text-sm font-medium text-destructive", className)}
{...$$restProps}
let:errors
let:fieldErrorsAttrs
let:errorAttrs
>
<slot {errors} {fieldErrorsAttrs} {errorAttrs}>
{#each errors as error}
<div {...errorAttrs} class={cn(errorClasses)}>{error}</div>
{/each}
</slot>
</FormPrimitive.FieldErrors>

View file

@ -0,0 +1,26 @@
<script lang="ts" context="module">
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import type { FormPath, SuperForm } from "sveltekit-superforms";
type T = Record<string, unknown>;
type U = unknown;
</script>
<script lang="ts" generics="T extends Record<string, unknown>, U extends FormPath<T>">
import type { HTMLAttributes } from "svelte/elements";
import * as FormPrimitive from "formsnap";
import { cn } from "$lib/utils.js";
type $$Props = FormPrimitive.FieldProps<T, U> & HTMLAttributes<HTMLElement>;
export let form: SuperForm<T>;
export let name: U;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<FormPrimitive.Field {form} {name} let:constraints let:errors let:tainted let:value>
<div class={cn("space-y-2", className)}>
<slot {constraints} {errors} {tainted} {value} />
</div>
</FormPrimitive.Field>

View file

@ -0,0 +1,31 @@
<script lang="ts" context="module">
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import type { FormPath, SuperForm } from "sveltekit-superforms";
type T = Record<string, unknown>;
type U = unknown;
</script>
<script lang="ts" generics="T extends Record<string, unknown>, U extends FormPath<T>">
import * as FormPrimitive from "formsnap";
import { cn } from "$lib/utils.js";
type $$Props = FormPrimitive.FieldsetProps<T, U>;
export let form: SuperForm<T>;
export let name: U;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<FormPrimitive.Fieldset
{form}
{name}
let:constraints
let:errors
let:tainted
let:value
class={cn("space-y-2", className)}
>
<slot {constraints} {errors} {tainted} {value} />
</FormPrimitive.Fieldset>

View file

@ -0,0 +1,17 @@
<script lang="ts">
import type { Label as LabelPrimitive } from 'bits-ui';
import { getFormControl } from 'formsnap';
import { cn } from '$lib/utils.js';
import { Label } from '$lib/components/ui/label/index.js';
type $$Props = LabelPrimitive.Props;
let className: $$Props['class'] = undefined;
export { className as class };
const { labelAttrs } = getFormControl();
</script>
<Label {...$labelAttrs} class={cn('data-[fs-error]:text-destructive', className)} {...$$restProps}>
<slot {labelAttrs} />
</Label>

View file

@ -0,0 +1,17 @@
<script lang="ts">
import * as FormPrimitive from "formsnap";
import { cn } from "$lib/utils.js";
type $$Props = FormPrimitive.LegendProps;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<FormPrimitive.Legend
{...$$restProps}
class={cn("text-sm font-medium leading-none data-[fs-error]:text-destructive", className)}
let:legendAttrs
>
<slot {legendAttrs} />
</FormPrimitive.Legend>

View file

@ -0,0 +1,33 @@
import * as FormPrimitive from "formsnap";
import Description from "./form-description.svelte";
import Label from "./form-label.svelte";
import FieldErrors from "./form-field-errors.svelte";
import Field from "./form-field.svelte";
import Fieldset from "./form-fieldset.svelte";
import Legend from "./form-legend.svelte";
import ElementField from "./form-element-field.svelte";
import Button from "./form-button.svelte";
const Control = FormPrimitive.Control;
export {
Field,
Control,
Label,
Button,
FieldErrors,
Description,
Fieldset,
Legend,
ElementField,
//
Field as FormField,
Control as FormControl,
Description as FormDescription,
Label as FormLabel,
FieldErrors as FormFieldErrors,
Fieldset as FormFieldset,
Legend as FormLegend,
ElementField as FormElementField,
Button as FormButton,
};

View file

@ -0,0 +1,27 @@
import Root from "./input.svelte";
export type FormInputEvent<T extends Event = Event> = T & {
currentTarget: EventTarget & HTMLInputElement;
};
export type InputEvents = {
blur: FormInputEvent<FocusEvent>;
change: FormInputEvent<Event>;
click: FormInputEvent<MouseEvent>;
focus: FormInputEvent<FocusEvent>;
focusin: FormInputEvent<FocusEvent>;
focusout: FormInputEvent<FocusEvent>;
keydown: FormInputEvent<KeyboardEvent>;
keypress: FormInputEvent<KeyboardEvent>;
keyup: FormInputEvent<KeyboardEvent>;
mouseover: FormInputEvent<MouseEvent>;
mouseenter: FormInputEvent<MouseEvent>;
mouseleave: FormInputEvent<MouseEvent>;
paste: FormInputEvent<ClipboardEvent>;
input: FormInputEvent<InputEvent>;
};
export {
Root,
//
Root as Input,
};

View file

@ -0,0 +1,35 @@
<script lang="ts">
import type { HTMLInputAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
import type { InputEvents } from "./index.js";
type $$Props = HTMLInputAttributes;
type $$Events = InputEvents;
let className: $$Props["class"] = undefined;
export let value: $$Props["value"] = undefined;
export { className as class };
</script>
<input
class={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
bind:value
on:blur
on:change
on:click
on:focus
on:focusin
on:focusout
on:keydown
on:keypress
on:keyup
on:mouseover
on:mouseenter
on:mouseleave
on:paste
on:input
{...$$restProps}
/>

View file

@ -0,0 +1,7 @@
import Root from "./label.svelte";
export {
Root,
//
Root as Label,
};

View file

@ -0,0 +1,21 @@
<script lang="ts">
import { Label as LabelPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
type $$Props = LabelPrimitive.Props;
type $$Events = LabelPrimitive.Events;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<LabelPrimitive.Root
class={cn(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
className
)}
{...$$restProps}
on:mousedown
>
<slot />
</LabelPrimitive.Root>

View file

@ -0,0 +1 @@
export { default as Toaster } from "./sonner.svelte";

View file

@ -0,0 +1,20 @@
<script lang="ts">
import { Toaster as Sonner, type ToasterProps as SonnerProps } from "svelte-sonner";
import { mode } from "mode-watcher";
type $$Props = SonnerProps;
</script>
<Sonner
theme={$mode}
class="toaster group"
toastOptions={{
classes: {
toast: "group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
description: "group-[.toast]:text-muted-foreground",
actionButton: "group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
cancelButton: "group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
},
}}
{...$$restProps}
/>

2
src/lib/config/index.ts Normal file
View file

@ -0,0 +1,2 @@
export * from './site';
export * from './navigation';

View file

@ -0,0 +1,57 @@
import type { ComponentType } from "svelte";
import type { Icon } from "lucide-svelte";
import BarChart2 from "lucide-svelte/icons/bar-chart-2";
import Code from "lucide-svelte/icons/code";
import Github from "lucide-svelte/icons/github";
import LayoutDashboard from "lucide-svelte/icons/layout-dashboard";
import LifeBuoy from "lucide-svelte/icons/life-buoy";
export type NavItem = {
name: string;
icon?: ComponentType<Icon>;
href?: string;
disabled?: boolean;
external?: boolean;
}
export type NavItemWithChildren = NavItem & {
children?: NavItemWithChildren[];
}
export type Navigation = NavItem[];
export const navigation: NavItemWithChildren[] = [
{
name: "Dashboard",
href: "/",
icon: LayoutDashboard
},
{
name: "Classement",
href: "/leaderboard",
icon: BarChart2
},
{
name: "Challenges",
href: "/chapters",
icon: Code
},
{
name: "Documentation",
children: [
{
name: "Git",
href: "/git",
external: true,
icon: Github
},
{
name: "Discord",
href: "/discord",
external: true,
icon: LifeBuoy
}
]
}
]

Some files were not shown because too many files have changed in this diff Show more