mirror of
https://github.com/vcscsvcscs/GenerationsHeritage.git
synced 2025-08-13 22:39:06 +02:00
fix lint issues with prettier
This commit is contained in:
36
apps/app/.github/workflows/playwright.yml
vendored
36
apps/app/.github/workflows/playwright.yml
vendored
@@ -1,27 +1,27 @@
|
||||
name: Playwright Tests
|
||||
on:
|
||||
push:
|
||||
branches: [ main, master ]
|
||||
branches: [main, master]
|
||||
pull_request:
|
||||
branches: [ main, master ]
|
||||
branches: [main, master]
|
||||
jobs:
|
||||
test:
|
||||
timeout-minutes: 60
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/*
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Install Playwright Browsers
|
||||
run: npx playwright install --with-deps
|
||||
- name: Run Playwright tests
|
||||
run: npx playwright test
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: ${{ !cancelled() }}
|
||||
with:
|
||||
name: playwright-report
|
||||
path: playwright-report/
|
||||
retention-days: 30
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/*
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Install Playwright Browsers
|
||||
run: npx playwright install --with-deps
|
||||
- name: Run Playwright tests
|
||||
run: npx playwright test
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: ${{ !cancelled() }}
|
||||
with:
|
||||
name: playwright-report
|
||||
path: playwright-report/
|
||||
retention-days: 30
|
||||
|
@@ -1,19 +1,16 @@
|
||||
import type { StorybookConfig } from '@storybook/sveltekit';
|
||||
|
||||
const config: StorybookConfig = {
|
||||
"stories": [
|
||||
"../src/**/*.mdx",
|
||||
"../src/**/*.stories.@(js|ts|svelte)"
|
||||
],
|
||||
"addons": [
|
||||
"@storybook/addon-svelte-csf",
|
||||
"@storybook/addon-essentials",
|
||||
"@chromatic-com/storybook",
|
||||
"@storybook/addon-interactions"
|
||||
],
|
||||
"framework": {
|
||||
"name": "@storybook/sveltekit",
|
||||
"options": {}
|
||||
}
|
||||
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|ts|svelte)'],
|
||||
addons: [
|
||||
'@storybook/addon-svelte-csf',
|
||||
'@storybook/addon-essentials',
|
||||
'@chromatic-com/storybook',
|
||||
'@storybook/addon-interactions'
|
||||
],
|
||||
framework: {
|
||||
name: '@storybook/sveltekit',
|
||||
options: {}
|
||||
}
|
||||
};
|
||||
export default config;
|
||||
export default config;
|
||||
|
@@ -1,14 +1,14 @@
|
||||
import type { Preview } from '@storybook/svelte'
|
||||
import type { Preview } from '@storybook/svelte';
|
||||
|
||||
const preview: Preview = {
|
||||
parameters: {
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/i,
|
||||
},
|
||||
},
|
||||
},
|
||||
parameters: {
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/i
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default preview;
|
||||
export default preview;
|
||||
|
2
apps/app/.vscode/settings.json
vendored
2
apps/app/.vscode/settings.json
vendored
@@ -2,4 +2,4 @@
|
||||
"files.associations": {
|
||||
"wrangler.json": "jsonc"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,18 +1,18 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test('has title', async ({ page }) => {
|
||||
await page.goto('https://playwright.dev/');
|
||||
await page.goto('https://playwright.dev/');
|
||||
|
||||
// Expect a title "to contain" a substring.
|
||||
await expect(page).toHaveTitle(/Playwright/);
|
||||
// Expect a title "to contain" a substring.
|
||||
await expect(page).toHaveTitle(/Playwright/);
|
||||
});
|
||||
|
||||
test('get started link', async ({ page }) => {
|
||||
await page.goto('https://playwright.dev/');
|
||||
await page.goto('https://playwright.dev/');
|
||||
|
||||
// Click the get started link.
|
||||
await page.getByRole('link', { name: 'Get started' }).click();
|
||||
// Click the get started link.
|
||||
await page.getByRole('link', { name: 'Get started' }).click();
|
||||
|
||||
// Expects page to have a heading with the name of Installation.
|
||||
await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible();
|
||||
// Expects page to have a heading with the name of Installation.
|
||||
await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible();
|
||||
});
|
||||
|
@@ -93,7 +93,7 @@
|
||||
"plant": "Plant",
|
||||
"politics": "Politics",
|
||||
"profile_id": "Profile ID",
|
||||
"profiel_id_registration":"If someone already created a profile for you, ask them for your profiles ID and enter it here.",
|
||||
"profiel_id_registration": "If someone already created a profile for you, ask them for your profiles ID and enter it here.",
|
||||
"profile_picture": "Profile Picture",
|
||||
"privacy_policy": "Privacy Policy",
|
||||
"recipe": "Recipe",
|
||||
|
@@ -1,141 +1,141 @@
|
||||
{
|
||||
"$schema": "https://inlang.com/schema/inlang-message-format",
|
||||
"about": "Rólunk",
|
||||
"accept": "Elfogadás",
|
||||
"add": "Hozzáadás",
|
||||
"address": "Cím",
|
||||
"alive": "Élő",
|
||||
"allergies": "Allergiák",
|
||||
"allow_family_tree_admin_access": "Családfa adminisztrátor hozzáférésének engedélyezése",
|
||||
"animal": "Állat",
|
||||
"audio": "Hang",
|
||||
"back": "Vissza",
|
||||
"baptized": "Megkeresztelve",
|
||||
"belief": "Hit",
|
||||
"birth_name": "Születési név",
|
||||
"blood_pressure": "Vérnyomás",
|
||||
"blood_type": "Vércsoport",
|
||||
"born": "Született",
|
||||
"cancel": "Mégse",
|
||||
"city": "Város",
|
||||
"child": "Gyermek",
|
||||
"citizenship": "Állampolgárság",
|
||||
"close": "Bezár",
|
||||
"coffee": "Kávé",
|
||||
"colour": "Szín",
|
||||
"connection": "Kapcsolat",
|
||||
"connection_type": "Kapcsolat típusa",
|
||||
"contact": "Kapcsolat",
|
||||
"cookie_disclaimer": "Ez a weboldal sütiket használ annak érdekében, hogy a lehető legjobb élményt nyújtsa, beleértve a téma tárolását és a felhasználói munkamenetek kezelését.",
|
||||
"cookie_policy": "Süti szabályzat",
|
||||
"country": "Ország",
|
||||
"dark": "Sötét",
|
||||
"death": "Halál",
|
||||
"deceased": "Elhunyt",
|
||||
"deny": "Elutasítás",
|
||||
"description": "Leírás",
|
||||
"details": "Részletek",
|
||||
"directions": "Útvonalak",
|
||||
"document": "Dokumentum",
|
||||
"download": "Letöltés",
|
||||
"edit": "Szerkesztés",
|
||||
"email": "Email",
|
||||
"$schema": "https://inlang.com/schema/inlang-message-format",
|
||||
"about": "Rólunk",
|
||||
"accept": "Elfogadás",
|
||||
"add": "Hozzáadás",
|
||||
"address": "Cím",
|
||||
"alive": "Élő",
|
||||
"allergies": "Allergiák",
|
||||
"allow_family_tree_admin_access": "Családfa adminisztrátor hozzáférésének engedélyezése",
|
||||
"animal": "Állat",
|
||||
"audio": "Hang",
|
||||
"back": "Vissza",
|
||||
"baptized": "Megkeresztelve",
|
||||
"belief": "Hit",
|
||||
"birth_name": "Születési név",
|
||||
"blood_pressure": "Vérnyomás",
|
||||
"blood_type": "Vércsoport",
|
||||
"born": "Született",
|
||||
"cancel": "Mégse",
|
||||
"city": "Város",
|
||||
"child": "Gyermek",
|
||||
"citizenship": "Állampolgárság",
|
||||
"close": "Bezár",
|
||||
"coffee": "Kávé",
|
||||
"colour": "Szín",
|
||||
"connection": "Kapcsolat",
|
||||
"connection_type": "Kapcsolat típusa",
|
||||
"contact": "Kapcsolat",
|
||||
"cookie_disclaimer": "Ez a weboldal sütiket használ annak érdekében, hogy a lehető legjobb élményt nyújtsa, beleértve a téma tárolását és a felhasználói munkamenetek kezelését.",
|
||||
"cookie_policy": "Süti szabályzat",
|
||||
"country": "Ország",
|
||||
"dark": "Sötét",
|
||||
"death": "Halál",
|
||||
"deceased": "Elhunyt",
|
||||
"deny": "Elutasítás",
|
||||
"description": "Leírás",
|
||||
"details": "Részletek",
|
||||
"directions": "Útvonalak",
|
||||
"document": "Dokumentum",
|
||||
"download": "Letöltés",
|
||||
"edit": "Szerkesztés",
|
||||
"email": "Email",
|
||||
"export_something": "{thing}Exportálás",
|
||||
"extra_names": "Extra nevek",
|
||||
"faith": "Vallás",
|
||||
"failed_to_create_user": "Felhasználó létrehozása sikertelen",
|
||||
"extra_names": "Extra nevek",
|
||||
"faith": "Vallás",
|
||||
"failed_to_create_user": "Felhasználó létrehozása sikertelen",
|
||||
"family_tree": "Családfa",
|
||||
"favourite": "Kedvenc",
|
||||
"favourite_recipes": "Kedvenc receptek",
|
||||
"file": "Fájl",
|
||||
"first_name": "Keresztnév",
|
||||
"flower": "Virág",
|
||||
"fruit": "Gyümölcs",
|
||||
"hair_colour": "Hajszín",
|
||||
"hello_world": "Helló, {name} innen: hu!",
|
||||
"height": "Magasság",
|
||||
"hobby": "Hobbi",
|
||||
"home": "Otthon",
|
||||
"id": "Azonosító",
|
||||
"ideology": "Ideológia",
|
||||
"illness": "Betegség",
|
||||
"image": "Kép",
|
||||
"ingridients": "Hozzávalók",
|
||||
"interest": "Érdeklődés",
|
||||
"language": "Nyelv",
|
||||
"last_name": "Vezetéknév",
|
||||
"life_events": "Életesemények",
|
||||
"light": "Világos",
|
||||
"loading": "Betöltés",
|
||||
"login": "Bejelentkezés",
|
||||
"logout": "Kijelentkezés",
|
||||
"medication": "Gyógyszer",
|
||||
"message_for_future_generations": "Üzenet a jövő generációinak",
|
||||
"middle_name": "Második név",
|
||||
"missing_field": "{field} mező hiányzik",
|
||||
"mothers_first_name": "Anyja keresztneve",
|
||||
"mothers_last_name": "Anyja vezetékneve",
|
||||
"nation": "Nemzet",
|
||||
"no": "Nem",
|
||||
"no_data": "Nincs adat",
|
||||
"notes": "Jegyzetek",
|
||||
"occupation": "Foglalkozás",
|
||||
"occupation_to_display": "Megjelenítendő foglalkozás",
|
||||
"others_said": "Mások mondták",
|
||||
"parent": "Szülő",
|
||||
"people": "Emberek",
|
||||
"person": "Személy",
|
||||
"pet": "Háziállat",
|
||||
"philosophy": "Filozófia",
|
||||
"photos": "Fotók",
|
||||
"place_of_birth": "Születési hely",
|
||||
"place_of_death": "Halálozási hely",
|
||||
"plant": "Növény",
|
||||
"politics": "Politika",
|
||||
"profile_picture": "Profilkép",
|
||||
"privacy_policy": "Adatvédelmi irányelvek",
|
||||
"recipe": "Recept",
|
||||
"recipes": "Receptek",
|
||||
"register": "Regisztráció",
|
||||
"relation": "Kapcsolat",
|
||||
"relation_type": "Kapcsolat típusa",
|
||||
"relationship": "Kapcsolat",
|
||||
"relationship_type": "Kapcsolat típusa",
|
||||
"religion": "Vallás",
|
||||
"remove": "Eltávolítás",
|
||||
"residence": "Lakóhely",
|
||||
"save": "Mentés",
|
||||
"search": "Keresés",
|
||||
"select": "Kiválasztás",
|
||||
"select_all": "Összes kiválasztása",
|
||||
"settings": "Beállítások",
|
||||
"sibling": "Testvér",
|
||||
"sign_in": "Bejelentkezés",
|
||||
"sign_out": "Kijelentkezés",
|
||||
"site_intro": "Üdvözöljük a Generációk Öröksége oldalán, ahol rögzítheti családfáját és megoszthatja családtörténetét szereteivel. Hozon létre digitális szellemi hagyatékot leszármazottai számára.",
|
||||
"skill": "Képesség",
|
||||
"skin_colour": "Bőrszín",
|
||||
"source": "Forrás",
|
||||
"source_url": "Forrás URL",
|
||||
"spouse": "Házastárs",
|
||||
"street": "Utca",
|
||||
"suffixes": "Utótagok",
|
||||
"system": "Rendszer",
|
||||
"talent": "Tehetség",
|
||||
"terms_and_conditions": "Felhasználási feltételek",
|
||||
"theme": "Téma",
|
||||
"title": "Generációk Öröksége {page}",
|
||||
"titles": "Címek",
|
||||
"tree": "Fa",
|
||||
"unselect_all": "Összes kiválasztásának megszüntetése",
|
||||
"unknown": "Ismeretlen",
|
||||
"upload": "Feltöltés",
|
||||
"vaccination": "Oltás",
|
||||
"vegetable": "Zöldség",
|
||||
"video": "Videó",
|
||||
"website": "Weboldal",
|
||||
"weight": "Súly",
|
||||
"welcome": "Üdvözöljük a Generációk Öröksége oldalán",
|
||||
"yes": "Igen",
|
||||
"zip_code": "Irányítószám"
|
||||
"favourite": "Kedvenc",
|
||||
"favourite_recipes": "Kedvenc receptek",
|
||||
"file": "Fájl",
|
||||
"first_name": "Keresztnév",
|
||||
"flower": "Virág",
|
||||
"fruit": "Gyümölcs",
|
||||
"hair_colour": "Hajszín",
|
||||
"hello_world": "Helló, {name} innen: hu!",
|
||||
"height": "Magasság",
|
||||
"hobby": "Hobbi",
|
||||
"home": "Otthon",
|
||||
"id": "Azonosító",
|
||||
"ideology": "Ideológia",
|
||||
"illness": "Betegség",
|
||||
"image": "Kép",
|
||||
"ingridients": "Hozzávalók",
|
||||
"interest": "Érdeklődés",
|
||||
"language": "Nyelv",
|
||||
"last_name": "Vezetéknév",
|
||||
"life_events": "Életesemények",
|
||||
"light": "Világos",
|
||||
"loading": "Betöltés",
|
||||
"login": "Bejelentkezés",
|
||||
"logout": "Kijelentkezés",
|
||||
"medication": "Gyógyszer",
|
||||
"message_for_future_generations": "Üzenet a jövő generációinak",
|
||||
"middle_name": "Második név",
|
||||
"missing_field": "{field} mező hiányzik",
|
||||
"mothers_first_name": "Anyja keresztneve",
|
||||
"mothers_last_name": "Anyja vezetékneve",
|
||||
"nation": "Nemzet",
|
||||
"no": "Nem",
|
||||
"no_data": "Nincs adat",
|
||||
"notes": "Jegyzetek",
|
||||
"occupation": "Foglalkozás",
|
||||
"occupation_to_display": "Megjelenítendő foglalkozás",
|
||||
"others_said": "Mások mondták",
|
||||
"parent": "Szülő",
|
||||
"people": "Emberek",
|
||||
"person": "Személy",
|
||||
"pet": "Háziállat",
|
||||
"philosophy": "Filozófia",
|
||||
"photos": "Fotók",
|
||||
"place_of_birth": "Születési hely",
|
||||
"place_of_death": "Halálozási hely",
|
||||
"plant": "Növény",
|
||||
"politics": "Politika",
|
||||
"profile_picture": "Profilkép",
|
||||
"privacy_policy": "Adatvédelmi irányelvek",
|
||||
"recipe": "Recept",
|
||||
"recipes": "Receptek",
|
||||
"register": "Regisztráció",
|
||||
"relation": "Kapcsolat",
|
||||
"relation_type": "Kapcsolat típusa",
|
||||
"relationship": "Kapcsolat",
|
||||
"relationship_type": "Kapcsolat típusa",
|
||||
"religion": "Vallás",
|
||||
"remove": "Eltávolítás",
|
||||
"residence": "Lakóhely",
|
||||
"save": "Mentés",
|
||||
"search": "Keresés",
|
||||
"select": "Kiválasztás",
|
||||
"select_all": "Összes kiválasztása",
|
||||
"settings": "Beállítások",
|
||||
"sibling": "Testvér",
|
||||
"sign_in": "Bejelentkezés",
|
||||
"sign_out": "Kijelentkezés",
|
||||
"site_intro": "Üdvözöljük a Generációk Öröksége oldalán, ahol rögzítheti családfáját és megoszthatja családtörténetét szereteivel. Hozon létre digitális szellemi hagyatékot leszármazottai számára.",
|
||||
"skill": "Képesség",
|
||||
"skin_colour": "Bőrszín",
|
||||
"source": "Forrás",
|
||||
"source_url": "Forrás URL",
|
||||
"spouse": "Házastárs",
|
||||
"street": "Utca",
|
||||
"suffixes": "Utótagok",
|
||||
"system": "Rendszer",
|
||||
"talent": "Tehetség",
|
||||
"terms_and_conditions": "Felhasználási feltételek",
|
||||
"theme": "Téma",
|
||||
"title": "Generációk Öröksége {page}",
|
||||
"titles": "Címek",
|
||||
"tree": "Fa",
|
||||
"unselect_all": "Összes kiválasztásának megszüntetése",
|
||||
"unknown": "Ismeretlen",
|
||||
"upload": "Feltöltés",
|
||||
"vaccination": "Oltás",
|
||||
"vegetable": "Zöldség",
|
||||
"video": "Videó",
|
||||
"website": "Weboldal",
|
||||
"weight": "Súly",
|
||||
"welcome": "Üdvözöljük a Generációk Öröksége oldalán",
|
||||
"yes": "Igen",
|
||||
"zip_code": "Irányítószám"
|
||||
}
|
||||
|
@@ -1,72 +1,72 @@
|
||||
{
|
||||
"name": "generations-heritage",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "npm run build && wrangler pages dev",
|
||||
"prepare": "svelte-kit sync || echo ''",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"format": "prettier --write .",
|
||||
"lint": "prettier --check . && eslint .",
|
||||
"test:unit": "vitest",
|
||||
"test": "npm run test:unit -- --run",
|
||||
"storybook": "storybook dev -p 6006",
|
||||
"build-storybook": "storybook build",
|
||||
"deploy-stage": "npm run build && wrangler pages deploy --env staging",
|
||||
"deploy-prod": "npm run build && wrangler pages deploy --env production",
|
||||
"cf-typegen": "wrangler types && move worker-configuration.d.ts src/"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@chromatic-com/storybook": "^3.2.4",
|
||||
"@cloudflare/workers-types": "^4.20250214.0",
|
||||
"@eslint/compat": "^1.2.5",
|
||||
"@eslint/js": "^9.18.0",
|
||||
"@playwright/test": "^1.50.1",
|
||||
"@storybook/addon-essentials": "^8.5.6",
|
||||
"@storybook/addon-interactions": "^8.5.6",
|
||||
"@storybook/addon-svelte-csf": "^5.0.0-next.23",
|
||||
"@storybook/blocks": "^8.5.6",
|
||||
"@storybook/svelte": "^8.5.6",
|
||||
"@storybook/sveltekit": "^8.5.6",
|
||||
"@storybook/test": "^8.5.6",
|
||||
"@sveltejs/adapter-auto": "^4.0.0",
|
||||
"@sveltejs/adapter-cloudflare": "^5.0.3",
|
||||
"@sveltejs/kit": "^2.16.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
||||
"@tailwindcss/postcss": "^4.0.12",
|
||||
"@types/node": "^22.13.9",
|
||||
"@vitest/coverage-v8": "^3.0.5",
|
||||
"daisyui": "^5.0.0",
|
||||
"eslint": "^9.18.0",
|
||||
"eslint-config-prettier": "^10.0.1",
|
||||
"eslint-plugin-svelte": "^2.46.1",
|
||||
"globals": "^15.14.0",
|
||||
"openapi-typescript": "^7.6.1",
|
||||
"prettier": "^3.4.2",
|
||||
"prettier-plugin-svelte": "^3.3.3",
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
"storybook": "^8.5.6",
|
||||
"svelte": "^5.0.0",
|
||||
"svelte-check": "^4.0.0",
|
||||
"tailwindcss": "^4.0.12",
|
||||
"typescript": "^5.0.0",
|
||||
"typescript-eslint": "^8.20.0",
|
||||
"vite": "^6.0.0",
|
||||
"vitest": "^3.0.0",
|
||||
"wrangler": "^3.109.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@inlang/paraglide-sveltekit": "^0.15.5",
|
||||
"@pilcrowjs/object-parser": "^0.0.4",
|
||||
"@types/pikaday": "^1.7.9",
|
||||
"@xyflow/svelte": "^1.0.0-next.3",
|
||||
"arctic": "^3.3.0",
|
||||
"neo4j-driver": "^5.28.1",
|
||||
"openapi-fetch": "^0.13.5",
|
||||
"pikaday": "^1.8.2"
|
||||
}
|
||||
"name": "generations-heritage",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "npm run build && wrangler pages dev",
|
||||
"prepare": "svelte-kit sync || echo ''",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"format": "prettier --write .",
|
||||
"lint": "prettier --check . && eslint .",
|
||||
"test:unit": "vitest",
|
||||
"test": "npm run test:unit -- --run",
|
||||
"storybook": "storybook dev -p 6006",
|
||||
"build-storybook": "storybook build",
|
||||
"deploy-stage": "npm run build && wrangler pages deploy --env staging",
|
||||
"deploy-prod": "npm run build && wrangler pages deploy --env production",
|
||||
"cf-typegen": "wrangler types && move worker-configuration.d.ts src/"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@chromatic-com/storybook": "^3.2.4",
|
||||
"@cloudflare/workers-types": "^4.20250214.0",
|
||||
"@eslint/compat": "^1.2.5",
|
||||
"@eslint/js": "^9.18.0",
|
||||
"@playwright/test": "^1.50.1",
|
||||
"@storybook/addon-essentials": "^8.5.6",
|
||||
"@storybook/addon-interactions": "^8.5.6",
|
||||
"@storybook/addon-svelte-csf": "^5.0.0-next.23",
|
||||
"@storybook/blocks": "^8.5.6",
|
||||
"@storybook/svelte": "^8.5.6",
|
||||
"@storybook/sveltekit": "^8.5.6",
|
||||
"@storybook/test": "^8.5.6",
|
||||
"@sveltejs/adapter-auto": "^4.0.0",
|
||||
"@sveltejs/adapter-cloudflare": "^5.0.3",
|
||||
"@sveltejs/kit": "^2.16.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
||||
"@tailwindcss/postcss": "^4.0.12",
|
||||
"@types/node": "^22.13.9",
|
||||
"@vitest/coverage-v8": "^3.0.5",
|
||||
"daisyui": "^5.0.0",
|
||||
"eslint": "^9.18.0",
|
||||
"eslint-config-prettier": "^10.0.1",
|
||||
"eslint-plugin-svelte": "^2.46.1",
|
||||
"globals": "^15.14.0",
|
||||
"openapi-typescript": "^7.6.1",
|
||||
"prettier": "^3.4.2",
|
||||
"prettier-plugin-svelte": "^3.3.3",
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
"storybook": "^8.5.6",
|
||||
"svelte": "^5.0.0",
|
||||
"svelte-check": "^4.0.0",
|
||||
"tailwindcss": "^4.0.12",
|
||||
"typescript": "^5.0.0",
|
||||
"typescript-eslint": "^8.20.0",
|
||||
"vite": "^6.0.0",
|
||||
"vitest": "^3.0.0",
|
||||
"wrangler": "^3.109.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@inlang/paraglide-sveltekit": "^0.15.5",
|
||||
"@pilcrowjs/object-parser": "^0.0.4",
|
||||
"@types/pikaday": "^1.7.9",
|
||||
"@xyflow/svelte": "^1.0.0-next.3",
|
||||
"arctic": "^3.3.0",
|
||||
"neo4j-driver": "^5.28.1",
|
||||
"openapi-fetch": "^0.13.5",
|
||||
"pikaday": "^1.8.2"
|
||||
}
|
||||
}
|
||||
|
@@ -12,68 +12,68 @@ import { defineConfig, devices } from '@playwright/test';
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*/
|
||||
export default defineConfig({
|
||||
testDir: './e2e',
|
||||
/* Run tests in files in parallel */
|
||||
fullyParallel: true,
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: !!process.env.CI,
|
||||
/* Retry on CI only */
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: 'html',
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
// baseURL: 'http://127.0.0.1:3000',
|
||||
testDir: './e2e',
|
||||
/* Run tests in files in parallel */
|
||||
fullyParallel: true,
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: !!process.env.CI,
|
||||
/* Retry on CI only */
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: 'html',
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
// baseURL: 'http://127.0.0.1:3000',
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: 'on-first-retry',
|
||||
},
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: 'on-first-retry'
|
||||
},
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
},
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'] }
|
||||
},
|
||||
|
||||
{
|
||||
name: 'firefox',
|
||||
use: { ...devices['Desktop Firefox'] },
|
||||
},
|
||||
{
|
||||
name: 'firefox',
|
||||
use: { ...devices['Desktop Firefox'] }
|
||||
},
|
||||
|
||||
{
|
||||
name: 'webkit',
|
||||
use: { ...devices['Desktop Safari'] },
|
||||
},
|
||||
{
|
||||
name: 'webkit',
|
||||
use: { ...devices['Desktop Safari'] }
|
||||
}
|
||||
|
||||
/* Test against mobile viewports. */
|
||||
// {
|
||||
// name: 'Mobile Chrome',
|
||||
// use: { ...devices['Pixel 5'] },
|
||||
// },
|
||||
// {
|
||||
// name: 'Mobile Safari',
|
||||
// use: { ...devices['iPhone 12'] },
|
||||
// },
|
||||
/* Test against mobile viewports. */
|
||||
// {
|
||||
// name: 'Mobile Chrome',
|
||||
// use: { ...devices['Pixel 5'] },
|
||||
// },
|
||||
// {
|
||||
// name: 'Mobile Safari',
|
||||
// use: { ...devices['iPhone 12'] },
|
||||
// },
|
||||
|
||||
/* Test against branded browsers. */
|
||||
// {
|
||||
// name: 'Microsoft Edge',
|
||||
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
|
||||
// },
|
||||
// {
|
||||
// name: 'Google Chrome',
|
||||
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
|
||||
// },
|
||||
],
|
||||
/* Test against branded browsers. */
|
||||
// {
|
||||
// name: 'Microsoft Edge',
|
||||
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
|
||||
// },
|
||||
// {
|
||||
// name: 'Google Chrome',
|
||||
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
|
||||
// },
|
||||
]
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
// webServer: {
|
||||
// command: 'npm run start',
|
||||
// url: 'http://127.0.0.1:3000',
|
||||
// reuseExistingServer: !process.env.CI,
|
||||
// },
|
||||
/* Run your local dev server before starting the tests */
|
||||
// webServer: {
|
||||
// command: 'npm run start',
|
||||
// url: 'http://127.0.0.1:3000',
|
||||
// reuseExistingServer: !process.env.CI,
|
||||
// },
|
||||
});
|
||||
|
@@ -1,5 +1,5 @@
|
||||
export default {
|
||||
plugins: {
|
||||
'@tailwindcss/postcss': {},
|
||||
'@tailwindcss/postcss': {}
|
||||
}
|
||||
};
|
||||
|
@@ -9,16 +9,24 @@
|
||||
color utility to any element that depends on these defaults.
|
||||
*/
|
||||
@layer base {
|
||||
*,
|
||||
::after,
|
||||
::before,
|
||||
::backdrop,
|
||||
::file-selector-button {
|
||||
border-color: var(--color-gray-200, currentColor);
|
||||
}
|
||||
|
||||
*,
|
||||
::after,
|
||||
::before,
|
||||
::backdrop,
|
||||
::file-selector-button {
|
||||
border-color: var(--color-gray-200, currentColor);
|
||||
}
|
||||
}
|
||||
|
||||
@plugin "daisyui" {
|
||||
themes: light --default, dark --prefersdark, light, dark, cyberpunk, synthwave, retro, coffee, dracula;
|
||||
themes:
|
||||
light --default,
|
||||
dark --prefersdark,
|
||||
light,
|
||||
dark,
|
||||
cyberpunk,
|
||||
synthwave,
|
||||
retro,
|
||||
coffee,
|
||||
dracula;
|
||||
}
|
||||
|
24
apps/app/src/app.d.ts
vendored
24
apps/app/src/app.d.ts
vendored
@@ -3,18 +3,18 @@ import { KVNamespace } from '@cloudflare/workers-types';
|
||||
// for information about these interfaces
|
||||
declare global {
|
||||
namespace App {
|
||||
interface Locals {
|
||||
session: Session | null;
|
||||
}
|
||||
interface Platform {
|
||||
env: {
|
||||
GH_MEDIA: R2Bucket;
|
||||
interface Locals {
|
||||
session: Session | null;
|
||||
}
|
||||
interface Platform {
|
||||
env: {
|
||||
GH_MEDIA: R2Bucket;
|
||||
GH_SESSIONS: KVNamespace;
|
||||
};
|
||||
cf: CfProperties
|
||||
ctx: ExecutionContext
|
||||
}
|
||||
}
|
||||
};
|
||||
cf: CfProperties;
|
||||
ctx: ExecutionContext;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
export {};
|
||||
|
@@ -1,47 +1,51 @@
|
||||
import type { Handle } from '@sveltejs/kit';
|
||||
import { themes } from '$lib/themes'
|
||||
import { themes } from '$lib/themes';
|
||||
import { i18n } from '$lib/i18n';
|
||||
import { validateSessionToken, setSessionTokenCookie, deleteSessionTokenCookie } from "$lib/server/session";
|
||||
import { sequence } from "@sveltejs/kit/hooks";
|
||||
import {
|
||||
validateSessionToken,
|
||||
setSessionTokenCookie,
|
||||
deleteSessionTokenCookie
|
||||
} from '$lib/server/session';
|
||||
import { sequence } from '@sveltejs/kit/hooks';
|
||||
|
||||
const handleParaglide: Handle = i18n.handle();
|
||||
|
||||
const authHandle: Handle = async ({ event, resolve }) => {
|
||||
const token = event.cookies.get("session") ?? null;
|
||||
if (token === null) {
|
||||
event.locals.session = null;
|
||||
return resolve(event);
|
||||
}
|
||||
const token = event.cookies.get('session') ?? null;
|
||||
if (token === null) {
|
||||
event.locals.session = null;
|
||||
return resolve(event);
|
||||
}
|
||||
|
||||
if(!event.platform || !event.platform.env || !event.platform.env.GH_SESSIONS){
|
||||
return new Response("Server configuration error. GH_SESSIONS KeyValue store missing", {
|
||||
if (!event.platform || !event.platform.env || !event.platform.env.GH_SESSIONS) {
|
||||
return new Response('Server configuration error. GH_SESSIONS KeyValue store missing', {
|
||||
status: 500
|
||||
});
|
||||
}
|
||||
|
||||
const session = await validateSessionToken(token, event.platform.env.GH_SESSIONS);
|
||||
if (session !== null) {
|
||||
setSessionTokenCookie(event, token, session.expiresAt);
|
||||
} else {
|
||||
deleteSessionTokenCookie(event);
|
||||
}
|
||||
const session = await validateSessionToken(token, event.platform.env.GH_SESSIONS);
|
||||
if (session !== null) {
|
||||
setSessionTokenCookie(event, token, session.expiresAt);
|
||||
} else {
|
||||
deleteSessionTokenCookie(event);
|
||||
}
|
||||
|
||||
event.locals.session = session;
|
||||
return resolve(event);
|
||||
event.locals.session = session;
|
||||
return resolve(event);
|
||||
};
|
||||
|
||||
const themeHandler: Handle = async ({ event, resolve }) => {
|
||||
const theme = event.cookies.get('theme')
|
||||
const theme = event.cookies.get('theme');
|
||||
|
||||
if (!theme || !themes.includes(theme)) {
|
||||
return await resolve(event)
|
||||
return await resolve(event);
|
||||
}
|
||||
|
||||
return await resolve(event, {
|
||||
transformPageChunk: ({ html }) => {
|
||||
return html.replace('data-theme=""', `data-theme="${theme}"`)
|
||||
},
|
||||
})
|
||||
}
|
||||
return html.replace('data-theme=""', `data-theme="${theme}"`);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const handle: Handle = sequence(handleParaglide, authHandle,themeHandler);
|
||||
export const handle: Handle = sequence(handleParaglide, authHandle, themeHandler);
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,20 @@
|
||||
<div role="alert" class="alert alert-vertical sm:alert-horizontal">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-info h-6 w-6 shrink-0">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
<span></span>
|
||||
<div>
|
||||
<button class="btn btn-sm">Deny</button>
|
||||
<button class="btn btn-sm btn-primary">Accept</button>
|
||||
</div>
|
||||
</div>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
class="stroke-info h-6 w-6 shrink-0"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
></path>
|
||||
</svg>
|
||||
<span></span>
|
||||
<div>
|
||||
<button class="btn btn-sm">Deny</button>
|
||||
<button class="btn btn-sm btn-primary">Accept</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -2,64 +2,64 @@ import { Node, Relationship, Date } from 'neo4j-driver';
|
||||
import { Integer } from 'neo4j-driver';
|
||||
|
||||
export interface PersonProperties {
|
||||
allow_admin_access: boolean;
|
||||
google_id: string;
|
||||
first_name: string;
|
||||
middle_name?: string;
|
||||
last_name: string;
|
||||
titles?: string[]; // e.g. Jr., Sr., III
|
||||
suffixes?: string[]; // e.g. Ph.D., M.D.
|
||||
extra_names?: string[];
|
||||
aliases?: string[];
|
||||
mothers_first_name?: string;
|
||||
mothers_last_name?: string;
|
||||
born?: Date<number>;
|
||||
place_of_birth?: string;
|
||||
died?: Date<number>;
|
||||
place_of_death?: string;
|
||||
life_events?: { [key: string]: {from: Date<number>, to:Date<number>, desription: string} }[];
|
||||
occupations?: string[];
|
||||
occupation_to_display?: string;
|
||||
others_said?: { [key: string]: string };
|
||||
limit: number;
|
||||
photos?: { [key: string]: string };
|
||||
videos?: { [key: string]: string };
|
||||
audios?: { [key: string]: string };
|
||||
profile_picture?: string;
|
||||
verified: boolean;
|
||||
email?: string;
|
||||
phone?: string;
|
||||
residence?: {
|
||||
city?: string;
|
||||
country?: string;
|
||||
zip_code?: string;
|
||||
address_line_1?: string;
|
||||
address_line_2?: string;
|
||||
};
|
||||
religion?: string;
|
||||
baptized?: string;
|
||||
ideology?: string;
|
||||
blood_type?: string;
|
||||
allergies?: string[];
|
||||
medications?: string[];
|
||||
medical_conditions?: string[];
|
||||
height?: number;
|
||||
weight?: number;
|
||||
hair_colour?: string;
|
||||
skin_colour?: string;
|
||||
eye_colour?: string;
|
||||
sports?: string[];
|
||||
hobbies?: string[];
|
||||
interests?: string[];
|
||||
languages?: { [key: string]: string };
|
||||
notes?: string;
|
||||
allow_admin_access: boolean;
|
||||
google_id: string;
|
||||
first_name: string;
|
||||
middle_name?: string;
|
||||
last_name: string;
|
||||
titles?: string[]; // e.g. Jr., Sr., III
|
||||
suffixes?: string[]; // e.g. Ph.D., M.D.
|
||||
extra_names?: string[];
|
||||
aliases?: string[];
|
||||
mothers_first_name?: string;
|
||||
mothers_last_name?: string;
|
||||
born?: Date<number>;
|
||||
place_of_birth?: string;
|
||||
died?: Date<number>;
|
||||
place_of_death?: string;
|
||||
life_events?: { [key: string]: { from: Date<number>; to: Date<number>; desription: string } }[];
|
||||
occupations?: string[];
|
||||
occupation_to_display?: string;
|
||||
others_said?: { [key: string]: string };
|
||||
limit: number;
|
||||
photos?: { [key: string]: string };
|
||||
videos?: { [key: string]: string };
|
||||
audios?: { [key: string]: string };
|
||||
profile_picture?: string;
|
||||
verified: boolean;
|
||||
email?: string;
|
||||
phone?: string;
|
||||
residence?: {
|
||||
city?: string;
|
||||
country?: string;
|
||||
zip_code?: string;
|
||||
address_line_1?: string;
|
||||
address_line_2?: string;
|
||||
};
|
||||
religion?: string;
|
||||
baptized?: string;
|
||||
ideology?: string;
|
||||
blood_type?: string;
|
||||
allergies?: string[];
|
||||
medications?: string[];
|
||||
medical_conditions?: string[];
|
||||
height?: number;
|
||||
weight?: number;
|
||||
hair_colour?: string;
|
||||
skin_colour?: string;
|
||||
eye_colour?: string;
|
||||
sports?: string[];
|
||||
hobbies?: string[];
|
||||
interests?: string[];
|
||||
languages?: { [key: string]: string };
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export interface FamilyRelationship {
|
||||
verified: boolean;
|
||||
notes?: string;
|
||||
from?: Date<number>;
|
||||
to?: Date<number>;
|
||||
verified: boolean;
|
||||
notes?: string;
|
||||
from?: Date<number>;
|
||||
to?: Date<number>;
|
||||
}
|
||||
|
||||
export type Person = Node<Integer, PersonProperties>;
|
||||
@@ -69,36 +69,38 @@ export type Spouse = Relationship<Integer, FamilyRelationship>;
|
||||
export type Child = Relationship<Integer, FamilyRelationship>;
|
||||
|
||||
export interface RecipeProperties {
|
||||
id: string;
|
||||
name: string;
|
||||
origin: string;
|
||||
category: string;
|
||||
first_recorded: Date<number>;
|
||||
description: string;
|
||||
ingredients: string[];
|
||||
instructions: string[];
|
||||
photo: string;
|
||||
notes?: string;
|
||||
id: string;
|
||||
name: string;
|
||||
origin: string;
|
||||
category: string;
|
||||
first_recorded: Date<number>;
|
||||
description: string;
|
||||
ingredients: string[];
|
||||
instructions: string[];
|
||||
photo: string;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export type Recipe = Node<Integer, RecipeProperties>;
|
||||
export type Likes = Relationship<Integer, {
|
||||
favourite: boolean;
|
||||
like_it: boolean;
|
||||
could_make_it: boolean;
|
||||
}>;
|
||||
export type Likes = Relationship<
|
||||
Integer,
|
||||
{
|
||||
favourite: boolean;
|
||||
like_it: boolean;
|
||||
could_make_it: boolean;
|
||||
}
|
||||
>;
|
||||
|
||||
export interface FamilyTree {
|
||||
ancestors: String;
|
||||
prel1: FamilyRelationship;
|
||||
children: String;
|
||||
prel2: FamilyRelationship;
|
||||
spouses: String;
|
||||
srel: FamilyRelationship;
|
||||
user: String;
|
||||
|
||||
ancestors: String;
|
||||
prel1: FamilyRelationship;
|
||||
children: String;
|
||||
prel2: FamilyRelationship;
|
||||
spouses: String;
|
||||
srel: FamilyRelationship;
|
||||
user: String;
|
||||
}
|
||||
|
||||
export interface FamilyMember {
|
||||
person: Person;
|
||||
}
|
||||
person: Person;
|
||||
}
|
||||
|
@@ -1,4 +1,8 @@
|
||||
import { Google } from "arctic";
|
||||
import { GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GOOGLE_CALLBACK_URI } from "$env/static/private";
|
||||
import { Google } from 'arctic';
|
||||
import { GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GOOGLE_CALLBACK_URI } from '$env/static/private';
|
||||
|
||||
export const google = new Google(GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GOOGLE_CALLBACK_URI || "http://localhost:5173/login/google/callback");
|
||||
export const google = new Google(
|
||||
GOOGLE_CLIENT_ID,
|
||||
GOOGLE_CLIENT_SECRET,
|
||||
GOOGLE_CALLBACK_URI || 'http://localhost:5173/login/google/callback'
|
||||
);
|
||||
|
@@ -1,15 +1,18 @@
|
||||
import type { KVNamespace } from "@cloudflare/workers-types";
|
||||
import { encodeBase32, encodeHexLowerCase } from "@oslojs/encoding";
|
||||
import { sha256 } from "@oslojs/crypto/sha2";
|
||||
import type { KVNamespace } from '@cloudflare/workers-types';
|
||||
import { encodeBase32, encodeHexLowerCase } from '@oslojs/encoding';
|
||||
import { sha256 } from '@oslojs/crypto/sha2';
|
||||
|
||||
import type { RequestEvent } from "@sveltejs/kit";
|
||||
import type { RequestEvent } from '@sveltejs/kit';
|
||||
|
||||
// in seconds
|
||||
const EXPIRATION_TTL: number = 60 * 60 * 24 * 7;
|
||||
|
||||
export async function validateSessionToken(token: string, sessions: KVNamespace): Promise<SessionValidationResult> {
|
||||
export async function validateSessionToken(
|
||||
token: string,
|
||||
sessions: KVNamespace
|
||||
): Promise<SessionValidationResult> {
|
||||
const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token)));
|
||||
const session: Session | null = await sessions.get(sessionId, { type: "json" });
|
||||
const session: Session | null = await sessions.get(sessionId, { type: 'json' });
|
||||
|
||||
if (!session) {
|
||||
return null;
|
||||
@@ -33,21 +36,21 @@ export async function invalidateUserSessions(userId: number, sessions: KVNamespa
|
||||
}
|
||||
|
||||
export function setSessionTokenCookie(event: RequestEvent, token: string, expiresAt: Date): void {
|
||||
event.cookies.set("session", token, {
|
||||
event.cookies.set('session', token, {
|
||||
httpOnly: true,
|
||||
path: "/",
|
||||
path: '/',
|
||||
secure: import.meta.env.PROD,
|
||||
sameSite: "lax",
|
||||
sameSite: 'lax',
|
||||
expires: expiresAt
|
||||
});
|
||||
}
|
||||
|
||||
export function deleteSessionTokenCookie(event: RequestEvent): void {
|
||||
event.cookies.set("session", "", {
|
||||
event.cookies.set('session', '', {
|
||||
httpOnly: true,
|
||||
path: "/",
|
||||
path: '/',
|
||||
secure: import.meta.env.PROD,
|
||||
sameSite: "lax",
|
||||
sameSite: 'lax',
|
||||
maxAge: 0
|
||||
});
|
||||
}
|
||||
@@ -59,7 +62,11 @@ export function generateSessionToken(): string {
|
||||
return token;
|
||||
}
|
||||
|
||||
export async function createSession(token: string, userId: string, sessions: KVNamespace): Promise<Session> {
|
||||
export async function createSession(
|
||||
token: string,
|
||||
userId: string,
|
||||
sessions: KVNamespace
|
||||
): Promise<Session> {
|
||||
const sessionId = `${userId}:${encodeHexLowerCase(sha256(new TextEncoder().encode(token)))}`;
|
||||
const session: Session = {
|
||||
id: sessionId,
|
||||
|
@@ -4,37 +4,37 @@ import { i18n } from '$lib/i18n';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
vi.mock('$lib/i18n', () => ({
|
||||
i18n: {
|
||||
route: vi.fn().mockImplementation((translatedPath: string) => ''),
|
||||
resolveRoute: vi.fn().mockImplementation((path: string, lang?: string) => '')
|
||||
}
|
||||
i18n: {
|
||||
route: vi.fn().mockImplementation((translatedPath: string) => ''),
|
||||
resolveRoute: vi.fn().mockImplementation((path: string, lang?: string) => '')
|
||||
}
|
||||
}));
|
||||
|
||||
vi.mock('$app/state', () => ({
|
||||
page: {
|
||||
url: {
|
||||
pathname: '/current-path'
|
||||
}
|
||||
}
|
||||
page: {
|
||||
url: {
|
||||
pathname: '/current-path'
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
vi.mock('$app/navigation', () => ({
|
||||
goto: vi.fn()
|
||||
goto: vi.fn()
|
||||
}));
|
||||
|
||||
describe('switchToLanguage', () => {
|
||||
it('should switch to the new language', () => {
|
||||
const newLanguage = 'en';
|
||||
const canonicalPath = '/canonical-path';
|
||||
const localisedPath = '/en/canonical-path';
|
||||
it('should switch to the new language', () => {
|
||||
const newLanguage = 'en';
|
||||
const canonicalPath = '/canonical-path';
|
||||
const localisedPath = '/en/canonical-path';
|
||||
|
||||
i18n.route.mockReturnValue(canonicalPath);
|
||||
i18n.resolveRoute.mockReturnValue(localisedPath);
|
||||
i18n.route.mockReturnValue(canonicalPath);
|
||||
i18n.resolveRoute.mockReturnValue(localisedPath);
|
||||
|
||||
switchToLanguage(newLanguage);
|
||||
switchToLanguage(newLanguage);
|
||||
|
||||
expect(i18n.route).toHaveBeenCalledWith('/current-path');
|
||||
expect(i18n.resolveRoute).toHaveBeenCalledWith(canonicalPath, newLanguage);
|
||||
expect(goto).toHaveBeenCalledWith(localisedPath);
|
||||
});
|
||||
});
|
||||
expect(i18n.route).toHaveBeenCalledWith('/current-path');
|
||||
expect(i18n.resolveRoute).toHaveBeenCalledWith(canonicalPath, newLanguage);
|
||||
expect(goto).toHaveBeenCalledWith(localisedPath);
|
||||
});
|
||||
});
|
||||
|
@@ -4,7 +4,7 @@ import { page } from '$app/state';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
export function switchToLanguage(newLanguage: AvailableLanguageTag) {
|
||||
const canonicalPath = i18n.route(page.url.pathname);
|
||||
const localisedPath = i18n.resolveRoute(canonicalPath, newLanguage);
|
||||
goto(localisedPath);
|
||||
}
|
||||
const canonicalPath = i18n.route(page.url.pathname);
|
||||
const localisedPath = i18n.resolveRoute(canonicalPath, newLanguage);
|
||||
goto(localisedPath);
|
||||
}
|
||||
|
@@ -37,7 +37,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="dropdown dropdown-end block ">
|
||||
<div class="dropdown dropdown-end block">
|
||||
<select
|
||||
bind:value={current_theme}
|
||||
data-choose-theme
|
||||
@@ -48,11 +48,7 @@
|
||||
{theme()}
|
||||
</option>
|
||||
{#each themes as theme}
|
||||
<option
|
||||
value={theme}
|
||||
class="theme-controller capitalize"
|
||||
>{themeMessages.get(theme)}</option
|
||||
>
|
||||
<option value={theme} class="theme-controller capitalize">{themeMessages.get(theme)}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
|
@@ -1 +1 @@
|
||||
export const themes = ['light', 'dark','coffee', 'cyberpunk', 'synthwave', 'retro', 'dracula'];
|
||||
export const themes = ['light', 'dark', 'coffee', 'cyberpunk', 'synthwave', 'retro', 'dracula'];
|
||||
|
@@ -1,10 +1,10 @@
|
||||
import { fail, redirect } from "@sveltejs/kit";
|
||||
import { deleteSessionTokenCookie, invalidateSession } from "$lib/server/session";
|
||||
import type { Actions, RequestEvent } from "./$types";
|
||||
import { fail, redirect } from '@sveltejs/kit';
|
||||
import { deleteSessionTokenCookie, invalidateSession } from '$lib/server/session';
|
||||
import type { Actions, RequestEvent } from './$types';
|
||||
|
||||
export async function load(event: RequestEvent) {
|
||||
if (event.locals.session === null /*|| event.locals.familytree === nul*/) {
|
||||
return redirect(302, "/login");
|
||||
return redirect(302, '/login');
|
||||
}
|
||||
return {
|
||||
// TODO - Add Family Graph
|
||||
@@ -19,14 +19,14 @@ async function logout(event: RequestEvent) {
|
||||
if (event.locals.session === null) {
|
||||
return fail(401);
|
||||
}
|
||||
|
||||
|
||||
if (event.platform && event.platform.env && event.platform.env.GH_SESSIONS) {
|
||||
invalidateSession(event.locals.session.id, event.platform.env.GH_SESSIONS);
|
||||
} else {
|
||||
return fail(500, { message: "Server configuration error" });
|
||||
return fail(500, { message: 'Server configuration error' });
|
||||
}
|
||||
|
||||
deleteSessionTokenCookie(event);
|
||||
|
||||
return redirect(302, "/login");
|
||||
}
|
||||
return redirect(302, '/login');
|
||||
}
|
||||
|
@@ -1,10 +1,17 @@
|
||||
<script lang="ts">
|
||||
import {title, family_tree} from '$lib/paraglide/messages.js';
|
||||
import { SvelteFlowProvider,Background, BackgroundVariant, SvelteFlow, Controls, MiniMap } from '@xyflow/svelte';
|
||||
import type { Node, Edge, NodeTypes, NodeProps } from '@xyflow/svelte';
|
||||
import { title, family_tree } from '$lib/paraglide/messages.js';
|
||||
import {
|
||||
SvelteFlowProvider,
|
||||
Background,
|
||||
BackgroundVariant,
|
||||
SvelteFlow,
|
||||
Controls,
|
||||
MiniMap
|
||||
} from '@xyflow/svelte';
|
||||
import type { Node, Edge, NodeTypes, NodeProps } from '@xyflow/svelte';
|
||||
|
||||
let nodes = $state.raw<Node[]>([]);
|
||||
let edges = $state.raw<Edge[]>([]);
|
||||
let nodes = $state.raw<Node[]>([]);
|
||||
let edges = $state.raw<Edge[]>([]);
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -19,4 +26,3 @@
|
||||
</SvelteFlow>
|
||||
</SvelteFlowProvider>
|
||||
</div>
|
||||
|
||||
|
@@ -1,11 +1,11 @@
|
||||
import { redirect } from "@sveltejs/kit";
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
|
||||
import type { RequestEvent } from "./$types";
|
||||
import type { RequestEvent } from './$types';
|
||||
|
||||
export async function load(event: RequestEvent) {
|
||||
if (event.locals.session !== null) {
|
||||
return redirect(302, "/");
|
||||
return redirect(302, '/');
|
||||
}
|
||||
|
||||
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
@@ -17,7 +17,7 @@
|
||||
<p class="py-6">
|
||||
{site_intro()}
|
||||
</p>
|
||||
<a href="/login/google" class="btn rounded-full bg-white text-black border-[#e5e5e5]">
|
||||
<a href="/login/google" class="btn rounded-full border-[#e5e5e5] bg-white text-black">
|
||||
<!-- Google -->
|
||||
<svg
|
||||
aria-label="Google logo"
|
||||
|
@@ -1,26 +1,26 @@
|
||||
import { google } from "$lib/server/oauth";
|
||||
import { generateCodeVerifier, generateState } from "arctic";
|
||||
import { google } from '$lib/server/oauth';
|
||||
import { generateCodeVerifier, generateState } from 'arctic';
|
||||
|
||||
import type { RequestEvent } from "./$types";
|
||||
import type { RequestEvent } from './$types';
|
||||
|
||||
export function GET(event: RequestEvent): Response {
|
||||
const state = generateState();
|
||||
const codeVerifier = generateCodeVerifier();
|
||||
const url = google.createAuthorizationURL(state, codeVerifier, ["openid", "profile", "email"]);
|
||||
const url = google.createAuthorizationURL(state, codeVerifier, ['openid', 'profile', 'email']);
|
||||
|
||||
event.cookies.set("google_oauth_state", state, {
|
||||
event.cookies.set('google_oauth_state', state, {
|
||||
httpOnly: true,
|
||||
maxAge: 60 * 10,
|
||||
secure: import.meta.env.PROD,
|
||||
path: "/",
|
||||
sameSite: "lax"
|
||||
path: '/',
|
||||
sameSite: 'lax'
|
||||
});
|
||||
event.cookies.set("google_code_verifier", codeVerifier, {
|
||||
event.cookies.set('google_code_verifier', codeVerifier, {
|
||||
httpOnly: true,
|
||||
maxAge: 60 * 10,
|
||||
secure: import.meta.env.PROD,
|
||||
path: "/",
|
||||
sameSite: "lax"
|
||||
path: '/',
|
||||
sameSite: 'lax'
|
||||
});
|
||||
|
||||
return new Response(null, {
|
||||
@@ -29,4 +29,4 @@ export function GET(event: RequestEvent): Response {
|
||||
Location: url.toString()
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -1,43 +1,51 @@
|
||||
import { google } from "$lib/server/oauth";
|
||||
import { ObjectParser } from "@pilcrowjs/object-parser";
|
||||
import { createUser, getUserFromGoogleId } from "$lib/server/user";
|
||||
import { DB } from "$lib/server/db";
|
||||
import { google } from '$lib/server/oauth';
|
||||
import { ObjectParser } from '@pilcrowjs/object-parser';
|
||||
import { createUser, getUserFromGoogleId } from '$lib/server/user';
|
||||
import { DB } from '$lib/server/db';
|
||||
import { browser } from '$app/environment';
|
||||
import { Date as neoDate } from 'neo4j-driver';
|
||||
import { createSession, generateSessionToken, setSessionTokenCookie } from "$lib/server/session";
|
||||
import { decodeIdToken } from "arctic";
|
||||
import { missing_field, last_name, first_name, mothers_first_name, mothers_last_name, born, failed_to_create_user } from "$lib/paraglide/messages";
|
||||
import { createSession, generateSessionToken, setSessionTokenCookie } from '$lib/server/session';
|
||||
import { decodeIdToken } from 'arctic';
|
||||
import {
|
||||
missing_field,
|
||||
last_name,
|
||||
first_name,
|
||||
mothers_first_name,
|
||||
mothers_last_name,
|
||||
born,
|
||||
failed_to_create_user
|
||||
} from '$lib/paraglide/messages';
|
||||
|
||||
import type { PageServerLoad, Actions, RequestEvent, PageData } from "./$types";
|
||||
import type { OAuth2Tokens } from "arctic";
|
||||
import type { PageServerLoad, Actions, RequestEvent, PageData } from './$types';
|
||||
import type { OAuth2Tokens } from 'arctic';
|
||||
import type { PersonProperties } from '$lib/model';
|
||||
import { error, redirect, fail } from "@sveltejs/kit";
|
||||
import { error, redirect, fail } from '@sveltejs/kit';
|
||||
|
||||
const StorageLimit = 200 * 1024 * 1024;
|
||||
|
||||
export const load: PageServerLoad = async (event: RequestEvent) => {
|
||||
//prevent loading in developer mode, due to some issues with universal load, even if this is a server only ts,it will still run on client in dev mode idk
|
||||
if (browser) {
|
||||
return {}
|
||||
if (browser) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const storedState = event.cookies.get("google_oauth_state") ?? null;
|
||||
const codeVerifier = event.cookies.get("google_code_verifier") ?? null;
|
||||
const code = event.url.searchParams.get("code");
|
||||
const state = event.url.searchParams.get("state");
|
||||
const storedState = event.cookies.get('google_oauth_state') ?? null;
|
||||
const codeVerifier = event.cookies.get('google_code_verifier') ?? null;
|
||||
const code = event.url.searchParams.get('code');
|
||||
const state = event.url.searchParams.get('state');
|
||||
|
||||
if (storedState === null || codeVerifier === null || code === null || state === null) {
|
||||
return error(400, { message: "Please restart the process." })
|
||||
return error(400, { message: 'Please restart the process.' });
|
||||
}
|
||||
if (storedState !== state) {
|
||||
return error(400, { message: "Please restart the process." })
|
||||
return error(400, { message: 'Please restart the process.' });
|
||||
}
|
||||
|
||||
let tokens: OAuth2Tokens;
|
||||
try {
|
||||
tokens = await google.validateAuthorizationCode(code, codeVerifier);
|
||||
} catch (e) {
|
||||
return error(400, { message: "Failed to validate authorization code with " + e });
|
||||
return error(400, { message: 'Failed to validate authorization code with ' + e });
|
||||
}
|
||||
|
||||
// if (!event.platform || !event.platform.env || !event.platform.env.GH_SESSIONS) {
|
||||
@@ -47,10 +55,10 @@ export const load: PageServerLoad = async (event: RequestEvent) => {
|
||||
const claims = decodeIdToken(tokens.idToken());
|
||||
const claimsParser = new ObjectParser(claims);
|
||||
|
||||
const sub = claimsParser.getString("sub");
|
||||
const family_name = claimsParser.getString("family_name");
|
||||
const first_name = claimsParser.getString("given_name");
|
||||
const email = claimsParser.getString("email");
|
||||
const sub = claimsParser.getString('sub');
|
||||
const family_name = claimsParser.getString('family_name');
|
||||
const first_name = claimsParser.getString('given_name');
|
||||
const email = claimsParser.getString('email');
|
||||
|
||||
const dbSession = DB.session();
|
||||
const existingUser = await getUserFromGoogleId(dbSession, sub);
|
||||
@@ -62,7 +70,7 @@ export const load: PageServerLoad = async (event: RequestEvent) => {
|
||||
// const session = await createSession(sessionToken, eUser.get('elementId'), event.platform.env.GH_SESSIONS);
|
||||
// setSessionTokenCookie(event, sessionToken, session.expiresAt);
|
||||
|
||||
return redirect(302, "/");
|
||||
return redirect(302, '/');
|
||||
}
|
||||
|
||||
let personP: PersonProperties = {
|
||||
@@ -72,13 +80,13 @@ export const load: PageServerLoad = async (event: RequestEvent) => {
|
||||
email: email,
|
||||
allow_admin_access: false,
|
||||
limit: StorageLimit,
|
||||
verified: false,
|
||||
verified: false
|
||||
};
|
||||
|
||||
return {
|
||||
props: personP
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const actions: Actions = {
|
||||
register: register
|
||||
@@ -89,15 +97,15 @@ async function register(event: RequestEvent) {
|
||||
// if (!event.platform || !event.platform.env || !event.platform.env.GH_SESSIONS) {
|
||||
// return fail(500, { message: "Server configuration error. GH_SESSIONS KeyValue store missing" });
|
||||
// }
|
||||
const google_id = data.get('google_id')
|
||||
const google_id = data.get('google_id');
|
||||
if (google_id === null) {
|
||||
return fail(400, {
|
||||
message: missing_field({
|
||||
field: "google_id"
|
||||
field: 'google_id'
|
||||
})
|
||||
});
|
||||
}
|
||||
const first_name_f = data.get('first_name')
|
||||
const first_name_f = data.get('first_name');
|
||||
if (first_name_f === null) {
|
||||
return fail(400, {
|
||||
message: missing_field({
|
||||
@@ -105,49 +113,44 @@ async function register(event: RequestEvent) {
|
||||
})
|
||||
});
|
||||
}
|
||||
const last_name_f = data.get('last_name')
|
||||
const last_name_f = data.get('last_name');
|
||||
if (last_name_f === null) {
|
||||
return fail(400, {
|
||||
message:
|
||||
missing_field({
|
||||
field: last_name()
|
||||
})
|
||||
message: missing_field({
|
||||
field: last_name()
|
||||
})
|
||||
});
|
||||
}
|
||||
const email = data.get('email')
|
||||
const email = data.get('email');
|
||||
if (email === null) {
|
||||
return fail(400, {
|
||||
message:
|
||||
missing_field({
|
||||
field: "Email"
|
||||
})
|
||||
message: missing_field({
|
||||
field: 'Email'
|
||||
})
|
||||
});
|
||||
}
|
||||
const birth_date = data.get('birth_date');
|
||||
if (birth_date === null) {
|
||||
return fail(400, {
|
||||
message:
|
||||
missing_field({
|
||||
field: born()
|
||||
})
|
||||
message: missing_field({
|
||||
field: born()
|
||||
})
|
||||
});
|
||||
}
|
||||
const mothers_first_name_f = data.get('mothers_first_name');
|
||||
if (mothers_first_name_f === null) {
|
||||
return fail(400, {
|
||||
message:
|
||||
missing_field({
|
||||
field: mothers_first_name()
|
||||
})
|
||||
message: missing_field({
|
||||
field: mothers_first_name()
|
||||
})
|
||||
});
|
||||
}
|
||||
const mothers_last_name_f = data.get('mothers_last_name');
|
||||
if (mothers_last_name_f === null) {
|
||||
return fail(400, {
|
||||
message:
|
||||
missing_field({
|
||||
field: mothers_last_name()
|
||||
})
|
||||
message: missing_field({
|
||||
field: mothers_last_name()
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
@@ -158,12 +161,16 @@ async function register(event: RequestEvent) {
|
||||
first_name: first_name_f as string,
|
||||
last_name: last_name_f as string,
|
||||
email: email as string,
|
||||
born: new neoDate(parsed_date.getFullYear(), parsed_date.getUTCMonth(), parsed_date.getUTCDate()),
|
||||
born: new neoDate(
|
||||
parsed_date.getFullYear(),
|
||||
parsed_date.getUTCMonth(),
|
||||
parsed_date.getUTCDate()
|
||||
),
|
||||
mothers_first_name: mothers_first_name_f as string,
|
||||
mothers_last_name: mothers_last_name_f as string,
|
||||
allow_admin_access: false,
|
||||
limit: StorageLimit,
|
||||
verified: false,
|
||||
verified: false
|
||||
};
|
||||
|
||||
const dbSession = DB.session();
|
||||
@@ -179,5 +186,5 @@ async function register(event: RequestEvent) {
|
||||
// const session = await createSession(sessionToken, user.get('elementId'), event.platform.env.GH_SESSIONS);
|
||||
// setSessionTokenCookie(event, sessionToken, session.expiresAt);
|
||||
|
||||
return redirect(302, "/");
|
||||
}
|
||||
return redirect(302, '/');
|
||||
}
|
||||
|
@@ -12,7 +12,7 @@
|
||||
last_name,
|
||||
first_name,
|
||||
email,
|
||||
allow_family_tree_admin_access,
|
||||
allow_family_tree_admin_access
|
||||
} from '$lib/paraglide/messages';
|
||||
import FamilyTree from '../../highresolution_icon_no_background_croped.png';
|
||||
let { data, form }: PageProps = $props();
|
||||
@@ -49,24 +49,65 @@
|
||||
<form method="POST" action="/register">
|
||||
<fieldset class="fieldset">
|
||||
{#if form?.message}
|
||||
<div role="alert" class="alert alert-error">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 shrink-0 stroke-current" fill="none" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span>{form.message}</span>
|
||||
</div>
|
||||
{/if }
|
||||
<div role="alert" class="alert alert-error">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6 shrink-0 stroke-current"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<span>{form.message}</span>
|
||||
</div>
|
||||
{/if}
|
||||
<label class="fieldset-label" for="email">Email</label>
|
||||
<input type="email" class="input" placeholder="Email" value="{data.props.email}" />
|
||||
<input type="text" class="hidden" id="google_id" placeholder="Google ID" value="{data.props.google_id}" />
|
||||
<input type="email" class="input" placeholder="Email" value={data.props.email} />
|
||||
<input
|
||||
type="text"
|
||||
class="hidden"
|
||||
id="google_id"
|
||||
placeholder="Google ID"
|
||||
value={data.props.google_id}
|
||||
/>
|
||||
<label class="fieldset-label" for="first_name">{first_name()}</label>
|
||||
<input type="text" class="input" id="first_name" placeholder="{first_name()}" value="{data.props.first_name}" />
|
||||
<input
|
||||
type="text"
|
||||
class="input"
|
||||
id="first_name"
|
||||
placeholder={first_name()}
|
||||
value={data.props.first_name}
|
||||
/>
|
||||
<label class="fieldset-label" for="last_name">{last_name()}</label>
|
||||
<input type="text" class="input" id="last_name" placeholder="{last_name()}" value="{data.props.last_name}" />
|
||||
<label class="fieldset-label" for="allow_admin_access">{allow_family_tree_admin_access()}</label>
|
||||
<input type="checkbox" class="input" id="allow_admin_access" checked="{data.props.allow_admin_access}" />
|
||||
<input
|
||||
type="text"
|
||||
class="input"
|
||||
id="last_name"
|
||||
placeholder={last_name()}
|
||||
value={data.props.last_name}
|
||||
/>
|
||||
<label class="fieldset-label" for="allow_admin_access"
|
||||
>{allow_family_tree_admin_access()}</label
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
class="input"
|
||||
id="allow_admin_access"
|
||||
checked={data.props.allow_admin_access}
|
||||
/>
|
||||
<label class="fieldset-label" for="birth_date">{born()}</label>
|
||||
<input type="text" class="input pika-single" id="birth_date" bind:this={birth_date} value={born()} />
|
||||
<input
|
||||
type="text"
|
||||
class="input pika-single"
|
||||
id="birth_date"
|
||||
bind:this={birth_date}
|
||||
value={born()}
|
||||
/>
|
||||
<label class="fieldset-label" for="mothers_last_name">{mothers_last_name()}</label>
|
||||
<input
|
||||
type="text"
|
||||
@@ -75,7 +116,7 @@
|
||||
placeholder={mothers_last_name()}
|
||||
/>
|
||||
<label class="fieldset-label" for="mothers_first_name">{mothers_first_name()}</label>
|
||||
<input
|
||||
<input
|
||||
type="text"
|
||||
class="input"
|
||||
id="mothers_first_name"
|
||||
|
@@ -1,7 +1,7 @@
|
||||
<script module>
|
||||
import Page from './+page.svelte';
|
||||
import Page from './+page.svelte';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page />
|
||||
</template>
|
||||
<Page />
|
||||
</template>
|
||||
|
@@ -1,24 +1,24 @@
|
||||
<script module>
|
||||
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||
import Button from './Button.svelte';
|
||||
import { fn } from '@storybook/test';
|
||||
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||
import Button from './Button.svelte';
|
||||
import { fn } from '@storybook/test';
|
||||
|
||||
// More on how to set up stories at: https://storybook.js.org/docs/writing-stories
|
||||
const { Story } = defineMeta({
|
||||
title: 'Example/Button',
|
||||
component: Button,
|
||||
tags: ['autodocs'],
|
||||
argTypes: {
|
||||
backgroundColor: { control: 'color' },
|
||||
size: {
|
||||
control: { type: 'select' },
|
||||
options: ['small', 'medium', 'large'],
|
||||
},
|
||||
},
|
||||
args: {
|
||||
onClick: fn(),
|
||||
}
|
||||
});
|
||||
// More on how to set up stories at: https://storybook.js.org/docs/writing-stories
|
||||
const { Story } = defineMeta({
|
||||
title: 'Example/Button',
|
||||
component: Button,
|
||||
tags: ['autodocs'],
|
||||
argTypes: {
|
||||
backgroundColor: { control: 'color' },
|
||||
size: {
|
||||
control: { type: 'select' },
|
||||
options: ['small', 'medium', 'large']
|
||||
}
|
||||
},
|
||||
args: {
|
||||
onClick: fn()
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- More on writing stories with args: https://storybook.js.org/docs/writing-stories/args -->
|
||||
|
@@ -1,29 +1,29 @@
|
||||
<script lang="ts">
|
||||
import './button.css';
|
||||
import './button.css';
|
||||
|
||||
interface Props {
|
||||
/** Is this the principal call to action on the page? */
|
||||
primary?: boolean;
|
||||
/** What background color to use */
|
||||
backgroundColor?: string;
|
||||
/** How large should the button be? */
|
||||
size?: 'small' | 'medium' | 'large';
|
||||
/** Button contents */
|
||||
label: string;
|
||||
/** The onclick event handler */
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
const { primary = false, backgroundColor, size = 'medium', label, onClick }: Props = $props();
|
||||
interface Props {
|
||||
/** Is this the principal call to action on the page? */
|
||||
primary?: boolean;
|
||||
/** What background color to use */
|
||||
backgroundColor?: string;
|
||||
/** How large should the button be? */
|
||||
size?: 'small' | 'medium' | 'large';
|
||||
/** Button contents */
|
||||
label: string;
|
||||
/** The onclick event handler */
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
const { primary = false, backgroundColor, size = 'medium', label, onClick }: Props = $props();
|
||||
</script>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class={['storybook-button', `storybook-button--${size}`].join(' ')}
|
||||
class:storybook-button--primary={primary}
|
||||
class:storybook-button--secondary={!primary}
|
||||
style:background-color={backgroundColor}
|
||||
onclick={onClick}
|
||||
type="button"
|
||||
class={['storybook-button', `storybook-button--${size}`].join(' ')}
|
||||
class:storybook-button--primary={primary}
|
||||
class:storybook-button--secondary={!primary}
|
||||
style:background-color={backgroundColor}
|
||||
onclick={onClick}
|
||||
>
|
||||
{label}
|
||||
{label}
|
||||
</button>
|
||||
|
@@ -1,35 +1,37 @@
|
||||
import { Meta } from "@storybook/blocks";
|
||||
import { Meta } from '@storybook/blocks';
|
||||
|
||||
import Github from "./assets/github.svg";
|
||||
import Discord from "./assets/discord.svg";
|
||||
import Youtube from "./assets/youtube.svg";
|
||||
import Tutorials from "./assets/tutorials.svg";
|
||||
import Styling from "./assets/styling.png";
|
||||
import Context from "./assets/context.png";
|
||||
import Assets from "./assets/assets.png";
|
||||
import Docs from "./assets/docs.png";
|
||||
import Share from "./assets/share.png";
|
||||
import FigmaPlugin from "./assets/figma-plugin.png";
|
||||
import Testing from "./assets/testing.png";
|
||||
import Accessibility from "./assets/accessibility.png";
|
||||
import Theming from "./assets/theming.png";
|
||||
import AddonLibrary from "./assets/addon-library.png";
|
||||
import Github from './assets/github.svg';
|
||||
import Discord from './assets/discord.svg';
|
||||
import Youtube from './assets/youtube.svg';
|
||||
import Tutorials from './assets/tutorials.svg';
|
||||
import Styling from './assets/styling.png';
|
||||
import Context from './assets/context.png';
|
||||
import Assets from './assets/assets.png';
|
||||
import Docs from './assets/docs.png';
|
||||
import Share from './assets/share.png';
|
||||
import FigmaPlugin from './assets/figma-plugin.png';
|
||||
import Testing from './assets/testing.png';
|
||||
import Accessibility from './assets/accessibility.png';
|
||||
import Theming from './assets/theming.png';
|
||||
import AddonLibrary from './assets/addon-library.png';
|
||||
|
||||
export const RightArrow = () => <svg
|
||||
viewBox="0 0 14 14"
|
||||
width="8px"
|
||||
height="14px"
|
||||
style={{
|
||||
marginLeft: '4px',
|
||||
display: 'inline-block',
|
||||
shapeRendering: 'inherit',
|
||||
verticalAlign: 'middle',
|
||||
fill: 'currentColor',
|
||||
'path fill': 'currentColor'
|
||||
}}
|
||||
>
|
||||
<path d="m11.1 7.35-5.5 5.5a.5.5 0 0 1-.7-.7L10.04 7 4.9 1.85a.5.5 0 1 1 .7-.7l5.5 5.5c.2.2.2.5 0 .7Z" />
|
||||
</svg>
|
||||
export const RightArrow = () => (
|
||||
<svg
|
||||
viewBox="0 0 14 14"
|
||||
width="8px"
|
||||
height="14px"
|
||||
style={{
|
||||
marginLeft: '4px',
|
||||
display: 'inline-block',
|
||||
shapeRendering: 'inherit',
|
||||
verticalAlign: 'middle',
|
||||
fill: 'currentColor',
|
||||
'path fill': 'currentColor'
|
||||
}}
|
||||
>
|
||||
<path d="m11.1 7.35-5.5 5.5a.5.5 0 0 1-.7-.7L10.04 7 4.9 1.85a.5.5 0 1 1 .7-.7l5.5 5.5c.2.2.2.5 0 .7Z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
<Meta title="Configure your project" />
|
||||
|
||||
@@ -38,6 +40,7 @@ export const RightArrow = () => <svg
|
||||
# Configure your project
|
||||
|
||||
Because Storybook works separately from your app, you'll need to configure it for your specific stack and setup. Below, explore guides for configuring Storybook with popular frameworks and tools. If you get stuck, learn how you can ask for help from our community.
|
||||
|
||||
</div>
|
||||
<div className="sb-section">
|
||||
<div className="sb-section-item">
|
||||
@@ -84,6 +87,7 @@ export const RightArrow = () => <svg
|
||||
# Do more with Storybook
|
||||
|
||||
Now that you know the basics, let's explore other parts of Storybook that will improve your experience. This list is just to get you started. You can customise Storybook in many ways to fit your needs.
|
||||
|
||||
</div>
|
||||
|
||||
<div className="sb-section">
|
||||
@@ -203,10 +207,11 @@ export const RightArrow = () => <svg
|
||||
target="_blank"
|
||||
>Discover tutorials<RightArrow /></a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<style>
|
||||
{`
|
||||
{`
|
||||
.sb-container {
|
||||
margin-bottom: 48px;
|
||||
}
|
||||
|
@@ -1,24 +1,24 @@
|
||||
<script module>
|
||||
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||
import Header from './Header.svelte';
|
||||
import { fn } from '@storybook/test';
|
||||
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||
import Header from './Header.svelte';
|
||||
import { fn } from '@storybook/test';
|
||||
|
||||
// More on how to set up stories at: https://storybook.js.org/docs/writing-stories
|
||||
const { Story } = defineMeta({
|
||||
title: 'Example/Header',
|
||||
component: Header,
|
||||
// This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
|
||||
tags: ['autodocs'],
|
||||
parameters: {
|
||||
// More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
|
||||
layout: 'fullscreen',
|
||||
},
|
||||
args: {
|
||||
onLogin: fn(),
|
||||
onLogout: fn(),
|
||||
onCreateAccount: fn(),
|
||||
}
|
||||
});
|
||||
// More on how to set up stories at: https://storybook.js.org/docs/writing-stories
|
||||
const { Story } = defineMeta({
|
||||
title: 'Example/Header',
|
||||
component: Header,
|
||||
// This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
|
||||
tags: ['autodocs'],
|
||||
parameters: {
|
||||
// More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
|
||||
layout: 'fullscreen'
|
||||
},
|
||||
args: {
|
||||
onLogin: fn(),
|
||||
onLogout: fn(),
|
||||
onCreateAccount: fn()
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<Story name="Logged In" args={{ user: { name: 'Jane Doe' } }} />
|
||||
|
@@ -1,45 +1,45 @@
|
||||
<script lang="ts">
|
||||
import './header.css';
|
||||
import Button from './Button.svelte';
|
||||
import './header.css';
|
||||
import Button from './Button.svelte';
|
||||
|
||||
interface Props {
|
||||
user?: { name: string };
|
||||
onLogin?: () => void;
|
||||
onLogout?: () => void;
|
||||
onCreateAccount?: () => void;
|
||||
}
|
||||
interface Props {
|
||||
user?: { name: string };
|
||||
onLogin?: () => void;
|
||||
onLogout?: () => void;
|
||||
onCreateAccount?: () => void;
|
||||
}
|
||||
|
||||
const { user, onLogin, onLogout, onCreateAccount }: Props = $props();
|
||||
const { user, onLogin, onLogout, onCreateAccount }: Props = $props();
|
||||
</script>
|
||||
|
||||
<header>
|
||||
<div class="storybook-header">
|
||||
<div>
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<path
|
||||
d="M10 0h12a10 10 0 0110 10v12a10 10 0 01-10 10H10A10 10 0 010 22V10A10 10 0 0110 0z"
|
||||
fill="#FFF"
|
||||
/>
|
||||
<path
|
||||
d="M5.3 10.6l10.4 6v11.1l-10.4-6v-11zm11.4-6.2l9.7 5.5-9.7 5.6V4.4z"
|
||||
fill="#555AB9"
|
||||
/>
|
||||
<path d="M27.2 10.6v11.2l-10.5 6V16.5l10.5-6zM15.7 4.4v11L6 10l9.7-5.5z" fill="#91BAF8" />
|
||||
</g>
|
||||
</svg>
|
||||
<h1>Acme</h1>
|
||||
</div>
|
||||
<div>
|
||||
{#if user}
|
||||
<span class="welcome">
|
||||
Welcome, <b>{user.name}</b>!
|
||||
</span>
|
||||
<Button size="small" onClick={onLogout} label="Log out" />
|
||||
{:else}
|
||||
<Button size="small" onClick={onLogin} label="Log in" />
|
||||
<Button primary size="small" onClick={onCreateAccount} label="Sign up" />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="storybook-header">
|
||||
<div>
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<path
|
||||
d="M10 0h12a10 10 0 0110 10v12a10 10 0 01-10 10H10A10 10 0 010 22V10A10 10 0 0110 0z"
|
||||
fill="#FFF"
|
||||
/>
|
||||
<path
|
||||
d="M5.3 10.6l10.4 6v11.1l-10.4-6v-11zm11.4-6.2l9.7 5.5-9.7 5.6V4.4z"
|
||||
fill="#555AB9"
|
||||
/>
|
||||
<path d="M27.2 10.6v11.2l-10.5 6V16.5l10.5-6zM15.7 4.4v11L6 10l9.7-5.5z" fill="#91BAF8" />
|
||||
</g>
|
||||
</svg>
|
||||
<h1>Acme</h1>
|
||||
</div>
|
||||
<div>
|
||||
{#if user}
|
||||
<span class="welcome">
|
||||
Welcome, <b>{user.name}</b>!
|
||||
</span>
|
||||
<Button size="small" onClick={onLogout} label="Log out" />
|
||||
{:else}
|
||||
<Button size="small" onClick={onLogin} label="Log in" />
|
||||
<Button primary size="small" onClick={onCreateAccount} label="Sign up" />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
@@ -1,30 +1,32 @@
|
||||
<script module>
|
||||
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||
import { expect, userEvent, waitFor, within } from '@storybook/test';
|
||||
import Page from './Page.svelte';
|
||||
import { fn } from '@storybook/test';
|
||||
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||
import { expect, userEvent, waitFor, within } from '@storybook/test';
|
||||
import Page from './Page.svelte';
|
||||
import { fn } from '@storybook/test';
|
||||
|
||||
// More on how to set up stories at: https://storybook.js.org/docs/writing-stories
|
||||
const { Story } = defineMeta({
|
||||
title: 'Example/Page',
|
||||
component: Page,
|
||||
parameters: {
|
||||
// More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
|
||||
layout: 'fullscreen',
|
||||
},
|
||||
});
|
||||
// More on how to set up stories at: https://storybook.js.org/docs/writing-stories
|
||||
const { Story } = defineMeta({
|
||||
title: 'Example/Page',
|
||||
component: Page,
|
||||
parameters: {
|
||||
// More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
|
||||
layout: 'fullscreen'
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<Story name="Logged In" play={async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const loginButton = canvas.getByRole('button', { name: /Log in/i });
|
||||
await expect(loginButton).toBeInTheDocument();
|
||||
await userEvent.click(loginButton);
|
||||
await waitFor(() => expect(loginButton).not.toBeInTheDocument());
|
||||
<Story
|
||||
name="Logged In"
|
||||
play={async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const loginButton = canvas.getByRole('button', { name: /Log in/i });
|
||||
await expect(loginButton).toBeInTheDocument();
|
||||
await userEvent.click(loginButton);
|
||||
await waitFor(() => expect(loginButton).not.toBeInTheDocument());
|
||||
|
||||
const logoutButton = canvas.getByRole('button', { name: /Log out/i });
|
||||
await expect(logoutButton).toBeInTheDocument();
|
||||
}}
|
||||
const logoutButton = canvas.getByRole('button', { name: /Log out/i });
|
||||
await expect(logoutButton).toBeInTheDocument();
|
||||
}}
|
||||
/>
|
||||
|
||||
<Story name="Logged Out" />
|
||||
|
@@ -1,70 +1,70 @@
|
||||
<script lang="ts">
|
||||
import './page.css';
|
||||
import Header from './Header.svelte';
|
||||
import './page.css';
|
||||
import Header from './Header.svelte';
|
||||
|
||||
let user = $state<{ name: string }>();
|
||||
let user = $state<{ name: string }>();
|
||||
</script>
|
||||
|
||||
<article>
|
||||
<Header
|
||||
{user}
|
||||
onLogin={() => (user = { name: 'Jane Doe' })}
|
||||
onLogout={() => (user = undefined)}
|
||||
onCreateAccount={() => (user = { name: 'Jane Doe' })}
|
||||
/>
|
||||
<Header
|
||||
{user}
|
||||
onLogin={() => (user = { name: 'Jane Doe' })}
|
||||
onLogout={() => (user = undefined)}
|
||||
onCreateAccount={() => (user = { name: 'Jane Doe' })}
|
||||
/>
|
||||
|
||||
<section class="storybook-page">
|
||||
<h2>Pages in Storybook</h2>
|
||||
<p>
|
||||
We recommend building UIs with a
|
||||
<a
|
||||
href="https://blog.hichroma.com/component-driven-development-ce1109d56c8e"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<strong>component-driven</strong>
|
||||
</a>
|
||||
process starting with atomic components and ending with pages.
|
||||
</p>
|
||||
<p>
|
||||
Render pages with mock data. This makes it easy to build and review page states without
|
||||
needing to navigate to them in your app. Here are some handy patterns for managing page data
|
||||
in Storybook:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
Use a higher-level connected component. Storybook helps you compose such data from the
|
||||
"args" of child component stories
|
||||
</li>
|
||||
<li>
|
||||
Assemble data in the page component from your services. You can mock these services out
|
||||
using Storybook.
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
Get a guided tutorial on component-driven development at
|
||||
<a href="https://storybook.js.org/tutorials/" target="_blank" rel="noopener noreferrer">
|
||||
Storybook tutorials
|
||||
</a>
|
||||
. Read more in the
|
||||
<a href="https://storybook.js.org/docs" target="_blank" rel="noopener noreferrer">docs</a>
|
||||
.
|
||||
</p>
|
||||
<div class="tip-wrapper">
|
||||
<span class="tip">Tip</span>
|
||||
Adjust the width of the canvas with the
|
||||
<svg width="10" height="10" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<path
|
||||
d="M1.5 5.2h4.8c.3 0 .5.2.5.4v5.1c-.1.2-.3.3-.4.3H1.4a.5.5 0
|
||||
<section class="storybook-page">
|
||||
<h2>Pages in Storybook</h2>
|
||||
<p>
|
||||
We recommend building UIs with a
|
||||
<a
|
||||
href="https://blog.hichroma.com/component-driven-development-ce1109d56c8e"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<strong>component-driven</strong>
|
||||
</a>
|
||||
process starting with atomic components and ending with pages.
|
||||
</p>
|
||||
<p>
|
||||
Render pages with mock data. This makes it easy to build and review page states without
|
||||
needing to navigate to them in your app. Here are some handy patterns for managing page data
|
||||
in Storybook:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
Use a higher-level connected component. Storybook helps you compose such data from the
|
||||
"args" of child component stories
|
||||
</li>
|
||||
<li>
|
||||
Assemble data in the page component from your services. You can mock these services out
|
||||
using Storybook.
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
Get a guided tutorial on component-driven development at
|
||||
<a href="https://storybook.js.org/tutorials/" target="_blank" rel="noopener noreferrer">
|
||||
Storybook tutorials
|
||||
</a>
|
||||
. Read more in the
|
||||
<a href="https://storybook.js.org/docs" target="_blank" rel="noopener noreferrer">docs</a>
|
||||
.
|
||||
</p>
|
||||
<div class="tip-wrapper">
|
||||
<span class="tip">Tip</span>
|
||||
Adjust the width of the canvas with the
|
||||
<svg width="10" height="10" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<path
|
||||
d="M1.5 5.2h4.8c.3 0 .5.2.5.4v5.1c-.1.2-.3.3-.4.3H1.4a.5.5 0
|
||||
01-.5-.4V5.7c0-.3.2-.5.5-.5zm0-2.1h6.9c.3 0 .5.2.5.4v7a.5.5 0 01-1 0V4H1.5a.5.5 0
|
||||
010-1zm0-2.1h9c.3 0 .5.2.5.4v9.1a.5.5 0 01-1 0V2H1.5a.5.5 0 010-1zm4.3 5.2H2V10h3.8V6.2z"
|
||||
id="a"
|
||||
fill="#999"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
Viewports addon in the toolbar
|
||||
</div>
|
||||
</section>
|
||||
id="a"
|
||||
fill="#999"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
Viewports addon in the toolbar
|
||||
</div>
|
||||
</section>
|
||||
</article>
|
||||
|
@@ -1,30 +1,30 @@
|
||||
.storybook-button {
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
border: 0;
|
||||
border-radius: 3em;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
border: 0;
|
||||
border-radius: 3em;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
}
|
||||
.storybook-button--primary {
|
||||
background-color: #555ab9;
|
||||
color: white;
|
||||
background-color: #555ab9;
|
||||
color: white;
|
||||
}
|
||||
.storybook-button--secondary {
|
||||
box-shadow: rgba(0, 0, 0, 0.15) 0px 0px 0px 1px inset;
|
||||
background-color: transparent;
|
||||
color: #333;
|
||||
box-shadow: rgba(0, 0, 0, 0.15) 0px 0px 0px 1px inset;
|
||||
background-color: transparent;
|
||||
color: #333;
|
||||
}
|
||||
.storybook-button--small {
|
||||
padding: 10px 16px;
|
||||
font-size: 12px;
|
||||
padding: 10px 16px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.storybook-button--medium {
|
||||
padding: 11px 20px;
|
||||
font-size: 14px;
|
||||
padding: 11px 20px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.storybook-button--large {
|
||||
padding: 12px 24px;
|
||||
font-size: 16px;
|
||||
padding: 12px 24px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
@@ -1,32 +1,32 @@
|
||||
.storybook-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
||||
padding: 15px 20px;
|
||||
font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
||||
padding: 15px 20px;
|
||||
font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
.storybook-header svg {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.storybook-header h1 {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
margin: 6px 0 6px 10px;
|
||||
font-weight: 700;
|
||||
font-size: 20px;
|
||||
line-height: 1;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
margin: 6px 0 6px 10px;
|
||||
font-weight: 700;
|
||||
font-size: 20px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.storybook-header button + button {
|
||||
margin-left: 10px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.storybook-header .welcome {
|
||||
margin-right: 10px;
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
margin-right: 10px;
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
@@ -1,68 +1,68 @@
|
||||
.storybook-page {
|
||||
margin: 0 auto;
|
||||
padding: 48px 20px;
|
||||
max-width: 600px;
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
line-height: 24px;
|
||||
font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
margin: 0 auto;
|
||||
padding: 48px 20px;
|
||||
max-width: 600px;
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
line-height: 24px;
|
||||
font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
.storybook-page h2 {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
margin: 0 0 4px;
|
||||
font-weight: 700;
|
||||
font-size: 32px;
|
||||
line-height: 1;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
margin: 0 0 4px;
|
||||
font-weight: 700;
|
||||
font-size: 32px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.storybook-page p {
|
||||
margin: 1em 0;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.storybook-page a {
|
||||
color: inherit;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.storybook-page ul {
|
||||
margin: 1em 0;
|
||||
padding-left: 30px;
|
||||
margin: 1em 0;
|
||||
padding-left: 30px;
|
||||
}
|
||||
|
||||
.storybook-page li {
|
||||
margin-bottom: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.storybook-page .tip {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
margin-right: 10px;
|
||||
border-radius: 1em;
|
||||
background: #e7fdd8;
|
||||
padding: 4px 12px;
|
||||
color: #357a14;
|
||||
font-weight: 700;
|
||||
font-size: 11px;
|
||||
line-height: 12px;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
margin-right: 10px;
|
||||
border-radius: 1em;
|
||||
background: #e7fdd8;
|
||||
padding: 4px 12px;
|
||||
color: #357a14;
|
||||
font-weight: 700;
|
||||
font-size: 11px;
|
||||
line-height: 12px;
|
||||
}
|
||||
|
||||
.storybook-page .tip-wrapper {
|
||||
margin-top: 40px;
|
||||
margin-bottom: 40px;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
margin-top: 40px;
|
||||
margin-bottom: 40px;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.storybook-page .tip-wrapper svg {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
margin-top: 3px;
|
||||
margin-right: 4px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
margin-top: 3px;
|
||||
margin-right: 4px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.storybook-page .tip-wrapper svg path {
|
||||
fill: #1ea7fd;
|
||||
fill: #1ea7fd;
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import adapter from "@sveltejs/adapter-cloudflare";
|
||||
import adapter from '@sveltejs/adapter-cloudflare';
|
||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
@@ -15,4 +15,4 @@ const config = {
|
||||
}
|
||||
};
|
||||
|
||||
export default config;
|
||||
export default config;
|
||||
|
@@ -1,437 +1,424 @@
|
||||
import { test, expect, type Page } from '@playwright/test';
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('https://demo.playwright.dev/todomvc');
|
||||
await page.goto('https://demo.playwright.dev/todomvc');
|
||||
});
|
||||
|
||||
const TODO_ITEMS = [
|
||||
'buy some cheese',
|
||||
'feed the cat',
|
||||
'book a doctors appointment'
|
||||
] as const;
|
||||
const TODO_ITEMS = ['buy some cheese', 'feed the cat', 'book a doctors appointment'] as const;
|
||||
|
||||
test.describe('New Todo', () => {
|
||||
test('should allow me to add todo items', async ({ page }) => {
|
||||
// create a new todo locator
|
||||
const newTodo = page.getByPlaceholder('What needs to be done?');
|
||||
test('should allow me to add todo items', async ({ page }) => {
|
||||
// create a new todo locator
|
||||
const newTodo = page.getByPlaceholder('What needs to be done?');
|
||||
|
||||
// Create 1st todo.
|
||||
await newTodo.fill(TODO_ITEMS[0]);
|
||||
await newTodo.press('Enter');
|
||||
// Create 1st todo.
|
||||
await newTodo.fill(TODO_ITEMS[0]);
|
||||
await newTodo.press('Enter');
|
||||
|
||||
// Make sure the list only has one todo item.
|
||||
await expect(page.getByTestId('todo-title')).toHaveText([
|
||||
TODO_ITEMS[0]
|
||||
]);
|
||||
// Make sure the list only has one todo item.
|
||||
await expect(page.getByTestId('todo-title')).toHaveText([TODO_ITEMS[0]]);
|
||||
|
||||
// Create 2nd todo.
|
||||
await newTodo.fill(TODO_ITEMS[1]);
|
||||
await newTodo.press('Enter');
|
||||
// Create 2nd todo.
|
||||
await newTodo.fill(TODO_ITEMS[1]);
|
||||
await newTodo.press('Enter');
|
||||
|
||||
// Make sure the list now has two todo items.
|
||||
await expect(page.getByTestId('todo-title')).toHaveText([
|
||||
TODO_ITEMS[0],
|
||||
TODO_ITEMS[1]
|
||||
]);
|
||||
// Make sure the list now has two todo items.
|
||||
await expect(page.getByTestId('todo-title')).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]);
|
||||
|
||||
await checkNumberOfTodosInLocalStorage(page, 2);
|
||||
});
|
||||
await checkNumberOfTodosInLocalStorage(page, 2);
|
||||
});
|
||||
|
||||
test('should clear text input field when an item is added', async ({ page }) => {
|
||||
// create a new todo locator
|
||||
const newTodo = page.getByPlaceholder('What needs to be done?');
|
||||
test('should clear text input field when an item is added', async ({ page }) => {
|
||||
// create a new todo locator
|
||||
const newTodo = page.getByPlaceholder('What needs to be done?');
|
||||
|
||||
// Create one todo item.
|
||||
await newTodo.fill(TODO_ITEMS[0]);
|
||||
await newTodo.press('Enter');
|
||||
// Create one todo item.
|
||||
await newTodo.fill(TODO_ITEMS[0]);
|
||||
await newTodo.press('Enter');
|
||||
|
||||
// Check that input is empty.
|
||||
await expect(newTodo).toBeEmpty();
|
||||
await checkNumberOfTodosInLocalStorage(page, 1);
|
||||
});
|
||||
// Check that input is empty.
|
||||
await expect(newTodo).toBeEmpty();
|
||||
await checkNumberOfTodosInLocalStorage(page, 1);
|
||||
});
|
||||
|
||||
test('should append new items to the bottom of the list', async ({ page }) => {
|
||||
// Create 3 items.
|
||||
await createDefaultTodos(page);
|
||||
test('should append new items to the bottom of the list', async ({ page }) => {
|
||||
// Create 3 items.
|
||||
await createDefaultTodos(page);
|
||||
|
||||
// create a todo count locator
|
||||
const todoCount = page.getByTestId('todo-count')
|
||||
|
||||
// Check test using different methods.
|
||||
await expect(page.getByText('3 items left')).toBeVisible();
|
||||
await expect(todoCount).toHaveText('3 items left');
|
||||
await expect(todoCount).toContainText('3');
|
||||
await expect(todoCount).toHaveText(/3/);
|
||||
// create a todo count locator
|
||||
const todoCount = page.getByTestId('todo-count');
|
||||
|
||||
// Check all items in one call.
|
||||
await expect(page.getByTestId('todo-title')).toHaveText(TODO_ITEMS);
|
||||
await checkNumberOfTodosInLocalStorage(page, 3);
|
||||
});
|
||||
// Check test using different methods.
|
||||
await expect(page.getByText('3 items left')).toBeVisible();
|
||||
await expect(todoCount).toHaveText('3 items left');
|
||||
await expect(todoCount).toContainText('3');
|
||||
await expect(todoCount).toHaveText(/3/);
|
||||
|
||||
// Check all items in one call.
|
||||
await expect(page.getByTestId('todo-title')).toHaveText(TODO_ITEMS);
|
||||
await checkNumberOfTodosInLocalStorage(page, 3);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Mark all as completed', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await createDefaultTodos(page);
|
||||
await checkNumberOfTodosInLocalStorage(page, 3);
|
||||
});
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await createDefaultTodos(page);
|
||||
await checkNumberOfTodosInLocalStorage(page, 3);
|
||||
});
|
||||
|
||||
test.afterEach(async ({ page }) => {
|
||||
await checkNumberOfTodosInLocalStorage(page, 3);
|
||||
});
|
||||
test.afterEach(async ({ page }) => {
|
||||
await checkNumberOfTodosInLocalStorage(page, 3);
|
||||
});
|
||||
|
||||
test('should allow me to mark all items as completed', async ({ page }) => {
|
||||
// Complete all todos.
|
||||
await page.getByLabel('Mark all as complete').check();
|
||||
test('should allow me to mark all items as completed', async ({ page }) => {
|
||||
// Complete all todos.
|
||||
await page.getByLabel('Mark all as complete').check();
|
||||
|
||||
// Ensure all todos have 'completed' class.
|
||||
await expect(page.getByTestId('todo-item')).toHaveClass(['completed', 'completed', 'completed']);
|
||||
await checkNumberOfCompletedTodosInLocalStorage(page, 3);
|
||||
});
|
||||
// Ensure all todos have 'completed' class.
|
||||
await expect(page.getByTestId('todo-item')).toHaveClass([
|
||||
'completed',
|
||||
'completed',
|
||||
'completed'
|
||||
]);
|
||||
await checkNumberOfCompletedTodosInLocalStorage(page, 3);
|
||||
});
|
||||
|
||||
test('should allow me to clear the complete state of all items', async ({ page }) => {
|
||||
const toggleAll = page.getByLabel('Mark all as complete');
|
||||
// Check and then immediately uncheck.
|
||||
await toggleAll.check();
|
||||
await toggleAll.uncheck();
|
||||
test('should allow me to clear the complete state of all items', async ({ page }) => {
|
||||
const toggleAll = page.getByLabel('Mark all as complete');
|
||||
// Check and then immediately uncheck.
|
||||
await toggleAll.check();
|
||||
await toggleAll.uncheck();
|
||||
|
||||
// Should be no completed classes.
|
||||
await expect(page.getByTestId('todo-item')).toHaveClass(['', '', '']);
|
||||
});
|
||||
// Should be no completed classes.
|
||||
await expect(page.getByTestId('todo-item')).toHaveClass(['', '', '']);
|
||||
});
|
||||
|
||||
test('complete all checkbox should update state when items are completed / cleared', async ({ page }) => {
|
||||
const toggleAll = page.getByLabel('Mark all as complete');
|
||||
await toggleAll.check();
|
||||
await expect(toggleAll).toBeChecked();
|
||||
await checkNumberOfCompletedTodosInLocalStorage(page, 3);
|
||||
test('complete all checkbox should update state when items are completed / cleared', async ({
|
||||
page
|
||||
}) => {
|
||||
const toggleAll = page.getByLabel('Mark all as complete');
|
||||
await toggleAll.check();
|
||||
await expect(toggleAll).toBeChecked();
|
||||
await checkNumberOfCompletedTodosInLocalStorage(page, 3);
|
||||
|
||||
// Uncheck first todo.
|
||||
const firstTodo = page.getByTestId('todo-item').nth(0);
|
||||
await firstTodo.getByRole('checkbox').uncheck();
|
||||
// Uncheck first todo.
|
||||
const firstTodo = page.getByTestId('todo-item').nth(0);
|
||||
await firstTodo.getByRole('checkbox').uncheck();
|
||||
|
||||
// Reuse toggleAll locator and make sure its not checked.
|
||||
await expect(toggleAll).not.toBeChecked();
|
||||
// Reuse toggleAll locator and make sure its not checked.
|
||||
await expect(toggleAll).not.toBeChecked();
|
||||
|
||||
await firstTodo.getByRole('checkbox').check();
|
||||
await checkNumberOfCompletedTodosInLocalStorage(page, 3);
|
||||
await firstTodo.getByRole('checkbox').check();
|
||||
await checkNumberOfCompletedTodosInLocalStorage(page, 3);
|
||||
|
||||
// Assert the toggle all is checked again.
|
||||
await expect(toggleAll).toBeChecked();
|
||||
});
|
||||
// Assert the toggle all is checked again.
|
||||
await expect(toggleAll).toBeChecked();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Item', () => {
|
||||
test('should allow me to mark items as complete', async ({ page }) => {
|
||||
// create a new todo locator
|
||||
const newTodo = page.getByPlaceholder('What needs to be done?');
|
||||
|
||||
test('should allow me to mark items as complete', async ({ page }) => {
|
||||
// create a new todo locator
|
||||
const newTodo = page.getByPlaceholder('What needs to be done?');
|
||||
// Create two items.
|
||||
for (const item of TODO_ITEMS.slice(0, 2)) {
|
||||
await newTodo.fill(item);
|
||||
await newTodo.press('Enter');
|
||||
}
|
||||
|
||||
// Create two items.
|
||||
for (const item of TODO_ITEMS.slice(0, 2)) {
|
||||
await newTodo.fill(item);
|
||||
await newTodo.press('Enter');
|
||||
}
|
||||
// Check first item.
|
||||
const firstTodo = page.getByTestId('todo-item').nth(0);
|
||||
await firstTodo.getByRole('checkbox').check();
|
||||
await expect(firstTodo).toHaveClass('completed');
|
||||
|
||||
// Check first item.
|
||||
const firstTodo = page.getByTestId('todo-item').nth(0);
|
||||
await firstTodo.getByRole('checkbox').check();
|
||||
await expect(firstTodo).toHaveClass('completed');
|
||||
// Check second item.
|
||||
const secondTodo = page.getByTestId('todo-item').nth(1);
|
||||
await expect(secondTodo).not.toHaveClass('completed');
|
||||
await secondTodo.getByRole('checkbox').check();
|
||||
|
||||
// Check second item.
|
||||
const secondTodo = page.getByTestId('todo-item').nth(1);
|
||||
await expect(secondTodo).not.toHaveClass('completed');
|
||||
await secondTodo.getByRole('checkbox').check();
|
||||
// Assert completed class.
|
||||
await expect(firstTodo).toHaveClass('completed');
|
||||
await expect(secondTodo).toHaveClass('completed');
|
||||
});
|
||||
|
||||
// Assert completed class.
|
||||
await expect(firstTodo).toHaveClass('completed');
|
||||
await expect(secondTodo).toHaveClass('completed');
|
||||
});
|
||||
test('should allow me to un-mark items as complete', async ({ page }) => {
|
||||
// create a new todo locator
|
||||
const newTodo = page.getByPlaceholder('What needs to be done?');
|
||||
|
||||
test('should allow me to un-mark items as complete', async ({ page }) => {
|
||||
// create a new todo locator
|
||||
const newTodo = page.getByPlaceholder('What needs to be done?');
|
||||
// Create two items.
|
||||
for (const item of TODO_ITEMS.slice(0, 2)) {
|
||||
await newTodo.fill(item);
|
||||
await newTodo.press('Enter');
|
||||
}
|
||||
|
||||
// Create two items.
|
||||
for (const item of TODO_ITEMS.slice(0, 2)) {
|
||||
await newTodo.fill(item);
|
||||
await newTodo.press('Enter');
|
||||
}
|
||||
const firstTodo = page.getByTestId('todo-item').nth(0);
|
||||
const secondTodo = page.getByTestId('todo-item').nth(1);
|
||||
const firstTodoCheckbox = firstTodo.getByRole('checkbox');
|
||||
|
||||
const firstTodo = page.getByTestId('todo-item').nth(0);
|
||||
const secondTodo = page.getByTestId('todo-item').nth(1);
|
||||
const firstTodoCheckbox = firstTodo.getByRole('checkbox');
|
||||
await firstTodoCheckbox.check();
|
||||
await expect(firstTodo).toHaveClass('completed');
|
||||
await expect(secondTodo).not.toHaveClass('completed');
|
||||
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
|
||||
|
||||
await firstTodoCheckbox.check();
|
||||
await expect(firstTodo).toHaveClass('completed');
|
||||
await expect(secondTodo).not.toHaveClass('completed');
|
||||
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
|
||||
await firstTodoCheckbox.uncheck();
|
||||
await expect(firstTodo).not.toHaveClass('completed');
|
||||
await expect(secondTodo).not.toHaveClass('completed');
|
||||
await checkNumberOfCompletedTodosInLocalStorage(page, 0);
|
||||
});
|
||||
|
||||
await firstTodoCheckbox.uncheck();
|
||||
await expect(firstTodo).not.toHaveClass('completed');
|
||||
await expect(secondTodo).not.toHaveClass('completed');
|
||||
await checkNumberOfCompletedTodosInLocalStorage(page, 0);
|
||||
});
|
||||
test('should allow me to edit an item', async ({ page }) => {
|
||||
await createDefaultTodos(page);
|
||||
|
||||
test('should allow me to edit an item', async ({ page }) => {
|
||||
await createDefaultTodos(page);
|
||||
const todoItems = page.getByTestId('todo-item');
|
||||
const secondTodo = todoItems.nth(1);
|
||||
await secondTodo.dblclick();
|
||||
await expect(secondTodo.getByRole('textbox', { name: 'Edit' })).toHaveValue(TODO_ITEMS[1]);
|
||||
await secondTodo.getByRole('textbox', { name: 'Edit' }).fill('buy some sausages');
|
||||
await secondTodo.getByRole('textbox', { name: 'Edit' }).press('Enter');
|
||||
|
||||
const todoItems = page.getByTestId('todo-item');
|
||||
const secondTodo = todoItems.nth(1);
|
||||
await secondTodo.dblclick();
|
||||
await expect(secondTodo.getByRole('textbox', { name: 'Edit' })).toHaveValue(TODO_ITEMS[1]);
|
||||
await secondTodo.getByRole('textbox', { name: 'Edit' }).fill('buy some sausages');
|
||||
await secondTodo.getByRole('textbox', { name: 'Edit' }).press('Enter');
|
||||
|
||||
// Explicitly assert the new text value.
|
||||
await expect(todoItems).toHaveText([
|
||||
TODO_ITEMS[0],
|
||||
'buy some sausages',
|
||||
TODO_ITEMS[2]
|
||||
]);
|
||||
await checkTodosInLocalStorage(page, 'buy some sausages');
|
||||
});
|
||||
// Explicitly assert the new text value.
|
||||
await expect(todoItems).toHaveText([TODO_ITEMS[0], 'buy some sausages', TODO_ITEMS[2]]);
|
||||
await checkTodosInLocalStorage(page, 'buy some sausages');
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Editing', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await createDefaultTodos(page);
|
||||
await checkNumberOfTodosInLocalStorage(page, 3);
|
||||
});
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await createDefaultTodos(page);
|
||||
await checkNumberOfTodosInLocalStorage(page, 3);
|
||||
});
|
||||
|
||||
test('should hide other controls when editing', async ({ page }) => {
|
||||
const todoItem = page.getByTestId('todo-item').nth(1);
|
||||
await todoItem.dblclick();
|
||||
await expect(todoItem.getByRole('checkbox')).not.toBeVisible();
|
||||
await expect(todoItem.locator('label', {
|
||||
hasText: TODO_ITEMS[1],
|
||||
})).not.toBeVisible();
|
||||
await checkNumberOfTodosInLocalStorage(page, 3);
|
||||
});
|
||||
test('should hide other controls when editing', async ({ page }) => {
|
||||
const todoItem = page.getByTestId('todo-item').nth(1);
|
||||
await todoItem.dblclick();
|
||||
await expect(todoItem.getByRole('checkbox')).not.toBeVisible();
|
||||
await expect(
|
||||
todoItem.locator('label', {
|
||||
hasText: TODO_ITEMS[1]
|
||||
})
|
||||
).not.toBeVisible();
|
||||
await checkNumberOfTodosInLocalStorage(page, 3);
|
||||
});
|
||||
|
||||
test('should save edits on blur', async ({ page }) => {
|
||||
const todoItems = page.getByTestId('todo-item');
|
||||
await todoItems.nth(1).dblclick();
|
||||
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages');
|
||||
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).dispatchEvent('blur');
|
||||
test('should save edits on blur', async ({ page }) => {
|
||||
const todoItems = page.getByTestId('todo-item');
|
||||
await todoItems.nth(1).dblclick();
|
||||
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages');
|
||||
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).dispatchEvent('blur');
|
||||
|
||||
await expect(todoItems).toHaveText([
|
||||
TODO_ITEMS[0],
|
||||
'buy some sausages',
|
||||
TODO_ITEMS[2],
|
||||
]);
|
||||
await checkTodosInLocalStorage(page, 'buy some sausages');
|
||||
});
|
||||
await expect(todoItems).toHaveText([TODO_ITEMS[0], 'buy some sausages', TODO_ITEMS[2]]);
|
||||
await checkTodosInLocalStorage(page, 'buy some sausages');
|
||||
});
|
||||
|
||||
test('should trim entered text', async ({ page }) => {
|
||||
const todoItems = page.getByTestId('todo-item');
|
||||
await todoItems.nth(1).dblclick();
|
||||
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill(' buy some sausages ');
|
||||
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter');
|
||||
test('should trim entered text', async ({ page }) => {
|
||||
const todoItems = page.getByTestId('todo-item');
|
||||
await todoItems.nth(1).dblclick();
|
||||
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill(' buy some sausages ');
|
||||
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter');
|
||||
|
||||
await expect(todoItems).toHaveText([
|
||||
TODO_ITEMS[0],
|
||||
'buy some sausages',
|
||||
TODO_ITEMS[2],
|
||||
]);
|
||||
await checkTodosInLocalStorage(page, 'buy some sausages');
|
||||
});
|
||||
await expect(todoItems).toHaveText([TODO_ITEMS[0], 'buy some sausages', TODO_ITEMS[2]]);
|
||||
await checkTodosInLocalStorage(page, 'buy some sausages');
|
||||
});
|
||||
|
||||
test('should remove the item if an empty text string was entered', async ({ page }) => {
|
||||
const todoItems = page.getByTestId('todo-item');
|
||||
await todoItems.nth(1).dblclick();
|
||||
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('');
|
||||
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter');
|
||||
test('should remove the item if an empty text string was entered', async ({ page }) => {
|
||||
const todoItems = page.getByTestId('todo-item');
|
||||
await todoItems.nth(1).dblclick();
|
||||
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('');
|
||||
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter');
|
||||
|
||||
await expect(todoItems).toHaveText([
|
||||
TODO_ITEMS[0],
|
||||
TODO_ITEMS[2],
|
||||
]);
|
||||
});
|
||||
await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]);
|
||||
});
|
||||
|
||||
test('should cancel edits on escape', async ({ page }) => {
|
||||
const todoItems = page.getByTestId('todo-item');
|
||||
await todoItems.nth(1).dblclick();
|
||||
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages');
|
||||
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Escape');
|
||||
await expect(todoItems).toHaveText(TODO_ITEMS);
|
||||
});
|
||||
test('should cancel edits on escape', async ({ page }) => {
|
||||
const todoItems = page.getByTestId('todo-item');
|
||||
await todoItems.nth(1).dblclick();
|
||||
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages');
|
||||
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Escape');
|
||||
await expect(todoItems).toHaveText(TODO_ITEMS);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Counter', () => {
|
||||
test('should display the current number of todo items', async ({ page }) => {
|
||||
// create a new todo locator
|
||||
const newTodo = page.getByPlaceholder('What needs to be done?');
|
||||
|
||||
// create a todo count locator
|
||||
const todoCount = page.getByTestId('todo-count')
|
||||
test('should display the current number of todo items', async ({ page }) => {
|
||||
// create a new todo locator
|
||||
const newTodo = page.getByPlaceholder('What needs to be done?');
|
||||
|
||||
await newTodo.fill(TODO_ITEMS[0]);
|
||||
await newTodo.press('Enter');
|
||||
// create a todo count locator
|
||||
const todoCount = page.getByTestId('todo-count');
|
||||
|
||||
await expect(todoCount).toContainText('1');
|
||||
await newTodo.fill(TODO_ITEMS[0]);
|
||||
await newTodo.press('Enter');
|
||||
|
||||
await newTodo.fill(TODO_ITEMS[1]);
|
||||
await newTodo.press('Enter');
|
||||
await expect(todoCount).toContainText('2');
|
||||
await expect(todoCount).toContainText('1');
|
||||
|
||||
await checkNumberOfTodosInLocalStorage(page, 2);
|
||||
});
|
||||
await newTodo.fill(TODO_ITEMS[1]);
|
||||
await newTodo.press('Enter');
|
||||
await expect(todoCount).toContainText('2');
|
||||
|
||||
await checkNumberOfTodosInLocalStorage(page, 2);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Clear completed button', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await createDefaultTodos(page);
|
||||
});
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await createDefaultTodos(page);
|
||||
});
|
||||
|
||||
test('should display the correct text', async ({ page }) => {
|
||||
await page.locator('.todo-list li .toggle').first().check();
|
||||
await expect(page.getByRole('button', { name: 'Clear completed' })).toBeVisible();
|
||||
});
|
||||
test('should display the correct text', async ({ page }) => {
|
||||
await page.locator('.todo-list li .toggle').first().check();
|
||||
await expect(page.getByRole('button', { name: 'Clear completed' })).toBeVisible();
|
||||
});
|
||||
|
||||
test('should remove completed items when clicked', async ({ page }) => {
|
||||
const todoItems = page.getByTestId('todo-item');
|
||||
await todoItems.nth(1).getByRole('checkbox').check();
|
||||
await page.getByRole('button', { name: 'Clear completed' }).click();
|
||||
await expect(todoItems).toHaveCount(2);
|
||||
await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]);
|
||||
});
|
||||
test('should remove completed items when clicked', async ({ page }) => {
|
||||
const todoItems = page.getByTestId('todo-item');
|
||||
await todoItems.nth(1).getByRole('checkbox').check();
|
||||
await page.getByRole('button', { name: 'Clear completed' }).click();
|
||||
await expect(todoItems).toHaveCount(2);
|
||||
await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]);
|
||||
});
|
||||
|
||||
test('should be hidden when there are no items that are completed', async ({ page }) => {
|
||||
await page.locator('.todo-list li .toggle').first().check();
|
||||
await page.getByRole('button', { name: 'Clear completed' }).click();
|
||||
await expect(page.getByRole('button', { name: 'Clear completed' })).toBeHidden();
|
||||
});
|
||||
test('should be hidden when there are no items that are completed', async ({ page }) => {
|
||||
await page.locator('.todo-list li .toggle').first().check();
|
||||
await page.getByRole('button', { name: 'Clear completed' }).click();
|
||||
await expect(page.getByRole('button', { name: 'Clear completed' })).toBeHidden();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Persistence', () => {
|
||||
test('should persist its data', async ({ page }) => {
|
||||
// create a new todo locator
|
||||
const newTodo = page.getByPlaceholder('What needs to be done?');
|
||||
test('should persist its data', async ({ page }) => {
|
||||
// create a new todo locator
|
||||
const newTodo = page.getByPlaceholder('What needs to be done?');
|
||||
|
||||
for (const item of TODO_ITEMS.slice(0, 2)) {
|
||||
await newTodo.fill(item);
|
||||
await newTodo.press('Enter');
|
||||
}
|
||||
for (const item of TODO_ITEMS.slice(0, 2)) {
|
||||
await newTodo.fill(item);
|
||||
await newTodo.press('Enter');
|
||||
}
|
||||
|
||||
const todoItems = page.getByTestId('todo-item');
|
||||
const firstTodoCheck = todoItems.nth(0).getByRole('checkbox');
|
||||
await firstTodoCheck.check();
|
||||
await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]);
|
||||
await expect(firstTodoCheck).toBeChecked();
|
||||
await expect(todoItems).toHaveClass(['completed', '']);
|
||||
const todoItems = page.getByTestId('todo-item');
|
||||
const firstTodoCheck = todoItems.nth(0).getByRole('checkbox');
|
||||
await firstTodoCheck.check();
|
||||
await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]);
|
||||
await expect(firstTodoCheck).toBeChecked();
|
||||
await expect(todoItems).toHaveClass(['completed', '']);
|
||||
|
||||
// Ensure there is 1 completed item.
|
||||
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
|
||||
// Ensure there is 1 completed item.
|
||||
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
|
||||
|
||||
// Now reload.
|
||||
await page.reload();
|
||||
await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]);
|
||||
await expect(firstTodoCheck).toBeChecked();
|
||||
await expect(todoItems).toHaveClass(['completed', '']);
|
||||
});
|
||||
// Now reload.
|
||||
await page.reload();
|
||||
await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]);
|
||||
await expect(firstTodoCheck).toBeChecked();
|
||||
await expect(todoItems).toHaveClass(['completed', '']);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Routing', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await createDefaultTodos(page);
|
||||
// make sure the app had a chance to save updated todos in storage
|
||||
// before navigating to a new view, otherwise the items can get lost :(
|
||||
// in some frameworks like Durandal
|
||||
await checkTodosInLocalStorage(page, TODO_ITEMS[0]);
|
||||
});
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await createDefaultTodos(page);
|
||||
// make sure the app had a chance to save updated todos in storage
|
||||
// before navigating to a new view, otherwise the items can get lost :(
|
||||
// in some frameworks like Durandal
|
||||
await checkTodosInLocalStorage(page, TODO_ITEMS[0]);
|
||||
});
|
||||
|
||||
test('should allow me to display active items', async ({ page }) => {
|
||||
const todoItem = page.getByTestId('todo-item');
|
||||
await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check();
|
||||
test('should allow me to display active items', async ({ page }) => {
|
||||
const todoItem = page.getByTestId('todo-item');
|
||||
await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check();
|
||||
|
||||
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
|
||||
await page.getByRole('link', { name: 'Active' }).click();
|
||||
await expect(todoItem).toHaveCount(2);
|
||||
await expect(todoItem).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]);
|
||||
});
|
||||
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
|
||||
await page.getByRole('link', { name: 'Active' }).click();
|
||||
await expect(todoItem).toHaveCount(2);
|
||||
await expect(todoItem).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]);
|
||||
});
|
||||
|
||||
test('should respect the back button', async ({ page }) => {
|
||||
const todoItem = page.getByTestId('todo-item');
|
||||
await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check();
|
||||
test('should respect the back button', async ({ page }) => {
|
||||
const todoItem = page.getByTestId('todo-item');
|
||||
await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check();
|
||||
|
||||
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
|
||||
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
|
||||
|
||||
await test.step('Showing all items', async () => {
|
||||
await page.getByRole('link', { name: 'All' }).click();
|
||||
await expect(todoItem).toHaveCount(3);
|
||||
});
|
||||
await test.step('Showing all items', async () => {
|
||||
await page.getByRole('link', { name: 'All' }).click();
|
||||
await expect(todoItem).toHaveCount(3);
|
||||
});
|
||||
|
||||
await test.step('Showing active items', async () => {
|
||||
await page.getByRole('link', { name: 'Active' }).click();
|
||||
});
|
||||
await test.step('Showing active items', async () => {
|
||||
await page.getByRole('link', { name: 'Active' }).click();
|
||||
});
|
||||
|
||||
await test.step('Showing completed items', async () => {
|
||||
await page.getByRole('link', { name: 'Completed' }).click();
|
||||
});
|
||||
await test.step('Showing completed items', async () => {
|
||||
await page.getByRole('link', { name: 'Completed' }).click();
|
||||
});
|
||||
|
||||
await expect(todoItem).toHaveCount(1);
|
||||
await page.goBack();
|
||||
await expect(todoItem).toHaveCount(2);
|
||||
await page.goBack();
|
||||
await expect(todoItem).toHaveCount(3);
|
||||
});
|
||||
await expect(todoItem).toHaveCount(1);
|
||||
await page.goBack();
|
||||
await expect(todoItem).toHaveCount(2);
|
||||
await page.goBack();
|
||||
await expect(todoItem).toHaveCount(3);
|
||||
});
|
||||
|
||||
test('should allow me to display completed items', async ({ page }) => {
|
||||
await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check();
|
||||
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
|
||||
await page.getByRole('link', { name: 'Completed' }).click();
|
||||
await expect(page.getByTestId('todo-item')).toHaveCount(1);
|
||||
});
|
||||
test('should allow me to display completed items', async ({ page }) => {
|
||||
await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check();
|
||||
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
|
||||
await page.getByRole('link', { name: 'Completed' }).click();
|
||||
await expect(page.getByTestId('todo-item')).toHaveCount(1);
|
||||
});
|
||||
|
||||
test('should allow me to display all items', async ({ page }) => {
|
||||
await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check();
|
||||
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
|
||||
await page.getByRole('link', { name: 'Active' }).click();
|
||||
await page.getByRole('link', { name: 'Completed' }).click();
|
||||
await page.getByRole('link', { name: 'All' }).click();
|
||||
await expect(page.getByTestId('todo-item')).toHaveCount(3);
|
||||
});
|
||||
test('should allow me to display all items', async ({ page }) => {
|
||||
await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check();
|
||||
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
|
||||
await page.getByRole('link', { name: 'Active' }).click();
|
||||
await page.getByRole('link', { name: 'Completed' }).click();
|
||||
await page.getByRole('link', { name: 'All' }).click();
|
||||
await expect(page.getByTestId('todo-item')).toHaveCount(3);
|
||||
});
|
||||
|
||||
test('should highlight the currently applied filter', async ({ page }) => {
|
||||
await expect(page.getByRole('link', { name: 'All' })).toHaveClass('selected');
|
||||
|
||||
//create locators for active and completed links
|
||||
const activeLink = page.getByRole('link', { name: 'Active' });
|
||||
const completedLink = page.getByRole('link', { name: 'Completed' });
|
||||
await activeLink.click();
|
||||
test('should highlight the currently applied filter', async ({ page }) => {
|
||||
await expect(page.getByRole('link', { name: 'All' })).toHaveClass('selected');
|
||||
|
||||
// Page change - active items.
|
||||
await expect(activeLink).toHaveClass('selected');
|
||||
await completedLink.click();
|
||||
//create locators for active and completed links
|
||||
const activeLink = page.getByRole('link', { name: 'Active' });
|
||||
const completedLink = page.getByRole('link', { name: 'Completed' });
|
||||
await activeLink.click();
|
||||
|
||||
// Page change - completed items.
|
||||
await expect(completedLink).toHaveClass('selected');
|
||||
});
|
||||
// Page change - active items.
|
||||
await expect(activeLink).toHaveClass('selected');
|
||||
await completedLink.click();
|
||||
|
||||
// Page change - completed items.
|
||||
await expect(completedLink).toHaveClass('selected');
|
||||
});
|
||||
});
|
||||
|
||||
async function createDefaultTodos(page: Page) {
|
||||
// create a new todo locator
|
||||
const newTodo = page.getByPlaceholder('What needs to be done?');
|
||||
// create a new todo locator
|
||||
const newTodo = page.getByPlaceholder('What needs to be done?');
|
||||
|
||||
for (const item of TODO_ITEMS) {
|
||||
await newTodo.fill(item);
|
||||
await newTodo.press('Enter');
|
||||
}
|
||||
for (const item of TODO_ITEMS) {
|
||||
await newTodo.fill(item);
|
||||
await newTodo.press('Enter');
|
||||
}
|
||||
}
|
||||
|
||||
async function checkNumberOfTodosInLocalStorage(page: Page, expected: number) {
|
||||
return await page.waitForFunction(e => {
|
||||
return JSON.parse(localStorage['react-todos']).length === e;
|
||||
}, expected);
|
||||
return await page.waitForFunction((e) => {
|
||||
return JSON.parse(localStorage['react-todos']).length === e;
|
||||
}, expected);
|
||||
}
|
||||
|
||||
async function checkNumberOfCompletedTodosInLocalStorage(page: Page, expected: number) {
|
||||
return await page.waitForFunction(e => {
|
||||
return JSON.parse(localStorage['react-todos']).filter((todo: any) => todo.completed).length === e;
|
||||
}, expected);
|
||||
return await page.waitForFunction((e) => {
|
||||
return (
|
||||
JSON.parse(localStorage['react-todos']).filter((todo: any) => todo.completed).length === e
|
||||
);
|
||||
}, expected);
|
||||
}
|
||||
|
||||
async function checkTodosInLocalStorage(page: Page, title: string) {
|
||||
return await page.waitForFunction(t => {
|
||||
return JSON.parse(localStorage['react-todos']).map((todo: any) => todo.title).includes(t);
|
||||
}, title);
|
||||
return await page.waitForFunction((t) => {
|
||||
return JSON.parse(localStorage['react-todos'])
|
||||
.map((todo: any) => todo.title)
|
||||
.includes(t);
|
||||
}, title);
|
||||
}
|
||||
|
@@ -10,9 +10,7 @@
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"moduleResolution": "bundler",
|
||||
"types": [
|
||||
"@cloudflare/workers-types/2023-07-01"
|
||||
]
|
||||
"types": ["@cloudflare/workers-types/2023-07-01"]
|
||||
}
|
||||
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
|
||||
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
|
||||
|
@@ -1,16 +1,18 @@
|
||||
import { paraglide } from '@inlang/paraglide-sveltekit/vite';
|
||||
import { defineConfig } from 'vitest/config';
|
||||
import { execSync } from "child_process";
|
||||
import { execSync } from 'child_process';
|
||||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
{
|
||||
name: "openapi-generate",
|
||||
name: 'openapi-generate',
|
||||
buildStart() {
|
||||
console.log("Generating TypeScript client from OpenAPI...");
|
||||
execSync("npx openapi-typescript ../../api/openapi.json --output src/lib/api/api.gen.ts", { stdio: "inherit" });
|
||||
console.log("OpenAPI client generated!");
|
||||
console.log('Generating TypeScript client from OpenAPI...');
|
||||
execSync('npx openapi-typescript ../../api/openapi.json --output src/lib/api/api.gen.ts', {
|
||||
stdio: 'inherit'
|
||||
});
|
||||
console.log('OpenAPI client generated!');
|
||||
}
|
||||
},
|
||||
sveltekit(),
|
||||
|
@@ -3,96 +3,94 @@
|
||||
* https://developers.cloudflare.com/workers/wrangler/configuration/
|
||||
*/
|
||||
{
|
||||
"$schema": "node_modules/wrangler/config-schema.json",
|
||||
"name": "generations-heritage",
|
||||
"compatibility_flags": [
|
||||
"nodejs_compat"
|
||||
],
|
||||
"compatibility_date": "2025-02-14",
|
||||
"pages_build_output_dir": ".svelte-kit/cloudflare",
|
||||
"observability": {
|
||||
"enabled": true
|
||||
},
|
||||
/**
|
||||
* Smart Placement
|
||||
* Docs: https://developers.cloudflare.com/workers/configuration/smart-placement/#smart-placement
|
||||
*/
|
||||
"placement": {
|
||||
"mode": "smart"
|
||||
},
|
||||
/**
|
||||
* Bindings
|
||||
* Bindings allow your Worker to interact with resources on the Cloudflare Developer Platform, including
|
||||
* databases, object storage, AI inference, real-time communication and more.
|
||||
* https://developers.cloudflare.com/workers/runtime-apis/bindings/
|
||||
*/
|
||||
"kv_namespaces": [
|
||||
{
|
||||
"binding": "GH_SESSIONS"
|
||||
}
|
||||
],
|
||||
"r2_buckets": [
|
||||
{
|
||||
"binding": "GH_MEDIA"
|
||||
}
|
||||
],
|
||||
/**
|
||||
* Environment Variables
|
||||
* https://developers.cloudflare.com/workers/wrangler/configuration/#environment-variables
|
||||
*/
|
||||
// "vars": { "MY_VARIABLE": "production_value" },
|
||||
"env": {
|
||||
"staging": {
|
||||
"name": "generations-heritage-stage",
|
||||
"route": "https://ghstage.varghacsongor.hu/*",
|
||||
"vars": {
|
||||
"GOOGLE_CALLBACK_URI": "https://ghstage.varghacsongor.hu/login/google/callback"
|
||||
},
|
||||
"kv_namespaces": [
|
||||
{
|
||||
"binding": "GH_SESSIONS",
|
||||
"id": "6f793c8813ab46549234572f4c6ae5a1"
|
||||
}
|
||||
],
|
||||
"r2_buckets": [
|
||||
{
|
||||
"binding": "GH_MEDIA",
|
||||
"bucket_name": "ghstaging"
|
||||
}
|
||||
]
|
||||
},
|
||||
"production": {
|
||||
"name": "generations-heritage-prod",
|
||||
"route": "https://csalad.varghacsongor.hu/*",
|
||||
"vars": {
|
||||
"GOOGLE_CALLBACK_URI": "https://csalad.varghacsongor.hu/login/google/callback"
|
||||
},
|
||||
"kv_namespaces": [
|
||||
{
|
||||
"binding": "GH_SESSIONS",
|
||||
"id": "4cedee65c36d49d7afc654bcc798d169"
|
||||
}
|
||||
],
|
||||
"r2_buckets": [
|
||||
{
|
||||
"binding": "GH_MEDIA",
|
||||
"bucket_name": "generations-heritage"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Note: Use secrets to store sensitive data.
|
||||
* https://developers.cloudflare.com/workers/configuration/secrets/
|
||||
*/
|
||||
/**
|
||||
* Static Assets
|
||||
* https://developers.cloudflare.com/workers/static-assets/binding/
|
||||
*/
|
||||
// "assets": { "directory": "./public/", "binding": "ASSETS" },
|
||||
/**
|
||||
* Service Bindings (communicate between multiple Workers)
|
||||
* https://developers.cloudflare.com/workers/wrangler/configuration/#service-bindings
|
||||
*/
|
||||
// "services": [{ "binding": "MY_SERVICE", "service": "my-service" }]
|
||||
}
|
||||
"$schema": "node_modules/wrangler/config-schema.json",
|
||||
"name": "generations-heritage",
|
||||
"compatibility_flags": ["nodejs_compat"],
|
||||
"compatibility_date": "2025-02-14",
|
||||
"pages_build_output_dir": ".svelte-kit/cloudflare",
|
||||
"observability": {
|
||||
"enabled": true
|
||||
},
|
||||
/**
|
||||
* Smart Placement
|
||||
* Docs: https://developers.cloudflare.com/workers/configuration/smart-placement/#smart-placement
|
||||
*/
|
||||
"placement": {
|
||||
"mode": "smart"
|
||||
},
|
||||
/**
|
||||
* Bindings
|
||||
* Bindings allow your Worker to interact with resources on the Cloudflare Developer Platform, including
|
||||
* databases, object storage, AI inference, real-time communication and more.
|
||||
* https://developers.cloudflare.com/workers/runtime-apis/bindings/
|
||||
*/
|
||||
"kv_namespaces": [
|
||||
{
|
||||
"binding": "GH_SESSIONS"
|
||||
}
|
||||
],
|
||||
"r2_buckets": [
|
||||
{
|
||||
"binding": "GH_MEDIA"
|
||||
}
|
||||
],
|
||||
/**
|
||||
* Environment Variables
|
||||
* https://developers.cloudflare.com/workers/wrangler/configuration/#environment-variables
|
||||
*/
|
||||
// "vars": { "MY_VARIABLE": "production_value" },
|
||||
"env": {
|
||||
"staging": {
|
||||
"name": "generations-heritage-stage",
|
||||
"route": "https://ghstage.varghacsongor.hu/*",
|
||||
"vars": {
|
||||
"GOOGLE_CALLBACK_URI": "https://ghstage.varghacsongor.hu/login/google/callback"
|
||||
},
|
||||
"kv_namespaces": [
|
||||
{
|
||||
"binding": "GH_SESSIONS",
|
||||
"id": "6f793c8813ab46549234572f4c6ae5a1"
|
||||
}
|
||||
],
|
||||
"r2_buckets": [
|
||||
{
|
||||
"binding": "GH_MEDIA",
|
||||
"bucket_name": "ghstaging"
|
||||
}
|
||||
]
|
||||
},
|
||||
"production": {
|
||||
"name": "generations-heritage-prod",
|
||||
"route": "https://csalad.varghacsongor.hu/*",
|
||||
"vars": {
|
||||
"GOOGLE_CALLBACK_URI": "https://csalad.varghacsongor.hu/login/google/callback"
|
||||
},
|
||||
"kv_namespaces": [
|
||||
{
|
||||
"binding": "GH_SESSIONS",
|
||||
"id": "4cedee65c36d49d7afc654bcc798d169"
|
||||
}
|
||||
],
|
||||
"r2_buckets": [
|
||||
{
|
||||
"binding": "GH_MEDIA",
|
||||
"bucket_name": "generations-heritage"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Note: Use secrets to store sensitive data.
|
||||
* https://developers.cloudflare.com/workers/configuration/secrets/
|
||||
*/
|
||||
/**
|
||||
* Static Assets
|
||||
* https://developers.cloudflare.com/workers/static-assets/binding/
|
||||
*/
|
||||
// "assets": { "directory": "./public/", "binding": "ASSETS" },
|
||||
/**
|
||||
* Service Bindings (communicate between multiple Workers)
|
||||
* https://developers.cloudflare.com/workers/wrangler/configuration/#service-bindings
|
||||
*/
|
||||
// "services": [{ "binding": "MY_SERVICE", "service": "my-service" }]
|
||||
}
|
||||
|
Reference in New Issue
Block a user