fix lint issues with prettier

This commit is contained in:
2025-03-27 22:08:01 +01:00
parent 03120d7242
commit a56454110a
45 changed files with 2766 additions and 2672 deletions

View File

@@ -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

View File

@@ -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;

View File

@@ -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;

View File

@@ -2,4 +2,4 @@
"files.associations": {
"wrangler.json": "jsonc"
}
}
}

View File

@@ -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();
});

View File

@@ -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",

View File

@@ -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"
}

View File

@@ -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"
}
}

View File

@@ -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,
// },
});

View File

@@ -1,5 +1,5 @@
export default {
plugins: {
'@tailwindcss/postcss': {},
'@tailwindcss/postcss': {}
}
};

View File

@@ -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
View File

@@ -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 {};

View File

@@ -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

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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'
);

View File

@@ -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,

View File

@@ -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);
});
});

View File

@@ -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);
}

View File

@@ -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>

View File

@@ -1 +1 @@
export const themes = ['light', 'dark','coffee', 'cyberpunk', 'synthwave', 'retro', 'dracula'];
export const themes = ['light', 'dark', 'coffee', 'cyberpunk', 'synthwave', 'retro', 'dracula'];

View File

@@ -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');
}

View File

@@ -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>

View File

@@ -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 {};
}
}

View File

@@ -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"

View File

@@ -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()
}
});
}
}

View File

@@ -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, '/');
}

View File

@@ -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"

View File

@@ -1,7 +1,7 @@
<script module>
import Page from './+page.svelte';
import Page from './+page.svelte';
</script>
<template>
<Page />
</template>
<Page />
</template>

View File

@@ -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 -->

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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' } }} />

View File

@@ -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>

View File

@@ -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" />

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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

View File

@@ -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(),

View File

@@ -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" }]
}