Compare commits
8 commits
a8cd8d8060
...
48bbcac9a6
| Author | SHA1 | Date | |
|---|---|---|---|
| 48bbcac9a6 | |||
| 568b907013 | |||
| bcc12e3fad | |||
| 4c60a3ca91 | |||
| e075f2dc39 | |||
| ae709ac8f2 | |||
| d1243a1997 | |||
| 149c821959 |
16 changed files with 315 additions and 143 deletions
File diff suppressed because one or more lines are too long
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
|
||||||
4
frontend/src/app.d.ts
vendored
4
frontend/src/app.d.ts
vendored
|
|
@ -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 {}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
|
||||||
4
frontend/src/lib/randomItemInArray.ts
Normal file
4
frontend/src/lib/randomItemInArray.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
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(--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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
13
frontend/src/routes/app/adventures/+page.server.ts
Normal file
13
frontend/src/routes/app/adventures/+page.server.ts
Normal 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
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,92 @@
|
||||||
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 = [
|
||||||
|
'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 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 {
|
return {
|
||||||
lengths: ['100-200 Words', '200-350 Words', '350-500 Words', '500-750 Words'],
|
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'],
|
competencies: ['A1', 'A2', 'B1', 'B2', 'C1'],
|
||||||
languages: [
|
languages: [
|
||||||
{ code: 'fr', name: 'French' },
|
{ code: 'fr', name: 'French' },
|
||||||
|
|
@ -15,18 +95,8 @@ export const load: PageServerLoad = async () => {
|
||||||
{ code: 'es', name: 'Spanish' },
|
{ code: 'es', name: 'Spanish' },
|
||||||
{ code: 'pt', name: 'Portuguese' }
|
{ code: 'pt', name: 'Portuguese' }
|
||||||
],
|
],
|
||||||
genres: shuffleArray([
|
genres: allGenres.sort((a, b) => a.localeCompare(b)),
|
||||||
'Crime Fiction',
|
selectedGenre: randomItemInArray(allGenres),
|
||||||
'Crime noir',
|
|
||||||
'Who-dun-it mystery',
|
|
||||||
'Paranormal',
|
|
||||||
'Horror',
|
|
||||||
'Psychological thriller',
|
|
||||||
'Romance',
|
|
||||||
'Family',
|
|
||||||
'Fantasy',
|
|
||||||
'Science Fiction'
|
|
||||||
]),
|
|
||||||
eras: [
|
eras: [
|
||||||
'Ancient/Classical',
|
'Ancient/Classical',
|
||||||
'Medieval',
|
'Medieval',
|
||||||
|
|
@ -51,52 +121,10 @@ export const load: PageServerLoad = async () => {
|
||||||
'Space',
|
'Space',
|
||||||
'Another planet'
|
'Another planet'
|
||||||
],
|
],
|
||||||
vibes: [
|
allVibes: allVibes,
|
||||||
'Melancholic',
|
selectedVibes: shuffleArray(allVibes)
|
||||||
'Gothic',
|
.slice(0, 6)
|
||||||
'Sun-drenched',
|
.sort((a, b) => a.localeCompare(b))
|
||||||
'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'
|
|
||||||
]
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue