Compare commits

...

48 Commits

Author SHA1 Message Date
Yanick Champoux 2eda5a388e add attribution 2023-01-16 15:03:58 -05:00
Yanick Champoux 365610184a navigation 2023-01-16 13:53:22 -05:00
Yanick Champoux 48292a0ca5 Merge branch 'change-color' 2023-01-16 11:55:38 -05:00
Yanick Champoux c1e3708b87 change yellow for orange 2023-01-16 11:55:30 -05:00
Yanick Champoux 4657b83fd6 Merge branch 'transition' 2023-01-16 11:31:18 -05:00
Yanick Champoux d6f07a26e5 add transitions 2023-01-16 11:30:09 -05:00
Yanick Champoux 8779d8ae31 sky26: can load straight from battle 2023-01-16 10:20:54 -05:00
Yanick Champoux 1919ef3850 Merge branch 'chapter4' 2023-01-16 10:12:48 -05:00
Yanick Champoux e2dbe8364f esthetic work 2023-01-15 17:34:39 -05:00
Yanick Champoux bd5ba3ff7a esthetic work 2023-01-15 16:27:49 -05:00
Yanick Champoux 3e9ceddb48 chapter 4 ui 2023-01-15 15:05:59 -05:00
Yanick Champoux bf23443f3a chapter 4 groundwork 2023-01-15 14:40:32 -05:00
Yanick Champoux 931cb9d237 genChapter4 2023-01-15 14:22:14 -05:00
Yanick Champoux 621252329b Merge branch 'chapter3' 2023-01-15 13:54:50 -05:00
Yanick Champoux 85ad1bcaad chapter 3 2023-01-15 13:54:16 -05:00
Yanick Champoux 8d1ec92cfd add histoire task 2023-01-15 13:36:02 -05:00
Yanick Champoux 29c1d2dc26 Merge branch 'chapter2-battle-ui' 2023-01-14 17:14:21 -05:00
Yanick Champoux 05f522095f ui for chapter 2 2023-01-14 17:14:05 -05:00
Yanick Champoux 3284e94ad4 Merge branch 'sky19-chapter2-battles' 2023-01-14 16:05:06 -05:00
Yanick Champoux 0b8e293613 generate chapter 2 character selections 2023-01-14 16:04:48 -05:00
Yanick Champoux e223d64864 Merge branch 'can-go-straight-to-sub-page' 2023-01-14 15:28:42 -05:00
Yanick Champoux c3980723f4 eslint 2023-01-14 15:26:15 -05:00
Yanick Champoux 0544b91872 don't assume the variables will be filled 2023-01-14 15:21:09 -05:00
Yanick Champoux 6e2bb0ddab Merge branch 'sky15-second-wave' 2023-01-14 15:00:51 -05:00
Yanick Champoux 01c8430995 deal with second waves for battles 2023-01-14 15:00:13 -05:00
Yanick Champoux 929b833a49 Merge branch 'sky14-battle-0' 2023-01-14 14:32:53 -05:00
Yanick Champoux b4f12a2347 fix chapter battle nbr 2023-01-14 14:32:36 -05:00
Yanick Champoux 3cb78f03ba Merge branch 'sky13-title' 2023-01-14 13:47:33 -05:00
Yanick Champoux 2f27f76d87 sky13: html title 2023-01-14 13:47:18 -05:00
Yanick Champoux c990154bad Merge branch 'sky12-app-bar' 2023-01-14 13:45:12 -05:00
Yanick Champoux efd596410b pre-commit 2023-01-14 13:44:53 -05:00
Yanick Champoux 6bbba634c8 the apps 2023-01-14 13:25:46 -05:00
Yanick Champoux 59d4856904 Merge branch 'fix-tests' 2023-01-13 17:27:21 -05:00
Yanick Champoux ed3535943d fix tests 2023-01-13 17:27:08 -05:00
Yanick Champoux 834b317b2f Merge branch 'battle-result' 2023-01-13 12:51:05 -05:00
Yanick Champoux 7b7b1c5ad4 misc work 2023-01-13 12:51:02 -05:00
Yanick Champoux 80261bc524 Merge branch 'sky10-pure-svelte' 2023-01-13 12:46:58 -05:00
Yanick Champoux 2a32828964 sky10: switch to pure svelte 2023-01-13 12:46:27 -05:00
Yanick Champoux aac908afce Merge branch 'sky9-pick-first-battle' 2023-01-12 18:02:35 -05:00
Yanick Champoux e96a022c8f genNextBattle 2023-01-12 18:02:29 -05:00
Yanick Champoux 41c6cae377 Merge branch 'sky6-store' 2023-01-12 16:42:45 -05:00
Yanick Champoux ef0da12a04 add the api store 2023-01-12 16:42:34 -05:00
Yanick Champoux db8bb6ccc0 add the api store 2023-01-12 14:20:54 -05:00
Yanick Champoux 286b5b9306 Merge branch 'sky5-main-page' 2023-01-12 11:14:12 -05:00
Yanick Champoux 5fba44a85c Add main campaigns component 2023-01-12 11:13:36 -05:00
Yanick Champoux 98a6ce6361 boilerplate files 2023-01-11 17:25:39 -05:00
Yanick Champoux 552df41139 Merge branch 'sky2-prettier' 2023-01-11 16:46:02 -05:00
Yanick Champoux 19f2bb9177 add prettier to the project 2023-01-11 16:45:41 -05:00
45 changed files with 1919 additions and 0 deletions

15
.eslintignore Normal file
View File

@ -0,0 +1,15 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
# Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml
package-lock.json
yarn.lock
Taskfile.yaml
.eslintignore

15
.eslintrc.cjs Normal file
View File

@ -0,0 +1,15 @@
module.exports = {
root: true,
extends: ['eslint:recommended', 'prettier'],
plugins: ['svelte3'],
overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }],
parserOptions: {
sourceType: 'module',
ecmaVersion: 2020
},
env: {
browser: true,
es2017: true,
node: true
}
};

12
.gitignore vendored Normal file
View File

@ -0,0 +1,12 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
pnpm-lock.yaml
.npmrc

14
.pre-commit-config.yaml Normal file
View File

