Compare commits
No commits in common. "48bbcac9a6661d1e08ea4fce11c7cc7de964fb29" and "a8cd8d80602a0496a83ff31e4f927919a54bdf0e" have entirely different histories.
48bbcac9a6
...
a8cd8d8060
16 changed files with 142 additions and 314 deletions
File diff suppressed because one or more lines are too long
|
|
@ -56,39 +56,13 @@
|
||||||
--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 --- */
|
||||||
--colour-outline: #8c908b;
|
--color-outline: #8c908b;
|
||||||
--colour-outline-variant: #afb3ac; /* use at 20% opacity as "ghost border" */
|
--color-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;
|
||||||
|
|
@ -244,12 +218,6 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
@ -325,23 +293,6 @@ 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
|
||||||
-------------------------------------------------------------------------- */
|
-------------------------------------------------------------------------- */
|
||||||
|
|
@ -371,7 +322,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(--colour-outline-variant) 20%, transparent);
|
border-bottom: 1px solid color-mix(in srgb, var(--color-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);
|
||||||
|
|
@ -389,11 +340,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(--colour-outline-variant) 20%, transparent);
|
border-bottom: 1px solid color-mix(in srgb, var(--color-outline-variant) 20%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.field-input::placeholder {
|
.field-input::placeholder {
|
||||||
color: var(--colour-outline-variant);
|
color: var(--color-outline-variant);
|
||||||
}
|
}
|
||||||
|
|
||||||
.field-input:focus {
|
.field-input:focus {
|
||||||
|
|
@ -404,7 +355,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(--colour-outline-variant) 20%, transparent);
|
border-bottom: 1px solid color-mix(in srgb, var(--color-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);
|
||||||
|
|
@ -424,16 +375,11 @@ 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(--colour-outline-variant) 20%, transparent);
|
border-bottom: 1px solid color-mix(in srgb, var(--color-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);
|
||||||
|
|
@ -447,7 +393,7 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
.field-textarea::placeholder {
|
.field-textarea::placeholder {
|
||||||
color: var(--colour-outline-variant);
|
color: var(--color-outline-variant);
|
||||||
}
|
}
|
||||||
|
|
||||||
.field-textarea:focus {
|
.field-textarea:focus {
|
||||||
|
|
@ -461,29 +407,6 @@ 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)
|
||||||
-------------------------------------------------------------------------- */
|
-------------------------------------------------------------------------- */
|
||||||
|
|
@ -595,15 +518,9 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert-error {
|
.alert-error {
|
||||||
background-color: color-mix(in srgb, var(--colour-red-700) 10%, var(--color-surface));
|
background-color: color-mix(in srgb, #b3261e 10%, var(--color-surface));
|
||||||
color: var(--colour-red-700);
|
color: #b3261e;
|
||||||
border-left: 3px solid var(--colour-red-700);
|
border-left: 3px solid #b3261e;
|
||||||
}
|
|
||||||
|
|
||||||
.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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
|
||||||
4
frontend/src/app.d.ts
vendored
4
frontend/src/app.d.ts
vendored
|
|
@ -9,10 +9,6 @@ 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 {}
|
||||||
|
|
|
||||||
|
|
@ -416,10 +416,6 @@ 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
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ 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) {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
<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 {
|
||||||
|
|
@ -13,7 +12,7 @@
|
||||||
|
|
||||||
<header class="topnav" role="banner">
|
<header class="topnav" role="banner">
|
||||||
<div class="topnav-inner">
|
<div class="topnav-inner">
|
||||||
<a href={resolve('/app')} class="wordmark" aria-label="Home">
|
<a href="/app" class="wordmark" aria-label="Home">
|
||||||
<span class="wordmark-text">Language Learning App</span>
|
<span class="wordmark-text">Language Learning App</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
|
@ -21,17 +20,7 @@
|
||||||
<ul class="nav-links" role="list">
|
<ul class="nav-links" role="list">
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
href={resolve('/app/adventures')}
|
href="/app/articles"
|
||||||
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}
|
||||||
|
|
@ -84,7 +73,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(--colour-outline-variant) 35%, transparent);
|
box-shadow: 0 1px 0 color-mix(in srgb, var(--color-outline-variant) 35%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.topnav-inner {
|
.topnav-inner {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,2 @@
|
||||||
// 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';
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
export function randomItemInArray<T>(arr: Array<T>): T {
|
|
||||||
const index = Math.floor(Math.random() * arr.length);
|
|
||||||
return arr[index];
|
|
||||||
}
|
|
||||||
|
|
@ -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(--colour-outline-variant) 35%, transparent);
|
box-shadow: 0 1px 0 color-mix(in srgb, var(--color-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(--colour-outline-variant);
|
border-right: 1px solid var(--color-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(--colour-outline-variant);
|
border-top: 1px solid var(--color-outline-variant);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -325,7 +325,7 @@
|
||||||
/* ---------- Right rail ---------- */
|
/* ---------- Right rail ---------- */
|
||||||
|
|
||||||
.right-rail {
|
.right-rail {
|
||||||
border-left: 1px dashed var(--colour-outline-variant);
|
border-left: 1px dashed var(--color-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(--colour-outline);
|
color: var(--color-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(--colour-outline-variant);
|
border-bottom: 1px solid var(--color-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;
|
||||||
|
|
|
||||||
|
|
@ -87,7 +87,7 @@
|
||||||
/* ---------- Left margin ---------- */
|
/* ---------- Left margin ---------- */
|
||||||
|
|
||||||
.left-margin {
|
.left-margin {
|
||||||
border-right: 1px solid var(--colour-outline-variant);
|
border-right: 1px solid var(--color-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(--colour-outline-variant);
|
border-top: 1px solid var(--color-outline-variant);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -249,7 +249,7 @@
|
||||||
/* ---------- Right rail ---------- */
|
/* ---------- Right rail ---------- */
|
||||||
|
|
||||||
.right-rail {
|
.right-rail {
|
||||||
border-left: 1px dashed var(--colour-outline-variant);
|
border-left: 1px dashed var(--color-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(--colour-outline);
|
color: var(--color-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(--colour-outline-variant);
|
border-bottom: 1px solid var(--color-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;
|
||||||
|
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
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
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
@ -1,19 +1,17 @@
|
||||||
<script lang="ts">
|
<script>
|
||||||
import type { PageProps } from './$types';
|
|
||||||
import { resolve } from '$app/paths';
|
import { resolve } from '$app/paths';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
import { getAdventures } from './getAdventures.remote';
|
import { getAdventures } from './getAdventures.remote';
|
||||||
|
onMount(async () => {
|
||||||
|
const _adventures = await getAdventures('');
|
||||||
|
if (_adventures) {
|
||||||
|
adventures = _adventures;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
let adventures = $derived(await getAdventures(''));
|
let adventures = $state([]);
|
||||||
|
|
||||||
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>
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,57 @@
|
||||||
import { error, redirect, type Action, type Actions } from '@sveltejs/kit';
|
import { error, type Action, type Actions } from '@sveltejs/kit';
|
||||||
import type { PageServerLoad } from './$types';
|
import type { PageServerLoad } from './$types';
|
||||||
import { createAdventureApiAdventuresPost, getUserProfileBffUserProfileGet } from '@client';
|
import { createAdventureApiAdventuresPost } from '@client';
|
||||||
import * as v from 'valibot';
|
import * as v from 'valibot';
|
||||||
import { randomItemInArray, shuffleArray } from '$lib';
|
import { shuffleArray } from '$lib';
|
||||||
import { formatLanguage } from '$lib/formatters';
|
|
||||||
|
|
||||||
const allVibes = [
|
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',
|
'Melancholic',
|
||||||
'Gothic',
|
'Gothic',
|
||||||
'Sun-drenched',
|
'Sun-drenched',
|
||||||
|
|
@ -50,81 +96,7 @@ const allVibes = [
|
||||||
'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))
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -157,9 +129,8 @@ 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.path?.[0].key}: ${e.message}`).join(', ')
|
message: data.issues.map((e) => e.message).join(', ')
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -191,12 +162,6 @@ export const actions = {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (apiError) {
|
console.log({ apiData, apiError });
|
||||||
return {
|
|
||||||
error: `Error creating new adventure, try again`
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
redirect(303, '/app/adventures?created=true');
|
|
||||||
}
|
}
|
||||||
} satisfies Actions;
|
} satisfies Actions;
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
<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;
|
||||||
|
|
||||||
|
|
@ -16,31 +18,29 @@
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let selectedVibes = $state([]);
|
let selectedVibes = $state([]);
|
||||||
|
let vibeShuffleCount = $state(0);
|
||||||
|
let shuffleTheVibes = () => (theVibes = shuffleArray(data.vibes).slice(0, 5));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
|
||||||
<title>Create a new Adventure</title>
|
|
||||||
</svelte:head>
|
|
||||||
|
|
||||||
<section class="app-page">
|
<section class="app-page">
|
||||||
<form class="form outline" method="POST">
|
<div class="page-header">
|
||||||
<div class="form-eyebrow">
|
<div class="page-title">Create new Adventure</div>
|
||||||
<a href={resolve('/app')}>Back home</a>
|
|
||||||
</div>
|
</div>
|
||||||
<h1 class="form-title">Create your own adventure</h1>
|
|
||||||
|
<form class="form" method="POST">
|
||||||
<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" class="field-select">
|
<select name="genre" id="genre">
|
||||||
{#each data.genres as genre (genre)}
|
{#each data.genres as genre (genre)}
|
||||||
<option value={genre} selected={data.selectedGenre === genre}>{genre}</option>
|
<option value={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" class="field-select">
|
<select name="setting" id="setting">
|
||||||
{#each data.settings as setting (setting)}
|
{#each ['Urban', 'Rural', 'Coastal', 'Mountain', 'Forest', 'Desert'] as setting (setting)}
|
||||||
<option value={setting}>{setting}</option>
|
<option value={setting}>{setting}</option>
|
||||||
{/each}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
|
|
@ -48,13 +48,25 @@
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<fieldset class="field-fieldset">
|
<fieldset class="field-fieldset">
|
||||||
<legend>Vibes (pick up to two)</legend>
|
<legend>Entry length</legend>
|
||||||
{#each data.selectedVibes as vibe (vibe)}
|
{#each data.lengths as length (length)}
|
||||||
|
{@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}
|
||||||
|
|
@ -66,59 +78,31 @@
|
||||||
</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" class="field-select">
|
<select name="language" id="language">
|
||||||
<option value={data.language.code}>{data.language.label}</option>
|
{#each data.languages as language (language.code)}
|
||||||
|
<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" class="field-select">
|
<select name="competency" id="competency">
|
||||||
{#each data.competencies as competency (competency)}
|
{#each data.competencies as competency (competency)}
|
||||||
<option value={competency} selected={data.competency === competency}>{competency}</option>
|
<option value={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
|
<input type="radio" name="protagonist_gender" value={gender} id={inputId} />
|
||||||
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}
|
||||||
|
|
@ -127,13 +111,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" class="field-select">
|
<select name="protagonist_age" id="protagonist_age">
|
||||||
{#each ['Adult', 'Teen', 'Young Adult', 'Older Adult', 'Middle-aged', 'Older'] as age (age)}
|
{#each ['Adult', 'Child', 'Teen', 'Young Adult', 'Older Adult', 'Middle-aged', 'Older'] as age (age)}
|
||||||
<option value={age} selected={age === 'Adult'}>{age}</option>
|
<option value={age}>{age}</option>
|
||||||
{/each}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<input type="submit" value="Create Adventure" class="btn-submit" />
|
<input type="submit" value="Create Adventure" class="submit-button" />
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
||||||
|
|
@ -171,7 +171,7 @@
|
||||||
.spinner {
|
.spinner {
|
||||||
width: 1.25rem;
|
width: 1.25rem;
|
||||||
height: 1.25rem;
|
height: 1.25rem;
|
||||||
border: 2px solid var(--colour-outline-variant);
|
border: 2px solid var(--color-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;
|
||||||
|
|
|
||||||
|
|
@ -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(--colour-outline-variant) 30%, transparent);
|
border-top: 1px solid color-mix(in srgb, var(--color-outline-variant) 30%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-title {
|
.section-title {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue