Compare commits

..

8 commits

Author SHA1 Message Date
48bbcac9a6 feat: [frontend] Build form and UI for creating a Choose Your Own
Some checks failed
/ test (push) Has been cancelled
Adventure
2026-05-04 08:02:47 +01:00
568b907013 refactor: [frontend] color -> colour 2026-05-04 08:00:59 +01:00
bcc12e3fad Update hooks.server.ts 2026-05-04 08:00:06 +01:00
4c60a3ca91 feat: [frontend] Add randomItemInArray helper 2026-05-04 07:59:59 +01:00
e075f2dc39 Update types.gen.ts 2026-05-04 07:59:34 +01:00
ae709ac8f2 styles: [frontend] Add various form and btn components 2026-05-04 07:59:27 +01:00
d1243a1997 Update app.d.ts 2026-05-04 07:59:03 +01:00
149c821959 Update openapi.json 2026-05-04 07:58:59 +01:00
16 changed files with 315 additions and 143 deletions

File diff suppressed because one or more lines are too long

View file

@ -56,13 +56,39 @@
--colour-grey-950: #030712; --colour-grey-950: #030712;
--colour-grey-1000: #000000; --colour-grey-1000: #000000;
/** Colour: Red palette, based on Material You's red palette */
--colour-red-50: #ffebee;
--colour-red-100: #ffcdd2;
--colour-red-200: #ef9a9a;
--colour-red-300: #e57373;
--colour-red-400: #ef5350;
--colour-red-500: #f44336;
--colour-red-600: #e53935;
--colour-red-700: #d32f2f;
--colour-red-800: #c62828;
--colour-red-900: #b71c1c;
--colour-red-950: #7f0000;
/** Colour: Green palette, based on Material You's green palette */
--colour-green-50: #e8f5e9;
--colour-green-100: #c8e6c9;
--colour-green-200: #a5d6a7;
--colour-green-300: #81c784;
--colour-green-400: #66bb6a;
--colour-green-500: #4caf50;
--colour-green-600: #43a047;
--colour-green-700: #388e3c;
--colour-green-800: #2e7d32;
--colour-green-900: #1b5e20;
--colour-green-950: #0b2f10;
/* --- Color: On-Surface --- */ /* --- Color: On-Surface --- */
--color-on-surface: #2f342e; /* replaces pure black */ --color-on-surface: #2f342e; /* replaces pure black */
--color-on-surface-variant: #5c605b; --color-on-surface-variant: #5c605b;
/* --- Color: Outline --- */ /* --- Color: Outline --- */
--color-outline: #8c908b; --colour-outline: #8c908b;
--color-outline-variant: #afb3ac; /* use at 20% opacity as "ghost border" */ --colour-outline-variant: #afb3ac; /* use at 20% opacity as "ghost border" */
/* --- Typography: Font Families --- */ /* --- Typography: Font Families --- */
--font-display: 'archivo', sans-serif; --font-display: 'archivo', sans-serif;
@ -218,6 +244,12 @@ body {
gap: var(--space-4); gap: var(--space-4);
} }
.form.outline {
padding: var(--space-4);
border: 1px solid var(--colour-outline);
border-radius: var(--radius-md);
}
.form-header { .form-header {
margin-bottom: var(--space-6); margin-bottom: var(--space-6);
} }
@ -293,6 +325,23 @@ body {
padding-inline: var(--space-2); padding-inline: var(--space-2);
} }
.btn-submit {
background-color: var(--color-primary);
color: var(--color-on-primary);
padding: var(--space-1) var(--space-6);
font-weight: var(--weight-semi-bold);
font-size: var(--text-body-md);
border: 1px solid var(--color-primary);
}
.btn-submit:hover {
opacity: 0.88;
}
.btn-submit:active {
opacity: 0.75;
}
/* -------------------------------------------------------------------------- /* --------------------------------------------------------------------------
Component: Input Component: Input
-------------------------------------------------------------------------- */ -------------------------------------------------------------------------- */
@ -322,7 +371,7 @@ body {
background-color: var(--color-surface-container-high); background-color: var(--color-surface-container-high);
color: var(--color-on-surface); color: var(--color-on-surface);
border: none; border: none;
border-bottom: 1px solid color-mix(in srgb, var(--color-outline-variant) 20%, transparent); border-bottom: 1px solid color-mix(in srgb, var(--colour-outline-variant) 20%, transparent);
border-radius: var(--radius-sm) var(--radius-sm) 0 0; border-radius: var(--radius-sm) var(--radius-sm) 0 0;
padding: var(--space-2) var(--space-3); padding: var(--space-2) var(--space-3);
font-family: var(--font-body); font-family: var(--font-body);
@ -340,11 +389,11 @@ body {
padding: var(--space-2) var(--space-3); padding: var(--space-2) var(--space-3);
background-color: var(--color-surface-container); background-color: var(--color-surface-container);
border-radius: var(--radius-sm) var(--radius-sm) 0 0; border-radius: var(--radius-sm) var(--radius-sm) 0 0;
border-bottom: 1px solid color-mix(in srgb, var(--color-outline-variant) 20%, transparent); border-bottom: 1px solid color-mix(in srgb, var(--colour-outline-variant) 20%, transparent);
} }
.field-input::placeholder { .field-input::placeholder {
color: var(--color-outline-variant); color: var(--colour-outline-variant);
} }
.field-input:focus { .field-input:focus {
@ -355,7 +404,7 @@ body {
background-color: var(--color-surface-container-high); background-color: var(--color-surface-container-high);
color: var(--color-on-surface); color: var(--color-on-surface);
border: none; border: none;
border-bottom: 1px solid color-mix(in srgb, var(--color-outline-variant) 20%, transparent); border-bottom: 1px solid color-mix(in srgb, var(--colour-outline-variant) 20%, transparent);
border-radius: var(--radius-sm) var(--radius-sm) 0 0; border-radius: var(--radius-sm) var(--radius-sm) 0 0;
padding: var(--space-2) var(--space-8) var(--space-2) var(--space-3); padding: var(--space-2) var(--space-8) var(--space-2) var(--space-3);
font-family: var(--font-body); font-family: var(--font-body);
@ -375,11 +424,16 @@ body {
border-bottom: 2px solid var(--color-primary); border-bottom: 2px solid var(--color-primary);
} }
.field-select:disabled {
cursor: not-allowed;
opacity: 0.75;
}
.field-textarea { .field-textarea {
background-color: var(--color-surface-container-high); background-color: var(--color-surface-container-high);
color: var(--color-on-surface); color: var(--color-on-surface);
border: none; border: none;
border-bottom: 1px solid color-mix(in srgb, var(--color-outline-variant) 20%, transparent); border-bottom: 1px solid color-mix(in srgb, var(--colour-outline-variant) 20%, transparent);
border-radius: var(--radius-sm) var(--radius-sm) 0 0; border-radius: var(--radius-sm) var(--radius-sm) 0 0;
padding: var(--space-2) var(--space-3); padding: var(--space-2) var(--space-3);
font-family: var(--font-body); font-family: var(--font-body);
@ -393,7 +447,7 @@ body {
} }
.field-textarea::placeholder { .field-textarea::placeholder {
color: var(--color-outline-variant); color: var(--colour-outline-variant);
} }
.field-textarea:focus { .field-textarea:focus {
@ -407,6 +461,29 @@ body {
margin-top: var(--space-1); margin-top: var(--space-1);
} }
/* --------------------------------------------------------------------------
Component: Form divider
-------------------------------------------------------------------------- */
.form-divider {
display: flex;
align-items: center;
gap: var(--space-3);
margin: var(--space-3) 0;
}
.form-divider-text {
font-family: var(--font-label);
color: var(--color-on-surface);
white-space: nowrap;
}
.form-divider-line {
flex: 1;
height: 1px;
background-color: color-mix(in srgb, var(--colour-outline-variant) 40%, transparent);
}
/* -------------------------------------------------------------------------- /* --------------------------------------------------------------------------
Component: Button (modifiers) Component: Button (modifiers)
-------------------------------------------------------------------------- */ -------------------------------------------------------------------------- */
@ -518,9 +595,15 @@ body {
} }
.alert-error { .alert-error {
background-color: color-mix(in srgb, #b3261e 10%, var(--color-surface)); background-color: color-mix(in srgb, var(--colour-red-700) 10%, var(--color-surface));
color: #b3261e; color: var(--colour-red-700);
border-left: 3px solid #b3261e; border-left: 3px solid var(--colour-red-700);
}
.alert.success {
background-color: color-mix(in srgb, var(--colour-green-700) 10%, var(--color-surface));
color: var(--colour-green-700);
border-left: 3px solid var(--colour-green-700);
} }
/* /*

View file

@ -9,6 +9,10 @@ declare global {
apiClient?: ApiClient; apiClient?: ApiClient;
authToken: string | null; authToken: string | null;
isAdmin: boolean; isAdmin: boolean;
flash?: {
type: 'success';
message: string;
};
} }
// interface PageData {} // interface PageData {}
// interface PageState {} // interface PageState {}

View file

@ -416,6 +416,10 @@ export type CreateAdventureRequest = {
* Protagonist * Protagonist
*/ */
protagonist: Array<string>; protagonist: Array<string>;
/**
* Entry Word Count Range
*/
entry_word_count_range: string;
/** /**
* Max Entry Count * Max Entry Count
*/ */

View file

@ -24,7 +24,6 @@ export const handle: Handle = async ({ event, resolve }) => {
const rawToken = event.cookies.get(COOKIE_NAME_AUTH_TOKEN); const rawToken = event.cookies.get(COOKIE_NAME_AUTH_TOKEN);
const isValid = rawToken ? await verifyJwt(rawToken, PRIVATE_JWT_SECRET) : false; const isValid = rawToken ? await verifyJwt(rawToken, PRIVATE_JWT_SECRET) : false;
console.log({ isValid });
event.locals.authToken = isValid ? rawToken! : null; event.locals.authToken = isValid ? rawToken! : null;
if (isValid && rawToken) { if (isValid && rawToken) {

View file

@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import { resolve } from '$app/paths';
import { page } from '$app/state'; import { page } from '$app/state';
interface Props { interface Props {
@ -12,7 +13,7 @@
<header class="topnav" role="banner"> <header class="topnav" role="banner">
<div class="topnav-inner"> <div class="topnav-inner">
<a href="/app" class="wordmark" aria-label="Home"> <a href={resolve('/app')} class="wordmark" aria-label="Home">
<span class="wordmark-text">Language Learning App</span> <span class="wordmark-text">Language Learning App</span>
</a> </a>
@ -20,7 +21,17 @@
<ul class="nav-links" role="list"> <ul class="nav-links" role="list">
<li> <li>
<a <a
href="/app/articles" href={resolve('/app/adventures')}
class="nav-link"
class:is-active={isActive('/app/adventures')}
aria-current={isActive('/app/adventures') ? 'page' : undefined}
>
Adventures
</a>
</li>
<li>
<a
href={resolve('/app/articles')}
class="nav-link" class="nav-link"
class:is-active={isActive('/app/articles')} class:is-active={isActive('/app/articles')}
aria-current={isActive('/app/articles') ? 'page' : undefined} aria-current={isActive('/app/articles') ? 'page' : undefined}
@ -73,7 +84,7 @@
background-color: var(--glass-bg); background-color: var(--glass-bg);
backdrop-filter: blur(var(--glass-blur)); backdrop-filter: blur(var(--glass-blur));
-webkit-backdrop-filter: blur(var(--glass-blur)); -webkit-backdrop-filter: blur(var(--glass-blur));
box-shadow: 0 1px 0 color-mix(in srgb, var(--color-outline-variant) 35%, transparent); box-shadow: 0 1px 0 color-mix(in srgb, var(--colour-outline-variant) 35%, transparent);
} }
.topnav-inner { .topnav-inner {

View file

@ -1,2 +1,3 @@
// place files you want to import through the `$lib` alias in this folder. // place files you want to import through the `$lib` alias in this folder.
export { shuffleArray } from './shuffleArray'; export { shuffleArray } from './shuffleArray';
export { randomItemInArray } from './randomItemInArray';

View file

@ -0,0 +1,4 @@
export function randomItemInArray<T>(arr: Array<T>): T {
const index = Math.floor(Math.random() * arr.length);
return arr[index];
}

View file

@ -130,7 +130,7 @@
background-color: var(--glass-bg); background-color: var(--glass-bg);
backdrop-filter: blur(var(--glass-blur)); backdrop-filter: blur(var(--glass-blur));
-webkit-backdrop-filter: blur(var(--glass-blur)); -webkit-backdrop-filter: blur(var(--glass-blur));
box-shadow: 0 1px 0 color-mix(in srgb, var(--color-outline-variant) 35%, transparent); box-shadow: 0 1px 0 color-mix(in srgb, var(--colour-outline-variant) 35%, transparent);
} }
.sitenav-inner { .sitenav-inner {
@ -187,7 +187,7 @@
/* ---------- Left margin ---------- */ /* ---------- Left margin ---------- */
.left-margin { .left-margin {
border-right: 1px solid var(--color-outline-variant); border-right: 1px solid var(--colour-outline-variant);
padding: var(--space-12) var(--space-5) var(--space-8); padding: var(--space-12) var(--space-5) var(--space-8);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -288,7 +288,7 @@
.divider { .divider {
border: none; border: none;
border-top: 1px solid var(--color-outline-variant); border-top: 1px solid var(--colour-outline-variant);
margin: 0; margin: 0;
} }
@ -325,7 +325,7 @@
/* ---------- Right rail ---------- */ /* ---------- Right rail ---------- */
.right-rail { .right-rail {
border-left: 1px dashed var(--color-outline-variant); border-left: 1px dashed var(--colour-outline-variant);
padding: var(--space-12) var(--space-4); padding: var(--space-12) var(--space-4);
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
@ -338,7 +338,7 @@
font-weight: var(--weight-medium); font-weight: var(--weight-medium);
letter-spacing: var(--tracking-wider); letter-spacing: var(--tracking-wider);
text-transform: uppercase; text-transform: uppercase;
color: var(--color-outline); color: var(--colour-outline);
writing-mode: vertical-rl; writing-mode: vertical-rl;
transform: rotate(180deg); transform: rotate(180deg);
white-space: nowrap; white-space: nowrap;
@ -364,7 +364,7 @@
.left-margin { .left-margin {
border-right: none; border-right: none;
border-bottom: 1px solid var(--color-outline-variant); border-bottom: 1px solid var(--colour-outline-variant);
padding: var(--space-6) var(--space-6) var(--space-5); padding: var(--space-6) var(--space-6) var(--space-5);
flex-direction: row; flex-direction: row;
flex-wrap: wrap; flex-wrap: wrap;

View file

@ -87,7 +87,7 @@
/* ---------- Left margin ---------- */ /* ---------- Left margin ---------- */
.left-margin { .left-margin {
border-right: 1px solid var(--color-outline-variant); border-right: 1px solid var(--colour-outline-variant);
padding: var(--space-12) var(--space-5) var(--space-8); padding: var(--space-12) var(--space-5) var(--space-8);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -188,7 +188,7 @@
.divider { .divider {
border: none; border: none;
border-top: 1px solid var(--color-outline-variant); border-top: 1px solid var(--colour-outline-variant);
margin: 0; margin: 0;
} }
@ -249,7 +249,7 @@
/* ---------- Right rail ---------- */ /* ---------- Right rail ---------- */
.right-rail { .right-rail {
border-left: 1px dashed var(--color-outline-variant); border-left: 1px dashed var(--colour-outline-variant);
padding: var(--space-12) var(--space-4); padding: var(--space-12) var(--space-4);
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
@ -262,7 +262,7 @@
font-weight: var(--weight-medium); font-weight: var(--weight-medium);
letter-spacing: var(--tracking-wider); letter-spacing: var(--tracking-wider);
text-transform: uppercase; text-transform: uppercase;
color: var(--color-outline); color: var(--colour-outline);
writing-mode: vertical-rl; writing-mode: vertical-rl;
transform: rotate(180deg); transform: rotate(180deg);
white-space: nowrap; white-space: nowrap;
@ -288,7 +288,7 @@
.left-margin { .left-margin {
border-right: none; border-right: none;
border-bottom: 1px solid var(--color-outline-variant); border-bottom: 1px solid var(--colour-outline-variant);
padding: var(--space-6) var(--space-6) var(--space-5); padding: var(--space-6) var(--space-6) var(--space-5);
flex-direction: row; flex-direction: row;
flex-wrap: wrap; flex-wrap: wrap;

View file

@ -0,0 +1,13 @@
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ locals, url }) => {
let successMessage: null | string = null;
if (url.searchParams.get('created')) {
successMessage = `Adventure created, check back in a few minutes`;
url.searchParams.delete('created');
url.searchParams.
}
return {
successMessage
};
};

View file

@ -1,17 +1,19 @@
<script> <script lang="ts">
import type { PageProps } from './$types';
import { resolve } from '$app/paths'; import { resolve } from '$app/paths';
import { onMount } from 'svelte';
import { getAdventures } from './getAdventures.remote';
onMount(async () => {
const _adventures = await getAdventures('');
if (_adventures) {
adventures = _adventures;
}
});
let adventures = $state([]); import { getAdventures } from './getAdventures.remote';
let adventures = $derived(await getAdventures(''));
const { data }: PageProps = $props();
</script> </script>
{#if data.successMessage !== null}
<div class="alert success">
{data.successMessage}
</div>
{/if}
<div class="app-page"> <div class="app-page">
<header class="page-header"> <header class="page-header">
<h1 class="page-title">Adventures</h1> <h1 class="page-title">Adventures</h1>

View file

@ -1,57 +1,11 @@
import { error, type Action, type Actions } from '@sveltejs/kit'; import { error, redirect, type Action, type Actions } from '@sveltejs/kit';
import type { PageServerLoad } from './$types'; import type { PageServerLoad } from './$types';
import { createAdventureApiAdventuresPost } from '@client'; import { createAdventureApiAdventuresPost, getUserProfileBffUserProfileGet } from '@client';
import * as v from 'valibot'; import * as v from 'valibot';
import { shuffleArray } from '$lib'; import { randomItemInArray, shuffleArray } from '$lib';
import { formatLanguage } from '$lib/formatters';
export const load: PageServerLoad = async () => { const allVibes = [
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', 'Melancholic',
'Gothic', 'Gothic',
'Sun-drenched', 'Sun-drenched',
@ -96,7 +50,81 @@ export const load: PageServerLoad = async () => {
'War', 'War',
'Spy thriller', 'Spy thriller',
'Time travel' 'Time travel'
] ];
const allGenres = [
'Crime Fiction',
'Crime noir',
'Who-dun-it mystery',
'Paranormal',
'Horror',
'Psychological thriller',
'Romance',
'Family',
'Fantasy',
'Science Fiction'
];
export const load: PageServerLoad = async ({ locals }) => {
let languageCode = 'fr';
let competency = 'A1';
const profile = await getUserProfileBffUserProfileGet({
headers: {
Authorization: `Bearer ${locals.authToken}`
}
});
if (profile.error) {
console.error({ error: profile.error });
} else {
languageCode = profile.data?.learnable_languages[0].target_language ?? 'fr';
competency = profile.data?.learnable_languages[0].proficiencies[0] ?? competency;
}
return {
lengths: ['100-200 Words', '200-350 Words', '350-500 Words', '500-750 Words'],
competency: competency,
language: {
code: languageCode,
label: formatLanguage(languageCode)
},
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: allGenres.sort((a, b) => a.localeCompare(b)),
selectedGenre: randomItemInArray(allGenres),
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'
],
allVibes: allVibes,
selectedVibes: shuffleArray(allVibes)
.slice(0, 6)
.sort((a, b) => a.localeCompare(b))
}; };
}; };
@ -129,8 +157,9 @@ export const actions = {
}); });
if (data.success == false) { if (data.success == false) {
console.error({ formError: data.issues });
throw error(400, { throw error(400, {
message: data.issues.map((e) => e.message).join(', ') message: data.issues.map((e) => `${e.path?.[0].key}: ${e.message}`).join(', ')
}); });
} }
@ -162,6 +191,12 @@ export const actions = {
} }
}); });
console.log({ apiData, apiError }); if (apiError) {
return {
error: `Error creating new adventure, try again`
};
}
redirect(303, '/app/adventures?created=true');
} }
} satisfies Actions; } satisfies Actions;

View file

@ -1,13 +1,11 @@
<script lang="ts"> <script lang="ts">
import { shuffleArray } from '$lib';
import type { ChangeEventHandler } from 'svelte/elements'; import type { ChangeEventHandler } from 'svelte/elements';
import type { PageServerData } from './$types'; import type { PageServerData } from './$types';
import { resolve } from '$app/paths';
const { data }: { data: PageServerData } = $props(); const { data }: { data: PageServerData } = $props();
let theVibes = $state(shuffleArray(data.vibes).slice(0, 7));
const handleVibeSelected: ChangeEventHandler<HTMLInputElement> = (e) => { const handleVibeSelected: ChangeEventHandler<HTMLInputElement> = (e) => {
const { checked, value } = e.currentTarget; const { checked, value } = e.currentTarget;
@ -18,29 +16,31 @@
} }
}; };
let selectedVibes = $state([]); let selectedVibes = $state([]);
let vibeShuffleCount = $state(0);
let shuffleTheVibes = () => (theVibes = shuffleArray(data.vibes).slice(0, 5));
</script> </script>
<section class="app-page"> <svelte:head>
<div class="page-header"> <title>Create a new Adventure</title>
<div class="page-title">Create new Adventure</div> </svelte:head>
</div>
<form class="form" method="POST"> <section class="app-page">
<form class="form outline" method="POST">
<div class="form-eyebrow">
<a href={resolve('/app')}>Back home</a>
</div>
<h1 class="form-title">Create your own adventure</h1>
<div class="field"> <div class="field">
<label for="genre" class="label">Genre</label> <label for="genre" class="label">Genre</label>
<select name="genre" id="genre"> <select name="genre" id="genre" class="field-select">
{#each data.genres as genre (genre)} {#each data.genres as genre (genre)}
<option value={genre}>{genre}</option> <option value={genre} selected={data.selectedGenre === genre}>{genre}</option>
{/each} {/each}
</select> </select>
</div> </div>
<div class="field"> <div class="field">
<label for="setting" class="label">Setting</label> <label for="setting" class="label">Setting</label>
<select name="setting" id="setting"> <select name="setting" id="setting" class="field-select">
{#each ['Urban', 'Rural', 'Coastal', 'Mountain', 'Forest', 'Desert'] as setting (setting)} {#each data.settings as setting (setting)}
<option value={setting}>{setting}</option> <option value={setting}>{setting}</option>
{/each} {/each}
</select> </select>
@ -48,25 +48,13 @@
<div class="field"> <div class="field">
<fieldset class="field-fieldset"> <fieldset class="field-fieldset">
<legend>Entry length</legend> <legend>Vibes (pick up to two)</legend>
{#each data.lengths as length (length)} {#each data.selectedVibes as vibe (vibe)}
{@const inputId = `length-${length}`}
<div class="radio-option">
<input type="radio" name="length" value={length} id={inputId} />
<label for={inputId}>{length}</label>
</div>
{/each}
</fieldset>
</div>
<div class="field">
<fieldset class="field-fieldset">
<legend>Vibes (pick two)</legend>
{#each theVibes as vibe (vibe)}
{@const theId = `vibe-${vibe}`} {@const theId = `vibe-${vibe}`}
<label for={theId} class="label"> <label for={theId} class="label">
<input <input
type="checkbox" type="checkbox"
name="vibes"
disabled={selectedVibes.length >= 2 && !selectedVibes.includes(vibe)} disabled={selectedVibes.length >= 2 && !selectedVibes.includes(vibe)}
id={theId} id={theId}
value={vibe} value={vibe}
@ -78,31 +66,59 @@
</fieldset> </fieldset>
</div> </div>
<div class="form-divider">
<div class="form-divider-text">Additional details</div>
<div class="form-divider-line"></div>
</div>
<div class="field"> <div class="field">
<label for="language" class="label">Language</label> <label for="language" class="label">Language</label>
<select name="language" id="language"> <select name="language" id="language" class="field-select">
{#each data.languages as language (language.code)} <option value={data.language.code}>{data.language.label}</option>
<option value={language.code}>{language.name}</option>
{/each}
</select> </select>
</div> </div>
<div class="field"> <div class="field">
<label for="competency" class="label">Competency</label> <label for="competency" class="label">Competency</label>
<select name="competency" id="competency"> <select name="competency" id="competency" class="field-select">
{#each data.competencies as competency (competency)} {#each data.competencies as competency (competency)}
<option value={competency}>{competency}</option> <option value={competency} selected={data.competency === competency}>{competency}</option>
{/each} {/each}
</select> </select>
</div> </div>
<div class="field">
<fieldset class="field-fieldset">
<legend>Entry length</legend>
{#each data.lengths as length (length)}
{@const inputId = `length-${length}`}
<div class="radio-option">
<input
type="radio"
name="length"
value={length}
id={inputId}
checked={length === '350-500 Words'}
/>
<label for={inputId}>{length}</label>
</div>
{/each}
</fieldset>
</div>
<div class="field"> <div class="field">
<fieldset class="field-fieldset"> <fieldset class="field-fieldset">
<legend>Protagonist gender</legend> <legend>Protagonist gender</legend>
{#each ['male', 'female', 'non-binary', 'any'] as gender (gender)} {#each ['male', 'female', 'non-binary', 'any'] as gender (gender)}
{@const inputId = `gender-${gender}`} {@const inputId = `gender-${gender}`}
<div class="radio-option"> <div class="radio-option">
<input type="radio" name="protagonist_gender" value={gender} id={inputId} /> <input
type="radio"
name="protagonist_gender"
value={gender}
id={inputId}
checked={gender === 'any'}
/>
<label for={inputId}>{gender}</label> <label for={inputId}>{gender}</label>
</div> </div>
{/each} {/each}
@ -111,13 +127,13 @@
<div class="field"> <div class="field">
<label for="protagonist_age" class="label">Protagonist age</label> <label for="protagonist_age" class="label">Protagonist age</label>
<select name="protagonist_age" id="protagonist_age"> <select name="protagonist_age" id="protagonist_age" class="field-select">
{#each ['Adult', 'Child', 'Teen', 'Young Adult', 'Older Adult', 'Middle-aged', 'Older'] as age (age)} {#each ['Adult', 'Teen', 'Young Adult', 'Older Adult', 'Middle-aged', 'Older'] as age (age)}
<option value={age}>{age}</option> <option value={age} selected={age === 'Adult'}>{age}</option>
{/each} {/each}
</select> </select>
</div> </div>
<input type="submit" value="Create Adventure" class="submit-button" /> <input type="submit" value="Create Adventure" class="btn-submit" />
</form> </form>
</section> </section>

View file

@ -171,7 +171,7 @@
.spinner { .spinner {
width: 1.25rem; width: 1.25rem;
height: 1.25rem; height: 1.25rem;
border: 2px solid var(--color-outline-variant); border: 2px solid var(--colour-outline-variant);
border-top-color: var(--color-primary); border-top-color: var(--color-primary);
border-radius: 50%; border-radius: 50%;
animation: spin 0.9s linear infinite; animation: spin 0.9s linear infinite;

View file

@ -59,7 +59,7 @@
flex-direction: column; flex-direction: column;
gap: var(--space-3); gap: var(--space-3);
padding-top: var(--space-4); padding-top: var(--space-4);
border-top: 1px solid color-mix(in srgb, var(--color-outline-variant) 30%, transparent); border-top: 1px solid color-mix(in srgb, var(--colour-outline-variant) 30%, transparent);
} }
.section-title { .section-title {