@ -0,0 +1,14 @@
#exclude: static/fontawesome|^build
default_stages:
- merge-commit
- push
repos:
- repo: local
hooks:
- id: lint
name: lint
entry: task lint:fix --
language: system
files: ''

13
.prettierignore Normal file
View File

@ -0,0 +1,13 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
# Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml
package-lock.json
yarn.lock

12
.prettierrc.cjs Normal file
View File

@ -0,0 +1,12 @@
module.exports = {
semi: true,
trailingComma: 'all',
singleQuote: true,
printWidth: 80,
tabWidth: 4,
useTabs: true,
plugins: ['prettier-plugin-svelte'],
svelteSortOrder: 'options-markup-scripts-styles',
svelteStrictMode: false,
svelteAllowShorthand: true,
};

38
README.md Normal file
View File

@ -0,0 +1,38 @@
# create-svelte
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte).
## Creating a project
If you're seeing this, you've probably already done this step. Congrats!
```bash
# create a new project in the current directory
npm create svelte@latest
# create a new project in my-app
npm create svelte@latest my-app
```
## Developing
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
```bash
npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
```
## Building
To create a production version of your app:
```bash
npm run build
```
You can preview the production build with `npm run preview`.
> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.

30
Taskfile.yaml Normal file
View File

@ -0,0 +1,30 @@
# https://taskfile.dev
version: '3'
vars:
GREETING: Hello, World!
FILES:
sh: git diff-ls --diff-filter=d main | grep -v .eslintignore
tasks:
build: vite build
dev: vite
test: vitest run src
test:watch: vitest src
story: histoire dev
default:
cmds:
- echo "{{.GREETING}}"
silent: true
lint:fix:
cmds:
- npx prettier --write {{.CLI_ARGS | default .FILES | catLines }}
- npx eslint --fix --quiet {{.CLI_ARGS | default .FILES | catLines }}
integrate:
cmds:
- git is-clean
- task: lint:fix
- task: test
- git co main
- git weld -

7
histoire.config.js Normal file
View File

@ -0,0 +1,7 @@
import { defineConfig } from 'histoire';
import { HstSvelte } from '@histoire/plugin-svelte';
export default defineConfig({
setupFile: '/src/histoire.setup.js',
plugins: [HstSvelte()],
});

16
index.html Normal file
View File

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Under Falling Skies</title>
<script>
var global = global === undefined ? window : global;
</script>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

17
jsconfig.json Normal file
View File

@ -0,0 +1,17 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": false
}
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias and https://kit.svelte.dev/docs/configuration#files
//
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
// from the referenced tsconfig.json - TypeScript does not merge them in
}

49
package.json Normal file
View File

@ -0,0 +1,49 @@
{
"name": "under-falling-skies",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch",
"test": "playwright test",
"test:unit": "vitest",
"lint": "prettier --plugin-search-dir . --check . && eslint .",
"format": "prettier --plugin-search-dir . --write ."
},
"devDependencies": {
"@histoire/plugin-svelte": "^0.12.4",
"@playwright/test": "^1.28.1",
"@sveltejs/adapter-auto": "^1.0.0",
"@sveltejs/kit": "^1.0.0",
"eslint": "^8.28.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-svelte3": "^4.0.0",
"histoire": "^0.12.4",
"prettier": "^2.8.2",
"prettier-plugin-svelte": "^2.8.1",
"svelte": "^3.54.0",
"svelte-check": "^2.9.2",
"vite": "^4.0.0",
"vitest": "^0.25.8"
},
"type": "module",
"dependencies": {
"@ernane/svelte-star-rating": "^1.1.2",
"@sveltejs/vite-plugin-svelte": "^2.0.2",
"@testing-library/svelte": "^3.2.2",
"@yanick/updeep-remeda": "^2.1.0",
"beercss": "^3.0.4",
"dexie": "^3.2.2",
"events": "^3.3.0",
"fake-indexeddb": "^4.0.1",
"material-dynamic-colors": "^0.1.5",
"pouchdb": "^8.0.0",
"pouchdb-adapter-memory": "^8.0.0",
"pouchdb-browser": "^8.0.0",
"remeda": "^1.3.0",
"svelte-spa-router": "^3.3.0"
}
}

10
playwright.config.js Normal file
View File

@ -0,0 +1,10 @@
/** @type {import('@playwright/test').PlaywrightTestConfig} */
const config = {
webServer: {
command: 'npm run build && npm run preview',
port: 4173
},
testDir: 'tests'
};
export default config;

9
src/app.d.ts vendored Normal file
View File

@ -0,0 +1,9 @@
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
// and what to do when importing types
declare namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface Platform {}
}

12
src/app.html Normal file
View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

1
src/histoire.setup.js Normal file
View File

@ -0,0 +1 @@
//global?.__SVELTEKIT_APP_VERSION_POLL_INTERVAL__ = 1000;

7
src/index.test.js Normal file
View File

@ -0,0 +1,7 @@
import { describe, it, expect } from 'vitest';
describe('sum test', () => {
it('adds 1 + 2 to equal 3', () => {
expect(1 + 2).toBe(3);
});
});

View File

@ -0,0 +1,46 @@
<Beer />
<main class="center">
<Router {routes} />
</main>
<script>
import { genApi } from '$lib/store/api.js';
import { writable } from 'svelte/store';
import { setContext } from 'svelte';
import Beer from '$lib/components/Beer.svelte';
import Router from 'svelte-spa-router';
import Campaigns from '$lib/components/Campaigns.svelte';
import Campaign from '$lib/components/Campaign.svelte';
import Battle from '$lib/components/Battle.svelte';
import { push } from 'svelte-spa-router';
const api = genApi();
setContext('api', api);
const transition = writable('');
transition.goRight = (uri) => {
transition.set('right');
push(uri);
};
transition.goLeft = (uri) => {
transition.set('left');
push(uri);
};
setContext('transition', transition);
const routes = {
// Exact path
'/': Campaigns,
'/campaign/:campaignId': Campaign,
'/campaign/:campaignId/battle/:battleId': Battle,
};
</script>
<style>
main {
max-width: 720px;
height: 100vh;
}
</style>

View File

