Skip to content
Extraits de code Groupes Projets
Valider a9c7826c rédigé par Steel's avatar Steel
Parcourir les fichiers

wip: level selector

parent fad6d9d5
Branches
Aucune étiquette associée trouvée
Aucune requête de fusion associée trouvée
...@@ -5,7 +5,7 @@ import { PROMOTION_QUERY, USER_DETAILS_QUERY } from './graphql/queries'; ...@@ -5,7 +5,7 @@ import { PROMOTION_QUERY, USER_DETAILS_QUERY } from './graphql/queries';
type UserId = string; type UserId = string;
type PromoCache = { type PromoCache = {
lastUpdateMs: number; lastUpdateMs: number;
promo: Set<UserId>; promotion: Set<UserId>;
}; };
const cache = new Map<number, PromoCache>(); const cache = new Map<number, PromoCache>();
...@@ -26,8 +26,8 @@ async function cacheImages(users: UserId[]): Promise<void> { ...@@ -26,8 +26,8 @@ async function cacheImages(users: UserId[]): Promise<void> {
} }
} }
async function fetchPromotion(promotion: number): Promise<PromoCache> { async function fetchPromotion(year: number): Promise<PromoCache> {
const array = await Array.fromAsync(pageIterator(PROMOTION_QUERY, { promotion })); const array = await Array.fromAsync(pageIterator(PROMOTION_QUERY, { year }));
const users = array.map((node) => node.id); const users = array.map((node) => node.id);
// background task // background task
...@@ -35,30 +35,31 @@ async function fetchPromotion(promotion: number): Promise<PromoCache> { ...@@ -35,30 +35,31 @@ async function fetchPromotion(promotion: number): Promise<PromoCache> {
return { return {
lastUpdateMs: new Date().getTime(), lastUpdateMs: new Date().getTime(),
promo: new Set(users) promotion: new Set(users)
}; };
} }
export async function getPromotion(promo: number): Promise<Set<UserId>> { export async function getPromotion(year: number): Promise<Set<UserId>> {
if (cache.has(promo)) { if (cache.has(year)) {
const data = cache.get(promo)!; const data = cache.get(year)!;
if (new Date().getTime() - data.lastUpdateMs < CACHE_DURATION_MS) return data.promo; if (new Date().getTime() - data.lastUpdateMs < CACHE_DURATION_MS) return data.promotion;
} }
const freshData = await fetchPromotion(promo); console.log('year', year, 'not in cache, fetching');
cache.set(promo, freshData); const freshData = await fetchPromotion(year);
return freshData.promo; cache.set(year, freshData);
return freshData.promotion;
} }
export async function* promotionIterator( export async function* promotionIterator(
min: number, min: number,
max: number max: number
): AsyncGenerator<UserId, void, undefined> { ): AsyncGenerator<UserId, void, undefined> {
for (let i = min; i <= max; i++) { for (let i = min; i < max; i++) {
const promo = await getPromotion(i); const promotion = await getPromotion(i);
yield* promo; yield* promotion;
} }
} }
export function getPromotionRange(min: number, max: number): Promise<Set<UserId>> { export function getPromotionRange(min: number, max = min): Promise<Set<UserId>> {
return Array.fromAsync(promotionIterator(min, max)).then((array) => new Set(array)); return Array.fromAsync(promotionIterator(min, max)).then((array) => new Set(array));
} }
...@@ -3,6 +3,7 @@ import { SecureCookie } from './cookie'; ...@@ -3,6 +3,7 @@ import { SecureCookie } from './cookie';
import { z } from 'zod'; import { z } from 'zod';
export enum GameStage { export enum GameStage {
NEW,
PLAYING, PLAYING,
SOLUTION, SOLUTION,
NEXT, NEXT,
...@@ -15,6 +16,9 @@ export const gameStateSchema = z.object({ ...@@ -15,6 +16,9 @@ export const gameStateSchema = z.object({
score: z.number(), score: z.number(),
options: z.array(z.string()), options: z.array(z.string()),
stage: z.nativeEnum(GameStage), stage: z.nativeEnum(GameStage),
year: z.number(),
maxYear: z.number().optional(),
label: z.string(),
solution: z.string() solution: z.string()
}); });
...@@ -22,7 +26,7 @@ export type GameState = z.infer<typeof gameStateSchema>; ...@@ -22,7 +26,7 @@ export type GameState = z.infer<typeof gameStateSchema>;
export class Game { export class Game {
state: GameState; state: GameState;
protected cookie_name = 'qui-est-ce'; protected cookie_name = 'quiestce-quiz';
protected cookie: SecureCookie<GameState>; protected cookie: SecureCookie<GameState>;
constructor(protected cookies: Cookies) { constructor(protected cookies: Cookies) {
...@@ -32,12 +36,14 @@ export class Game { ...@@ -32,12 +36,14 @@ export class Game {
protected defaultState(): GameState { protected defaultState(): GameState {
return { return {
stage: GameStage.NEXT, stage: GameStage.NEW,
history: [], history: [],
step: 0, step: 0,
score: 0, score: 0,
year: 0,
options: [], options: [],
solution: '' solution: '',
label: '?'
}; };
} }
......
import { graphql } from '.'; import { graphql } from '.';
export const PROMOTION_QUERY = graphql(` export const PROMOTION_QUERY = graphql(`
query GetPromotion($first: Int!, $after: String, $promotion: Int!) { query GetYear($first: Int!, $after: String, $year: Int!) {
page: users( page: users(
first: $first first: $first
after: $after after: $after
filter: { promotion: { eq: [$promotion] }, nickname: { null: false }, photo: { null: false } } filter: { year: { eq: [$year] }, nickname: { null: false }, photo: { null: false } }
) { ) {
pageInfo { pageInfo {
endCursor endCursor
......
...@@ -11,3 +11,5 @@ export function getRandomItems<T>(array: T[], count: number): T[] { ...@@ -11,3 +11,5 @@ export function getRandomItems<T>(array: T[], count: number): T[] {
items.push(item); items.push(item);
return items; return items;
} }
export const sum = (a: number, b: number) => a + b;
...@@ -3,8 +3,8 @@ import { zod } from 'sveltekit-superforms/adapters'; ...@@ -3,8 +3,8 @@ import { zod } from 'sveltekit-superforms/adapters';
import { schema } from './schema'; import { schema } from './schema';
import { fail, redirect } from '@sveltejs/kit'; import { fail, redirect } from '@sveltejs/kit';
import { getRandomItems } from '$lib/utils'; import { getRandomItems } from '$lib/utils';
import { GameStage, Game } from '$lib/game'; import { GameStage, Game, type GameState } from '$lib/game';
import { getPromotion } from '$lib/data'; import { getPromotionRange } from '$lib/data';
import { client } from '$lib/graphql'; import { client } from '$lib/graphql';
import { USER_DETAILS_QUERY } from '$lib/graphql/queries'; import { USER_DETAILS_QUERY } from '$lib/graphql/queries';
import { handleGqlError } from '$lib/graphql/error'; import { handleGqlError } from '$lib/graphql/error';
...@@ -14,21 +14,30 @@ type Option = { ...@@ -14,21 +14,30 @@ type Option = {
label: string; label: string;
}; };
export async function load(event) { async function next(state: GameState) {
const game = new Game(event.cookies); const all = await getPromotionRange(state.year, state.maxYear);
const previous = new Set(state.history);
if (game.state.stage === GameStage.GAME_OVER) redirect(303, '/quiz/game-over'); const available = all.difference(previous);
if (game.state.stage === GameStage.NEXT) { state.options = getRandomItems(Array.from(available), 4);
const all = await getPromotion(2023); state.solution = getRandomItems([...state.options], 1)[0];
state.stage = GameStage.PLAYING;
state.step++;
}
const previous = new Set(game.state.history); export async function load(event) {
const available = all.difference(previous); const game = new Game(event.cookies);
game.state.options = getRandomItems(Array.from(available), 4); switch (game.state.stage) {
game.state.solution = getRandomItems([...game.state.options], 1)[0]; case GameStage.NEW:
game.state.stage = GameStage.PLAYING; redirect(303, '/quiz/new');
game.state.step++; break;
case GameStage.GAME_OVER:
redirect(303, '/quiz/game-over');
break;
case GameStage.NEXT:
await next(game.state);
break;
} }
const details = await client const details = await client
...@@ -46,7 +55,14 @@ export async function load(event) { ...@@ -46,7 +55,14 @@ export async function load(event) {
const photo = users.find((node) => node.id === game.state.solution)?.photo; const photo = users.find((node) => node.id === game.state.solution)?.photo;
const form = await superValidate(zod(schema)); const form = await superValidate(zod(schema));
return { form, options, score: game.state.score, step: game.state.step, photo }; return {
form,
options,
score: game.state.score,
step: game.state.step,
photo,
label: game.state.label
};
} }
export const actions = { export const actions = {
......
...@@ -5,37 +5,39 @@ ...@@ -5,37 +5,39 @@
export let data; export let data;
export let form; export let form;
const form2 = superForm(data.form, { const sForm = superForm(data.form, {
// On récupère les valeurs après affichage des résultats // On récupère les valeurs après affichage des résultats
resetForm: false resetForm: false
}); });
const { form: formData, enhance } = form2; const { form: formData, enhance } = sForm;
const questionAmount = 10; const questionAmount = 10;
</script> </script>
<div class="relative mx-auto my-12 w-full grow"> <div class="relative mx-auto my-12 w-full grow">
<section class="relative m-auto flex flex-col items-center"> <section class="relative m-auto flex flex-col items-center">
<h1 class="absolute -top-8 text-xl">Qui est-ce ?</h1> <h1 class="absolute -top-8 text-xl">Qui est-ce ? {data.label}</h1>
<div class="relative"> <div class="relative">
<span class="absolute -left-14 flex flex-col items-end"> <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 class="text-9xl leading-[0.4] text-random-300"> {data.step} </span>
<span> / {questionAmount} </span> <span> / {questionAmount} </span>
</span> </span>
<span <span class="inline-block h-1.5 w-10 bg-zinc-800"></span>
class="absolute right-[calc(100%_+_0.5rem)] top-24 whitespace-nowrap pt-4 text-xl text-zinc-800 before:absolute before:right-0 before:top-0 before:h-1.5 before:w-10 before:bg-zinc-800" <br />
> <span class="top-24 pt-4 text-xl text-zinc-800">
{data.score} {data.score}
<small>pts</small> <small>pts</small>
</span> </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]" 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" /> <img src={data.photo?.url} alt="Chargement..." class="w-auto" />
</div> </div>
<form method="post" use:enhance> <form method="post" use:enhance>
<Fieldset form={form2} name="choice"> <Fieldset form={sForm} name="choice">
<div <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" 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"
> >
...@@ -53,8 +55,7 @@ ...@@ -53,8 +55,7 @@
/> />
<Label <Label
tabindex={0} tabindex={0}
data-solution={form?.solution || null} 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"
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 data-[solution]:cursor-default data-[solution]:text-[#94acbd] peer-checked:data-[solution]:bg-red-400 peer-checked:data-[solution]:text-inherit peer-data-[valid]:!bg-lime-400 peer-data-[valid]:text-inherit"
> >
{option.label} {option.label}
</Label> </Label>
......
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;
};
type Level = BaseLevel & {
size: number;
};
const VIIEUX_YEAR = 20;
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.baby
},
{
year: 2,
name: '2A',
image: images.baby
},
{
year: 3,
name: '3A',
image: images.student
},
{
year: 1,
maxYear: 3,
name: '1-3A',
image: images.student
},
{
year: 4,
name: '4A',
image: images.student
},
{
year: 5,
name: '5A',
image: images.student
},
{
year: 4,
maxYear: VIIEUX_YEAR,
name: 'Viieux',
image: images.student
},
{
year: 1,
maxYear: VIIEUX_YEAR,
name: 'IIEns',
image: images.student
}
];
const promoSizes = await Promise.all(
Array.from(new Array(VIIEUX_YEAR), (_, i) => getPromotion(i + 1).then((promo) => promo.size))
);
const levels: Level[] = baseLevels.map((level) => {
const end = (level.maxYear ?? level.year) + 1;
return {
size: promoSizes.slice(level.year, end).reduce(sum, 0),
...level
};
});
const form = await superValidate(zod(schema));
return { form, levels };
}
export const actions = {
async default(event) {
const form = await superValidate(event, zod(schema));
if (!form.valid) {
return fail(400, { form });
}
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.save();
redirect(303, '/quiz');
}
};
<script lang="ts">
import { superForm } from 'sveltekit-superforms';
import { Control, Field } from 'formsnap';
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;
</script>
<div class="m-16">
<div class="flex flex-wrap items-center justify-center gap-8 text-gray-700">
{#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"
class="col-span-1 flex h-72 w-72 flex-col divide-y divide-gray-200 rounded-2xl border-6 border-solid border-zinc-800 bg-slate-100 text-center shadow transition-[0.45s] hover:translate-y-[-3px] hover:bg-slate-300 focus:border-indigo-500"
>
<div class="flex flex-1 flex-col p-8">
<img class="mx-auto h-32 w-32 flex-shrink-0" src={level.image} alt="" />
<h1 class="mt-4 text-6xl font-medium">{level.name}</h1>
<dl class="mt-1 flex flex-grow flex-col justify-between">
<dd class="text-sm font-light text-gray-500">{level.size} personnes</dd>
</dl>
</div>
</button>
</form>
{/each}
</div>
</div>
src/routes/quiz/new/images/baby.png

7,01 ko

import baby from './baby.png';
import student from './student.png';
export default {
baby,
student
};
src/routes/quiz/new/images/student.png

6,63 ko

import { z } from 'zod';
export const schema = z.object({
year: z.number(),
maxYear: z.number().optional(),
label: z.string()
});
0% Chargement en cours ou .
You are about to add 0 people to the discussion. Proceed with caution.
Veuillez vous inscrire ou vous pour commenter