From 81027dc2a3de1a7ed247265fa1863ce7a449de56 Mon Sep 17 00:00:00 2001 From: steel <mael.acier@ensiie.fr> Date: Mon, 2 Sep 2024 00:38:02 +0200 Subject: [PATCH] use prettier defaults --- .prettierrc | 22 +- .vscode/settings.json | 4 +- eslint.config.js | 58 +-- package.json | 106 ++-- postcss.config.js | 8 +- src/app.css | 30 +- src/app.d.ts | 16 +- src/app.html | 38 +- src/hooks.server.ts | 14 +- src/lib/auth/index.ts | 14 +- src/lib/cookie.ts | 54 +-- src/lib/crypto.ts | 60 +-- src/lib/data.ts | 84 ++-- src/lib/env.ts | 32 +- src/lib/game.ts | 100 ++-- src/lib/graphql/client.ts | 18 +- src/lib/graphql/error.ts | 10 +- src/lib/graphql/index.ts | 20 +- src/lib/graphql/queries.ts | 60 +-- src/lib/graphql/query.ts | 60 +-- src/lib/utils.ts | 14 +- src/routes/+layout.svelte | 2 +- src/routes/+page.server.ts | 4 +- src/routes/pokemon/+page.svelte | 528 ++++++++++---------- src/routes/pokemon/style.css | 567 +++++++++++----------- src/routes/quiz/+error.svelte | 20 +- src/routes/quiz/+layout.server.ts | 60 +-- src/routes/quiz/+layout.svelte | 15 +- src/routes/quiz/+page.server.ts | 195 ++++---- src/routes/quiz/+page.svelte | 192 ++++---- src/routes/quiz/game-over/+page.server.ts | 20 +- src/routes/quiz/game-over/+page.svelte | 72 +-- src/routes/quiz/new/+page.server.ts | 196 ++++---- src/routes/quiz/new/+page.svelte | 94 ++-- src/routes/quiz/new/images/index.ts | 32 +- src/routes/quiz/new/schema.ts | 8 +- src/routes/quiz/schema.ts | 4 +- static/site.webmanifest | 34 +- svelte.config.js | 22 +- tailwind.config.ts | 42 +- tsconfig.json | 38 +- vite.config.ts | 6 +- 42 files changed, 1515 insertions(+), 1458 deletions(-) diff --git a/.prettierrc b/.prettierrc index 7ebb855..06b86c1 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,15 +1,11 @@ { - "useTabs": true, - "singleQuote": true, - "trailingComma": "none", - "printWidth": 100, - "plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"], - "overrides": [ - { - "files": "*.svelte", - "options": { - "parser": "svelte" - } - } - ] + "plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"], + "overrides": [ + { + "files": "*.svelte", + "options": { + "parser": "svelte" + } + } + ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 4824881..fae8e3d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,4 @@ { - "typescript.tsdk": "node_modules/typescript/lib", - "typescript.enablePromptUseWorkspaceTsdk": true + "typescript.tsdk": "node_modules/typescript/lib", + "typescript.enablePromptUseWorkspaceTsdk": true } diff --git a/eslint.config.js b/eslint.config.js index 62dbd03..b5a66dc 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,33 +1,33 @@ -import js from '@eslint/js'; -import ts from 'typescript-eslint'; -import svelte from 'eslint-plugin-svelte'; -import prettier from 'eslint-config-prettier'; -import globals from 'globals'; +import js from "@eslint/js"; +import ts from "typescript-eslint"; +import svelte from "eslint-plugin-svelte"; +import prettier from "eslint-config-prettier"; +import globals from "globals"; /** @type {import('eslint').Linter.Config[]} */ export default [ - js.configs.recommended, - ...ts.configs.recommended, - ...svelte.configs['flat/recommended'], - prettier, - ...svelte.configs['flat/prettier'], - { - languageOptions: { - globals: { - ...globals.browser, - ...globals.node - } - } - }, - { - files: ['**/*.svelte'], - languageOptions: { - parserOptions: { - parser: ts.parser - } - } - }, - { - ignores: ['build/', '.svelte-kit/', 'dist/'] - } + js.configs.recommended, + ...ts.configs.recommended, + ...svelte.configs["flat/recommended"], + prettier, + ...svelte.configs["flat/prettier"], + { + languageOptions: { + globals: { + ...globals.browser, + ...globals.node, + }, + }, + }, + { + files: ["**/*.svelte"], + languageOptions: { + parserOptions: { + parser: ts.parser, + }, + }, + }, + { + ignores: ["build/", ".svelte-kit/", "dist/"], + }, ]; diff --git a/package.json b/package.json index cf98036..e08ccb4 100644 --- a/package.json +++ b/package.json @@ -1,55 +1,55 @@ { - "name": "qui-est-ce", - "version": "0.0.1", - "private": true, - "type": "module", - "engines": { - "node": "22.x" - }, - "packageManager": "pnpm@9.7.0+sha512.dc09430156b427f5ecfc79888899e1c39d2d690f004be70e05230b72cb173d96839587545d09429b55ac3c429c801b4dc3c0e002f653830a420fa2dd4e3cf9cf", - "scripts": { - "start": "node build", - "dev": "vite dev", - "build": "vite build", - "prebuild": "npm run schema && npm run graphql-dts", - "preview": "vite preview", - "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", - "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", - "lint": "prettier --check . && eslint .", - "format": "prettier --write .", - "schema-env": "gql.tada generate-schema $API_ORIGIN/graphql/v0 --header \"Authorization: Basic $API_TOKEN\" -o src/lib/graphql/schema.gql", - "schema": "dotenv -- pnpm run schema-env", - "graphql-dts": "gql.tada generate-output" - }, - "devDependencies": { - "@sveltejs/adapter-node": "^5.2.2", - "@sveltejs/kit": "^2.0.0", - "@sveltejs/vite-plugin-svelte": "^3.0.0", - "@types/eslint": "^9.6.0", - "autoprefixer": "^10.4.20", - "dotenv-cli": "^7.4.2", - "eslint": "^9.0.0", - "eslint-config-prettier": "^9.1.0", - "eslint-plugin-svelte": "^2.36.0", - "globals": "^15.0.0", - "prettier": "^3.1.1", - "prettier-plugin-svelte": "^3.1.2", - "prettier-plugin-tailwindcss": "^0.6.5", - "svelte": "^4.2.7", - "svelte-check": "^3.6.0", - "tailwindcss": "^3.4.9", - "typescript": "^5.0.0", - "typescript-eslint": "^8.0.0", - "vite": "^5.0.3" - }, - "dependencies": { - "@arise/aidc-sveltekit": "^0.4.1", - "@urql/core": "^5.0.6", - "formsnap": "^1.0.1", - "gql.tada": "^1.8.6", - "http-status-codes": "^2.3.0", - "msgpackr": "^1.11.0", - "sveltekit-superforms": "^2.17.0", - "zod": "^3.23.8" - } + "name": "qui-est-ce", + "version": "0.0.1", + "private": true, + "type": "module", + "engines": { + "node": "22.x" + }, + "packageManager": "pnpm@9.7.0+sha512.dc09430156b427f5ecfc79888899e1c39d2d690f004be70e05230b72cb173d96839587545d09429b55ac3c429c801b4dc3c0e002f653830a420fa2dd4e3cf9cf", + "scripts": { + "start": "node build", + "dev": "vite dev", + "build": "vite build", + "prebuild": "npm run schema && npm run graphql-dts", + "preview": "vite preview", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + "lint": "prettier --check . && eslint .", + "format": "prettier --write .", + "schema-env": "gql.tada generate-schema $API_ORIGIN/graphql/v0 --header \"Authorization: Basic $API_TOKEN\" -o src/lib/graphql/schema.gql", + "schema": "dotenv -- pnpm run schema-env", + "graphql-dts": "gql.tada generate-output" + }, + "devDependencies": { + "@sveltejs/adapter-node": "^5.2.2", + "@sveltejs/kit": "^2.0.0", + "@sveltejs/vite-plugin-svelte": "^3.0.0", + "@types/eslint": "^9.6.0", + "autoprefixer": "^10.4.20", + "dotenv-cli": "^7.4.2", + "eslint": "^9.0.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-svelte": "^2.36.0", + "globals": "^15.0.0", + "prettier": "^3.1.1", + "prettier-plugin-svelte": "^3.1.2", + "prettier-plugin-tailwindcss": "^0.6.5", + "svelte": "^4.2.7", + "svelte-check": "^3.6.0", + "tailwindcss": "^3.4.9", + "typescript": "^5.0.0", + "typescript-eslint": "^8.0.0", + "vite": "^5.0.3" + }, + "dependencies": { + "@arise/aidc-sveltekit": "^0.4.1", + "@urql/core": "^5.0.6", + "formsnap": "^1.0.1", + "gql.tada": "^1.8.6", + "http-status-codes": "^2.3.0", + "msgpackr": "^1.11.0", + "sveltekit-superforms": "^2.17.0", + "zod": "^3.23.8" + } } diff --git a/postcss.config.js b/postcss.config.js index 0f77216..2aa7205 100644 --- a/postcss.config.js +++ b/postcss.config.js @@ -1,6 +1,6 @@ export default { - plugins: { - tailwindcss: {}, - autoprefixer: {} - } + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, }; diff --git a/src/app.css b/src/app.css index 7167622..96b2867 100644 --- a/src/app.css +++ b/src/app.css @@ -1,27 +1,27 @@ -@import 'tailwindcss/base'; -@import 'tailwindcss/components'; -@import 'tailwindcss/utilities'; +@import "tailwindcss/base"; +@import "tailwindcss/components"; +@import "tailwindcss/utilities"; @font-face { - font-family: 'Londrina Solid'; - font-weight: 100; - src: url('/fonts/LondrinaSolid-Thin.ttf'); + font-family: "Londrina Solid"; + font-weight: 100; + src: url("/fonts/LondrinaSolid-Thin.ttf"); } @font-face { - font-family: 'Londrina Solid'; - font-weight: 300; - src: url('/fonts/LondrinaSolid-Light.ttf'); + font-family: "Londrina Solid"; + font-weight: 300; + src: url("/fonts/LondrinaSolid-Light.ttf"); } @font-face { - font-family: 'Londrina Solid'; - font-weight: 400; - src: url('/fonts/LondrinaSolid-Regular.ttf'); + font-family: "Londrina Solid"; + font-weight: 400; + src: url("/fonts/LondrinaSolid-Regular.ttf"); } @font-face { - font-family: 'Londrina Solid'; - font-weight: 900; - src: url('/fonts/LondrinaSolid-Black.ttf'); + font-family: "Londrina Solid"; + font-weight: 900; + src: url("/fonts/LondrinaSolid-Black.ttf"); } diff --git a/src/app.d.ts b/src/app.d.ts index 3628d0b..a527f1d 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -1,15 +1,15 @@ -import type { Locals as AriseLocals } from '@arise/aidc-sveltekit/types'; +import type { Locals as AriseLocals } from "@arise/aidc-sveltekit/types"; // See https://kit.svelte.dev/docs/types#app // for information about these interfaces declare global { - namespace App { - // interface Error {} - type Locals = AriseLocals; - // interface PageData {} - // interface PageState {} - // interface Platform {} - } + namespace App { + // interface Error {} + type Locals = AriseLocals; + // interface PageData {} + // interface PageState {} + // interface Platform {} + } } export {}; diff --git a/src/app.html b/src/app.html index 6c3b558..8f206f4 100644 --- a/src/app.html +++ b/src/app.html @@ -1,25 +1,25 @@ <!doctype html> <html lang="fr" class="h-full"> - <head> - <meta charset="utf-8" /> - <link rel="icon" href="%sveltekit.assets%/favicon.png" /> - <meta name="viewport" content="width=device-width, initial-scale=1" /> + <head> + <meta charset="utf-8" /> + <link rel="icon" href="%sveltekit.assets%/favicon.png" /> + <meta name="viewport" content="width=device-width, initial-scale=1" /> - <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" /> - <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" /> - <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" /> - <link rel="manifest" href="/site.webmanifest" /> - <link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5" /> - <meta name="apple-mobile-web-app-title" content="Qui est-ce ?" /> - <meta name="application-name" content="Qui est-ce ?" /> - <meta name="msapplication-TileColor" content="#2b5797" /> - <meta name="theme-color" content="#ffffff" /> + <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" /> + <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" /> + <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" /> + <link rel="manifest" href="/site.webmanifest" /> + <link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5" /> + <meta name="apple-mobile-web-app-title" content="Qui est-ce ?" /> + <meta name="application-name" content="Qui est-ce ?" /> + <meta name="msapplication-TileColor" content="#2b5797" /> + <meta name="theme-color" content="#ffffff" /> - <title>Qui est-ce ?</title> + <title>Qui est-ce ?</title> - %sveltekit.head% - </head> - <body data-sveltekit-preload-data="hover"> - <div style="display: contents">%sveltekit.body%</div> - </body> + %sveltekit.head% + </head> + <body data-sveltekit-preload-data="hover"> + <div style="display: contents">%sveltekit.body%</div> + </body> </html> diff --git a/src/hooks.server.ts b/src/hooks.server.ts index a97b74c..e78272b 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -1,12 +1,12 @@ -import { sequence } from '@sveltejs/kit/hooks'; -import { type Handle, redirect } from '@sveltejs/kit'; -import { aidc } from '$lib/auth'; +import { sequence } from "@sveltejs/kit/hooks"; +import { type Handle, redirect } from "@sveltejs/kit"; +import { aidc } from "$lib/auth"; const authGuard: Handle = async ({ event, resolve }) => { - if (!event.locals.user) { - redirect(303, event.locals.authPaths.login); - } - return resolve(event); + if (!event.locals.user) { + redirect(303, event.locals.authPaths.login); + } + return resolve(event); }; export const handle = sequence(aidc.handler(), authGuard); diff --git a/src/lib/auth/index.ts b/src/lib/auth/index.ts index 3f4b947..5617f15 100644 --- a/src/lib/auth/index.ts +++ b/src/lib/auth/index.ts @@ -1,10 +1,10 @@ -import env from '$lib/env'; -import { AriseIdConnect } from '@arise/aidc-sveltekit'; -import { defaultLucia } from '@arise/aidc-sveltekit/default'; +import env from "$lib/env"; +import { AriseIdConnect } from "@arise/aidc-sveltekit"; +import { defaultLucia } from "@arise/aidc-sveltekit/default"; export const aidc = await AriseIdConnect.init({ - client_id: env.AIDC_CLIENT_ID, - client_secret: env.AIDC_CLIENT_SECRET, - scope: 'openid offline profile', - wrapper: defaultLucia + client_id: env.AIDC_CLIENT_ID, + client_secret: env.AIDC_CLIENT_SECRET, + scope: "openid offline profile", + wrapper: defaultLucia, }); diff --git a/src/lib/cookie.ts b/src/lib/cookie.ts index 80b9420..55fe2c7 100644 --- a/src/lib/cookie.ts +++ b/src/lib/cookie.ts @@ -1,35 +1,35 @@ -import type { Cookies } from '@sveltejs/kit'; -import Cryptr from './crypto'; -import env from '$lib/env'; -import type { Schema } from 'zod'; -import { encode, decode } from 'msgpackr'; +import type { Cookies } from "@sveltejs/kit"; +import Cryptr from "./crypto"; +import env from "$lib/env"; +import type { Schema } from "zod"; +import { encode, decode } from "msgpackr"; const cryptr = new Cryptr(env.COOKIE_SECRET); export class SecureCookie<T> { - constructor( - protected cookies: Cookies, - protected schema: Schema<T>, - readonly name: string - ) {} + constructor( + protected cookies: Cookies, + protected schema: Schema<T>, + readonly name: string, + ) {} - read(): T | null { - const cookie = this.cookies.get(this.name); - if (!cookie) return null; + read(): T | null { + const cookie = this.cookies.get(this.name); + if (!cookie) return null; - try { - const decryptedBuffer = cryptr.decrypt(cookie); - const json = decode(decryptedBuffer); - const data = this.schema.parse(json); - return data; - } catch { - return null; - } - } + try { + const decryptedBuffer = cryptr.decrypt(cookie); + const json = decode(decryptedBuffer); + const data = this.schema.parse(json); + return data; + } catch { + return null; + } + } - write(data: T) { - const compactJson = encode(data); - const encryptedBuffer = cryptr.encrypt(compactJson); - this.cookies.set(this.name, encryptedBuffer, { path: '/' }); - } + write(data: T) { + const compactJson = encode(data); + const encryptedBuffer = cryptr.encrypt(compactJson); + this.cookies.set(this.name, encryptedBuffer, { path: "/" }); + } } diff --git a/src/lib/crypto.ts b/src/lib/crypto.ts index 44b85a8..5a8afc1 100644 --- a/src/lib/crypto.ts +++ b/src/lib/crypto.ts @@ -1,45 +1,49 @@ -import crypto from 'crypto'; +import crypto from "crypto"; // Inspiré de https://github.com/MauriceButler/cryptr/blob/master/index.js export default class Cryptr { - algorithm = 'aes-128-gcm' as const; - encoding = 'base64' as const; - ivLength = 16; - tagLength = 16; - encryptedPosition = this.ivLength + this.tagLength; + algorithm = "aes-128-gcm" as const; + encoding = "base64" as const; + ivLength = 16; + tagLength = 16; + encryptedPosition = this.ivLength + this.tagLength; - constructor(public secret: string) {} + constructor(public secret: string) {} - getKey() { - return crypto.createHash('sha256').update(this.secret).digest().subarray(0, 16); - } + getKey() { + return crypto + .createHash("sha256") + .update(this.secret) + .digest() + .subarray(0, 16); + } - encrypt(value: crypto.BinaryLike): string { - const iv = crypto.randomBytes(this.ivLength); + encrypt(value: crypto.BinaryLike): string { + const iv = crypto.randomBytes(this.ivLength); - const key = this.getKey(); + const key = this.getKey(); - const cipher = crypto.createCipheriv(this.algorithm, key, iv); - const encrypted = Buffer.concat([cipher.update(value), cipher.final()]); + const cipher = crypto.createCipheriv(this.algorithm, key, iv); + const encrypted = Buffer.concat([cipher.update(value), cipher.final()]); - const tag = cipher.getAuthTag(); + const tag = cipher.getAuthTag(); - return Buffer.concat([iv, tag, encrypted]).toString(this.encoding); - } + return Buffer.concat([iv, tag, encrypted]).toString(this.encoding); + } - decrypt(value: string): Buffer { - const stringValue = Buffer.from(String(value), this.encoding); + decrypt(value: string): Buffer { + const stringValue = Buffer.from(String(value), this.encoding); - const iv = stringValue.subarray(0, this.ivLength); - const tag = stringValue.subarray(this.ivLength, this.encryptedPosition); - const encrypted = stringValue.subarray(this.encryptedPosition); + const iv = stringValue.subarray(0, this.ivLength); + const tag = stringValue.subarray(this.ivLength, this.encryptedPosition); + const encrypted = stringValue.subarray(this.encryptedPosition); - const key = this.getKey(); + const key = this.getKey(); - const decipher = crypto.createDecipheriv(this.algorithm, key, iv); + const decipher = crypto.createDecipheriv(this.algorithm, key, iv); - decipher.setAuthTag(tag); + decipher.setAuthTag(tag); - return Buffer.concat([decipher.update(encrypted), decipher.final()]); - } + return Buffer.concat([decipher.update(encrypted), decipher.final()]); + } } diff --git a/src/lib/data.ts b/src/lib/data.ts index a4a3336..2e536e0 100644 --- a/src/lib/data.ts +++ b/src/lib/data.ts @@ -1,11 +1,11 @@ -import { pageIterator } from '$lib/graphql/query'; -import { client } from './graphql'; -import { PROMOTION_QUERY, USER_DETAILS_QUERY } from './graphql/queries'; +import { pageIterator } from "$lib/graphql/query"; +import { client } from "./graphql"; +import { PROMOTION_QUERY, USER_DETAILS_QUERY } from "./graphql/queries"; type UserId = string; type PromoCache = { - lastUpdateMs: number; - promotion: Set<UserId>; + lastUpdateMs: number; + promotion: Set<UserId>; }; const cache = new Map<number, PromoCache>(); @@ -13,54 +13,60 @@ const cache = new Map<number, PromoCache>(); const CACHE_DURATION_MS = 1000 * 60 * 60; async function cacheImages(users: UserId[]): Promise<void> { - for (const user of users) { - const response = await client.query(USER_DETAILS_QUERY, { idList: [user] }); + for (const user of users) { + const response = await client.query(USER_DETAILS_QUERY, { idList: [user] }); - if (response.error) { - throw response.error; - } + if (response.error) { + throw response.error; + } - const photo = response.data?.page.nodes[0].photo; - if (!photo?.url) continue; - await fetch(photo.url, { method: 'HEAD' }); - } - console.log('cached', users.length, 'images'); + const photo = response.data?.page.nodes[0].photo; + if (!photo?.url) continue; + await fetch(photo.url, { method: "HEAD" }); + } + console.log("cached", users.length, "images"); } async function fetchPromotion(year: number): Promise<PromoCache> { - const array = await Array.fromAsync(pageIterator(PROMOTION_QUERY, { year })); - const users = array.map((node) => node.id); + const array = await Array.fromAsync(pageIterator(PROMOTION_QUERY, { year })); + const users = array.map((node) => node.id); - // background task - cacheImages(users); + // background task + cacheImages(users); - return { - lastUpdateMs: new Date().getTime(), - promotion: new Set(users) - }; + return { + lastUpdateMs: new Date().getTime(), + promotion: new Set(users), + }; } export async function getPromotion(year: number): Promise<Set<UserId>> { - if (cache.has(year)) { - const data = cache.get(year)!; - if (new Date().getTime() - data.lastUpdateMs < CACHE_DURATION_MS) return data.promotion; - } - console.log('year', year, 'not in cache, fetching'); - const freshData = await fetchPromotion(year); - cache.set(year, freshData); - return freshData.promotion; + if (cache.has(year)) { + const data = cache.get(year)!; + if (new Date().getTime() - data.lastUpdateMs < CACHE_DURATION_MS) + return data.promotion; + } + console.log("year", year, "not in cache, fetching"); + const freshData = await fetchPromotion(year); + cache.set(year, freshData); + return freshData.promotion; } export async function* promotionIterator( - min: number, - max: number + min: number, + max: number, ): AsyncGenerator<UserId, void, undefined> { - for (let i = min; i <= max; i++) { - const promotion = await getPromotion(i); - yield* promotion; - } + for (let i = min; i <= max; i++) { + const promotion = await getPromotion(i); + yield* promotion; + } } -export function getPromotionRange(min: number, max = min): Promise<Set<UserId>> { - return Array.fromAsync(promotionIterator(min, max)).then((array) => new Set(array)); +export function getPromotionRange( + min: number, + max = min, +): Promise<Set<UserId>> { + return Array.fromAsync(promotionIterator(min, max)).then( + (array) => new Set(array), + ); } diff --git a/src/lib/env.ts b/src/lib/env.ts index 52aff63..7ead058 100644 --- a/src/lib/env.ts +++ b/src/lib/env.ts @@ -1,26 +1,26 @@ -import * as assert from 'assert'; -import { env } from '$env/dynamic/private'; +import * as assert from "assert"; +import { env } from "$env/dynamic/private"; type RecordFromKeys<T extends readonly string[]> = Record<T[number], string>; function ensureEnv<K extends readonly string[]>(keys: K): RecordFromKeys<K> { - const cleanEnv = {} as RecordFromKeys<K>; + const cleanEnv = {} as RecordFromKeys<K>; - for (const key of keys) { - const value = env[key]; - if (!env.CI) { - assert.ok(value, `Variable d'environnement ${key} manquante`); - } - cleanEnv[key as keyof RecordFromKeys<K>] = value!; - } + for (const key of keys) { + const value = env[key]; + if (!env.CI) { + assert.ok(value, `Variable d'environnement ${key} manquante`); + } + cleanEnv[key as keyof RecordFromKeys<K>] = value!; + } - return cleanEnv; + return cleanEnv; } export default ensureEnv([ - 'API_ORIGIN', - 'API_TOKEN', - 'COOKIE_SECRET', - 'AIDC_CLIENT_ID', - 'AIDC_CLIENT_SECRET' + "API_ORIGIN", + "API_TOKEN", + "COOKIE_SECRET", + "AIDC_CLIENT_ID", + "AIDC_CLIENT_SECRET", ] as const); diff --git a/src/lib/game.ts b/src/lib/game.ts index 85fa5c1..e54f42c 100644 --- a/src/lib/game.ts +++ b/src/lib/game.ts @@ -1,65 +1,65 @@ -import type { Cookies } from '@sveltejs/kit'; -import { SecureCookie } from './cookie'; -import { z } from 'zod'; +import type { Cookies } from "@sveltejs/kit"; +import { SecureCookie } from "./cookie"; +import { z } from "zod"; export enum GameStage { - NEW, - PLAYING, - SOLUTION, - NEXT, - GAME_OVER + NEW, + PLAYING, + SOLUTION, + NEXT, + GAME_OVER, } export const gameStateSchema = z.object({ - history: z.array(z.string()), - step: z.number(), - score: z.number(), - options: z.array(z.string()), - stage: z.nativeEnum(GameStage), - year: z.number(), - maxYear: z.number().optional(), - label: z.string(), - solution: z.string(), - timestamp: z.number() + history: z.array(z.string()), + step: z.number(), + score: z.number(), + options: z.array(z.string()), + stage: z.nativeEnum(GameStage), + year: z.number(), + maxYear: z.number().optional(), + label: z.string(), + solution: z.string(), + timestamp: z.number(), }); export type GameState = z.infer<typeof gameStateSchema>; export class Game { - state: GameState; - protected cookie_name = 'quiestce-quiz'; - protected cookie: SecureCookie<GameState>; + state: GameState; + protected cookie_name = "quiestce-quiz"; + protected cookie: SecureCookie<GameState>; - constructor(protected cookies: Cookies) { - this.cookie = new SecureCookie(cookies, gameStateSchema, this.cookie_name); - this.state = this.cookie.read() ?? this.defaultState(); - // console.log('LOAD:', GameStage[this.state.stage], this.state); - } + constructor(protected cookies: Cookies) { + this.cookie = new SecureCookie(cookies, gameStateSchema, this.cookie_name); + this.state = this.cookie.read() ?? this.defaultState(); + // console.log('LOAD:', GameStage[this.state.stage], this.state); + } - protected defaultState(): GameState { - return { - stage: GameStage.NEW, - history: [], - step: 0, - score: 0, - year: 0, - options: [], - solution: '', - label: '?', - timestamp: 0 - }; - } + protected defaultState(): GameState { + return { + stage: GameStage.NEW, + history: [], + step: 0, + score: 0, + year: 0, + options: [], + solution: "", + label: "?", + timestamp: 0, + }; + } - save() { - // console.log('SAVE:', GameStage[this.state.stage], this.state); - this.state.timestamp = Date.now(); - this.cookie.write(this.state); - } + save() { + // console.log('SAVE:', GameStage[this.state.stage], this.state); + this.state.timestamp = Date.now(); + this.cookie.write(this.state); + } - reset() { - if (this.state.stage === GameStage.GAME_OVER) { - this.state = this.defaultState(); - this.save(); - } - } + reset() { + if (this.state.stage === GameStage.GAME_OVER) { + this.state = this.defaultState(); + this.save(); + } + } } diff --git a/src/lib/graphql/client.ts b/src/lib/graphql/client.ts index 3de9bd7..737a915 100644 --- a/src/lib/graphql/client.ts +++ b/src/lib/graphql/client.ts @@ -1,12 +1,12 @@ -import env from '$lib/env'; -import { Client, cacheExchange, fetchExchange } from '@urql/core'; +import env from "$lib/env"; +import { Client, cacheExchange, fetchExchange } from "@urql/core"; export const client = new Client({ - url: `${env.API_ORIGIN}/graphql/v0`, - exchanges: [cacheExchange, fetchExchange], - fetchOptions: { - headers: { - Authorization: `Basic ${env.API_TOKEN}` - } - } + url: `${env.API_ORIGIN}/graphql/v0`, + exchanges: [cacheExchange, fetchExchange], + fetchOptions: { + headers: { + Authorization: `Basic ${env.API_TOKEN}`, + }, + }, }); diff --git a/src/lib/graphql/error.ts b/src/lib/graphql/error.ts index 9f6cf23..6d7011c 100644 --- a/src/lib/graphql/error.ts +++ b/src/lib/graphql/error.ts @@ -1,8 +1,8 @@ -import { error } from '@sveltejs/kit'; -import type { OperationResult } from '@urql/core'; +import { error } from "@sveltejs/kit"; +import type { OperationResult } from "@urql/core"; export function handleGqlError(result: OperationResult): void { - if (result.error) { - error(500, { message: result.error.message }); - } + if (result.error) { + error(500, { message: result.error.message }); + } } diff --git a/src/lib/graphql/index.ts b/src/lib/graphql/index.ts index bfaa2b2..94bad92 100644 --- a/src/lib/graphql/index.ts +++ b/src/lib/graphql/index.ts @@ -1,14 +1,14 @@ -import { initGraphQLTada } from 'gql.tada'; -import type { introspection } from './types'; +import { initGraphQLTada } from "gql.tada"; +import type { introspection } from "./types"; export const graphql = initGraphQLTada<{ - introspection: introspection; - scalars: { - Identifier: string; - Base64: string; - Url: string; - }; + introspection: introspection; + scalars: { + Identifier: string; + Base64: string; + Url: string; + }; }>(); -export { readFragment } from 'gql.tada'; -export { client } from './client'; +export { readFragment } from "gql.tada"; +export { client } from "./client"; diff --git a/src/lib/graphql/queries.ts b/src/lib/graphql/queries.ts index f3aa344..105febf 100644 --- a/src/lib/graphql/queries.ts +++ b/src/lib/graphql/queries.ts @@ -1,34 +1,38 @@ -import { graphql } from '.'; +import { graphql } from "."; export const PROMOTION_QUERY = graphql(` - query GetYear($first: Int!, $after: String, $year: Int!) { - page: users( - first: $first - after: $after - filter: { year: { eq: [$year] }, nickname: { null: false }, photo: { null: false } } - ) { - pageInfo { - endCursor - hasNextPage - } - nodes { - id - } - } - } + query GetYear($first: Int!, $after: String, $year: Int!) { + page: users( + first: $first + after: $after + filter: { + year: { eq: [$year] } + nickname: { null: false } + photo: { null: false } + } + ) { + pageInfo { + endCursor + hasNextPage + } + nodes { + id + } + } + } `); export const USER_DETAILS_QUERY = graphql(` - query UserDetails($first: Int = 4, $idList: [String!]!) { - page: users(first: $first, filter: { id: { like: $idList } }) { - nodes { - id - nickname - photo { - url - thumbnailHash - } - } - } - } + query UserDetails($first: Int = 4, $idList: [String!]!) { + page: users(first: $first, filter: { id: { like: $idList } }) { + nodes { + id + nickname + photo { + url + thumbnailHash + } + } + } + } `); diff --git a/src/lib/graphql/query.ts b/src/lib/graphql/query.ts index 3dad155..91e185a 100644 --- a/src/lib/graphql/query.ts +++ b/src/lib/graphql/query.ts @@ -1,42 +1,46 @@ -import { client } from '$lib/graphql'; -import type { DocumentInput } from '@urql/core'; -import { handleGqlError } from './error'; +import { client } from "$lib/graphql"; +import type { DocumentInput } from "@urql/core"; +import { handleGqlError } from "./error"; type Page<T> = { - page: { - pageInfo: { - hasNextPage: boolean; - endCursor: string | null; - }; - nodes: T[]; - }; + page: { + pageInfo: { + hasNextPage: boolean; + endCursor: string | null; + }; + nodes: T[]; + }; }; interface PageArgs { - first: number; - after: string | null; + first: number; + after: string | null; } export async function* pageIterator<T, Variables extends PageArgs>( - query: DocumentInput<Page<T>, Variables>, - variables: Omit<Variables, 'first' | 'after'>, - pageSize = 100 + query: DocumentInput<Page<T>, Variables>, + variables: Omit<Variables, "first" | "after">, + pageSize = 100, ): AsyncGenerator<T, void, undefined> { - let cursor: string | null = null; - let hasNextPage = true; + let cursor: string | null = null; + let hasNextPage = true; - while (hasNextPage) { - const result = await client - .query(query, { first: pageSize, after: cursor, ...variables } as Variables) - .toPromise(); + while (hasNextPage) { + const result = await client + .query(query, { + first: pageSize, + after: cursor, + ...variables, + } as Variables) + .toPromise(); - handleGqlError(result); + handleGqlError(result); - const page: Page<T>['page'] | undefined = result.data?.page; - if (!page) return; + const page: Page<T>["page"] | undefined = result.data?.page; + if (!page) return; - yield* page.nodes ?? []; - cursor = page.pageInfo.endCursor ?? null; - hasNextPage = page.pageInfo.hasNextPage; - } + yield* page.nodes ?? []; + cursor = page.pageInfo.endCursor ?? null; + hasNextPage = page.pageInfo.hasNextPage; + } } diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 7a0f532..5ac8734 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,12 +1,12 @@ export function getRandomItems<T>(array: T[], count: number): T[] { - if (count === 0 || array.length === 0) return []; - const index = Math.floor(Math.random() * array.length); - const item = array[index]; - array.splice(index, 1); + if (count === 0 || array.length === 0) return []; + const index = Math.floor(Math.random() * array.length); + const item = array[index]; + array.splice(index, 1); - const items = getRandomItems(array, count - 1); - items.push(item); - return items; + const items = getRandomItems(array, count - 1); + items.push(item); + return items; } export const sum = (a: number, b: number) => a + b; diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index e551b53..872f217 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -1,5 +1,5 @@ <script> - import '../app.css'; + import "../app.css"; </script> <slot></slot> diff --git a/src/routes/+page.server.ts b/src/routes/+page.server.ts index 51fcb1c..efa1e00 100644 --- a/src/routes/+page.server.ts +++ b/src/routes/+page.server.ts @@ -1,5 +1,5 @@ -import { redirect } from '@sveltejs/kit'; +import { redirect } from "@sveltejs/kit"; export function load() { - redirect(303, '/quiz'); + redirect(303, "/quiz"); } diff --git a/src/routes/pokemon/+page.svelte b/src/routes/pokemon/+page.svelte index fc5adb8..c71e458 100644 --- a/src/routes/pokemon/+page.svelte +++ b/src/routes/pokemon/+page.svelte @@ -1,283 +1,291 @@ <script lang="ts"> - import './style.css'; - import { onMount } from 'svelte'; + import "./style.css"; + import { onMount } from "svelte"; - const pkmnTotal = 802; - const url = `https://pokeapi.co/api/v2/pokemon/?limit=${pkmnTotal}`; - const optionAmount = 4; - let pokemonData: Answer[] = []; - const prettyNames = { - 'nidoran-f': 'nidoran♀', - 'nidoran-m': 'nidoran♂', - 'mr-mime': 'mr. mime', - 'deoxys-normal': 'deoxys', - 'wormadam-plant': 'wormadam', - 'mime-jr': 'mime jr.', - 'giratina-altered': 'giratina', - 'shaymin-land': 'shaymin', - 'basculin-red-striped': 'basculin', - 'darmanitan-standard': 'darmanitan', - 'tornadus-incarnate': 'tornadus', - 'thundurus-incarnate': 'thundurus', - 'landorus-incarnate': 'landorus', - 'keldeo-ordinary': 'keldeo', - 'meloetta-aria': 'meloetta', - 'meowstic-male': 'meowstic', - 'aegislash-shield': 'aegislash', - 'pumpkaboo-average': 'pumpkaboo', - 'gourgeist-average': 'gourgeist', - 'oricorio-baile': 'oricorio', - 'lycanroc-midday': 'lycanroc', - 'wishiwashi-solo': 'wishiwashi', - 'type-null': 'type: null', - 'minior-red-meteor': 'minior', - 'mimikyu-disguised': 'mimikyu', - 'tapu-koko': 'tapu koko', - 'tapu-lele': 'tapu lele', - 'tapu-bulu': 'tapu bulu', - 'tapu-fini': 'tapu fini' - }; + const pkmnTotal = 802; + const url = `https://pokeapi.co/api/v2/pokemon/?limit=${pkmnTotal}`; + const optionAmount = 4; + let pokemonData: Answer[] = []; + const prettyNames = { + "nidoran-f": "nidoran♀", + "nidoran-m": "nidoran♂", + "mr-mime": "mr. mime", + "deoxys-normal": "deoxys", + "wormadam-plant": "wormadam", + "mime-jr": "mime jr.", + "giratina-altered": "giratina", + "shaymin-land": "shaymin", + "basculin-red-striped": "basculin", + "darmanitan-standard": "darmanitan", + "tornadus-incarnate": "tornadus", + "thundurus-incarnate": "thundurus", + "landorus-incarnate": "landorus", + "keldeo-ordinary": "keldeo", + "meloetta-aria": "meloetta", + "meowstic-male": "meowstic", + "aegislash-shield": "aegislash", + "pumpkaboo-average": "pumpkaboo", + "gourgeist-average": "gourgeist", + "oricorio-baile": "oricorio", + "lycanroc-midday": "lycanroc", + "wishiwashi-solo": "wishiwashi", + "type-null": "type: null", + "minior-red-meteor": "minior", + "mimikyu-disguised": "mimikyu", + "tapu-koko": "tapu koko", + "tapu-lele": "tapu lele", + "tapu-bulu": "tapu bulu", + "tapu-fini": "tapu fini", + }; - type Answer = { - url: string; - name: string; - index: number; - }; + type Answer = { + url: string; + name: string; + index: number; + }; - let pokemon: Answer[] = []; - let pkmnAmount = 0; - let score = 0; - let question = 0; - let questionAmount = 10; - let answer: Answer | null = null; - let selected: Answer | null = null; - let options: Answer[] = []; - let isPlaying = false; - let isDone = false; - let isChecked = false; - let trainerHovered: string | null = null; + let pokemon: Answer[] = []; + let pkmnAmount = 0; + let score = 0; + let question = 0; + let questionAmount = 10; + let answer: Answer | null = null; + let selected: Answer | null = null; + let options: Answer[] = []; + let isPlaying = false; + let isDone = false; + let isChecked = false; + let trainerHovered: string | null = null; - let image: string; + let image: string; - $: classic = pkmnAmount <= 151; - $: if (answer) { - image = getImage(classic, answer); - } + $: classic = pkmnAmount <= 151; + $: if (answer) { + image = getImage(classic, answer); + } - function getImage(classic: boolean, answer: Answer) { - let url = 'https://raw.githubusercontent.com/tiffachoo/pokesprites/master/pokemon/'; - let imageUrl = `${url}${classic ? 'redblue' : 'sunmoon'}/`; - let number = answer.url.match(/\/(\d+)/)![1]; - return `${imageUrl}${number}.png`; - } + function getImage(classic: boolean, answer: Answer) { + let url = + "https://raw.githubusercontent.com/tiffachoo/pokesprites/master/pokemon/"; + let imageUrl = `${url}${classic ? "redblue" : "sunmoon"}/`; + let number = answer.url.match(/\/(\d+)/)![1]; + return `${imageUrl}${number}.png`; + } - onMount(() => { - let pokeList = localStorage.getItem('pokeList'); + onMount(() => { + let pokeList = localStorage.getItem("pokeList"); - if (pokeList) { - pokemonData = JSON.parse(pokeList); - } else { - getData().then((res) => { - pokemonData = res.results; - localStorage.setItem('pokeList', JSON.stringify(res.results)); - }); - } - }); + if (pokeList) { + pokemonData = JSON.parse(pokeList); + } else { + getData().then((res) => { + pokemonData = res.results; + localStorage.setItem("pokeList", JSON.stringify(res.results)); + }); + } + }); - function prettifyName(name: string) { - return prettyNames[name as keyof typeof prettyNames] || name; - } + function prettifyName(name: string) { + return prettyNames[name as keyof typeof prettyNames] || name; + } - function getData() { - return fetch(url) - .then((res) => res.json()) - .catch((err) => console.log('errrr')); - } - function startGame(val: number) { - question = 0; - score = 0; - isPlaying = true; - pokemon = [...pokemonData]; + function getData() { + return fetch(url) + .then((res) => res.json()) + .catch((err) => console.log("errrr")); + } + function startGame(val: number) { + question = 0; + score = 0; + isPlaying = true; + pokemon = [...pokemonData]; - pkmnAmount = val || pkmnTotal; + pkmnAmount = val || pkmnTotal; - getNextQuestion(); - } - function getNextQuestion() { - question += 1; - resetAnswer(); + getNextQuestion(); + } + function getNextQuestion() { + question += 1; + resetAnswer(); - if (question <= questionAmount) { - let removed: Answer; - for (let i = 1; i <= optionAmount; i++) { - removed = pokemon.splice(getRandomPokemon(i), 1)[0]; - if (i === 1) { - answer = removed; - } else { - options.push(removed); - } - } + if (question <= questionAmount) { + let removed: Answer; + for (let i = 1; i <= optionAmount; i++) { + removed = pokemon.splice(getRandomPokemon(i), 1)[0]; + if (i === 1) { + answer = removed; + } else { + options.push(removed); + } + } - let pos = Math.floor(Math.random() * optionAmount); - options.splice(pos, 0, answer as Answer); - } else { - isPlaying = false; - isDone = true; - resetAnswer(); - } - } - function selectAnswer(ans: Answer) { - if (!isChecked) { - selected = ans; - } - } - function checkAnswer() { - isChecked = true; + let pos = Math.floor(Math.random() * optionAmount); + options.splice(pos, 0, answer as Answer); + } else { + isPlaying = false; + isDone = true; + resetAnswer(); + } + } + function selectAnswer(ans: Answer) { + if (!isChecked) { + selected = ans; + } + } + function checkAnswer() { + isChecked = true; - if (selected && answer && selected.name === answer.name) { - score += 10; - } - } - function getRandomPokemon(index: number) { - const diff = (question - 1) * 4 + index; - return Math.floor(Math.random() * (pkmnAmount + 1 - diff)); - } - function resetAnswer() { - options = []; - selected = null; - answer = null; - isChecked = false; - } - function restartGame() { - isDone = false; - } + if (selected && answer && selected.name === answer.name) { + score += 10; + } + } + function getRandomPokemon(index: number) { + const diff = (question - 1) * 4 + index; + return Math.floor(Math.random() * (pkmnAmount + 1 - diff)); + } + function resetAnswer() { + options = []; + selected = null; + answer = null; + isChecked = false; + } + function restartGame() { + isDone = false; + } - function trainerHover(val: string | MouseEvent | FocusEvent) { - trainerHovered = val as string; - } + function trainerHover(val: string | MouseEvent | FocusEvent) { + trainerHovered = val as string; + } </script> <div id="app"> - <div class:poke-classic={classic} class="container"> - <transition name="animate-section"> - {#if !isPlaying && !isDone} - <section class="poke-section"> - <h2>What type of trainer are you?</h2> - <div class="poke-intro-trainer"> - <div class="poke-ball"></div> - <img - class:active={trainerHovered === 'classic'} - class="poke-trainer-img poke-trainer-img-classic" - src="https://raw.githubusercontent.com/tiffachoo/pokesprites/master/trainers/red-rb.png" - alt="Trainer red" - /> - <img - class:active={trainerHovered === 'master'} - class="poke-trainer-img poke-trainer-img-master" - src="https://raw.githubusercontent.com/tiffachoo/pokesprites/master/trainers/red-sm.png" - alt="Trainer red again" - /> - </div> - <button - class="button spacer" - on:click={() => startGame(151)} - on:mouseover={() => trainerHover('classic')} - on:focus={() => trainerHover('classic')} - on:mouseout={trainerHover} - on:blur={trainerHover} - > - Classic - </button> - <button - class="button" - on:click={() => startGame(0)} - on:mouseover={() => trainerHover('master')} - on:focus={() => trainerHover('master')} - on:mouseout={trainerHover} - on:blur={trainerHover} - > - Master - </button> - </section> - {/if} - </transition> + <div class:poke-classic={classic} class="container"> + <transition name="animate-section"> + {#if !isPlaying && !isDone} + <section class="poke-section"> + <h2>What type of trainer are you?</h2> + <div class="poke-intro-trainer"> + <div class="poke-ball"></div> + <img + class:active={trainerHovered === "classic"} + class="poke-trainer-img poke-trainer-img-classic" + src="https://raw.githubusercontent.com/tiffachoo/pokesprites/master/trainers/red-rb.png" + alt="Trainer red" + /> + <img + class:active={trainerHovered === "master"} + class="poke-trainer-img poke-trainer-img-master" + src="https://raw.githubusercontent.com/tiffachoo/pokesprites/master/trainers/red-sm.png" + alt="Trainer red again" + /> + </div> + <button + class="button spacer" + on:click={() => startGame(151)} + on:mouseover={() => trainerHover("classic")} + on:focus={() => trainerHover("classic")} + on:mouseout={trainerHover} + on:blur={trainerHover} + > + Classic + </button> + <button + class="button" + on:click={() => startGame(0)} + on:mouseover={() => trainerHover("master")} + on:focus={() => trainerHover("master")} + on:mouseout={trainerHover} + on:blur={trainerHover} + > + Master + </button> + </section> + {/if} + </transition> - <transition name="animate-section"> - {#if isPlaying} - <section class="poke-section"> - <h1 class="poke-title">Who's that pokemon?</h1> - <div class="poke-question-wrapper"> - <span class="poke-question"> - <span class="poke-question-number"> - {question} - </span> - <span class="poke-question-amount"> - / {questionAmount} - </span> - </span> - <span class="poke-score"> - {score} - <small>pts</small> - </span> - <div - class="poke-image" - class:poke-image-success={isChecked && - selected && - answer && - selected.name === answer.name} - class:poke-image-error={isChecked && - selected && - answer && - selected.name !== answer.name} - > - <img src={image} alt="No cheating" class="poke-image-img" /> - </div> - <transition-group - tag="div" - name="animate-options" - class:poke-options-answers={isChecked} - class="poke-options" - > - {#each options as pokemon, index} - <button - data-index={index} - class:selected={selected && selected.index === index} - class:success={isChecked && answer && pokemon.name === answer.name} - class:error={isChecked && - selected && - answer && - selected.index === index && - selected.name !== answer.name} - class="poke-options-button" - on:click={() => selectAnswer({ ...pokemon, index })} - >{prettifyName(pokemon.name)}</button - > - {/each} - </transition-group> - <footer class="poke-buttons"> - <button - disabled={isChecked || (selected && Object.keys(selected).length < 1)} - class="button" - on:click={checkAnswer}>Submit</button - > - <button disabled={!isChecked} class="button" on:click={getNextQuestion}>Next</button> - </footer> - </div> - </section> - {/if} - </transition> + <transition name="animate-section"> + {#if isPlaying} + <section class="poke-section"> + <h1 class="poke-title">Who's that pokemon?</h1> + <div class="poke-question-wrapper"> + <span class="poke-question"> + <span class="poke-question-number"> + {question} + </span> + <span class="poke-question-amount"> + / {questionAmount} + </span> + </span> + <span class="poke-score"> + {score} + <small>pts</small> + </span> + <div + class="poke-image" + class:poke-image-success={isChecked && + selected && + answer && + selected.name === answer.name} + class:poke-image-error={isChecked && + selected && + answer && + selected.name !== answer.name} + > + <img src={image} alt="No cheating" class="poke-image-img" /> + </div> + <transition-group + tag="div" + name="animate-options" + class:poke-options-answers={isChecked} + class="poke-options" + > + {#each options as pokemon, index} + <button + data-index={index} + class:selected={selected && selected.index === index} + class:success={isChecked && + answer && + pokemon.name === answer.name} + class:error={isChecked && + selected && + answer && + selected.index === index && + selected.name !== answer.name} + class="poke-options-button" + on:click={() => selectAnswer({ ...pokemon, index })} + >{prettifyName(pokemon.name)}</button + > + {/each} + </transition-group> + <footer class="poke-buttons"> + <button + disabled={isChecked || + (selected && Object.keys(selected).length < 1)} + class="button" + on:click={checkAnswer}>Submit</button + > + <button + disabled={!isChecked} + class="button" + on:click={getNextQuestion}>Next</button + > + </footer> + </div> + </section> + {/if} + </transition> - <transition name="animate-section"> - {#if isDone} - <section class="poke-final"> - <h2>Final score</h2> - <span class="poke-final-score"> - <span class="poke-final-score-number">{score}</span> - pts - </span> - <button class="button" on:click={restartGame}>Play again</button> - </section> - {/if} - </transition> - </div> + <transition name="animate-section"> + {#if isDone} + <section class="poke-final"> + <h2>Final score</h2> + <span class="poke-final-score"> + <span class="poke-final-score-number">{score}</span> + pts + </span> + <button class="button" on:click={restartGame}>Play again</button> + </section> + {/if} + </transition> + </div> </div> diff --git a/src/routes/pokemon/style.css b/src/routes/pokemon/style.css index 2df9b7c..3c251cf 100644 --- a/src/routes/pokemon/style.css +++ b/src/routes/pokemon/style.css @@ -1,457 +1,460 @@ -@import 'https://fonts.googleapis.com/css?family=VT323'; +@import "https://fonts.googleapis.com/css?family=VT323"; * { - box-sizing: border-box; + box-sizing: border-box; } body { - background-color: #f65a52; - font-family: 'VT323', monospace; - font-size: 16px; - line-height: 1.875em; - color: #333; + background-color: #f65a52; + font-family: "VT323", monospace; + font-size: 16px; + line-height: 1.875em; + color: #333; } .container { - width: 100%; - max-width: 400px; - position: relative; - margin: 50px auto; + width: 100%; + max-width: 400px; + position: relative; + margin: 50px auto; } h2 { - font-size: 1.25rem; - white-space: nowrap; + font-size: 1.25rem; + white-space: nowrap; } .spacer { - margin-bottom: 0.5rem; + margin-bottom: 0.5rem; } .button { - padding: 0.5em 1.5em; - border-radius: 1rem; - border: solid 1px transparent; - font-family: 'VT323', monospace; - font-size: 1.5rem; - background-color: #f32c22; - color: #333; - cursor: pointer; - transition: 0.35s; + padding: 0.5em 1.5em; + border-radius: 1rem; + border: solid 1px transparent; + font-family: "VT323", monospace; + font-size: 1.5rem; + background-color: #f32c22; + color: #333; + cursor: pointer; + transition: 0.35s; } .button:focus { - outline: none; - border: 1px dotted #f87f79; + outline: none; + border: 1px dotted #f87f79; } .button:not([disabled]):hover { - background-color: #333; - color: #f65a52; + background-color: #333; + color: #f65a52; } .poke-section { - display: flex; - flex-direction: column; - align-items: center; - position: relative; - max-width: 500px; - margin: auto; + display: flex; + flex-direction: column; + align-items: center; + position: relative; + max-width: 500px; + margin: auto; } .poke-intro-trainer { - position: relative; - margin-bottom: 1rem; - height: 200px; - width: 200px; + position: relative; + margin-bottom: 1rem; + height: 200px; + width: 200px; } .poke-intro-trainer .poke-trainer-img { - position: absolute; - left: 50%; - bottom: 0; - height: 200px; - opacity: 0; - transition: 0.4s cubic-bezier(0.22, 0.75, 0.53, 0.99); + position: absolute; + left: 50%; + bottom: 0; + height: 200px; + opacity: 0; + transition: 0.4s cubic-bezier(0.22, 0.75, 0.53, 0.99); } @media (max-width: 479px) { - .poke-intro-trainer .poke-trainer-img { - display: none; - } + .poke-intro-trainer .poke-trainer-img { + display: none; + } } .poke-intro-trainer .poke-trainer-img.active { - transform: translateX(-50%); - opacity: 1; + transform: translateX(-50%); + opacity: 1; } .poke-intro-trainer .poke-trainer-img-classic { - bottom: 5px; - height: 180px; - -ms-interpolation-mode: nearest-neighbor; - image-rendering: -moz-crisp-edges; - image-rendering: pixelated; - transform: translateX(-80%); + bottom: 5px; + height: 180px; + -ms-interpolation-mode: nearest-neighbor; + image-rendering: -moz-crisp-edges; + image-rendering: pixelated; + transform: translateX(-80%); } .poke-intro-trainer .poke-trainer-img-master { - transform: translateX(-20%); + transform: translateX(-20%); } .poke-ball { - position: absolute; - top: 50%; - left: 50%; - height: 150px; - width: 150px; - border-radius: 50%; - background-color: #f32c22; - transform: translate(-50%, -50%); - overflow: hidden; + position: absolute; + top: 50%; + left: 50%; + height: 150px; + width: 150px; + border-radius: 50%; + background-color: #f32c22; + transform: translate(-50%, -50%); + overflow: hidden; } .poke-ball::before, .poke-ball::after { - content: ''; - position: absolute; + content: ""; + position: absolute; } .poke-ball::before { - z-index: 2; - top: 50%; - left: 50%; - height: 40px; - width: 40px; - border-radius: 50%; - border: solid 6px #f65a52; - background-color: #fa9f9b; - transform: translate(-50%, -50%); + z-index: 2; + top: 50%; + left: 50%; + height: 40px; + width: 40px; + border-radius: 50%; + border: solid 6px #f65a52; + background-color: #fa9f9b; + transform: translate(-50%, -50%); } .poke-ball::after { - z-index: 1; - top: 50%; - height: 50%; - width: 100%; - border-top: solid 6px #f65a52; - background-color: #fa9f9b; + z-index: 1; + top: 50%; + height: 50%; + width: 100%; + border-top: solid 6px #f65a52; + background-color: #fa9f9b; } .poke-title { - position: absolute; - top: -2rem; + position: absolute; + top: -2rem; } .poke-question { - position: absolute; - right: calc(100% + 0.5rem); - display: flex; - flex-direction: column; - align-items: flex-end; + position: absolute; + right: calc(100% + 0.5rem); + display: flex; + flex-direction: column; + align-items: flex-end; } .poke-question-wrapper { - position: relative; - width: 250px; + position: relative; + width: 250px; } .poke-question-number { - font-size: 8rem; - line-height: 0.4; - color: #fa9f9b; + font-size: 8rem; + line-height: 0.4; + color: #fa9f9b; } .poke-score { - position: absolute; - top: 6rem; - right: calc(100% + 0.5rem); - padding-top: 1rem; - font-size: 1.25rem; - white-space: nowrap; - color: #333; + position: absolute; + top: 6rem; + right: calc(100% + 0.5rem); + padding-top: 1rem; + font-size: 1.25rem; + white-space: nowrap; + color: #333; } .poke-score::before { - content: ''; - position: absolute; - top: 0; - right: 0; - width: 40px; - height: 6px; - background-color: #333; + content: ""; + position: absolute; + top: 0; + right: 0; + width: 40px; + height: 6px; + background-color: #333; } .poke-image { - position: relative; - z-index: 2; - display: flex; - justify-content: center; - align-items: center; - width: 250px; - height: 250px; - border-radius: 1rem; - border: solid 6px #333; - background-color: #fff; - overflow: hidden; + position: relative; + z-index: 2; + display: flex; + justify-content: center; + align-items: center; + width: 250px; + height: 250px; + border-radius: 1rem; + border: solid 6px #333; + background-color: #fff; + overflow: hidden; } .poke-image::before, .poke-image::after { - content: ''; - position: absolute; - z-index: -1; - border-radius: 50%; + content: ""; + position: absolute; + z-index: -1; + border-radius: 50%; } .poke-image::before { - width: 100px; - height: 100px; - background-color: #c5d5ee; - opacity: 1; - transition: 0.65s ease-in-out; + width: 100px; + height: 100px; + background-color: #c5d5ee; + opacity: 1; + transition: 0.65s ease-in-out; } .poke-image::after { - width: 100px; - height: 100px; - border: solid 12px #c5d5ee; - transform: scale(0); - transition: 0.4s ease-in-out; + width: 100px; + height: 100px; + border: solid 12px #c5d5ee; + transform: scale(0); + transition: 0.4s ease-in-out; } .poke-image-img { - width: auto; - height: 150px; + width: auto; + height: 150px; } .poke-image-success::before, .poke-image-error::before { - transform: scale(4); - opacity: 0.5; + transform: scale(4); + opacity: 0.5; } .poke-image-success::after, .poke-image-error::after { - transform: scale(1); + transform: scale(1); } .poke-image-success::before { - background-color: #7bd55a; + background-color: #7bd55a; } .poke-image-success::after { - border-color: #7bd55a; + border-color: #7bd55a; } .poke-image-error::before { - background-color: #ff8b62; + background-color: #ff8b62; } .poke-image-error::after { - border-color: #ff8b62; - width: 10px; - border-radius: 1rem; - transform: rotate(45deg); + border-color: #ff8b62; + width: 10px; + border-radius: 1rem; + transform: rotate(45deg); } .poke-options { - position: relative; - display: flex; - flex-direction: column; - align-items: center; - z-index: 3; - top: -30px; - padding: 0 20px; - margin: 0 auto; - width: 170px; - border-radius: 1rem; - background-color: #333; -} -.poke-options:not(.poke-options-answers) .poke-options-button:not(.selected):hover { - background-color: #a9c1e6; - transform: translateY(-3px); -} -.poke-options:not(.poke-options-answers) .poke-options-button:not(.selected):active::before { - transform: translate(-50%, -50%) scale(1); + position: relative; + display: flex; + flex-direction: column; + align-items: center; + z-index: 3; + top: -30px; + padding: 0 20px; + margin: 0 auto; + width: 170px; + border-radius: 1rem; + background-color: #333; +} +.poke-options:not(.poke-options-answers) + .poke-options-button:not(.selected):hover { + background-color: #a9c1e6; + transform: translateY(-3px); +} +.poke-options:not(.poke-options-answers) + .poke-options-button:not(.selected):active::before { + transform: translate(-50%, -50%) scale(1); } .poke-options.poke-options-answers .poke-options-button { - cursor: default; + cursor: default; } -.poke-options.poke-options-answers .poke-options-button:not(.error):not(.success) { - color: #94acbd; +.poke-options.poke-options-answers + .poke-options-button:not(.error):not(.success) { + color: #94acbd; } .poke-options-button { - position: relative; - width: 100%; - padding: 0.5em; - min-width: 200px; - max-height: 48px; - border: solid 6px #333; - border-radius: 1rem; - background-color: #c5d5ee; - font-family: 'VT323', monospace; - font-size: 1.125rem; - transition: 0.45s; - cursor: pointer; - overflow: hidden; + position: relative; + width: 100%; + padding: 0.5em; + min-width: 200px; + max-height: 48px; + border: solid 6px #333; + border-radius: 1rem; + background-color: #c5d5ee; + font-family: "VT323", monospace; + font-size: 1.125rem; + transition: 0.45s; + cursor: pointer; + overflow: hidden; } .poke-options-button:focus { - outline: none; + outline: none; } .poke-options-button::before { - content: ''; - position: absolute; - z-index: -1; - left: 50%; - top: 50%; - height: 200px; - width: 200px; - border-radius: 50%; - background-color: #94acbd; - transform: translate(-50%, -50%) scale(0); - transition: 0.2s ease-in-out; + content: ""; + position: absolute; + z-index: -1; + left: 50%; + top: 50%; + height: 200px; + width: 200px; + border-radius: 50%; + background-color: #94acbd; + transform: translate(-50%, -50%) scale(0); + transition: 0.2s ease-in-out; } .poke-options-button:not(:last-child) { - margin-bottom: 3px; + margin-bottom: 3px; } .poke-options-button.selected { - background-color: #94acbd; + background-color: #94acbd; } .poke-options-button.error { - background-color: #ff8b62; + background-color: #ff8b62; } .poke-options-button.success { - background-color: #7bd55a; + background-color: #7bd55a; } .poke-buttons { - text-align: center; + text-align: center; } @media (min-width: 480px) { - .poke-buttons { - position: absolute; - top: 20px; - left: 100%; - } - .poke-buttons .button { - padding-left: calc(1em + 10px); - border-top-left-radius: 0; - border-bottom-left-radius: 0; - transform: translateX(-10px); - } + .poke-buttons { + position: absolute; + top: 20px; + left: 100%; + } + .poke-buttons .button { + padding-left: calc(1em + 10px); + border-top-left-radius: 0; + border-bottom-left-radius: 0; + transform: translateX(-10px); + } } .poke-buttons .button { - padding: 1em; - width: 110px; - height: 100px; - color: #fff; + padding: 1em; + width: 110px; + height: 100px; + color: #fff; } .poke-buttons .button[disabled] { - color: #fa9f9b; - opacity: 0.7; - cursor: default; + color: #fa9f9b; + opacity: 0.7; + cursor: default; } .poke-buttons .button:not([disabled]):hover { - transform: translateX(0); + transform: translateX(0); } .poke-buttons .button:not(:last-child) { - margin-bottom: 6px; + margin-bottom: 6px; } .poke-final { - text-align: center; + text-align: center; } .poke-final-score { - display: block; - position: relative; - margin-bottom: 1rem; + display: block; + position: relative; + margin-bottom: 1rem; } .poke-final-score::before { - content: ''; - position: absolute; - z-index: -1; - top: 50%; - left: 50%; - height: 100px; - width: 100px; - border-radius: 50%; - border: solid 12px #f32c22; - transform: translate(-50%, -50%); - opacity: 0.3; - -webkit-animation: grow 2s infinite ease-in-out; - animation: grow 2s infinite ease-in-out; + content: ""; + position: absolute; + z-index: -1; + top: 50%; + left: 50%; + height: 100px; + width: 100px; + border-radius: 50%; + border: solid 12px #f32c22; + transform: translate(-50%, -50%); + opacity: 0.3; + -webkit-animation: grow 2s infinite ease-in-out; + animation: grow 2s infinite ease-in-out; } .poke-final-score-number { - font-size: 8rem; - line-height: 0.4; - color: #fa9f9b; + font-size: 8rem; + line-height: 0.4; + color: #fa9f9b; } .poke-classic .poke-image-img { - -ms-interpolation-mode: nearest-neighbor; - image-rendering: -moz-crisp-edges; - image-rendering: pixelated; + -ms-interpolation-mode: nearest-neighbor; + image-rendering: -moz-crisp-edges; + image-rendering: pixelated; } .animate-section-enter-active, .animate-section-leave-active { - transition: 0.4s ease-in-out; + transition: 0.4s ease-in-out; } .animate-section-enter, .animate-section-leave-to { - opacity: 0; + opacity: 0; } .animate-section-enter .poke-final-score-number { - transform: translateY(-30px); + transform: translateY(-30px); } .animate-section-leave-active { - transform: translateX(-30%); + transform: translateX(-30%); } .animate-section-enter-active { - transition-delay: 0.1s; - position: absolute; - top: 0; - left: 50%; - transform: translateX(-50%); + transition-delay: 0.1s; + position: absolute; + top: 0; + left: 50%; + transform: translateX(-50%); } .animate-options-enter-active { - transition: 0.4s ease-in-out; + transition: 0.4s ease-in-out; } .animate-options-enter-active:nth-child(4) { - transition-delay: 0s; + transition-delay: 0s; } .animate-options-enter-active:nth-child(5) { - transition-delay: 0.2s; + transition-delay: 0.2s; } .animate-options-enter-active:nth-child(6) { - transition-delay: 0.4s; + transition-delay: 0.4s; } .animate-options-enter-active:nth-child(7) { - transition-delay: 0.6s; + transition-delay: 0.6s; } .animate-options-enter-active:nth-child(8) { - transition-delay: 0.8s; + transition-delay: 0.8s; } .animate-options-enter { - transform: rotateX(-45deg); - transform-origin: top center; - opacity: 0; + transform: rotateX(-45deg); + transform-origin: top center; + opacity: 0; } .animate-options-leave-active { - position: absolute; - z-index: -1; - transition: 0.8s ease-in-out; + position: absolute; + z-index: -1; + transition: 0.8s ease-in-out; } -.animate-options-leave-active[data-index='0'] { - top: 0; +.animate-options-leave-active[data-index="0"] { + top: 0; } -.animate-options-leave-active[data-index='1'] { - top: 51px; +.animate-options-leave-active[data-index="1"] { + top: 51px; } -.animate-options-leave-active[data-index='2'] { - top: 102px; +.animate-options-leave-active[data-index="2"] { + top: 102px; } -.animate-options-leave-active[data-index='3'] { - top: 153px; +.animate-options-leave-active[data-index="3"] { + top: 153px; } .animate-options-leave-to { - opacity: 0; + opacity: 0; } @-webkit-keyframes grow { - 0%, - 100% { - transform: translate(-50%, -50%) scale(1); - } - 50% { - transform: translate(-50%, -50%) scale(0.6); - } + 0%, + 100% { + transform: translate(-50%, -50%) scale(1); + } + 50% { + transform: translate(-50%, -50%) scale(0.6); + } } @keyframes grow { - 0%, - 100% { - transform: translate(-50%, -50%) scale(1); - } - 50% { - transform: translate(-50%, -50%) scale(0.6); - } + 0%, + 100% { + transform: translate(-50%, -50%) scale(1); + } + 50% { + transform: translate(-50%, -50%) scale(0.6); + } } diff --git a/src/routes/quiz/+error.svelte b/src/routes/quiz/+error.svelte index cdc25a7..2aae615 100644 --- a/src/routes/quiz/+error.svelte +++ b/src/routes/quiz/+error.svelte @@ -1,14 +1,16 @@ <script> - import { page } from '$app/stores'; - import { getReasonPhrase } from 'http-status-codes'; + import { page } from "$app/stores"; + import { getReasonPhrase } from "http-status-codes"; </script> <main class="grid min-h-full place-items-center px-6 py-24 sm:py-32 lg:px-8"> - <div class="max-w-lg text-center"> - <p class="text-7xl font-semibold text-red-200">{$page.status}</p> - <h1 class="mt-4 text-3xl tracking-tight text-white sm:text-5xl"> - {getReasonPhrase($page.status)} - </h1> - <p class="mt-6 text-lg font-light leading-7 text-red-100">{$page.error?.message}</p> - </div> + <div class="max-w-lg text-center"> + <p class="text-7xl font-semibold text-red-200">{$page.status}</p> + <h1 class="mt-4 text-3xl tracking-tight text-white sm:text-5xl"> + {getReasonPhrase($page.status)} + </h1> + <p class="mt-6 text-lg font-light leading-7 text-red-100"> + {$page.error?.message} + </p> + </div> </main> diff --git a/src/routes/quiz/+layout.server.ts b/src/routes/quiz/+layout.server.ts index 3293ca2..aeb9cfc 100644 --- a/src/routes/quiz/+layout.server.ts +++ b/src/routes/quiz/+layout.server.ts @@ -1,42 +1,42 @@ -import colors from 'tailwindcss/colors'; -import { getRandomItems } from '$lib/utils'; +import colors from "tailwindcss/colors"; +import { getRandomItems } from "$lib/utils"; type Color = keyof typeof colors; function getColor(currentColor?: string): Color { - const palette: Color[] = [ - 'red', - 'orange', - 'amber', - 'yellow', - 'lime', - 'green', - 'emerald', - 'teal', - 'cyan', - 'sky', - 'blue', - 'indigo', - 'violet', - 'purple', - 'fuchsia', - 'pink', - 'rose' - ]; + const palette: Color[] = [ + "red", + "orange", + "amber", + "yellow", + "lime", + "green", + "emerald", + "teal", + "cyan", + "sky", + "blue", + "indigo", + "violet", + "purple", + "fuchsia", + "pink", + "rose", + ]; - if (palette.includes(currentColor as Color)) { - return currentColor as Color; - } + if (palette.includes(currentColor as Color)) { + return currentColor as Color; + } - return getRandomItems(palette, 1)[0]; + return getRandomItems(palette, 1)[0]; } export function load({ cookies }) { - const colorName = getColor(cookies.get('color')); + const colorName = getColor(cookies.get("color")); - cookies.set('color', colorName, { path: '/' }); + cookies.set("color", colorName, { path: "/" }); - return { - color: colors[colorName] - }; + return { + color: colors[colorName], + }; } diff --git a/src/routes/quiz/+layout.svelte b/src/routes/quiz/+layout.svelte index 2e001b9..a6ff402 100644 --- a/src/routes/quiz/+layout.svelte +++ b/src/routes/quiz/+layout.svelte @@ -1,11 +1,14 @@ <script lang="ts"> - export let data; + export let data; - $: cssVarStyles = Object.entries(data.color) - .map(([key, value]) => `--tw-random-color-${key}:${value}`) - .join(';'); + $: cssVarStyles = Object.entries(data.color) + .map(([key, value]) => `--tw-random-color-${key}:${value}`) + .join(";"); </script> -<div style={cssVarStyles} class="flex min-h-screen flex-col bg-random-500 font-game text-zinc-800"> - <slot /> +<div + style={cssVarStyles} + class="flex min-h-screen flex-col bg-random-500 font-game text-zinc-800" +> + <slot /> </div> diff --git a/src/routes/quiz/+page.server.ts b/src/routes/quiz/+page.server.ts index ce29c90..90b3993 100644 --- a/src/routes/quiz/+page.server.ts +++ b/src/routes/quiz/+page.server.ts @@ -1,65 +1,68 @@ -import { superValidate } from 'sveltekit-superforms'; -import { zod } from 'sveltekit-superforms/adapters'; -import { schema } from './schema'; -import { fail, redirect } from '@sveltejs/kit'; -import { getRandomItems } from '$lib/utils'; -import { GameStage, Game, type GameState } from '$lib/game'; -import { getPromotionRange } from '$lib/data'; -import { client } from '$lib/graphql'; -import { USER_DETAILS_QUERY } from '$lib/graphql/queries'; -import { handleGqlError } from '$lib/graphql/error'; +import { superValidate } from "sveltekit-superforms"; +import { zod } from "sveltekit-superforms/adapters"; +import { schema } from "./schema"; +import { fail, redirect } from "@sveltejs/kit"; +import { getRandomItems } from "$lib/utils"; +import { GameStage, Game, type GameState } from "$lib/game"; +import { getPromotionRange } from "$lib/data"; +import { client } from "$lib/graphql"; +import { USER_DETAILS_QUERY } from "$lib/graphql/queries"; +import { handleGqlError } from "$lib/graphql/error"; type Option = { - value: string; - label: string; + value: string; + label: string; }; async function next(state: GameState) { - const all = await getPromotionRange(state.year, state.maxYear); - const previous = new Set(state.history); - const available = all.difference(previous); - - state.options = getRandomItems(Array.from(available), 4); - state.solution = getRandomItems([...state.options], 1)[0]; - state.stage = GameStage.PLAYING; - state.step++; + const all = await getPromotionRange(state.year, state.maxYear); + const previous = new Set(state.history); + const available = all.difference(previous); + + state.options = getRandomItems(Array.from(available), 4); + state.solution = getRandomItems([...state.options], 1)[0]; + state.stage = GameStage.PLAYING; + state.step++; } export async function load(event) { - const game = new Game(event.cookies); - - switch (game.state.stage) { - case GameStage.NEW: - redirect(303, '/quiz/new'); - break; - case GameStage.GAME_OVER: - redirect(303, '/quiz/game-over'); - break; - case GameStage.NEXT: - await next(game.state); - game.save(); - break; - } - - const details = await client.query(USER_DETAILS_QUERY, { idList: game.state.options }); - handleGqlError(details); - - const users = details.data!.page.nodes ?? []; - - const options: Option[] = users.map((node) => ({ label: node.nickname!, value: node.id })) ?? []; - - const photo = users.find((node) => node.id === game.state.solution)?.photo; - - const form = await superValidate(zod(schema)); - - return { - form, - options, - score: game.state.score, - step: game.state.step, - photo, - label: game.state.label - }; + const game = new Game(event.cookies); + + switch (game.state.stage) { + case GameStage.NEW: + redirect(303, "/quiz/new"); + break; + case GameStage.GAME_OVER: + redirect(303, "/quiz/game-over"); + break; + case GameStage.NEXT: + await next(game.state); + game.save(); + break; + } + + const details = await client.query(USER_DETAILS_QUERY, { + idList: game.state.options, + }); + handleGqlError(details); + + const users = details.data!.page.nodes ?? []; + + const options: Option[] = + users.map((node) => ({ label: node.nickname!, value: node.id })) ?? []; + + const photo = users.find((node) => node.id === game.state.solution)?.photo; + + const form = await superValidate(zod(schema)); + + return { + form, + options, + score: game.state.score, + step: game.state.step, + photo, + label: game.state.label, + }; } const MAX_POINTS = 10; @@ -68,48 +71,50 @@ const MIN_POINTS = 0; const SERVER_DELAY_MAX = 3; function computePoints(timestamp: number): number { - const elapsed = (Date.now() - timestamp) / 1000; - return Math.max( - MIN_POINTS, - Math.floor(MAX_POINTS - Math.min(MAX_POINTS + SERVER_DELAY_MAX, elapsed)) - ); + const elapsed = (Date.now() - timestamp) / 1000; + return Math.max( + MIN_POINTS, + Math.floor(MAX_POINTS - Math.min(MAX_POINTS + SERVER_DELAY_MAX, elapsed)), + ); } export const actions = { - async results(event) { - const form = await superValidate(event, zod(schema)); - - if (!form.valid) { - return fail(400, { form }); - } - - const state = new Game(event.cookies); - - const points = - form.data.choice === state.state.solution ? computePoints(state.state.timestamp) : 0; - state.state.score += points; - state.state.history.push(state.state.solution); - state.save(); - - return { - form, - solution: state.state.solution, - points - }; - }, - - async next(event) { - const form = await superValidate(zod(schema)); - - const game = new Game(event.cookies); - - if (game.state.step >= 10) { - game.state.stage = GameStage.GAME_OVER; - } else { - game.state.stage = GameStage.NEXT; - } - game.save(); - - return { form }; - } + async results(event) { + const form = await superValidate(event, zod(schema)); + + if (!form.valid) { + return fail(400, { form }); + } + + const state = new Game(event.cookies); + + const points = + form.data.choice === state.state.solution + ? computePoints(state.state.timestamp) + : 0; + state.state.score += points; + state.state.history.push(state.state.solution); + state.save(); + + return { + form, + solution: state.state.solution, + points, + }; + }, + + async next(event) { + const form = await superValidate(zod(schema)); + + const game = new Game(event.cookies); + + if (game.state.step >= 10) { + game.state.stage = GameStage.GAME_OVER; + } else { + game.state.stage = GameStage.NEXT; + } + game.save(); + + return { form }; + }, }; diff --git a/src/routes/quiz/+page.svelte b/src/routes/quiz/+page.svelte index a0f2d9b..792f1f9 100644 --- a/src/routes/quiz/+page.svelte +++ b/src/routes/quiz/+page.svelte @@ -1,101 +1,109 @@ <script lang="ts"> - import { Fieldset, Control, Label } from 'formsnap'; - import { superForm } from 'sveltekit-superforms'; - import { scale } from 'svelte/transition'; + import { Fieldset, Control, Label } from "formsnap"; + import { superForm } from "sveltekit-superforms"; + import { scale } from "svelte/transition"; - export let data; - export let form; + export let data; + export let form; - const sForm = superForm(data.form, { - // On récupère les valeurs après affichage des résultats - resetForm: false - }); + const sForm = superForm(data.form, { + // On récupère les valeurs après affichage des résultats + resetForm: false, + }); - const { form: formData, enhance } = sForm; + const { form: formData, enhance } = sForm; - const questionAmount = 10; + const questionAmount = 10; </script> <div class="relative mx-auto my-12 w-full grow"> - <section class="relative m-auto flex flex-col items-center"> - <h1 class="absolute -top-8 text-xl">Qui est-ce ?</h1> - <div class="relative"> - <span class="absolute right-[calc(100%_+_0.5rem)] text-right"> - <span class="flex flex-col items-end"> - <span class="text-9xl leading-[0.4] text-random-300"> {data.step} </span> - <span> / {questionAmount} </span> - </span> - <span class="inline-block h-1.5 w-10 bg-zinc-800"></span> - <br /> - <span class="top-24 pt-4 text-xl text-zinc-800"> - {data.score - (form?.points ?? 0)} - <small>pts</small> - </span> - </span> - <div> - <div - class="relative z-10 flex h-64 w-64 items-center justify-center overflow-hidden rounded-2xl border-6 border-solid border-zinc-800 bg-white before:absolute before:-z-10 before:h-32 before:w-32 before:rounded-full before:bg-slate-300 before:opacity-100 before:transition-[0.65s] before:duration-[ease-in-out] after:absolute after:-z-10 after:h-32 after:w-32 after:scale-0 after:rounded-full after:border-solid after:border-slate-300 after:transition-[0.4s] after:duration-[ease-in-out]" - > - <img src={data.photo?.url} alt="Chargement..." class="w-auto" /> - </div> - <span - class="absolute left-0 top-0 z-20 rounded-br-xl rounded-tl-2xl bg-zinc-800 px-2 text-xl text-white" - >{data.label}</span - > - </div> - <form method="post" use:enhance> - <Fieldset form={sForm} name="choice"> - <div - class="relative -top-6 z-20 mx-auto my-0 flex w-44 flex-col items-center space-y-0.5 rounded-2xl bg-zinc-800 px-5 py-0" - > - {#each data.options as option} - {@const valid = (form?.solution && option.value === form?.solution) || null} - <div class="max-h-14 w-full min-w-52"> - <Control let:attrs> - <input - type="radio" - {...attrs} - bind:group={$formData.choice} - value={option.value} - class="peer sr-only" - disabled={form?.solution !== undefined} - data-valid={valid} - /> - <Label - tabindex={0} - class="relative block cursor-pointer select-none overflow-hidden rounded-2xl border-6 border-solid border-zinc-800 bg-slate-300 p-2 text-center text-lg transition-[0.45s] before:absolute before:left-2/4 before:top-2/4 before:-z-10 before:h-[200px] before:w-[200px] before:-translate-x-2/4 before:-translate-y-2/4 before:scale-0 before:rounded-full before:bg-indigo-300 before:transition-[0.2s] before:duration-[ease-in-out] focus:[outline:none] active:before:-translate-x-2/4 active:before:-translate-y-2/4 active:before:scale-100 peer-enabled:hover:translate-y-[-3px] peer-enabled:hover:bg-slate-400 peer-enabled:focus:border-indigo-500 peer-checked:peer-enabled:bg-indigo-300 peer-disabled:cursor-default peer-disabled:text-[#94acbd] peer-checked:peer-disabled:bg-red-400 peer-checked:peer-disabled:text-inherit peer-data-[valid]:!bg-lime-400 peer-data-[valid]:text-inherit" - > - {option.label} - {#if valid && form?.points} - <span class="absolute right-0 mr-1.5 rounded-full bg-white px-1.5" in:scale> - +{form.points} - </span> - {/if} - </Label> - </Control> - </div> - {/each} - </div> - </Fieldset> - <footer class="space-y-1.5 text-center sm:absolute sm:left-full sm:top-5"> - <button - type="submit" - formaction="?/results" - disabled={form?.solution !== undefined} - class="h-[100px] w-[110px] cursor-pointer rounded-2xl border-transparent bg-random-600 p-4 text-2xl text-white transition-[0.35s] duration-300 focus:border focus:border-dotted focus:border-random-400 focus:[outline:none] enabled:hover:translate-x-0 enabled:hover:bg-[#333] enabled:hover:text-random-500 disabled:cursor-default disabled:text-random-300 disabled:opacity-70 sm:-translate-x-2.5 sm:rounded-bl-none sm:rounded-tl-none" - > - OK - </button> - <button - type="submit" - formaction="?/next" - disabled={form?.solution === undefined} - class="h-[100px] w-[110px] cursor-pointer rounded-2xl border-transparent bg-random-600 p-4 text-2xl text-white transition-[0.35s] duration-300 focus:border focus:border-dotted focus:border-random-400 focus:[outline:none] enabled:hover:translate-x-0 enabled:hover:bg-[#333] enabled:hover:text-random-500 disabled:cursor-default disabled:text-random-300 disabled:opacity-70 sm:-translate-x-2.5 sm:rounded-bl-none sm:rounded-tl-none" - > - Suivant - </button> - </footer> - </form> - </div> - </section> + <section class="relative m-auto flex flex-col items-center"> + <h1 class="absolute -top-8 text-xl">Qui est-ce ?</h1> + <div class="relative"> + <span class="absolute right-[calc(100%_+_0.5rem)] text-right"> + <span class="flex flex-col items-end"> + <span class="text-9xl leading-[0.4] text-random-300"> + {data.step} + </span> + <span> / {questionAmount} </span> + </span> + <span class="inline-block h-1.5 w-10 bg-zinc-800"></span> + <br /> + <span class="top-24 pt-4 text-xl text-zinc-800"> + {data.score - (form?.points ?? 0)} + <small>pts</small> + </span> + </span> + <div> + <div + class="relative z-10 flex h-64 w-64 items-center justify-center overflow-hidden rounded-2xl border-6 border-solid border-zinc-800 bg-white before:absolute before:-z-10 before:h-32 before:w-32 before:rounded-full before:bg-slate-300 before:opacity-100 before:transition-[0.65s] before:duration-[ease-in-out] after:absolute after:-z-10 after:h-32 after:w-32 after:scale-0 after:rounded-full after:border-solid after:border-slate-300 after:transition-[0.4s] after:duration-[ease-in-out]" + > + <img src={data.photo?.url} alt="Chargement..." class="w-auto" /> + </div> + <span + class="absolute left-0 top-0 z-20 rounded-br-xl rounded-tl-2xl bg-zinc-800 px-2 text-xl text-white" + >{data.label}</span + > + </div> + <form method="post" use:enhance> + <Fieldset form={sForm} name="choice"> + <div + class="relative -top-6 z-20 mx-auto my-0 flex w-44 flex-col items-center space-y-0.5 rounded-2xl bg-zinc-800 px-5 py-0" + > + {#each data.options as option} + {@const valid = + (form?.solution && option.value === form?.solution) || null} + <div class="max-h-14 w-full min-w-52"> + <Control let:attrs> + <input + type="radio" + {...attrs} + bind:group={$formData.choice} + value={option.value} + class="peer sr-only" + disabled={form?.solution !== undefined} + data-valid={valid} + /> + <Label + tabindex={0} + class="relative block cursor-pointer select-none overflow-hidden rounded-2xl border-6 border-solid border-zinc-800 bg-slate-300 p-2 text-center text-lg transition-[0.45s] before:absolute before:left-2/4 before:top-2/4 before:-z-10 before:h-[200px] before:w-[200px] before:-translate-x-2/4 before:-translate-y-2/4 before:scale-0 before:rounded-full before:bg-indigo-300 before:transition-[0.2s] before:duration-[ease-in-out] focus:[outline:none] active:before:-translate-x-2/4 active:before:-translate-y-2/4 active:before:scale-100 peer-enabled:hover:translate-y-[-3px] peer-enabled:hover:bg-slate-400 peer-enabled:focus:border-indigo-500 peer-checked:peer-enabled:bg-indigo-300 peer-disabled:cursor-default peer-disabled:text-[#94acbd] peer-checked:peer-disabled:bg-red-400 peer-checked:peer-disabled:text-inherit peer-data-[valid]:!bg-lime-400 peer-data-[valid]:text-inherit" + > + {option.label} + {#if valid && form?.points} + <span + class="absolute right-0 mr-1.5 rounded-full bg-white px-1.5" + in:scale + > + +{form.points} + </span> + {/if} + </Label> + </Control> + </div> + {/each} + </div> + </Fieldset> + <footer + class="space-y-1.5 text-center sm:absolute sm:left-full sm:top-5" + > + <button + type="submit" + formaction="?/results" + disabled={form?.solution !== undefined} + class="h-[100px] w-[110px] cursor-pointer rounded-2xl border-transparent bg-random-600 p-4 text-2xl text-white transition-[0.35s] duration-300 focus:border focus:border-dotted focus:border-random-400 focus:[outline:none] enabled:hover:translate-x-0 enabled:hover:bg-[#333] enabled:hover:text-random-500 disabled:cursor-default disabled:text-random-300 disabled:opacity-70 sm:-translate-x-2.5 sm:rounded-bl-none sm:rounded-tl-none" + > + OK + </button> + <button + type="submit" + formaction="?/next" + disabled={form?.solution === undefined} + class="h-[100px] w-[110px] cursor-pointer rounded-2xl border-transparent bg-random-600 p-4 text-2xl text-white transition-[0.35s] duration-300 focus:border focus:border-dotted focus:border-random-400 focus:[outline:none] enabled:hover:translate-x-0 enabled:hover:bg-[#333] enabled:hover:text-random-500 disabled:cursor-default disabled:text-random-300 disabled:opacity-70 sm:-translate-x-2.5 sm:rounded-bl-none sm:rounded-tl-none" + > + Suivant + </button> + </footer> + </form> + </div> + </section> </div> diff --git a/src/routes/quiz/game-over/+page.server.ts b/src/routes/quiz/game-over/+page.server.ts index c4d603a..36df8dc 100644 --- a/src/routes/quiz/game-over/+page.server.ts +++ b/src/routes/quiz/game-over/+page.server.ts @@ -1,20 +1,20 @@ -import { GameStage, Game } from '$lib/game'; -import { redirect } from '@sveltejs/kit'; +import { GameStage, Game } from "$lib/game"; +import { redirect } from "@sveltejs/kit"; export async function load(event) { - const game = new Game(event.cookies); + const game = new Game(event.cookies); - if (game.state.stage !== GameStage.GAME_OVER) redirect(303, '/quiz'); + if (game.state.stage !== GameStage.GAME_OVER) redirect(303, "/quiz"); - return { score: game.state.score }; + return { score: game.state.score }; } export const actions = { - async default(event) { - const game = new Game(event.cookies); + async default(event) { + const game = new Game(event.cookies); - game.reset(); + game.reset(); - event.cookies.delete('color', { path: '/' }); - } + event.cookies.delete("color", { path: "/" }); + }, }; diff --git a/src/routes/quiz/game-over/+page.svelte b/src/routes/quiz/game-over/+page.svelte index 6bbf498..6faebae 100644 --- a/src/routes/quiz/game-over/+page.svelte +++ b/src/routes/quiz/game-over/+page.svelte @@ -1,45 +1,47 @@ <script lang="ts"> - import { enhance } from '$app/forms'; + import { enhance } from "$app/forms"; - export let data; + export let data; </script> <div class="relative mx-auto my-[50px] w-full max-w-[400px]"> - <section class="text-center"> - <h2>Score final</h2> - <span - class="relative mb-4 block before:absolute before:left-2/4 before:top-2/4 before:z-[-1] before:h-[100px] before:w-[100px] before:-translate-x-2/4 before:-translate-y-2/4 before:animate-[grow_2s_infinite_ease-in-out] before:rounded-[50%] before:border-6 before:border-solid before:border-random-600 before:opacity-30" - > - <span class="translate-y-[-30px] text-9xl text-random-300">{data.score}</span> - pts - </span> - <form method="post" use:enhance> - <button - type="submit" - class="cursor-pointer rounded-2xl border-transparent bg-random-600 px-[1.5em] py-[0.5em] text-2xl text-white transition-[0.35s] hover:bg-zinc-800 hover:text-random-500 focus:border focus:border-dotted focus:border-random-400 focus:[outline:none]" - > - Rejouer - </button> - </form> - </section> + <section class="text-center"> + <h2>Score final</h2> + <span + class="relative mb-4 block before:absolute before:left-2/4 before:top-2/4 before:z-[-1] before:h-[100px] before:w-[100px] before:-translate-x-2/4 before:-translate-y-2/4 before:animate-[grow_2s_infinite_ease-in-out] before:rounded-[50%] before:border-6 before:border-solid before:border-random-600 before:opacity-30" + > + <span class="translate-y-[-30px] text-9xl text-random-300" + >{data.score}</span + > + pts + </span> + <form method="post" use:enhance> + <button + type="submit" + class="cursor-pointer rounded-2xl border-transparent bg-random-600 px-[1.5em] py-[0.5em] text-2xl text-white transition-[0.35s] hover:bg-zinc-800 hover:text-random-500 focus:border focus:border-dotted focus:border-random-400 focus:[outline:none]" + > + Rejouer + </button> + </form> + </section> </div> <style> - :global(body) { - background-color: #f65a52; - font-family: 'Londrina Solid', monospace; - font-size: 16px; - line-height: 1.875em; - color: #333; - } + :global(body) { + background-color: #f65a52; + font-family: "Londrina Solid", monospace; + font-size: 16px; + line-height: 1.875em; + color: #333; + } - @keyframes -global-grow { - 0%, - 100% { - transform: translate(-50%, -50%) scale(1.2); - } - 50% { - transform: translate(-50%, -50%) scale(0.6); - } - } + @keyframes -global-grow { + 0%, + 100% { + transform: translate(-50%, -50%) scale(1.2); + } + 50% { + transform: translate(-50%, -50%) scale(0.6); + } + } </style> diff --git a/src/routes/quiz/new/+page.server.ts b/src/routes/quiz/new/+page.server.ts index 3494569..dcb8a77 100644 --- a/src/routes/quiz/new/+page.server.ts +++ b/src/routes/quiz/new/+page.server.ts @@ -1,22 +1,22 @@ -import { superValidate } from 'sveltekit-superforms'; -import { zod } from 'sveltekit-superforms/adapters'; -import { schema } from './schema'; -import { fail, redirect } from '@sveltejs/kit'; -import { Game, GameStage } from '$lib/game'; -import { getPromotion } from '$lib/data'; -import images from './images'; -import { sum } from '$lib/utils'; +import { superValidate } from "sveltekit-superforms"; +import { zod } from "sveltekit-superforms/adapters"; +import { schema } from "./schema"; +import { fail, redirect } from "@sveltejs/kit"; +import { Game, GameStage } from "$lib/game"; +import { getPromotion } from "$lib/data"; +import images from "./images"; +import { sum } from "$lib/utils"; type BaseLevel = { - year: number; - maxYear?: number; - name: string; - image: string; + year: number; + maxYear?: number; + name: string; + image: string; }; type Level = BaseLevel & { - size: number; - disabled: boolean; + size: number; + disabled: boolean; }; const VIIEUX_YEAR = 20; @@ -24,96 +24,98 @@ const VIIEUX_YEAR = 20; const MINIMUM_SIZE = 15; export async function load(event) { - const game = new Game(event.cookies); - - if (game.state.stage !== GameStage.NEW) redirect(303, '/quiz'); - - const baseLevels: BaseLevel[] = [ - { - year: 1, - name: '1A', - image: images.stroller - }, - { - year: 2, - name: '2A', - image: images.running - }, - { - year: 3, - name: '3A', - image: images.stickFighting - }, - { - year: 1, - maxYear: 3, - name: '1-3A', - image: images.conferenceCall - }, - { - year: 4, - name: '4A', - image: images.certificate - }, - { - year: 5, - name: '5A', - image: images.wallet - }, - { - year: 4, - maxYear: VIIEUX_YEAR, - name: 'Viieux', - image: images.elderlyPerson - }, - { - year: 1, - maxYear: VIIEUX_YEAR, - name: 'IIEns', - image: images.crowd - } - ]; - - const promoSizes = await Promise.all( - Array.from(new Array(VIIEUX_YEAR+1), (_, i) => getPromotion(i).then((promo) => promo.size)) - ); - - const levels: Level[] = baseLevels.map((level) => { - const end = (level.maxYear ?? level.year) + 1; - const size = promoSizes.slice(level.year, end).reduce(sum, 0); - return { - size, - disabled: size < MINIMUM_SIZE, - ...level - }; - }); - - const form = await superValidate(zod(schema)); - return { form, levels }; + const game = new Game(event.cookies); + + if (game.state.stage !== GameStage.NEW) redirect(303, "/quiz"); + + const baseLevels: BaseLevel[] = [ + { + year: 1, + name: "1A", + image: images.stroller, + }, + { + year: 2, + name: "2A", + image: images.running, + }, + { + year: 3, + name: "3A", + image: images.stickFighting, + }, + { + year: 1, + maxYear: 3, + name: "1-3A", + image: images.conferenceCall, + }, + { + year: 4, + name: "4A", + image: images.certificate, + }, + { + year: 5, + name: "5A", + image: images.wallet, + }, + { + year: 4, + maxYear: VIIEUX_YEAR, + name: "Viieux", + image: images.elderlyPerson, + }, + { + year: 1, + maxYear: VIIEUX_YEAR, + name: "IIEns", + image: images.crowd, + }, + ]; + + const promoSizes = await Promise.all( + Array.from(new Array(VIIEUX_YEAR + 1), (_, i) => + getPromotion(i).then((promo) => promo.size), + ), + ); + + const levels: Level[] = baseLevels.map((level) => { + const end = (level.maxYear ?? level.year) + 1; + const size = promoSizes.slice(level.year, end).reduce(sum, 0); + return { + size, + disabled: size < MINIMUM_SIZE, + ...level, + }; + }); + + const form = await superValidate(zod(schema)); + return { form, levels }; } export const actions = { - async default(event) { - const form = await superValidate(event, zod(schema)); + async default(event) { + const form = await superValidate(event, zod(schema)); - if (!form.valid) { - return fail(400, { form }); - } + if (!form.valid) { + return fail(400, { form }); + } - const promotion = await getPromotion(form.data.year); - if (promotion.size < MINIMUM_SIZE) { - return fail(400, { form }); - } + const promotion = await getPromotion(form.data.year); + if (promotion.size < MINIMUM_SIZE) { + return fail(400, { form }); + } - const state = new Game(event.cookies); + const state = new Game(event.cookies); - state.state.year = form.data.year; - state.state.label = form.data.label; - state.state.maxYear = form.data.maxYear; - state.state.stage = GameStage.NEXT; + state.state.year = form.data.year; + state.state.label = form.data.label; + state.state.maxYear = form.data.maxYear; + state.state.stage = GameStage.NEXT; - state.save(); + state.save(); - redirect(303, '/quiz'); - } + redirect(303, "/quiz"); + }, }; diff --git a/src/routes/quiz/new/+page.svelte b/src/routes/quiz/new/+page.svelte index 74c17d3..ed4344f 100644 --- a/src/routes/quiz/new/+page.svelte +++ b/src/routes/quiz/new/+page.svelte @@ -1,49 +1,59 @@ <script lang="ts"> - import { superForm } from 'sveltekit-superforms'; - import { Control, Field } from 'formsnap'; + import { superForm } from "sveltekit-superforms"; + import { Control, Field } from "formsnap"; - export let data; + export let data; - const form = superForm(data.form, { - // On récupère les valeurs après affichage des résultats - resetForm: false - }); - const { enhance } = form; + const form = superForm(data.form, { + // On récupère les valeurs après affichage des résultats + resetForm: false, + }); + const { enhance } = form; </script> <div class="my-auto p-8 sm:p-16"> - <div class="flex flex-wrap items-center justify-center gap-4 text-gray-700 sm:gap-8"> - {#each data.levels as level} - <form method="post" use:enhance> - <Field {form} name="year"> - <Control let:attrs> - <input type="hidden" {...attrs} value={level.year} /> - </Control> - </Field> - <Field {form} name="maxYear"> - <Control let:attrs> - <input type="hidden" {...attrs} value={level.maxYear} /> - </Control> - </Field> - <Field {form} name="label"> - <Control let:attrs> - <input type="hidden" {...attrs} value={level.name} /> - </Control> - </Field> - <button - type="submit" - disabled={level.disabled} - class="col-span-1 flex h-36 w-36 flex-col divide-y divide-gray-200 rounded-2xl border-6 border-solid border-zinc-800 bg-red-300 cursor-not-allowed enabled:cursor-pointer enabled:bg-slate-100 text-center shadow transition-[0.45s] enabled:hover:translate-y-[-3px] enabled:hover:bg-slate-300 enabled:focus:border-indigo-500 sm:h-72 sm:w-72" - > - <div class="flex flex-1 flex-col p-2 sm:p-8"> - <img class="mx-auto h-16 flex-shrink-0 sm:h-32 sm:w-32" src={level.image} alt="" /> - <h1 class="mt-1 text-2xl font-medium sm:mt-4 sm:text-6xl">{level.name}</h1> - <dl class="flex flex-grow flex-col justify-between sm:mt-1"> - <dd class="text-sm font-light text-gray-500">{level.size} personnes</dd> - </dl> - </div> - </button> - </form> - {/each} - </div> + <div + class="flex flex-wrap items-center justify-center gap-4 text-gray-700 sm:gap-8" + > + {#each data.levels as level} + <form method="post" use:enhance> + <Field {form} name="year"> + <Control let:attrs> + <input type="hidden" {...attrs} value={level.year} /> + </Control> + </Field> + <Field {form} name="maxYear"> + <Control let:attrs> + <input type="hidden" {...attrs} value={level.maxYear} /> + </Control> + </Field> + <Field {form} name="label"> + <Control let:attrs> + <input type="hidden" {...attrs} value={level.name} /> + </Control> + </Field> + <button + type="submit" + disabled={level.disabled} + class="col-span-1 flex h-36 w-36 cursor-not-allowed flex-col divide-y divide-gray-200 rounded-2xl border-6 border-solid border-zinc-800 bg-red-300 text-center shadow transition-[0.45s] enabled:cursor-pointer enabled:bg-slate-100 enabled:hover:translate-y-[-3px] enabled:hover:bg-slate-300 enabled:focus:border-indigo-500 sm:h-72 sm:w-72" + > + <div class="flex flex-1 flex-col p-2 sm:p-8"> + <img + class="mx-auto h-16 flex-shrink-0 sm:h-32 sm:w-32" + src={level.image} + alt="" + /> + <h1 class="mt-1 text-2xl font-medium sm:mt-4 sm:text-6xl"> + {level.name} + </h1> + <dl class="flex flex-grow flex-col justify-between sm:mt-1"> + <dd class="text-sm font-light text-gray-500"> + {level.size} personnes + </dd> + </dl> + </div> + </button> + </form> + {/each} + </div> </div> diff --git a/src/routes/quiz/new/images/index.ts b/src/routes/quiz/new/images/index.ts index 9cc1875..fd66f7f 100644 --- a/src/routes/quiz/new/images/index.ts +++ b/src/routes/quiz/new/images/index.ts @@ -1,19 +1,19 @@ -import elderlyPerson from './elderly-person.png'; -import crowd from './crowd.png'; -import stroller from './stroller.png'; -import wallet from './wallet.png'; -import conferenceCall from './conference-call.png'; -import certificate from './certificate.png'; -import running from './running.png'; -import stickFighting from './stick-fighting.png'; +import elderlyPerson from "./elderly-person.png"; +import crowd from "./crowd.png"; +import stroller from "./stroller.png"; +import wallet from "./wallet.png"; +import conferenceCall from "./conference-call.png"; +import certificate from "./certificate.png"; +import running from "./running.png"; +import stickFighting from "./stick-fighting.png"; export default { - elderlyPerson, - crowd, - stroller, - wallet, - conferenceCall, - certificate, - running, - stickFighting + elderlyPerson, + crowd, + stroller, + wallet, + conferenceCall, + certificate, + running, + stickFighting, }; diff --git a/src/routes/quiz/new/schema.ts b/src/routes/quiz/new/schema.ts index d561d4e..cc9ea48 100644 --- a/src/routes/quiz/new/schema.ts +++ b/src/routes/quiz/new/schema.ts @@ -1,7 +1,7 @@ -import { z } from 'zod'; +import { z } from "zod"; export const schema = z.object({ - year: z.number(), - maxYear: z.number().optional(), - label: z.string() + year: z.number(), + maxYear: z.number().optional(), + label: z.string(), }); diff --git a/src/routes/quiz/schema.ts b/src/routes/quiz/schema.ts index 6e7360b..bda55eb 100644 --- a/src/routes/quiz/schema.ts +++ b/src/routes/quiz/schema.ts @@ -1,5 +1,5 @@ -import { z } from 'zod'; +import { z } from "zod"; export const schema = z.object({ - choice: z.string() + choice: z.string(), }); diff --git a/static/site.webmanifest b/static/site.webmanifest index a274be8..34adf30 100644 --- a/static/site.webmanifest +++ b/static/site.webmanifest @@ -1,19 +1,19 @@ { - "name": "Qui est-ce ?", - "short_name": "Qui est-ce ?", - "icons": [ - { - "src": "/android-chrome-192x192.png", - "sizes": "192x192", - "type": "image/png" - }, - { - "src": "/android-chrome-256x256.png", - "sizes": "256x256", - "type": "image/png" - } - ], - "theme_color": "#ffffff", - "background_color": "#ffffff", - "display": "standalone" + "name": "Qui est-ce ?", + "short_name": "Qui est-ce ?", + "icons": [ + { + "src": "/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/android-chrome-256x256.png", + "sizes": "256x256", + "type": "image/png" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" } diff --git a/svelte.config.js b/svelte.config.js index 4c435a4..450b9fb 100644 --- a/svelte.config.js +++ b/svelte.config.js @@ -1,18 +1,18 @@ -import adapter from '@sveltejs/adapter-node'; -import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; +import adapter from "@sveltejs/adapter-node"; +import { vitePreprocess } from "@sveltejs/vite-plugin-svelte"; /** @type {import('@sveltejs/kit').Config} */ const config = { - // Consult https://kit.svelte.dev/docs/integrations#preprocessors - // for more information about preprocessors - preprocess: vitePreprocess(), + // Consult https://kit.svelte.dev/docs/integrations#preprocessors + // for more information about preprocessors + preprocess: vitePreprocess(), - kit: { - adapter: adapter(), - alias: { - $houdini: './$houdini' - } - } + kit: { + adapter: adapter(), + alias: { + $houdini: "./$houdini", + }, + }, }; export default config; diff --git a/tailwind.config.ts b/tailwind.config.ts index c7309ef..7c4c86a 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -1,26 +1,26 @@ -import type { Config } from 'tailwindcss'; +import type { Config } from "tailwindcss"; export default { - content: ['./src/**/*.{html,js,svelte,ts}'], + content: ["./src/**/*.{html,js,svelte,ts}"], - theme: { - extend: { - fontFamily: { - game: ['Londrina Solid', 'monospace'] - }, - borderWidth: { - 6: '6px' - }, - colors: { - random: Object.fromEntries( - [50, 100, 200, 300, 400, 500, 600, 700, 800, 900].map((shade) => [ - shade, - `var(--tw-random-color-${shade})` - ]) - ) - } - } - }, + theme: { + extend: { + fontFamily: { + game: ["Londrina Solid", "monospace"], + }, + borderWidth: { + 6: "6px", + }, + colors: { + random: Object.fromEntries( + [50, 100, 200, 300, 400, 500, 600, 700, 800, 900].map((shade) => [ + shade, + `var(--tw-random-color-${shade})`, + ]), + ), + }, + }, + }, - plugins: [] + plugins: [], } as Config; diff --git a/tsconfig.json b/tsconfig.json index 994a6af..7989293 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,21 +1,21 @@ { - "extends": "./.svelte-kit/tsconfig.json", - "compilerOptions": { - "allowJs": true, - "checkJs": true, - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true, - "skipLibCheck": true, - "sourceMap": true, - "strict": true, - "moduleResolution": "bundler", - "plugins": [ - { - "name": "gql.tada/ts-plugin", - "schema": "./src/lib/graphql/schema.gql", - "tadaOutputLocation": "./src/lib/graphql/types.d.ts" - } - ] - } + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "moduleResolution": "bundler", + "plugins": [ + { + "name": "gql.tada/ts-plugin", + "schema": "./src/lib/graphql/schema.gql", + "tadaOutputLocation": "./src/lib/graphql/types.d.ts" + } + ] + } } diff --git a/vite.config.ts b/vite.config.ts index bbf8c7d..80864b9 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,6 +1,6 @@ -import { sveltekit } from '@sveltejs/kit/vite'; -import { defineConfig } from 'vite'; +import { sveltekit } from "@sveltejs/kit/vite"; +import { defineConfig } from "vite"; export default defineConfig({ - plugins: [sveltekit()] + plugins: [sveltekit()], }); -- GitLab