@ -0,0 +1,25 @@
<Beer />
<Hst.Story>
<Battle {status} params={{ battleId: 1 }} />
<svelte:fragment slot="controls">
<Hst.Select
bind:value={status}
title="status"
options={Object.fromEntries(statuses.map((s) => [s, s]))}
/>
<Hst.Number bind:value={wave} title="wave" />
</svelte:fragment>
</Hst.Story>
<script>
/** @type any */
export let Hst;
import Beer from './Beer.svelte';
import Battle from './Battle.svelte';
let status = 'upcoming';
let wave = '1';
let statuses = ['upcoming', 'prep', 'ongoing', 'won', 'lost'];
</script>

View File

@ -0,0 +1,325 @@
{#if !$activeCampaign}
<div class="fill medium-height middle-align center-align">
<div class="center-align">
<h5>Loading...</h5>
<div class="space">
<a class="loader large" />
</div>
</div>
</div>
{:else}
<article class="scroll">
<header class="fixed fill">
<nav>
<a href="#/">
<button class="circle transparent">
<i>menu</i>
</button></a
>
<h5 class="max right-align">
{#if $activeCampaign}
<a href={`#/campaign/${$activeCampaign._id}`}>
{$activeCampaign.name}
</a>
{/if}
</h5>
</nav>
</header>
{#key params.battleId}
<div in:fly={flyParams} class="body">
<h6>
<span
>Chapter {chapter},
<span class="battleNbr">battle {chapterBattle}</span
></span
>
<span class="status">{status}</span>
</h6>
<dl>
{#if status === 'ongoing' && chapter !== 4}
<dt>wave</dt>
<dd>{wave === 2 ? 'second' : 'first'}</dd>
{/if}
<dt><i>home</i></dt>
<dd>
{#if typeof city === 'string'}
{city}
{:else}
<AdditionalCharacter
on:change={changeCity}
selection={city?.selection}
choices={status !== 'ongoing'
? []
: city?.choices?.filter(
(c) => c !== city.selection,
)}
/>
{/if}
</dd>
<dt><i>view_kanban</i></dt>
<dd>{scenario}</dd>
{#if character}
<dt><i>person</i></dt>
<dd>{character}</dd>
{/if}
{#if additionalCharacters}
{#each additionalCharacters as c, i (c.selection)}
<dt><i>person</i><i>star</i></dt>
<dd>
<AdditionalCharacter
on:change={changeCharacter(i)}
selection={c.selection}
choices={minusSelected(
status === 'ongoing' && wave == 1
? c.choices
: [],
)}
/>
</dd>
{/each}
{/if}
<dt>difficulty</dt>
<dd>
<div class="field ">
<input
type="number"
step="0.5"
value={difficulty}
on:change={({ target: { value } }) =>
event.setBattleDifficulty(
params.battleId,
value,
)}
/>
</div>
<!--
<Stars
on:change={(e) => (difficulty = e.target.value)}
config={{
readOnly: false, //status !== 'preparation',
countStars: 5,
score: difficulty,
showScore: false,
starConfig: {
size: 15,
fillColor: '#F9ED4F',
strokeColor: '#BB8511',
unfilledColor: '#FFF',
strokeUnfilledColor: '#000',
},
range: { min: 0, max: 5, step: 0.5 },
}}
/>
{difficulty}
-->
</dd>
</dl>
<div class="space" />
<div class="actions">
{#if status === 'upcoming'}
<button>Start battle</button>
{:else if status === 'ongoing'}
<button on:click={battleVerdict('won')}
>Battle won</button
>
<button
class="tertiary"
on:click={battleVerdict('lost')}>Battle lost</button
>
{/if}
</div>
<div class="space" />
</div>
{/key}
<footer class="absolute center bottom">
<nav>
<a
on:click={() =>
transition.goLeft(
`#/campaign/${$activeCampaign?._id}` +
(battle?.id == 1
? ''
: `/battle/${parseInt(battle?.id) - 1}`),
)}
>
<button class="circle transparent">
<i>arrow_backward</i>
</button>
</a>
<a href={`#/campaign/${$activeCampaign?._id}`}>
<button class="circle transparent">
<i>arrow_upward</i>
</button>
</a>
<button
on:click={() =>
transition.goRight(
`/campaign/${$activeCampaign?._id}/battle/${
battle?.id + 1
}`,
)}
disabled={battles?.length <= battle?.id}
class="circle transparent"
>
<i>arrow_forward</i>
</button>
</nav>
</footer>
</article>
{/if}
<script>
import { push } from 'svelte-spa-router';
import * as R from 'remeda';
import { getContext } from 'svelte';
import AdditionalCharacter from './Battle/AdditionalCharacter.svelte';
import { fly } from 'svelte/transition';
export let params;
export let api = getContext('api');
export let activeCampaign = api?.activeCampaign;
export let event = api?.event;
event?.setActiveCampaign(params.campaignId);
$: battles = $activeCampaign?.battles ?? [];
$: battle = battles && battles[params.battleId - 1];
$: scenario = battle?.scenario;
$: status = battle?.status;
$: wave = battle?.wave;
$: difficulty = battle?.difficulty ?? 0;
$: character = battle?.character;
$: city = battle?.city;
$: additionalCharacters = battle?.additionalCharacters;
$: console.log(additionalCharacters);
$: minusSelected =
additionalCharacters &&
R.difference(additionalCharacters.map(R.prop('selection')).flat());
$: chapter = 1 + parseInt((params.battleId - 1) / 2);
$: chapterBattle =
params.battleId >= 7
? params.battleId - 6
: 1 + ((params.battleId - 1) % 2);
const battleVerdict = (verdict) => () => {
event?.setBattleVerdict(params.battleId, verdict);
if (verdict === 'lost') {
if (chapter === 4) {
push('/campaign/' + $activeCampaign._id);
} else if (wave === 2) {
push(
'/campaign/' +
$activeCampaign._id +
'/battle/' +
(parseInt(params.battleId) + 1),
);
}
} else {
if (chapter === 4) {
push('/campaign/' + $activeCampaign._id);
} else {
push(
'/campaign/' +
$activeCampaign._id +
'/battle/' +
(parseInt(params.battleId) + 1),
);
}
}
};
// $: event.setBattleDifficulty(params.battleId, difficulty);
const changeCharacter =
(index) =>
({ target: { value } }) => {
event?.setCharacter(params?.battleId, index, value);
};
const changeCity = ({ target: { value } }) => {
event?.setCity(params.battleId, value);
};
const transition = getContext('transition');
let flyParams = { x: 200, duration: 0 };
$: if ($transition) {
flyParams.duration = 200;
flyParams.x = $transition === 'right' ? 200 : -200;
transition.set('');
} else {
flyParams.duration = 0;
}
</script>
<style>
article {
height: 100vh;
min-height: 100vh;
}
h5 {
flex: 1;
}
h6 {
display: flex;
margin-bottom: 1em;
}
h6 .status {
flex: 1;
text-align: right;
padding-right: 1em;
}
dl {
width: 100%;
display: grid;
grid-template-columns: 8em auto;
grid-row-gap: 0.75em;
font-size: 1.25rem;
}
dt {
text-align: right;
padding-right: 2em;
}
dd {
display: flex;
}
article :global(.stars-container) {
justify-content: left !important;
margin-right: 1em;
}
input {
width: 5em;
}
footer {
width: 100%;
padding: 0px 1em;
}
footer nav {
display: flex;
justify-content: space-between;
}
nav h5 {
padding-right: 1em;
}
header nav {
max-width: 100%;
}
.actions {
text-align: center;
}
</style>

View File

@ -0,0 +1,28 @@
{#if choices.length === 0}
{selection}
{:else}
<div class="field suffix">
<select on:change>
<option>{selection}</option>
{#each choices as c (c)}
<option>{c}</option>
{/each}
</select>
<i>arrow_drop_down</i>
</div>
{/if}
<script>
export let selection;
export let choices;
$: console.log(choices);
</script>
<style>
select {
/*width: 90%;*/
}
.field {
margin-bottom: 0px;
}
</style>

View File

@ -0,0 +1,50 @@
<Beer />
<Hst.Story>
<Battle {status} params={{ battleId: 3 }} {api} />
<svelte:fragment slot="controls">
<Hst.Select
bind:value={status}
title="status"
options={Object.fromEntries(statuses.map((s) => [s, s]))}
/>
<Hst.Number bind:value={wave} title="wave" />
</svelte:fragment>
</Hst.Story>
<script>
/** @type any */
export let Hst;
import { logEvent } from 'histoire/client';
import { readable } from 'svelte/store';
import Beer from '../Beer.svelte';
import Battle from '../Battle.svelte';
let status = 'upcoming';
let wave = '1';
const api = {
event: {
setCharacter: (...e) => logEvent('setCharacter', e),
setActiveCampaign: () => {},
},
activeCampaign: readable({
battles: [
null,
null,
{
wave,
status: 'ongoing',
scenario: 's',
city: 'c',
character: 'bob',
additionalCharacters: [
{ choices: ['one', 'two'], selection: 'one' },
],
},
],
}),
};
let statuses = ['upcoming', 'prep', 'ongoing', 'won', 'lost'];
</script>

View File

@ -0,0 +1,53 @@
<Beer />
<Hst.Story>
<Battle {status} params={{ battleId: 5 }} {api} />
<svelte:fragment slot="controls">
<Hst.Select
bind:value={status}
title="status"
options={Object.fromEntries(statuses.map((s) => [s, s]))}
/>
<Hst.Number bind:value={wave} title="wave" />
</svelte:fragment>
</Hst.Story>
<script>
/** @type any */
export let Hst;
import { logEvent } from 'histoire/client';
import { readable } from 'svelte/store';
import Beer from '../Beer.svelte';
import Battle from '../Battle.svelte';
let status = 'upcoming';
let wave = '1';
const api = {
event: {
setCharacter: (...e) => logEvent('setCharacter', e),
setActiveCampaign: () => {},
},
activeCampaign: readable({
battles: [
null,
null,
null,
null,
{
wave,
status: 'ongoing',
scenario: 's',
city: 'c',
character: 'bob',
additionalCharacters: [
{ choices: ['one', 'two'], selection: 'one' },
{ choices: ['three', 'four'], selection: 'three' },
],
},
],
}),
};
let statuses = ['upcoming', 'prep', 'ongoing', 'won', 'lost'];
</script>

View File

@ -0,0 +1,59 @@
<Beer />
<Hst.Story>
<Battle {status} params={{ battleId: 7 }} {api} />
<svelte:fragment slot="controls">
<Hst.Select
bind:value={status}
title="status"
options={Object.fromEntries(statuses.map((s) => [s, s]))}
/>
<Hst.Number bind:value={wave} title="wave" />
</svelte:fragment>
</Hst.Story>
<script>
/** @type any */
export let Hst;
import { logEvent } from 'histoire/client';
import { readable } from 'svelte/store';
import Beer from '../Beer.svelte';
import Battle from '../Battle.svelte';
let status = 'upcoming';
let wave = '1';
const choices = ['one', 'two', 'three', 'four'];
const api = {
event: {
setCharacter: (...e) => logEvent('setCharacter', e),
setActiveCampaign: () => {},
},
activeCampaign: readable({
battles: [
null,
null,
null,
null,
null,
null,
{
status: 'ongoing',
wave: 1,
scenario: 'The Last Battle',
city: {
choices: ['a', 'b'],
selection: 'a',
},
additionalCharacters: [
{ choices, selection: 'one' },
{ choices, selection: 'three' },
{ choices, selection: 'two' },
],
},
],
}),
};
let statuses = ['upcoming', 'prep', 'ongoing', 'won', 'lost'];
</script>

View File

@ -0,0 +1,31 @@
import { test, expect, afterEach } from 'vitest';
import { render, cleanup } from '@testing-library/svelte';
import Battle from '../Battle.svelte';
import { readable } from 'svelte/store';
const activeCampaign = readable({
battles: Array(12)
.fill(null)
.map((_, i) => ({
id: i + 1,
})),
});
afterEach(cleanup);
test.each([
[2, 2],
[3, 1],
[7, 1],
[8, 2],
[9, 3],
])('chapter battle (%i,%i))', (battleId, expected) => {
const { container } = render(Battle, {
params: { battleId },
activeCampaign,
});
expect(container.querySelector('.battleNbr').innerHTML).toMatch(
'battle ' + expected,
);
});

View File

@ -0,0 +1,11 @@
<script>
import "beercss/dist/cdn/beer.min.css";
import "beercss/dist/cdn/material-symbols-outlined.woff2";
import "beercss/dist/cdn/roboto-flex-cyrillic-ext.woff2";
import "beercss/dist/cdn/roboto-flex-cyrillic.woff2";
import "beercss/dist/cdn/roboto-flex-greek.woff2";
import "beercss/dist/cdn/roboto-flex-latin-ext.woff2";
import "beercss/dist/cdn/roboto-flex-latin.woff2";
import "beercss/dist/cdn/roboto-flex-vietnamese.woff2";
</script>

View File

@ -0,0 +1,190 @@
<article class="scroll">
<header class="fixed fill">
<nav>
<a href="#/">
<button class="circle transparent">
<i>menu</i>
</button></a
>
<h5 class="max right-align">
{#if $activeCampaign}
{$activeCampaign.name}
{/if}
</h5>
</nav>
</header>
{#key params.campaignId}
<div in:fly={flyParams} class="body">
{#if $activeCampaign}
{#if status === 'won'}
<h4 class="blue-text">
{status.toUpperCase()}
&nbsp; &nbsp;
{score}
<i>star</i>
</h4>
{:else if status === 'lost'}
<h4 class="red-text">
{status.toUpperCase()}
</h4>
{:else}
<div class="score">
score {#if status !== 'ongoing'}so far{/if}: {score}
<i>star</i>
</div>
{/if}
<h6>Chapter 1</h6>
<Battle
{...battles[0]}
{campaignId}
chapter={1}
chapterBattle={1}
/>
<div class="medium-divider" />
<Battle
{...battles[1] ?? { id: 'notYet' }}
{campaignId}
chapter={1}
chapterBattle={2}
/>
<div class="medium-divider" {campaignId} />
<h6>Chapter 2</h6>
<Battle
{...battles[2] ?? { id: 'notYet' }}
{campaignId}
chapter={2}
chapterBattle={1}
/>
<div class="medium-divider" />
<Battle
{...battles[3] ?? { id: 'notYet' }}
{campaignId}
chapter={2}
chapterBattle={2}
/>
<div class="medium-divider" />
<h6>Chapter 3</h6>
<Battle
{...battles[4] ?? { id: 'notYet' }}
{campaignId}
chapter={3}
chapterBattle={1}
/>
<div class="medium-divider" />
<Battle
{...battles[5] ?? { id: 'notYet' }}
{campaignId}
chapter={3}
chapterBattle={2}
/>
<div class="medium-divider" />
<h6>Chapter 4</h6>
<Battle
{...battles[6] ?? { id: 'notYet' }}
{campaignId}
chapter={4}
chapterBattle={1}
/>
<div class="medium-divider" />
<Battle
{...battles[7] ?? { id: 'notYet' }}
{campaignId}
chapter={4}
chapterBattle={2}
/>
<div class="medium-divider" />
<Battle
{...battles[8] ?? { id: 'notYet' }}
{campaignId}
chapter={4}
chapterBattle={3}
/>
<div class="medium-divider" />
<Battle
{...battles[9] ?? { id: 'notYet' }}
{campaignId}
chapter={4}
chapterBattle={4}
/>
<div class="medium-divider" />
<Battle
{...battles[10] ?? { id: 'notYet' }}
{campaignId}
chapter={4}
chapterBattle={5}
/>
<div class="medium-divider" />
<Battle
{...battles[11] ?? { id: 'notYet' }}
{campaignId}
chapter={4}
chapterBattle={6}
/>
<div class="medium-divider" />
{/if}
</div>{/key}
<footer class="fixed">
<nav>
<a href={`#/campaign/${$activeCampaign?._id}/battle/1`}>
<button class="circle transparent">
<i>arrow_forward</i>
</button>
</a>
</nav>
</footer>
</article>
<script>
import * as R from 'remeda';
import Battle from './Campaign/Battle.svelte';
import { fly } from 'svelte/transition';
import { getContext } from 'svelte';
export let params;
const api = getContext('api');
api.event.setActiveCampaign(params.campaignId);
const { activeCampaign } = getContext('api');
$: battles = $activeCampaign?.battles ?? [];
const campaignId = params.campaignId;
$: score = $activeCampaign?.score;
$: status = $activeCampaign?.status;
$: byChapter = R.pipe(
$activeCampaign?.battles ?? [],
R.groupBy(({ id }) => R.clamp(1 + parseInt(id / 2), { max: 4 })),
);
let flyParams = { x: 200, duration: 200 };
</script>
<style>
.score {
text-align: right;
}
article {
height: 100vh;
}
footer {
display: flex;
justify-content: end;
}
footer nav {
text-align: right;
}
nav h5 {
padding-right: 1em;
}
h4 {
text-align: center;
width: 100%;
display: inline-block;
}
</style>

View File

@ -0,0 +1,48 @@
<div class="row">
<p>
{#if id == 'notYet'}
???
{:else}
<a href={`#/campaign/${campaignId}/battle/${id}`}>
Battle of {typeof city === 'string' ? city : city.selection}</a
>
<div>
{#if status === 'ongoing' && wave == 2}
<i class="orange-text">mood_bad</i>
{:else if status === 'ongoing' && wave == 1}
<i style="color: blue">sentiment_neutral</i>
{:else if status === 'lost'}
<i style="color: red">sentiment_very_dissatisfied</i>
{:else if status === 'won'}
<i style="color: green">mood</i>
{/if}
</div>
<div class="difficulty">{difficulty} <i>star</i></div>
{/if}
</p>
</div>
<script>
export let city = '';
export let status = '';
export let id = 0;
export let wave = 1;
export let campaignId;
export let chapter;
export let chapterBattle;
export let difficulty = 0;
</script>
<style>
.row p {
display: flex;
width: 100%;
}
.row p a {
display: inline-block;
flex: 1;
}
.difficulty {
margin-left: 1em;
}
</style>

View File

@ -0,0 +1,25 @@
<Beer />
<Hst.Story>
<Campaigns {campaigns} />
</Hst.Story>
<script>
import { setContext } from 'svelte';
export let Hst;
import Beer from './Beer.svelte';
import Campaigns from './Campaigns.svelte';
import { genApi } from '$lib/store/api.js';
global.__SVELTEKIT_APP_VERSION_POLL_INTERVAL__ = 1000;
const campaigns = [{ name: 'Earth 613' }];
setContext(
'api',
genApi({
local: campaigns,
}),
);
</script>

View File

@ -0,0 +1,125 @@
<header class="fixed fill">
<nav>
<h5 class="max center-align">Campaigns</h5>
<button class="circle small" on:click={newCampaign}>
<i>add</i>
</button>
</nav>
</header>
<article>
{#each $campaigns as campaign (campaign._id)}
<div class="row">
<i class="light-green-text">south_america</i>
<div class="max">
<h6>
<a href={`#/campaign/${campaign._id}`}>
{campaign.name}
</a>
</h6>
<p>
{#if campaign.battles}
<a href={`#/campaign/${campaign._id}`}>
chapter {currentChapter(campaign)}, battle of {currentCity(
campaign,
)}.
</a>
{/if}
</p>
</div>
<button on:click={deleteCampaign(campaign)} class="none"
>Delete</button
>
</div>
{/each}
<footer class="bottom absolute center">
fan-made campaign manager for <a
href="https://czechgames.com/en/under-falling-skies/"
>Under Falling Skies</a
><br /> written by
<a href="mailto:yanick@sky.babyl.ca">Yanick Champoux</a>
</footer>
</article>
{#if showNewCampaign}
<div class="modal active">
<form>
<h5>New campaign</h5>
<div>
<div class="field border">
<input
autofocus
type="text"
bind:value={newCampaignName}
placeholder="Campaign name"
/>
</div>
</div>
<nav class="right-align">
<button
class="border"
on:click={() => (showNewCampaign = false)}>Cancel</button
>
<button on:click={saveNewCampaign}>Create</button>
</nav>
</form>
</div>
{/if}
<script>
import * as R from 'remeda';
import { getContext } from 'svelte';
// import { goto } from '$app/navigation';
const goto = () => {};
export let api = getContext('api');
export let campaigns = api?.campaigns;
let showNewCampaign = false;
let newCampaignName = '';
const currentChapter = ({ battles }) =>
R.clamp(1 + parseInt(battles?.length / 2), { max: 4 });
const currentCity = ({ battles }) => {
const c = R.last(battles)?.city;
return typeof c === 'string' ? c : c?.selection;
};
async function deleteCampaign({ name, _id }) {
if (!window.confirm(`delete campaign ${name}?`)) return;
api.event.deleteCampaign(_id);
}
const newCampaign = () => {
showNewCampaign = true;
};
const saveNewCampaign = () => {
console.log('sent');
api.event.addCampaign(newCampaignName);
newCampaignName = '';
showNewCampaign = false;
};
</script>
<style>
nav {
width: 100%;
}
header {
display: flex;
}
h5 {
flex: 1;
}
article {
height: 90vh;
}
footer {
width: 90%;
text-align: center;
}
footer a {
text-decoration: underline;
}
</style>

199
src/lib/store/api.js Normal file
View File

@ -0,0 +1,199 @@
import * as R from 'remeda';
import PouchDB from 'pouchdb';
import { writable, derived, get } from 'svelte/store';
import { genNextBattle } from './genNextBattle.js';
import u from '@yanick/updeep-remeda';
const seedCampaign = {
status: 'ongoing', // win, lost
score: 0,
battles: [],
};
function campaignStatus(campaign) {
if (campaign.battles.length <= 6) return 'ongoing';
if (R.last(campaign.battles).status === 'won') return 'won';
if (
campaign.battles.length - 6 >=
campaign.battles.slice(0, 6).filter(({ status }) => status === 'won')
)
return 'lost';
return 'ongoing';
}
function calculateScore(campaign) {
const values = [
-campaign.battles.slice(0, 6).filter(({ wave }) => wave === 2).length,
-campaign.battles.slice(6).filter(({ status }) => status === 'lost')
.length,
...campaign.battles
.map(({ difficulty }) => parseInt(difficulty))
.map((x) => (Number.isNaN(x) ? 0 : x)),
];
console.log(values);
return values.reduce((a, b) => a + b, 0);
}
export function updateBattle(campaign, battleId, status) {
const battle = campaign.battles[battleId - 1];
if (status === 'lost' && battle.wave == 1 && battle.id < 7) {
campaign = u.updateIn(campaign, `battles.${battleId - 1}`, {
wave: 2,
});
campaign = u.updateIn(campaign, 'score', calculateScore(campaign));
return campaign;
}
campaign = u.updateIn(campaign, `battles.${battleId - 1}`, {
status,
});
campaign = u.updateIn(campaign, 'score', calculateScore(campaign));
console.log(campaign);
campaign = u(campaign, {
status: campaignStatus(campaign),
});
if (campaign.status === 'ongoing') {
campaign = u(campaign, {
battles: (battles) => [
...battles,
u(genNextBattle(battles), {
id: battles.length + 1,
}),
],
});
console.log('updated', campaign);
}
return campaign;
}
export function genApi(options = {}) {
// if (options.local) MyPouch.plugin(pouchMem);
if (options.pouch) options.pouch(PouchDB);
const pouchdb = new PouchDB(
'Campaigns',
options.local
? {
adapter: 'memory',
}
: {},
);
const campaignsCore = writable([]);
pouchdb
.allDocs({ include_docs: true })
.then((r) => r.rows.map(({ id, doc }) => [id, doc]))
.then(Object.fromEntries)
.then(campaignsCore.set);
pouchdb
.changes({ since: 'now', live: true, include_docs: true })
.on('change', (change) => {
if (change.deleted) {
campaignsCore.update(u.updateIn(change.id, u.skip));
} else {
campaignsCore.update(u.updateIn(change.id, change.doc));
}
});
const campaigns = derived(campaignsCore, Object.values);
const addCampaign = (name) =>
pouchdb.post({
name,
...seedCampaign,
battles: [{ id: 1, ...genNextBattle() }],
});
const activeCampaignId = writable(0);
const setActiveCampaign = activeCampaignId.set;
const activeCampaign = derived(
[campaignsCore, activeCampaignId],
([$c, $id]) => $c[$id],
);
const setBattleDifficulty = (battleId, difficulty) => {
pouchdb
.put(
u.updateIn(get(activeCampaign), `battles.${battleId - 1}`, {
difficulty,
}),
)
.catch((e) => console.error(e));
};
const setBattleVerdict = (battleId, status) => {
const campaign = updateBattle(get(activeCampaign), battleId, status);
pouchdb.put(campaign).catch((e) => console.error(e));
};
const deleteCampaign = (_id) =>
pouchdb.remove(get(campaigns).find(u.matches({ _id })));
const setCharacter = (battleId, characterId, value) => {
const campaign = u.updateIn(
get(activeCampaign),
['battles', battleId, 'additionalCharacters', 'selection'],
value,
);
pouchdb.put(campaign);
};
const setCity = (battleId, value) => {
const campaign = u.updateIn(
get(activeCampaign),
['battles', battleId, 'city', 'selection'],
value,
);
pouchdb.put(campaign);
};
return {
campaigns,
activeCampaign,
event: {
setCity,
setCharacter,
deleteCampaign,
addCampaign,
setActiveCampaign,
setBattleVerdict,
setBattleDifficulty,
},
};
/*
const activeCampaign = writeable();
addCampaign: (name) =>
storage.campaigns.add({
name,
...seedCampaign,
battles: [{ id: 1, ...genNextBattle() }],
}),
return {
campaigns,
event: {
,
addCampaign: (name) =>
storage.campaigns.add({
name,
...seedCampaign,
battles: [{ id: 1, ...genNextBattle() }],
}),
deleteCampaign: (id) => storage.campaigns.delete(id),
},
};
*/
}

37
src/lib/store/api.test.js Normal file
View File

@ -0,0 +1,37 @@
import { test, expect } from 'vitest';
import { get } from 'svelte/store';
import { genApi } from './api.js';
import adapter from 'pouchdb-adapter-memory';
const waitUntil = (store, condition) => {
return new Promise((resolve) => {
store.subscribe((v) => condition(v) && resolve(v));
});
};
test('create and add and remove campaigns', async () => {
const api = genApi({
local: true,
pouch: (p) => p.plugin(adapter),
});
let result = waitUntil(api.campaigns, (r) => r.length === 2);
api.event.addCampaign('C1');
api.event.addCampaign('C2');
await result;
const [c] = get(api.campaigns);
expect(c).toMatchObject({
name: 'C1',
});
const r = waitUntil(api.campaigns, (r) => r.length === 1);
api.event.deleteCampaign(c._id);
await expect(r).resolves.toBeTruthy();
});

40
src/lib/store/chapters.js Normal file
View File

@ -0,0 +1,40 @@
export default [
{
cities: ['Havana', 'Montreal', 'Mexico City', 'Rio de Janeiro'],
characters: [
'Clinton Harper',
'Jackson Moss',
'Lucia Ortego',
'Samantha Legrand',
],
scenarios: [
'Evacuation',
'Reinforcements',
'Battle for the sky',
'Satellites',
],
},
{
cities: ['Johannesburg', 'Moscow', 'Cairo', 'London', 'Paris'],
characters: [
'Jaroslav Ruzicka',
'Karima Almasi',
'Shanti Aumann',
'Pieter Bernstein',
],
scenarios: [
'Command ship',
'Reactor leak',
'Dangerous research',
'Repairing the base',
],
},
{
cities: ['Seoul', 'Beijing', 'Tokyo', 'Sydney', 'Singapore'],
characters: ['Wang Lin', "Iz'ox", 'Jang Chanwook', 'Archie Bell'],
scenarios: ['Storm', 'Contamination', 'Kamikaze', 'Saboteur'],
},
{
scenarios: ['The final battle'],
},
];

View File

@ -0,0 +1,142 @@
import * as R from 'remeda';
import chapters from './chapters.js';
const pickOne = (choices) => choices[parseInt(Math.random() * choices.length)];
function genChapter1Battle(battles) {
const chapter = 1;
const scenario = pickOne(
chapters[chapter - 1].scenarios.filter(
(s) => !battles.map(R.prop('scenario')).includes(s),
),
);
const character = pickOne(
chapters[chapter - 1].characters.filter(
(s) => !battles.map(R.prop('character')).includes(s),
),
);
const city = pickOne(
chapters[chapter - 1].cities.filter(
(s) => !battles.map(R.prop('city')).includes(s),
),
);
return { scenario, character, city, status: 'ongoing', wave: 1 };
}
export function genChapter2Battle(battles) {
const chapter = 2;
const scenario = pickOne(
chapters[chapter - 1].scenarios.filter(
(s) => !battles.map(R.prop('scenario')).includes(s),
),
);
const character = pickOne(
chapters[chapter - 1].characters.filter(
(s) => !battles.map(R.prop('character')).includes(s),
),
);
const city = pickOne(
chapters[chapter - 1].cities.filter(
(s) => !battles.map(R.prop('city')).includes(s),
),
);
let choices = battles.slice(0, 2).map(R.prop('character'));
if (battles.length == 3)
choices = choices.filter(
(n) => n !== R.last(battles).additionalCharacters[0].selection,
);
return {
scenario,
character,
city,
status: 'ongoing',
wave: 1,
additionalCharacters: [{ choices, selection: R.first(choices) }],
};
}
export function genChapter3Battle(battles) {
const chapter = 3;
const scenario = pickOne(
chapters[chapter - 1].scenarios.filter(
(s) => !battles.map(R.prop('scenario')).includes(s),
),
);
const character = pickOne(
chapters[chapter - 1].characters.filter(
(s) => !battles.map(R.prop('character')).includes(s),
),
);
const city = pickOne(
chapters[chapter - 1].cities.filter(
(s) => !battles.map(R.prop('city')).includes(s),
),
);
let choices1 = battles.slice(0, 2).map(R.prop('character'));
let choices2 = battles.slice(2, 4).map(R.prop('character'));
if (battles.length == 5) {
choices1 = choices1.filter(
(n) => n !== R.last(battles).additionalCharacters[0].selection,
);
choices2 = choices2.filter(
(n) => n !== R.last(battles).additionalCharacters[1].selection,
);
}
return {
scenario,
character,
city,
status: 'ongoing',
wave: 1,
additionalCharacters: [choices1, choices2].map((choices) => ({
choices,
selection: R.first(choices),
})),
};
}
export function genChapter4Battle(battles) {
const chapter = 4;
const scenario = 'The Last Battle';
const cities = R.difference(
battles
.slice(0, 6)
.filter(({ status }) => status !== 'lost')
.map(R.prop('city')),
battles.slice(6).map((b) => b.city?.selection),
);
const characters = battles.slice(0, 6).map(R.prop('character'));
return {
wave: 1,
scenario,
city: { choices: cities, selection: R.first(cities) },
status: 'ongoing',
additionalCharacters: R.range(0, 3).map((i) => ({
choices: characters,
selection: characters[i],
})),
};
}
export function genNextBattle(battles = []) {
let chapter = R.clamp(1 + parseInt(battles.length / 2), { min: 1, max: 4 });
if (chapter === 1) return genChapter1Battle(battles);
if (chapter === 2) return genChapter2Battle(battles);
if (chapter === 3) return genChapter3Battle(battles);
return genChapter4Battle(battles);
}

View File

@ -0,0 +1,86 @@
import { test, expect } from 'vitest';
import {
genNextBattle,
genChapter2Battle,
genChapter3Battle,
genChapter4Battle,
} from './genNextBattle.js';
test('generate for the first chapter', () => {
const next = genNextBattle();
expect(next).toHaveProperty('city');
expect(next).toHaveProperty('status', 'ongoing');
});
test('chapter 2, first battle', () => {
const result = genChapter2Battle([
{ character: 'one' },
{ character: 'two' },
]);
expect(result.additionalCharacters[0].selection).toEqual('one');
expect(result.additionalCharacters[0].choices).toEqual(['one', 'two']);
});
test('chapter 2, second battle', () => {
const result = genChapter2Battle([
{ character: 'one' },
{ character: 'two' },
{
character: 'three',
additionalCharacters: [{ selection: 'two' }],
},
]);
expect(result.additionalCharacters[0].selection).toEqual('one');
expect(result.additionalCharacters[0].choices).toEqual(['one']);
});
test('chapter 3, first battle', () => {
const result = genChapter3Battle([
{ character: 'one' },
{ character: 'two' },
{ character: 'three' },
{ character: 'four' },
]);
expect(result.additionalCharacters[0].selection).toEqual('one');
expect(result.additionalCharacters[1].selection).toEqual('three');
});
test('chapter 3, second battle', () => {
const result = genChapter3Battle([
{ character: 'one' },
{ character: 'two' },
{ character: 'three' },
{ character: 'four' },
{
character: 'five',
additionalCharacters: [
{ selection: 'one' },
{ selection: 'three' },
],
},
]);
expect(result.additionalCharacters[0].selection).toEqual('two');
expect(result.additionalCharacters[1].selection).toEqual('four');
});
test.only('chapter 4, first battle', () => {
const result = genChapter4Battle([
{ character: 'one', city: 'a' },
{ character: 'two', city: 'b', status: 'lost' },
{ character: 'three', city: 'c' },
{ character: 'four', city: 'd' },
{ character: 'five', city: 'e' },
{ character: 'six', city: 'f' },
]);
expect(result.additionalCharacters[0].selection).toEqual('one');
expect(result.additionalCharacters[1].selection).toEqual('two');
expect(result.additionalCharacters[2].selection).toEqual('three');
expect(result.city.choices).not.toContain('b');
});

View File

@ -0,0 +1,27 @@
import { test, expect } from 'vitest';
import { updateBattle } from './api.js';
test('lost, wave 1', () => {
const result = updateBattle(
{
battles: [{ id: 1, wave: 1, status: 'ongoing' }],
},
1,
'lost',
);
expect(result).toHaveProperty('battles.0.wave', 2);
expect(result).toHaveProperty('battles.0.status', 'ongoing');
});
test('lost, wave 2', () => {
const result = updateBattle(
{
battles: [{ id: 1, wave: 2, status: 'ongoing' }],
},
1,
'lost',
);
expect(result).toHaveProperty('battles.0.status', 'lost');
});

8
src/main.js Normal file
View File

@ -0,0 +1,8 @@
//import './app.css'
import App from './lib/components/App.svelte';
const app = new App({
target: document.getElementById('app'),
});
export default app;

21
src/routes/+layout.svelte Normal file
View File

@ -0,0 +1,21 @@
<Beer />
<main class="center">
<slot />
</main>
<script>
import { genApi } from '$lib/store/api.js';
import { setContext } from 'svelte';
import Beer from '$lib/components/Beer.svelte';
const api = genApi();
setContext('api', api);
</script>
<style>
main {
max-width: 720px;
height: 100vh;
}
</style>

12
src/routes/+page.svelte Normal file
View File

@ -0,0 +1,12 @@
<Campaigns campaigns={$campaigns} />
<script>
import Campaigns from '$lib/components/Campaigns.svelte';
import { getContext } from 'svelte';
const { campaigns } = getContext('api');
</script>
<style>
</style>

View File

@ -0,0 +1,4 @@
export function load({ params }) {
params.campaignId = parseInt(params?.campaignId);
return params;
}

View File

@ -0,0 +1,14 @@
<slot />
<script>
import { getContext } from 'svelte';
export let data;
const api = getContext('api');
api.event.setActiveCampaign(data.campaignId);
</script>
<style>
</style>

BIN
static/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

10
svelte.config.js Normal file
View File

@ -0,0 +1,10 @@
import adapter from '@sveltejs/adapter-auto';
/** @type {import('@sveltejs/kit').Config} */
const config = {
kit: {
adapter: adapter(),
},
};
export default config;

6
tests/test.js Normal file
View File

@ -0,0 +1,6 @@
import { expect, test } from '@playwright/test';
test('index page has expected h1', async ({ page }) => {
await page.goto('/');
expect(await page.textContent('h1')).toBe('Welcome to SvelteKit');
});

20
vite.config.js Normal file
View File

@ -0,0 +1,20 @@
import { sveltekit } from '@sveltejs/kit/vite';
import { svelte } from '@sveltejs/vite-plugin-svelte';
/** @type {import('vite').UserConfig} */
const config = {
plugins: [svelte()],
test: {
include: ['src/**/*.{test,spec}.{js,ts}'],
environment: 'jsdom',
},
resolve: {
alias: {
$lib:
import.meta.url.replace('file://', '').replace(/[^/]+$/, '') +
'src/lib',
},
},
};
export default config;