From a8cd8d80602a0496a83ff31e4f927919a54bdf0e Mon Sep 17 00:00:00 2001 From: wilson Date: Sun, 3 May 2026 22:39:03 +0100 Subject: [PATCH] feat: [frontend] First implementation of the "Create new adventure" form. --- frontend/src/lib/index.ts | 1 + frontend/src/lib/shuffleArray.ts | 3 + .../src/routes/app/adventures/+page.svelte | 28 +++ .../app/adventures/getAdventures.remote.ts | 12 ++ .../routes/app/adventures/new/+page.server.ts | 167 ++++++++++++++++++ .../routes/app/adventures/new/+page.svelte | 123 +++++++++++++ frontend/src/routes/app/articles/+page.svelte | 15 +- frontend/svelte.config.js | 2 +- 8 files changed, 339 insertions(+), 12 deletions(-) create mode 100644 frontend/src/lib/shuffleArray.ts create mode 100644 frontend/src/routes/app/adventures/+page.svelte create mode 100644 frontend/src/routes/app/adventures/getAdventures.remote.ts create mode 100644 frontend/src/routes/app/adventures/new/+page.server.ts create mode 100644 frontend/src/routes/app/adventures/new/+page.svelte diff --git a/frontend/src/lib/index.ts b/frontend/src/lib/index.ts index 856f2b6..042c1cf 100644 --- a/frontend/src/lib/index.ts +++ b/frontend/src/lib/index.ts @@ -1 +1,2 @@ // place files you want to import through the `$lib` alias in this folder. +export { shuffleArray } from './shuffleArray'; diff --git a/frontend/src/lib/shuffleArray.ts b/frontend/src/lib/shuffleArray.ts new file mode 100644 index 0000000..ebefaee --- /dev/null +++ b/frontend/src/lib/shuffleArray.ts @@ -0,0 +1,3 @@ +export function shuffleArray(data: Array): Array { + return data.sort(() => Math.random() - 0.5); +} diff --git a/frontend/src/routes/app/adventures/+page.svelte b/frontend/src/routes/app/adventures/+page.svelte new file mode 100644 index 0000000..bab68ce --- /dev/null +++ b/frontend/src/routes/app/adventures/+page.svelte @@ -0,0 +1,28 @@ + + +
+ + + {#each adventures as adventure (adventure.id)} +
+

{adventure.title}

+

{adventure.description}

+
+ {/each} +
diff --git a/frontend/src/routes/app/adventures/getAdventures.remote.ts b/frontend/src/routes/app/adventures/getAdventures.remote.ts new file mode 100644 index 0000000..ae0a896 --- /dev/null +++ b/frontend/src/routes/app/adventures/getAdventures.remote.ts @@ -0,0 +1,12 @@ +import { getRequestEvent, query } from '$app/server'; +import { listAdventuresApiAdventuresGet } from '@client'; + +export const getAdventures = query('unchecked', async () => { + const request = getRequestEvent(); + const { data } = await listAdventuresApiAdventuresGet({ + headers: { + Authorization: `Bearer ${request.locals.authToken}` + } + }); + return data; +}); diff --git a/frontend/src/routes/app/adventures/new/+page.server.ts b/frontend/src/routes/app/adventures/new/+page.server.ts new file mode 100644 index 0000000..ba78079 --- /dev/null +++ b/frontend/src/routes/app/adventures/new/+page.server.ts @@ -0,0 +1,167 @@ +import { error, type Action, type Actions } from '@sveltejs/kit'; +import type { PageServerLoad } from './$types'; +import { createAdventureApiAdventuresPost } from '@client'; +import * as v from 'valibot'; +import { shuffleArray } from '$lib'; + +export const load: PageServerLoad = async () => { + return { + lengths: ['100-200 Words', '200-350 Words', '350-500 Words', '500-750 Words'], + competencies: ['A1', 'A2', 'B1', 'B2', 'C1'], + languages: [ + { code: 'fr', name: 'French' }, + { code: 'de', name: 'German' }, + { code: 'it', name: 'Italian' }, + { code: 'es', name: 'Spanish' }, + { code: 'pt', name: 'Portuguese' } + ], + genres: shuffleArray([ + 'Crime Fiction', + 'Crime noir', + 'Who-dun-it mystery', + 'Paranormal', + 'Horror', + 'Psychological thriller', + 'Romance', + 'Family', + 'Fantasy', + 'Science Fiction' + ]), + eras: [ + 'Ancient/Classical', + 'Medieval', + '1500-1800', + 'Renaissance', + '19th century', + 'Early 20th century', + 'Mid-century', + 'Contemporary', + 'Near future', + 'Far future' + ], + settings: [ + 'Capital city', + 'Large city (not the capital)', + 'Urban', + 'The suburbs', + 'Rural', + 'Pastoral', + 'Small town', + 'Wilderness', + 'Space', + 'Another planet' + ], + vibes: [ + 'Melancholic', + 'Gothic', + 'Sun-drenched', + 'Bleak', + 'Whimsical', + 'Eerie', + 'Cosy', + 'Tense', + 'Witty', + 'Propulsive', + 'Mentor and student', + 'Unlikely duo', + 'Lone wolf', + 'Queer-norm', + 'Class tensions', + 'Chosen family', + 'Diaspora', + 'Academia', + 'Small town', + 'The sea', + 'Grand house', + 'Road trip', + 'A single night', + 'Heist', + 'Mystery box', + 'Reluctant hero', + 'Redemption', + 'Animal companions', + 'Gentle', + 'Happy ever after', + 'Bittersweet', + 'Epistolary (letters / diary entries)', + "A big city that isn't the capital", + 'Parenthood', + 'Sly', + 'Slapstick', + 'Recovery', + 'Political', + 'Apocalyptic', + 'Post-apocalyptic', + 'Survival', + 'War', + 'Spy thriller', + 'Time travel' + ] + }; +}; + +const CreateFormSchema = v.object({ + vibes: v.array(v.string()), + genre: v.string(), + setting: v.string(), + competency: v.pipe(v.string(), v.picklist(['A1', 'A2', 'B1', 'B2', 'C1'])), + language: v.pipe(v.string(), v.picklist(['fr', 'it', 'de', 'it', 'es'])), + length: v.string(), + protagonist_gender: v.string(), + protagonist_age: v.string() +}); + +export const actions = { + default: async ({ locals, request }) => { + const { authToken } = locals; + + const formData = await request.formData(); + + const data = v.safeParse(CreateFormSchema, { + vibes: formData.getAll('vibes') as string[], + genre: formData.get('genre') as string, + setting: formData.get('setting') as string, + competency: formData.get('competency') as string, + language: formData.get('language') as string, + length: formData.get('length') as string, + protagonist_gender: formData.get('protagonist_gender') as string, + protagonist_age: formData.get('protagonist_age') as string + }); + + if (data.success == false) { + throw error(400, { + message: data.issues.map((e) => e.message).join(', ') + }); + } + + const { + competency, + language, + length, + genre, + setting, + protagonist_gender, + protagonist_age, + vibes + } = data.output; + + const { data: apiData, error: apiError } = await createAdventureApiAdventuresPost({ + headers: { + Authorization: `Bearer ${authToken}` + }, + body: { + competencies: [competency], + language: language, + genres: [genre], + setting: [setting], + vibes: vibes, + protagonist: [protagonist_gender, protagonist_age], + source_language: 'en', + entry_word_count_range: length, + max_entry_count: 6 + } + }); + + console.log({ apiData, apiError }); + } +} satisfies Actions; diff --git a/frontend/src/routes/app/adventures/new/+page.svelte b/frontend/src/routes/app/adventures/new/+page.svelte new file mode 100644 index 0000000..847f906 --- /dev/null +++ b/frontend/src/routes/app/adventures/new/+page.svelte @@ -0,0 +1,123 @@ + + +
+ + +
+
+ + +
+ +
+ + +
+ +
+
+ Entry length + {#each data.lengths as length (length)} + {@const inputId = `length-${length}`} +
+ + +
+ {/each} +
+
+ +
+
+ Vibes (pick two) + {#each theVibes as vibe (vibe)} + {@const theId = `vibe-${vibe}`} + + {/each} +
+
+ +
+ + +
+ +
+ + +
+ +
+
+ Protagonist gender + {#each ['male', 'female', 'non-binary', 'any'] as gender (gender)} + {@const inputId = `gender-${gender}`} +
+ + +
+ {/each} +
+
+ +
+ + +
+ + +
+
diff --git a/frontend/src/routes/app/articles/+page.svelte b/frontend/src/routes/app/articles/+page.svelte index 860497b..482aff6 100644 --- a/frontend/src/routes/app/articles/+page.svelte +++ b/frontend/src/routes/app/articles/+page.svelte @@ -25,7 +25,7 @@ }).format(new Date(iso)); -
+

{article.target_title}

{article.source_title}

- + {/each} @@ -58,15 +60,6 @@