mirror of
https://github.com/vcscsvcscs/GenerationsHeritage.git
synced 2025-08-14 23:09:07 +02:00
fix lint issues with prettier
This commit is contained in:
@@ -9,16 +9,24 @@
|
||||
color utility to any element that depends on these defaults.
|
||||
*/
|
||||
@layer base {
|
||||
*,
|
||||
::after,
|
||||
::before,
|
||||
::backdrop,
|
||||
::file-selector-button {
|
||||
border-color: var(--color-gray-200, currentColor);
|
||||
}
|
||||
|
||||
*,
|
||||
::after,
|
||||
::before,
|
||||
::backdrop,
|
||||
::file-selector-button {
|
||||
border-color: var(--color-gray-200, currentColor);
|
||||
}
|
||||
}
|
||||
|
||||
@plugin "daisyui" {
|
||||
themes: light --default, dark --prefersdark, light, dark, cyberpunk, synthwave, retro, coffee, dracula;
|
||||
themes:
|
||||
light --default,
|
||||
dark --prefersdark,
|
||||
light,
|
||||
dark,
|
||||
cyberpunk,
|
||||
synthwave,
|
||||
retro,
|
||||
coffee,
|
||||
dracula;
|
||||
}
|
||||
|
24
apps/app/src/app.d.ts
vendored
24
apps/app/src/app.d.ts
vendored
@@ -3,18 +3,18 @@ import { KVNamespace } from '@cloudflare/workers-types';
|
||||
// for information about these interfaces
|
||||
declare global {
|
||||
namespace App {
|
||||
interface Locals {
|
||||
session: Session | null;
|
||||
}
|
||||
interface Platform {
|
||||
env: {
|
||||
GH_MEDIA: R2Bucket;
|
||||
interface Locals {
|
||||
session: Session | null;
|
||||
}
|
||||
interface Platform {
|
||||
env: {
|
||||
GH_MEDIA: R2Bucket;
|
||||
GH_SESSIONS: KVNamespace;
|
||||
};
|
||||
cf: CfProperties
|
||||
ctx: ExecutionContext
|
||||
}
|
||||
}
|
||||
};
|
||||
cf: CfProperties;
|
||||
ctx: ExecutionContext;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
export {};
|
||||
|
@@ -1,47 +1,51 @@
|
||||
import type { Handle } from '@sveltejs/kit';
|
||||
import { themes } from '$lib/themes'
|
||||
import { themes } from '$lib/themes';
|
||||
import { i18n } from '$lib/i18n';
|
||||
import { validateSessionToken, setSessionTokenCookie, deleteSessionTokenCookie } from "$lib/server/session";
|
||||
import { sequence } from "@sveltejs/kit/hooks";
|
||||
import {
|
||||
validateSessionToken,
|
||||
setSessionTokenCookie,
|
||||
deleteSessionTokenCookie
|
||||
} from '$lib/server/session';
|
||||
import { sequence } from '@sveltejs/kit/hooks';
|
||||
|
||||
const handleParaglide: Handle = i18n.handle();
|
||||
|
||||
const authHandle: Handle = async ({ event, resolve }) => {
|
||||
const token = event.cookies.get("session") ?? null;
|
||||
if (token === null) {
|
||||
event.locals.session = null;
|
||||
return resolve(event);
|
||||
}
|
||||
const token = event.cookies.get('session') ?? null;
|
||||
if (token === null) {
|
||||
event.locals.session = null;
|
||||
return resolve(event);
|
||||
}
|
||||
|
||||
if(!event.platform || !event.platform.env || !event.platform.env.GH_SESSIONS){
|
||||
return new Response("Server configuration error. GH_SESSIONS KeyValue store missing", {
|
||||
if (!event.platform || !event.platform.env || !event.platform.env.GH_SESSIONS) {
|
||||
return new Response('Server configuration error. GH_SESSIONS KeyValue store missing', {
|
||||
status: 500
|
||||
});
|
||||
}
|
||||
|
||||
const session = await validateSessionToken(token, event.platform.env.GH_SESSIONS);
|
||||
if (session !== null) {
|
||||
setSessionTokenCookie(event, token, session.expiresAt);
|
||||
} else {
|
||||
deleteSessionTokenCookie(event);
|
||||
}
|
||||
const session = await validateSessionToken(token, event.platform.env.GH_SESSIONS);
|
||||
if (session !== null) {
|
||||
setSessionTokenCookie(event, token, session.expiresAt);
|
||||
} else {
|
||||
deleteSessionTokenCookie(event);
|
||||
}
|
||||
|
||||
event.locals.session = session;
|
||||
return resolve(event);
|
||||
event.locals.session = session;
|
||||
return resolve(event);
|
||||
};
|
||||
|
||||
const themeHandler: Handle = async ({ event, resolve }) => {
|
||||
const theme = event.cookies.get('theme')
|
||||
const theme = event.cookies.get('theme');
|
||||
|
||||
if (!theme || !themes.includes(theme)) {
|
||||
return await resolve(event)
|
||||
return await resolve(event);
|
||||
}
|
||||
|
||||
return await resolve(event, {
|
||||
transformPageChunk: ({ html }) => {
|
||||
return html.replace('data-theme=""', `data-theme="${theme}"`)
|
||||
},
|
||||
})
|
||||
}
|
||||
return html.replace('data-theme=""', `data-theme="${theme}"`);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const handle: Handle = sequence(handleParaglide, authHandle,themeHandler);
|
||||
export const handle: Handle = sequence(handleParaglide, authHandle, themeHandler);
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,20 @@
|
||||
<div role="alert" class="alert alert-vertical sm:alert-horizontal">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-info h-6 w-6 shrink-0">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
<span></span>
|
||||
<div>
|
||||
<button class="btn btn-sm">Deny</button>
|
||||
<button class="btn btn-sm btn-primary">Accept</button>
|
||||
</div>
|
||||
</div>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
class="stroke-info h-6 w-6 shrink-0"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
></path>
|
||||
</svg>
|
||||
<span></span>
|
||||
<div>
|
||||
<button class="btn btn-sm">Deny</button>
|
||||
<button class="btn btn-sm btn-primary">Accept</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -2,64 +2,64 @@ import { Node, Relationship, Date } from 'neo4j-driver';
|
||||
import { Integer } from 'neo4j-driver';
|
||||
|
||||
export interface PersonProperties {
|
||||
allow_admin_access: boolean;
|
||||
google_id: string;
|
||||
first_name: string;
|
||||
middle_name?: string;
|
||||
last_name: string;
|
||||
titles?: string[]; // e.g. Jr., Sr., III
|
||||
suffixes?: string[]; // e.g. Ph.D., M.D.
|
||||
extra_names?: string[];
|
||||
aliases?: string[];
|
||||
mothers_first_name?: string;
|
||||
mothers_last_name?: string;
|
||||
born?: Date<number>;
|
||||
place_of_birth?: string;
|
||||
died?: Date<number>;
|
||||
place_of_death?: string;
|
||||
life_events?: { [key: string]: {from: Date<number>, to:Date<number>, desription: string} }[];
|
||||
occupations?: string[];
|
||||
occupation_to_display?: string;
|
||||
others_said?: { [key: string]: string };
|
||||
limit: number;
|
||||
photos?: { [key: string]: string };
|
||||
videos?: { [key: string]: string };
|
||||
audios?: { [key: string]: string };
|
||||
profile_picture?: string;
|
||||
verified: boolean;
|
||||
email?: string;
|
||||
phone?: string;
|
||||
residence?: {
|
||||
city?: string;
|
||||
country?: string;
|
||||
zip_code?: string;
|
||||
address_line_1?: string;
|
||||
address_line_2?: string;
|
||||
};
|
||||
religion?: string;
|
||||
baptized?: string;
|
||||
ideology?: string;
|
||||
blood_type?: string;
|
||||
allergies?: string[];
|
||||
medications?: string[];
|
||||
medical_conditions?: string[];
|
||||
height?: number;
|
||||
weight?: number;
|
||||
hair_colour?: string;
|
||||
skin_colour?: string;
|
||||
eye_colour?: string;
|
||||
sports?: string[];
|
||||
hobbies?: string[];
|
||||
interests?: string[];
|
||||
languages?: { [key: string]: string };
|
||||
notes?: string;
|
||||
allow_admin_access: boolean;
|
||||
google_id: string;
|
||||
first_name: string;
|
||||
middle_name?: string;
|
||||
last_name: string;
|
||||
titles?: string[]; // e.g. Jr., Sr., III
|
||||
suffixes?: string[]; // e.g. Ph.D., M.D.
|
||||
extra_names?: string[];
|
||||
aliases?: string[];
|
||||
mothers_first_name?: string;
|
||||
mothers_last_name?: string;
|
||||
born?: Date<number>;
|
||||
place_of_birth?: string;
|
||||
died?: Date<number>;
|
||||
place_of_death?: string;
|
||||
life_events?: { [key: string]: { from: Date<number>; to: Date<number>; desription: string } }[];
|
||||
occupations?: string[];
|
||||
occupation_to_display?: string;
|
||||
others_said?: { [key: string]: string };
|
||||
limit: number;
|
||||
photos?: { [key: string]: string };
|
||||
videos?: { [key: string]: string };
|
||||
audios?: { [key: string]: string };
|
||||
profile_picture?: string;
|
||||
verified: boolean;
|
||||
email?: string;
|
||||
phone?: string;
|
||||
residence?: {
|
||||
city?: string;
|
||||
country?: string;
|
||||
zip_code?: string;
|
||||
address_line_1?: string;
|
||||
address_line_2?: string;
|
||||
};
|
||||
religion?: string;
|
||||
baptized?: string;
|
||||
ideology?: string;
|
||||
blood_type?: string;
|
||||
allergies?: string[];
|
||||
medications?: string[];
|
||||
medical_conditions?: string[];
|
||||
height?: number;
|
||||
weight?: number;
|
||||
hair_colour?: string;
|
||||
skin_colour?: string;
|
||||
eye_colour?: string;
|
||||
sports?: string[];
|
||||
hobbies?: string[];
|
||||
interests?: string[];
|
||||
languages?: { [key: string]: string };
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export interface FamilyRelationship {
|
||||
verified: boolean;
|
||||
notes?: string;
|
||||
from?: Date<number>;
|
||||
to?: Date<number>;
|
||||
verified: boolean;
|
||||
notes?: string;
|
||||
from?: Date<number>;
|
||||
to?: Date<number>;
|
||||
}
|
||||
|
||||
export type Person = Node<Integer, PersonProperties>;
|
||||
@@ -69,36 +69,38 @@ export type Spouse = Relationship<Integer, FamilyRelationship>;
|
||||
export type Child = Relationship<Integer, FamilyRelationship>;
|
||||
|
||||
export interface RecipeProperties {
|
||||
id: string;
|
||||
name: string;
|
||||
origin: string;
|
||||
category: string;
|
||||
first_recorded: Date<number>;
|
||||
description: string;
|
||||
ingredients: string[];
|
||||
instructions: string[];
|
||||
photo: string;
|
||||
notes?: string;
|
||||
id: string;
|
||||
name: string;
|
||||
origin: string;
|
||||
category: string;
|
||||
first_recorded: Date<number>;
|
||||
description: string;
|
||||
ingredients: string[];
|
||||
instructions: string[];
|
||||
photo: string;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export type Recipe = Node<Integer, RecipeProperties>;
|
||||
export type Likes = Relationship<Integer, {
|
||||
favourite: boolean;
|
||||
like_it: boolean;
|
||||
could_make_it: boolean;
|
||||
}>;
|
||||
export type Likes = Relationship<
|
||||
Integer,
|
||||
{
|
||||
favourite: boolean;
|
||||
like_it: boolean;
|
||||
could_make_it: boolean;
|
||||
}
|
||||
>;
|
||||
|
||||
export interface FamilyTree {
|
||||
ancestors: String;
|
||||
prel1: FamilyRelationship;
|
||||
children: String;
|
||||
prel2: FamilyRelationship;
|
||||
spouses: String;
|
||||
srel: FamilyRelationship;
|
||||
user: String;
|
||||
|
||||
ancestors: String;
|
||||
prel1: FamilyRelationship;
|
||||
children: String;
|
||||
prel2: FamilyRelationship;
|
||||
spouses: String;
|
||||
srel: FamilyRelationship;
|
||||
user: String;
|
||||
}
|
||||
|
||||
export interface FamilyMember {
|
||||
person: Person;
|
||||
}
|
||||
person: Person;
|
||||
}
|
||||
|
@@ -1,4 +1,8 @@
|
||||
import { Google } from "arctic";
|
||||
import { GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GOOGLE_CALLBACK_URI } from "$env/static/private";
|
||||
import { Google } from 'arctic';
|
||||
import { GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GOOGLE_CALLBACK_URI } from '$env/static/private';
|
||||
|
||||
export const google = new Google(GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GOOGLE_CALLBACK_URI || "http://localhost:5173/login/google/callback");
|
||||
export const google = new Google(
|
||||
GOOGLE_CLIENT_ID,
|
||||
GOOGLE_CLIENT_SECRET,
|
||||
GOOGLE_CALLBACK_URI || 'http://localhost:5173/login/google/callback'
|
||||
);
|
||||
|
@@ -1,15 +1,18 @@
|
||||
import type { KVNamespace } from "@cloudflare/workers-types";
|
||||
import { encodeBase32, encodeHexLowerCase } from "@oslojs/encoding";
|
||||
import { sha256 } from "@oslojs/crypto/sha2";
|
||||
import type { KVNamespace } from '@cloudflare/workers-types';
|
||||
import { encodeBase32, encodeHexLowerCase } from '@oslojs/encoding';
|
||||
import { sha256 } from '@oslojs/crypto/sha2';
|
||||
|
||||
import type { RequestEvent } from "@sveltejs/kit";
|
||||
import type { RequestEvent } from '@sveltejs/kit';
|
||||
|
||||
// in seconds
|
||||
const EXPIRATION_TTL: number = 60 * 60 * 24 * 7;
|
||||
|
||||
export async function validateSessionToken(token: string, sessions: KVNamespace): Promise<SessionValidationResult> {
|
||||
export async function validateSessionToken(
|
||||
token: string,
|
||||
sessions: KVNamespace
|
||||
): Promise<SessionValidationResult> {
|
||||
const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token)));
|
||||
const session: Session | null = await sessions.get(sessionId, { type: "json" });
|
||||
const session: Session | null = await sessions.get(sessionId, { type: 'json' });
|
||||
|
||||
if (!session) {
|
||||
return null;
|
||||
@@ -33,21 +36,21 @@ export async function invalidateUserSessions(userId: number, sessions: KVNamespa
|
||||
}
|
||||
|
||||
export function setSessionTokenCookie(event: RequestEvent, token: string, expiresAt: Date): void {
|
||||
event.cookies.set("session", token, {
|
||||
event.cookies.set('session', token, {
|
||||
httpOnly: true,
|
||||
path: "/",
|
||||
path: '/',
|
||||
secure: import.meta.env.PROD,
|
||||
sameSite: "lax",
|
||||
sameSite: 'lax',
|
||||
expires: expiresAt
|
||||
});
|
||||
}
|
||||
|
||||
export function deleteSessionTokenCookie(event: RequestEvent): void {
|
||||
event.cookies.set("session", "", {
|
||||
event.cookies.set('session', '', {
|
||||
httpOnly: true,
|
||||
path: "/",
|
||||
path: '/',
|
||||
secure: import.meta.env.PROD,
|
||||
sameSite: "lax",
|
||||
sameSite: 'lax',
|
||||
maxAge: 0
|
||||
});
|
||||
}
|
||||
@@ -59,7 +62,11 @@ export function generateSessionToken(): string {
|
||||
return token;
|
||||
}
|
||||
|
||||
export async function createSession(token: string, userId: string, sessions: KVNamespace): Promise<Session> {
|
||||
export async function createSession(
|
||||
token: string,
|
||||
userId: string,
|
||||
sessions: KVNamespace
|
||||
): Promise<Session> {
|
||||
const sessionId = `${userId}:${encodeHexLowerCase(sha256(new TextEncoder().encode(token)))}`;
|
||||
const session: Session = {
|
||||
id: sessionId,
|
||||
|
@@ -4,37 +4,37 @@ import { i18n } from '$lib/i18n';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
vi.mock('$lib/i18n', () => ({
|
||||
i18n: {
|
||||
route: vi.fn().mockImplementation((translatedPath: string) => ''),
|
||||
resolveRoute: vi.fn().mockImplementation((path: string, lang?: string) => '')
|
||||
}
|
||||
i18n: {
|
||||
route: vi.fn().mockImplementation((translatedPath: string) => ''),
|
||||
resolveRoute: vi.fn().mockImplementation((path: string, lang?: string) => '')
|
||||
}
|
||||
}));
|
||||
|
||||
vi.mock('$app/state', () => ({
|
||||
page: {
|
||||
url: {
|
||||
pathname: '/current-path'
|
||||
}
|
||||
}
|
||||
page: {
|
||||
url: {
|
||||
pathname: '/current-path'
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
vi.mock('$app/navigation', () => ({
|
||||
goto: vi.fn()
|
||||
goto: vi.fn()
|
||||
}));
|
||||
|
||||
describe('switchToLanguage', () => {
|
||||
it('should switch to the new language', () => {
|
||||
const newLanguage = 'en';
|
||||
const canonicalPath = '/canonical-path';
|
||||
const localisedPath = '/en/canonical-path';
|
||||
it('should switch to the new language', () => {
|
||||
const newLanguage = 'en';
|
||||
const canonicalPath = '/canonical-path';
|
||||
const localisedPath = '/en/canonical-path';
|
||||
|
||||
i18n.route.mockReturnValue(canonicalPath);
|
||||
i18n.resolveRoute.mockReturnValue(localisedPath);
|
||||
i18n.route.mockReturnValue(canonicalPath);
|
||||
i18n.resolveRoute.mockReturnValue(localisedPath);
|
||||
|
||||
switchToLanguage(newLanguage);
|
||||
switchToLanguage(newLanguage);
|
||||
|
||||
expect(i18n.route).toHaveBeenCalledWith('/current-path');
|
||||
expect(i18n.resolveRoute).toHaveBeenCalledWith(canonicalPath, newLanguage);
|
||||
expect(goto).toHaveBeenCalledWith(localisedPath);
|
||||
});
|
||||
});
|
||||
expect(i18n.route).toHaveBeenCalledWith('/current-path');
|
||||
expect(i18n.resolveRoute).toHaveBeenCalledWith(canonicalPath, newLanguage);
|
||||
expect(goto).toHaveBeenCalledWith(localisedPath);
|
||||
});
|
||||
});
|
||||
|
@@ -4,7 +4,7 @@ import { page } from '$app/state';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
export function switchToLanguage(newLanguage: AvailableLanguageTag) {
|
||||
const canonicalPath = i18n.route(page.url.pathname);
|
||||
const localisedPath = i18n.resolveRoute(canonicalPath, newLanguage);
|
||||
goto(localisedPath);
|
||||
}
|
||||
const canonicalPath = i18n.route(page.url.pathname);
|
||||
const localisedPath = i18n.resolveRoute(canonicalPath, newLanguage);
|
||||
goto(localisedPath);
|
||||
}
|
||||
|
@@ -37,7 +37,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="dropdown dropdown-end block ">
|
||||
<div class="dropdown dropdown-end block">
|
||||
<select
|
||||
bind:value={current_theme}
|
||||
data-choose-theme
|
||||
@@ -48,11 +48,7 @@
|
||||
{theme()}
|
||||
</option>
|
||||
{#each themes as theme}
|
||||
<option
|
||||
value={theme}
|
||||
class="theme-controller capitalize"
|
||||
>{themeMessages.get(theme)}</option
|
||||
>
|
||||
<option value={theme} class="theme-controller capitalize">{themeMessages.get(theme)}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
|
@@ -1 +1 @@
|
||||
export const themes = ['light', 'dark','coffee', 'cyberpunk', 'synthwave', 'retro', 'dracula'];
|
||||
export const themes = ['light', 'dark', 'coffee', 'cyberpunk', 'synthwave', 'retro', 'dracula'];
|
||||
|
@@ -1,10 +1,10 @@
|
||||
import { fail, redirect } from "@sveltejs/kit";
|
||||
import { deleteSessionTokenCookie, invalidateSession } from "$lib/server/session";
|
||||
import type { Actions, RequestEvent } from "./$types";
|
||||
import { fail, redirect } from '@sveltejs/kit';
|
||||
import { deleteSessionTokenCookie, invalidateSession } from '$lib/server/session';
|
||||
import type { Actions, RequestEvent } from './$types';
|
||||
|
||||
export async function load(event: RequestEvent) {
|
||||
if (event.locals.session === null /*|| event.locals.familytree === nul*/) {
|
||||
return redirect(302, "/login");
|
||||
return redirect(302, '/login');
|
||||
}
|
||||
return {
|
||||
// TODO - Add Family Graph
|
||||
@@ -19,14 +19,14 @@ async function logout(event: RequestEvent) {
|
||||
if (event.locals.session === null) {
|
||||
return fail(401);
|
||||
}
|
||||
|
||||
|
||||
if (event.platform && event.platform.env && event.platform.env.GH_SESSIONS) {
|
||||
invalidateSession(event.locals.session.id, event.platform.env.GH_SESSIONS);
|
||||
} else {
|
||||
return fail(500, { message: "Server configuration error" });
|
||||
return fail(500, { message: 'Server configuration error' });
|
||||
}
|
||||
|
||||
deleteSessionTokenCookie(event);
|
||||
|
||||
return redirect(302, "/login");
|
||||
}
|
||||
return redirect(302, '/login');
|
||||
}
|
||||
|
@@ -1,10 +1,17 @@
|
||||
<script lang="ts">
|
||||
import {title, family_tree} from '$lib/paraglide/messages.js';
|
||||
import { SvelteFlowProvider,Background, BackgroundVariant, SvelteFlow, Controls, MiniMap } from '@xyflow/svelte';
|
||||
import type { Node, Edge, NodeTypes, NodeProps } from '@xyflow/svelte';
|
||||
import { title, family_tree } from '$lib/paraglide/messages.js';
|
||||
import {
|
||||
SvelteFlowProvider,
|
||||
Background,
|
||||
BackgroundVariant,
|
||||
SvelteFlow,
|
||||
Controls,
|
||||
MiniMap
|
||||
} from '@xyflow/svelte';
|
||||
import type { Node, Edge, NodeTypes, NodeProps } from '@xyflow/svelte';
|
||||
|
||||
let nodes = $state.raw<Node[]>([]);
|
||||
let edges = $state.raw<Edge[]>([]);
|
||||
let nodes = $state.raw<Node[]>([]);
|
||||
let edges = $state.raw<Edge[]>([]);
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -19,4 +26,3 @@
|
||||
</SvelteFlow>
|
||||
</SvelteFlowProvider>
|
||||
</div>
|
||||
|
||||
|
@@ -1,11 +1,11 @@
|
||||
import { redirect } from "@sveltejs/kit";
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
|
||||
import type { RequestEvent } from "./$types";
|
||||
import type { RequestEvent } from './$types';
|
||||
|
||||
export async function load(event: RequestEvent) {
|
||||
if (event.locals.session !== null) {
|
||||
return redirect(302, "/");
|
||||
return redirect(302, '/');
|
||||
}
|
||||
|
||||
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
@@ -17,7 +17,7 @@
|
||||
<p class="py-6">
|
||||
{site_intro()}
|
||||
</p>
|
||||
<a href="/login/google" class="btn rounded-full bg-white text-black border-[#e5e5e5]">
|
||||
<a href="/login/google" class="btn rounded-full border-[#e5e5e5] bg-white text-black">
|
||||
<!-- Google -->
|
||||
<svg
|
||||
aria-label="Google logo"
|
||||
|
@@ -1,26 +1,26 @@
|
||||
import { google } from "$lib/server/oauth";
|
||||
import { generateCodeVerifier, generateState } from "arctic";
|
||||
import { google } from '$lib/server/oauth';
|
||||
import { generateCodeVerifier, generateState } from 'arctic';
|
||||
|
||||
import type { RequestEvent } from "./$types";
|
||||
import type { RequestEvent } from './$types';
|
||||
|
||||
export function GET(event: RequestEvent): Response {
|
||||
const state = generateState();
|
||||
const codeVerifier = generateCodeVerifier();
|
||||
const url = google.createAuthorizationURL(state, codeVerifier, ["openid", "profile", "email"]);
|
||||
const url = google.createAuthorizationURL(state, codeVerifier, ['openid', 'profile', 'email']);
|
||||
|
||||
event.cookies.set("google_oauth_state", state, {
|
||||
event.cookies.set('google_oauth_state', state, {
|
||||
httpOnly: true,
|
||||
maxAge: 60 * 10,
|
||||
secure: import.meta.env.PROD,
|
||||
path: "/",
|
||||
sameSite: "lax"
|
||||
path: '/',
|
||||
sameSite: 'lax'
|
||||
});
|
||||
event.cookies.set("google_code_verifier", codeVerifier, {
|
||||
event.cookies.set('google_code_verifier', codeVerifier, {
|
||||
httpOnly: true,
|
||||
maxAge: 60 * 10,
|
||||
secure: import.meta.env.PROD,
|
||||
path: "/",
|
||||
sameSite: "lax"
|
||||
path: '/',
|
||||
sameSite: 'lax'
|
||||
});
|
||||
|
||||
return new Response(null, {
|
||||
@@ -29,4 +29,4 @@ export function GET(event: RequestEvent): Response {
|
||||
Location: url.toString()
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -1,43 +1,51 @@
|
||||
import { google } from "$lib/server/oauth";
|
||||
import { ObjectParser } from "@pilcrowjs/object-parser";
|
||||
import { createUser, getUserFromGoogleId } from "$lib/server/user";
|
||||
import { DB } from "$lib/server/db";
|
||||
import { google } from '$lib/server/oauth';
|
||||
import { ObjectParser } from '@pilcrowjs/object-parser';
|
||||
import { createUser, getUserFromGoogleId } from '$lib/server/user';
|
||||
import { DB } from '$lib/server/db';
|
||||
import { browser } from '$app/environment';
|
||||
import { Date as neoDate } from 'neo4j-driver';
|
||||
import { createSession, generateSessionToken, setSessionTokenCookie } from "$lib/server/session";
|
||||
import { decodeIdToken } from "arctic";
|
||||
import { missing_field, last_name, first_name, mothers_first_name, mothers_last_name, born, failed_to_create_user } from "$lib/paraglide/messages";
|
||||
import { createSession, generateSessionToken, setSessionTokenCookie } from '$lib/server/session';
|
||||
import { decodeIdToken } from 'arctic';
|
||||
import {
|
||||
missing_field,
|
||||
last_name,
|
||||
first_name,
|
||||
mothers_first_name,
|
||||
mothers_last_name,
|
||||
born,
|
||||
failed_to_create_user
|
||||
} from '$lib/paraglide/messages';
|
||||
|
||||
import type { PageServerLoad, Actions, RequestEvent, PageData } from "./$types";
|
||||
import type { OAuth2Tokens } from "arctic";
|
||||
import type { PageServerLoad, Actions, RequestEvent, PageData } from './$types';
|
||||
import type { OAuth2Tokens } from 'arctic';
|
||||
import type { PersonProperties } from '$lib/model';
|
||||
import { error, redirect, fail } from "@sveltejs/kit";
|
||||
import { error, redirect, fail } from '@sveltejs/kit';
|
||||
|
||||
const StorageLimit = 200 * 1024 * 1024;
|
||||
|
||||
export const load: PageServerLoad = async (event: RequestEvent) => {
|
||||
//prevent loading in developer mode, due to some issues with universal load, even if this is a server only ts,it will still run on client in dev mode idk
|
||||
if (browser) {
|
||||
return {}
|
||||
if (browser) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const storedState = event.cookies.get("google_oauth_state") ?? null;
|
||||
const codeVerifier = event.cookies.get("google_code_verifier") ?? null;
|
||||
const code = event.url.searchParams.get("code");
|
||||
const state = event.url.searchParams.get("state");
|
||||
const storedState = event.cookies.get('google_oauth_state') ?? null;
|
||||
const codeVerifier = event.cookies.get('google_code_verifier') ?? null;
|
||||
const code = event.url.searchParams.get('code');
|
||||
const state = event.url.searchParams.get('state');
|
||||
|
||||
if (storedState === null || codeVerifier === null || code === null || state === null) {
|
||||
return error(400, { message: "Please restart the process." })
|
||||
return error(400, { message: 'Please restart the process.' });
|
||||
}
|
||||
if (storedState !== state) {
|
||||
return error(400, { message: "Please restart the process." })
|
||||
return error(400, { message: 'Please restart the process.' });
|
||||
}
|
||||
|
||||
let tokens: OAuth2Tokens;
|
||||
try {
|
||||
tokens = await google.validateAuthorizationCode(code, codeVerifier);
|
||||
} catch (e) {
|
||||
return error(400, { message: "Failed to validate authorization code with " + e });
|
||||
return error(400, { message: 'Failed to validate authorization code with ' + e });
|
||||
}
|
||||
|
||||
// if (!event.platform || !event.platform.env || !event.platform.env.GH_SESSIONS) {
|
||||
@@ -47,10 +55,10 @@ export const load: PageServerLoad = async (event: RequestEvent) => {
|
||||
const claims = decodeIdToken(tokens.idToken());
|
||||
const claimsParser = new ObjectParser(claims);
|
||||
|
||||
const sub = claimsParser.getString("sub");
|
||||
const family_name = claimsParser.getString("family_name");
|
||||
const first_name = claimsParser.getString("given_name");
|
||||
const email = claimsParser.getString("email");
|
||||
const sub = claimsParser.getString('sub');
|
||||
const family_name = claimsParser.getString('family_name');
|
||||
const first_name = claimsParser.getString('given_name');
|
||||
const email = claimsParser.getString('email');
|
||||
|
||||
const dbSession = DB.session();
|
||||
const existingUser = await getUserFromGoogleId(dbSession, sub);
|
||||
@@ -62,7 +70,7 @@ export const load: PageServerLoad = async (event: RequestEvent) => {
|
||||
// const session = await createSession(sessionToken, eUser.get('elementId'), event.platform.env.GH_SESSIONS);
|
||||
// setSessionTokenCookie(event, sessionToken, session.expiresAt);
|
||||
|
||||
return redirect(302, "/");
|
||||
return redirect(302, '/');
|
||||
}
|
||||
|
||||
let personP: PersonProperties = {
|
||||
@@ -72,13 +80,13 @@ export const load: PageServerLoad = async (event: RequestEvent) => {
|
||||
email: email,
|
||||
allow_admin_access: false,
|
||||
limit: StorageLimit,
|
||||
verified: false,
|
||||
verified: false
|
||||
};
|
||||
|
||||
return {
|
||||
props: personP
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const actions: Actions = {
|
||||
register: register
|
||||
@@ -89,15 +97,15 @@ async function register(event: RequestEvent) {
|
||||
// if (!event.platform || !event.platform.env || !event.platform.env.GH_SESSIONS) {
|
||||
// return fail(500, { message: "Server configuration error. GH_SESSIONS KeyValue store missing" });
|
||||
// }
|
||||
const google_id = data.get('google_id')
|
||||
const google_id = data.get('google_id');
|
||||
if (google_id === null) {
|
||||
return fail(400, {
|
||||
message: missing_field({
|
||||
field: "google_id"
|
||||
field: 'google_id'
|
||||
})
|
||||
});
|
||||
}
|
||||
const first_name_f = data.get('first_name')
|
||||
const first_name_f = data.get('first_name');
|
||||
if (first_name_f === null) {
|
||||
return fail(400, {
|
||||
message: missing_field({
|
||||
@@ -105,49 +113,44 @@ async function register(event: RequestEvent) {
|
||||
})
|
||||
});
|
||||
}
|
||||
const last_name_f = data.get('last_name')
|
||||
const last_name_f = data.get('last_name');
|
||||
if (last_name_f === null) {
|
||||
return fail(400, {
|
||||
message:
|
||||
missing_field({
|
||||
field: last_name()
|
||||
})
|
||||
message: missing_field({
|
||||
field: last_name()
|
||||
})
|
||||
});
|
||||
}
|
||||
const email = data.get('email')
|
||||
const email = data.get('email');
|
||||
if (email === null) {
|
||||
return fail(400, {
|
||||
message:
|
||||
missing_field({
|
||||
field: "Email"
|
||||
})
|
||||
message: missing_field({
|
||||
field: 'Email'
|
||||
})
|
||||
});
|
||||
}
|
||||
const birth_date = data.get('birth_date');
|
||||
if (birth_date === null) {
|
||||
return fail(400, {
|
||||
message:
|
||||
missing_field({
|
||||
field: born()
|
||||
})
|
||||
message: missing_field({
|
||||
field: born()
|
||||
})
|
||||
});
|
||||
}
|
||||
const mothers_first_name_f = data.get('mothers_first_name');
|
||||
if (mothers_first_name_f === null) {
|
||||
return fail(400, {
|
||||
message:
|
||||
missing_field({
|
||||
field: mothers_first_name()
|
||||
})
|
||||
message: missing_field({
|
||||
field: mothers_first_name()
|
||||
})
|
||||
});
|
||||
}
|
||||
const mothers_last_name_f = data.get('mothers_last_name');
|
||||
if (mothers_last_name_f === null) {
|
||||
return fail(400, {
|
||||
message:
|
||||
missing_field({
|
||||
field: mothers_last_name()
|
||||
})
|
||||
message: missing_field({
|
||||
field: mothers_last_name()
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
@@ -158,12 +161,16 @@ async function register(event: RequestEvent) {
|
||||
first_name: first_name_f as string,
|
||||
last_name: last_name_f as string,
|
||||
email: email as string,
|
||||
born: new neoDate(parsed_date.getFullYear(), parsed_date.getUTCMonth(), parsed_date.getUTCDate()),
|
||||
born: new neoDate(
|
||||
parsed_date.getFullYear(),
|
||||
parsed_date.getUTCMonth(),
|
||||
parsed_date.getUTCDate()
|
||||
),
|
||||
mothers_first_name: mothers_first_name_f as string,
|
||||
mothers_last_name: mothers_last_name_f as string,
|
||||
allow_admin_access: false,
|
||||
limit: StorageLimit,
|
||||
verified: false,
|
||||
verified: false
|
||||
};
|
||||
|
||||
const dbSession = DB.session();
|
||||
@@ -179,5 +186,5 @@ async function register(event: RequestEvent) {
|
||||
// const session = await createSession(sessionToken, user.get('elementId'), event.platform.env.GH_SESSIONS);
|
||||
// setSessionTokenCookie(event, sessionToken, session.expiresAt);
|
||||
|
||||
return redirect(302, "/");
|
||||
}
|
||||
return redirect(302, '/');
|
||||
}
|
||||
|
@@ -12,7 +12,7 @@
|
||||
last_name,
|
||||
first_name,
|
||||
email,
|
||||
allow_family_tree_admin_access,
|
||||
allow_family_tree_admin_access
|
||||
} from '$lib/paraglide/messages';
|
||||
import FamilyTree from '../../highresolution_icon_no_background_croped.png';
|
||||
let { data, form }: PageProps = $props();
|
||||
@@ -49,24 +49,65 @@
|
||||
<form method="POST" action="/register">
|
||||
<fieldset class="fieldset">
|
||||
{#if form?.message}
|
||||
<div role="alert" class="alert alert-error">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 shrink-0 stroke-current" fill="none" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span>{form.message}</span>
|
||||
</div>
|
||||
{/if }
|
||||
<div role="alert" class="alert alert-error">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6 shrink-0 stroke-current"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<span>{form.message}</span>
|
||||
</div>
|
||||
{/if}
|
||||
<label class="fieldset-label" for="email">Email</label>
|
||||
<input type="email" class="input" placeholder="Email" value="{data.props.email}" />
|
||||
<input type="text" class="hidden" id="google_id" placeholder="Google ID" value="{data.props.google_id}" />
|
||||
<input type="email" class="input" placeholder="Email" value={data.props.email} />
|
||||
<input
|
||||
type="text"
|
||||
class="hidden"
|
||||
id="google_id"
|
||||
placeholder="Google ID"
|
||||
value={data.props.google_id}
|
||||
/>
|
||||
<label class="fieldset-label" for="first_name">{first_name()}</label>
|
||||
<input type="text" class="input" id="first_name" placeholder="{first_name()}" value="{data.props.first_name}" />
|
||||
<input
|
||||
type="text"
|
||||
class="input"
|
||||
id="first_name"
|
||||
placeholder={first_name()}
|
||||
value={data.props.first_name}
|
||||
/>
|
||||
<label class="fieldset-label" for="last_name">{last_name()}</label>
|
||||
<input type="text" class="input" id="last_name" placeholder="{last_name()}" value="{data.props.last_name}" />
|
||||
<label class="fieldset-label" for="allow_admin_access">{allow_family_tree_admin_access()}</label>
|
||||
<input type="checkbox" class="input" id="allow_admin_access" checked="{data.props.allow_admin_access}" />
|
||||
<input
|
||||
type="text"
|
||||
class="input"
|
||||
id="last_name"
|
||||
placeholder={last_name()}
|
||||
value={data.props.last_name}
|
||||
/>
|
||||
<label class="fieldset-label" for="allow_admin_access"
|
||||
>{allow_family_tree_admin_access()}</label
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
class="input"
|
||||
id="allow_admin_access"
|
||||
checked={data.props.allow_admin_access}
|
||||
/>
|
||||
<label class="fieldset-label" for="birth_date">{born()}</label>
|
||||
<input type="text" class="input pika-single" id="birth_date" bind:this={birth_date} value={born()} />
|
||||
<input
|
||||
type="text"
|
||||
class="input pika-single"
|
||||
id="birth_date"
|
||||
bind:this={birth_date}
|
||||
value={born()}
|
||||
/>
|
||||
<label class="fieldset-label" for="mothers_last_name">{mothers_last_name()}</label>
|
||||
<input
|
||||
type="text"
|
||||
@@ -75,7 +116,7 @@
|
||||
placeholder={mothers_last_name()}
|
||||
/>
|
||||
<label class="fieldset-label" for="mothers_first_name">{mothers_first_name()}</label>
|
||||
<input
|
||||
<input
|
||||
type="text"
|
||||
class="input"
|
||||
id="mothers_first_name"
|
||||
|
@@ -1,7 +1,7 @@
|
||||
<script module>
|
||||
import Page from './+page.svelte';
|
||||
import Page from './+page.svelte';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page />
|
||||
</template>
|
||||
<Page />
|
||||
</template>
|
||||
|
@@ -1,24 +1,24 @@
|
||||
<script module>
|
||||
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||
import Button from './Button.svelte';
|
||||
import { fn } from '@storybook/test';
|
||||
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||
import Button from './Button.svelte';
|
||||
import { fn } from '@storybook/test';
|
||||
|
||||
// More on how to set up stories at: https://storybook.js.org/docs/writing-stories
|
||||
const { Story } = defineMeta({
|
||||
title: 'Example/Button',
|
||||
component: Button,
|
||||
tags: ['autodocs'],
|
||||
argTypes: {
|
||||
backgroundColor: { control: 'color' },
|
||||
size: {
|
||||
control: { type: 'select' },
|
||||
options: ['small', 'medium', 'large'],
|
||||
},
|
||||
},
|
||||
args: {
|
||||
onClick: fn(),
|
||||
}
|
||||
});
|
||||
// More on how to set up stories at: https://storybook.js.org/docs/writing-stories
|
||||
const { Story } = defineMeta({
|
||||
title: 'Example/Button',
|
||||
component: Button,
|
||||
tags: ['autodocs'],
|
||||
argTypes: {
|
||||
backgroundColor: { control: 'color' },
|
||||
size: {
|
||||
control: { type: 'select' },
|
||||
options: ['small', 'medium', 'large']
|
||||
}
|
||||
},
|
||||
args: {
|
||||
onClick: fn()
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- More on writing stories with args: https://storybook.js.org/docs/writing-stories/args -->
|
||||
|
@@ -1,29 +1,29 @@
|
||||
<script lang="ts">
|
||||
import './button.css';
|
||||
import './button.css';
|
||||
|
||||
interface Props {
|
||||
/** Is this the principal call to action on the page? */
|
||||
primary?: boolean;
|
||||
/** What background color to use */
|
||||
backgroundColor?: string;
|
||||
/** How large should the button be? */
|
||||
size?: 'small' | 'medium' | 'large';
|
||||
/** Button contents */
|
||||
label: string;
|
||||
/** The onclick event handler */
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
const { primary = false, backgroundColor, size = 'medium', label, onClick }: Props = $props();
|
||||
interface Props {
|
||||
/** Is this the principal call to action on the page? */
|
||||
primary?: boolean;
|
||||
/** What background color to use */
|
||||
backgroundColor?: string;
|
||||
/** How large should the button be? */
|
||||
size?: 'small' | 'medium' | 'large';
|
||||
/** Button contents */
|
||||
label: string;
|
||||
/** The onclick event handler */
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
const { primary = false, backgroundColor, size = 'medium', label, onClick }: Props = $props();
|
||||
</script>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class={['storybook-button', `storybook-button--${size}`].join(' ')}
|
||||
class:storybook-button--primary={primary}
|
||||
class:storybook-button--secondary={!primary}
|
||||
style:background-color={backgroundColor}
|
||||
onclick={onClick}
|
||||
type="button"
|
||||
class={['storybook-button', `storybook-button--${size}`].join(' ')}
|
||||
class:storybook-button--primary={primary}
|
||||
class:storybook-button--secondary={!primary}
|
||||
style:background-color={backgroundColor}
|
||||
onclick={onClick}
|
||||
>
|
||||
{label}
|
||||
{label}
|
||||
</button>
|
||||
|
@@ -1,35 +1,37 @@
|
||||
import { Meta } from "@storybook/blocks";
|
||||
import { Meta } from '@storybook/blocks';
|
||||
|
||||
import Github from "./assets/github.svg";
|
||||
import Discord from "./assets/discord.svg";
|
||||
import Youtube from "./assets/youtube.svg";
|
||||
import Tutorials from "./assets/tutorials.svg";
|
||||
import Styling from "./assets/styling.png";
|
||||
import Context from "./assets/context.png";
|
||||
import Assets from "./assets/assets.png";
|
||||
import Docs from "./assets/docs.png";
|
||||
import Share from "./assets/share.png";
|
||||
import FigmaPlugin from "./assets/figma-plugin.png";
|
||||
import Testing from "./assets/testing.png";
|
||||
import Accessibility from "./assets/accessibility.png";
|
||||
import Theming from "./assets/theming.png";
|
||||
import AddonLibrary from "./assets/addon-library.png";
|
||||
import Github from './assets/github.svg';
|
||||
import Discord from './assets/discord.svg';
|
||||
import Youtube from './assets/youtube.svg';
|
||||
import Tutorials from './assets/tutorials.svg';
|
||||
import Styling from './assets/styling.png';
|
||||
import Context from './assets/context.png';
|
||||
import Assets from './assets/assets.png';
|
||||
import Docs from './assets/docs.png';
|
||||
import Share from './assets/share.png';
|
||||
import FigmaPlugin from './assets/figma-plugin.png';
|
||||
import Testing from './assets/testing.png';
|
||||
import Accessibility from './assets/accessibility.png';
|
||||
import Theming from './assets/theming.png';
|
||||
import AddonLibrary from './assets/addon-library.png';
|
||||
|
||||
export const RightArrow = () => <svg
|
||||
viewBox="0 0 14 14"
|
||||
width="8px"
|
||||
height="14px"
|
||||
style={{
|
||||
marginLeft: '4px',
|
||||
display: 'inline-block',
|
||||
shapeRendering: 'inherit',
|
||||
verticalAlign: 'middle',
|
||||
fill: 'currentColor',
|
||||
'path fill': 'currentColor'
|
||||
}}
|
||||
>
|
||||
<path d="m11.1 7.35-5.5 5.5a.5.5 0 0 1-.7-.7L10.04 7 4.9 1.85a.5.5 0 1 1 .7-.7l5.5 5.5c.2.2.2.5 0 .7Z" />
|
||||
</svg>
|
||||
export const RightArrow = () => (
|
||||
<svg
|
||||
viewBox="0 0 14 14"
|
||||
width="8px"
|
||||
height="14px"
|
||||
style={{
|
||||
marginLeft: '4px',
|
||||
display: 'inline-block',
|
||||
shapeRendering: 'inherit',
|
||||
verticalAlign: 'middle',
|
||||
fill: 'currentColor',
|
||||
'path fill': 'currentColor'
|
||||
}}
|
||||
>
|
||||
<path d="m11.1 7.35-5.5 5.5a.5.5 0 0 1-.7-.7L10.04 7 4.9 1.85a.5.5 0 1 1 .7-.7l5.5 5.5c.2.2.2.5 0 .7Z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
<Meta title="Configure your project" />
|
||||
|
||||
@@ -38,6 +40,7 @@ export const RightArrow = () => <svg
|
||||
# Configure your project
|
||||
|
||||
Because Storybook works separately from your app, you'll need to configure it for your specific stack and setup. Below, explore guides for configuring Storybook with popular frameworks and tools. If you get stuck, learn how you can ask for help from our community.
|
||||
|
||||
</div>
|
||||
<div className="sb-section">
|
||||
<div className="sb-section-item">
|
||||
@@ -84,6 +87,7 @@ export const RightArrow = () => <svg
|
||||
# Do more with Storybook
|
||||
|
||||
Now that you know the basics, let's explore other parts of Storybook that will improve your experience. This list is just to get you started. You can customise Storybook in many ways to fit your needs.
|
||||
|
||||
</div>
|
||||
|
||||
<div className="sb-section">
|
||||
@@ -203,10 +207,11 @@ export const RightArrow = () => <svg
|
||||
target="_blank"
|
||||
>Discover tutorials<RightArrow /></a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<style>
|
||||
{`
|
||||
{`
|
||||
.sb-container {
|
||||
margin-bottom: 48px;
|
||||
}
|
||||
|
@@ -1,24 +1,24 @@
|
||||
<script module>
|
||||
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||
import Header from './Header.svelte';
|
||||
import { fn } from '@storybook/test';
|
||||
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||
import Header from './Header.svelte';
|
||||
import { fn } from '@storybook/test';
|
||||
|
||||
// More on how to set up stories at: https://storybook.js.org/docs/writing-stories
|
||||
const { Story } = defineMeta({
|
||||
title: 'Example/Header',
|
||||
component: Header,
|
||||
// This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
|
||||
tags: ['autodocs'],
|
||||
parameters: {
|
||||
// More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
|
||||
layout: 'fullscreen',
|
||||
},
|
||||
args: {
|
||||
onLogin: fn(),
|
||||
onLogout: fn(),
|
||||
onCreateAccount: fn(),
|
||||
}
|
||||
});
|
||||
// More on how to set up stories at: https://storybook.js.org/docs/writing-stories
|
||||
const { Story } = defineMeta({
|
||||
title: 'Example/Header',
|
||||
component: Header,
|
||||
// This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
|
||||
tags: ['autodocs'],
|
||||
parameters: {
|
||||
// More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
|
||||
layout: 'fullscreen'
|
||||
},
|
||||
args: {
|
||||
onLogin: fn(),
|
||||
onLogout: fn(),
|
||||
onCreateAccount: fn()
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<Story name="Logged In" args={{ user: { name: 'Jane Doe' } }} />
|
||||
|
@@ -1,45 +1,45 @@
|
||||
<script lang="ts">
|
||||
import './header.css';
|
||||
import Button from './Button.svelte';
|
||||
import './header.css';
|
||||
import Button from './Button.svelte';
|
||||
|
||||
interface Props {
|
||||
user?: { name: string };
|
||||
onLogin?: () => void;
|
||||
onLogout?: () => void;
|
||||
onCreateAccount?: () => void;
|
||||
}
|
||||
interface Props {
|
||||
user?: { name: string };
|
||||
onLogin?: () => void;
|
||||
onLogout?: () => void;
|
||||
onCreateAccount?: () => void;
|
||||
}
|
||||
|
||||
const { user, onLogin, onLogout, onCreateAccount }: Props = $props();
|
||||
const { user, onLogin, onLogout, onCreateAccount }: Props = $props();
|
||||
</script>
|
||||
|
||||
<header>
|
||||
<div class="storybook-header">
|
||||
<div>
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<path
|
||||
d="M10 0h12a10 10 0 0110 10v12a10 10 0 01-10 10H10A10 10 0 010 22V10A10 10 0 0110 0z"
|
||||
fill="#FFF"
|
||||
/>
|
||||
<path
|
||||
d="M5.3 10.6l10.4 6v11.1l-10.4-6v-11zm11.4-6.2l9.7 5.5-9.7 5.6V4.4z"
|
||||
fill="#555AB9"
|
||||
/>
|
||||
<path d="M27.2 10.6v11.2l-10.5 6V16.5l10.5-6zM15.7 4.4v11L6 10l9.7-5.5z" fill="#91BAF8" />
|
||||
</g>
|
||||
</svg>
|
||||
<h1>Acme</h1>
|
||||
</div>
|
||||
<div>
|
||||
{#if user}
|
||||
<span class="welcome">
|
||||
Welcome, <b>{user.name}</b>!
|
||||
</span>
|
||||
<Button size="small" onClick={onLogout} label="Log out" />
|
||||
{:else}
|
||||
<Button size="small" onClick={onLogin} label="Log in" />
|
||||
<Button primary size="small" onClick={onCreateAccount} label="Sign up" />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="storybook-header">
|
||||
<div>
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<path
|
||||
d="M10 0h12a10 10 0 0110 10v12a10 10 0 01-10 10H10A10 10 0 010 22V10A10 10 0 0110 0z"
|
||||
fill="#FFF"
|
||||
/>
|
||||
<path
|
||||
d="M5.3 10.6l10.4 6v11.1l-10.4-6v-11zm11.4-6.2l9.7 5.5-9.7 5.6V4.4z"
|
||||
fill="#555AB9"
|
||||
/>
|
||||
<path d="M27.2 10.6v11.2l-10.5 6V16.5l10.5-6zM15.7 4.4v11L6 10l9.7-5.5z" fill="#91BAF8" />
|
||||
</g>
|
||||
</svg>
|
||||
<h1>Acme</h1>
|
||||
</div>
|
||||
<div>
|
||||
{#if user}
|
||||
<span class="welcome">
|
||||
Welcome, <b>{user.name}</b>!
|
||||
</span>
|
||||
<Button size="small" onClick={onLogout} label="Log out" />
|
||||
{:else}
|
||||
<Button size="small" onClick={onLogin} label="Log in" />
|
||||
<Button primary size="small" onClick={onCreateAccount} label="Sign up" />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
@@ -1,30 +1,32 @@
|
||||
<script module>
|
||||
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||
import { expect, userEvent, waitFor, within } from '@storybook/test';
|
||||
import Page from './Page.svelte';
|
||||
import { fn } from '@storybook/test';
|
||||
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||
import { expect, userEvent, waitFor, within } from '@storybook/test';
|
||||
import Page from './Page.svelte';
|
||||
import { fn } from '@storybook/test';
|
||||
|
||||
// More on how to set up stories at: https://storybook.js.org/docs/writing-stories
|
||||
const { Story } = defineMeta({
|
||||
title: 'Example/Page',
|
||||
component: Page,
|
||||
parameters: {
|
||||
// More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
|
||||
layout: 'fullscreen',
|
||||
},
|
||||
});
|
||||
// More on how to set up stories at: https://storybook.js.org/docs/writing-stories
|
||||
const { Story } = defineMeta({
|
||||
title: 'Example/Page',
|
||||
component: Page,
|
||||
parameters: {
|
||||
// More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
|
||||
layout: 'fullscreen'
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<Story name="Logged In" play={async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const loginButton = canvas.getByRole('button', { name: /Log in/i });
|
||||
await expect(loginButton).toBeInTheDocument();
|
||||
await userEvent.click(loginButton);
|
||||
await waitFor(() => expect(loginButton).not.toBeInTheDocument());
|
||||
<Story
|
||||
name="Logged In"
|
||||
play={async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const loginButton = canvas.getByRole('button', { name: /Log in/i });
|
||||
await expect(loginButton).toBeInTheDocument();
|
||||
await userEvent.click(loginButton);
|
||||
await waitFor(() => expect(loginButton).not.toBeInTheDocument());
|
||||
|
||||
const logoutButton = canvas.getByRole('button', { name: /Log out/i });
|
||||
await expect(logoutButton).toBeInTheDocument();
|
||||
}}
|
||||
const logoutButton = canvas.getByRole('button', { name: /Log out/i });
|
||||
await expect(logoutButton).toBeInTheDocument();
|
||||
}}
|
||||
/>
|
||||
|
||||
<Story name="Logged Out" />
|
||||
|
@@ -1,70 +1,70 @@
|
||||
<script lang="ts">
|
||||
import './page.css';
|
||||
import Header from './Header.svelte';
|
||||
import './page.css';
|
||||
import Header from './Header.svelte';
|
||||
|
||||
let user = $state<{ name: string }>();
|
||||
let user = $state<{ name: string }>();
|
||||
</script>
|
||||
|
||||
<article>
|
||||
<Header
|
||||
{user}
|
||||
onLogin={() => (user = { name: 'Jane Doe' })}
|
||||
onLogout={() => (user = undefined)}
|
||||
onCreateAccount={() => (user = { name: 'Jane Doe' })}
|
||||
/>
|
||||
<Header
|
||||
{user}
|
||||
onLogin={() => (user = { name: 'Jane Doe' })}
|
||||
onLogout={() => (user = undefined)}
|
||||
onCreateAccount={() => (user = { name: 'Jane Doe' })}
|
||||
/>
|
||||
|
||||
<section class="storybook-page">
|
||||
<h2>Pages in Storybook</h2>
|
||||
<p>
|
||||
We recommend building UIs with a
|
||||
<a
|
||||
href="https://blog.hichroma.com/component-driven-development-ce1109d56c8e"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<strong>component-driven</strong>
|
||||
</a>
|
||||
process starting with atomic components and ending with pages.
|
||||
</p>
|
||||
<p>
|
||||
Render pages with mock data. This makes it easy to build and review page states without
|
||||
needing to navigate to them in your app. Here are some handy patterns for managing page data
|
||||
in Storybook:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
Use a higher-level connected component. Storybook helps you compose such data from the
|
||||
"args" of child component stories
|
||||
</li>
|
||||
<li>
|
||||
Assemble data in the page component from your services. You can mock these services out
|
||||
using Storybook.
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
Get a guided tutorial on component-driven development at
|
||||
<a href="https://storybook.js.org/tutorials/" target="_blank" rel="noopener noreferrer">
|
||||
Storybook tutorials
|
||||
</a>
|
||||
. Read more in the
|
||||
<a href="https://storybook.js.org/docs" target="_blank" rel="noopener noreferrer">docs</a>
|
||||
.
|
||||
</p>
|
||||
<div class="tip-wrapper">
|
||||
<span class="tip">Tip</span>
|
||||
Adjust the width of the canvas with the
|
||||
<svg width="10" height="10" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<path
|
||||
d="M1.5 5.2h4.8c.3 0 .5.2.5.4v5.1c-.1.2-.3.3-.4.3H1.4a.5.5 0
|
||||
<section class="storybook-page">
|
||||
<h2>Pages in Storybook</h2>
|
||||
<p>
|
||||
We recommend building UIs with a
|
||||
<a
|
||||
href="https://blog.hichroma.com/component-driven-development-ce1109d56c8e"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<strong>component-driven</strong>
|
||||
</a>
|
||||
process starting with atomic components and ending with pages.
|
||||
</p>
|
||||
<p>
|
||||
Render pages with mock data. This makes it easy to build and review page states without
|
||||
needing to navigate to them in your app. Here are some handy patterns for managing page data
|
||||
in Storybook:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
Use a higher-level connected component. Storybook helps you compose such data from the
|
||||
"args" of child component stories
|
||||
</li>
|
||||
<li>
|
||||
Assemble data in the page component from your services. You can mock these services out
|
||||
using Storybook.
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
Get a guided tutorial on component-driven development at
|
||||
<a href="https://storybook.js.org/tutorials/" target="_blank" rel="noopener noreferrer">
|
||||
Storybook tutorials
|
||||
</a>
|
||||
. Read more in the
|
||||
<a href="https://storybook.js.org/docs" target="_blank" rel="noopener noreferrer">docs</a>
|
||||
.
|
||||
</p>
|
||||
<div class="tip-wrapper">
|
||||
<span class="tip">Tip</span>
|
||||
Adjust the width of the canvas with the
|
||||
<svg width="10" height="10" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<path
|
||||
d="M1.5 5.2h4.8c.3 0 .5.2.5.4v5.1c-.1.2-.3.3-.4.3H1.4a.5.5 0
|
||||
01-.5-.4V5.7c0-.3.2-.5.5-.5zm0-2.1h6.9c.3 0 .5.2.5.4v7a.5.5 0 01-1 0V4H1.5a.5.5 0
|
||||
010-1zm0-2.1h9c.3 0 .5.2.5.4v9.1a.5.5 0 01-1 0V2H1.5a.5.5 0 010-1zm4.3 5.2H2V10h3.8V6.2z"
|
||||
id="a"
|
||||
fill="#999"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
Viewports addon in the toolbar
|
||||
</div>
|
||||
</section>
|
||||
id="a"
|
||||
fill="#999"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
Viewports addon in the toolbar
|
||||
</div>
|
||||
</section>
|
||||
</article>
|
||||
|
@@ -1,30 +1,30 @@
|
||||
.storybook-button {
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
border: 0;
|
||||
border-radius: 3em;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
border: 0;
|
||||
border-radius: 3em;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
}
|
||||
.storybook-button--primary {
|
||||
background-color: #555ab9;
|
||||
color: white;
|
||||
background-color: #555ab9;
|
||||
color: white;
|
||||
}
|
||||
.storybook-button--secondary {
|
||||
box-shadow: rgba(0, 0, 0, 0.15) 0px 0px 0px 1px inset;
|
||||
background-color: transparent;
|
||||
color: #333;
|
||||
box-shadow: rgba(0, 0, 0, 0.15) 0px 0px 0px 1px inset;
|
||||
background-color: transparent;
|
||||
color: #333;
|
||||
}
|
||||
.storybook-button--small {
|
||||
padding: 10px 16px;
|
||||
font-size: 12px;
|
||||
padding: 10px 16px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.storybook-button--medium {
|
||||
padding: 11px 20px;
|
||||
font-size: 14px;
|
||||
padding: 11px 20px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.storybook-button--large {
|
||||
padding: 12px 24px;
|
||||
font-size: 16px;
|
||||
padding: 12px 24px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
@@ -1,32 +1,32 @@
|
||||
.storybook-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
||||
padding: 15px 20px;
|
||||
font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
||||
padding: 15px 20px;
|
||||
font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
.storybook-header svg {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.storybook-header h1 {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
margin: 6px 0 6px 10px;
|
||||
font-weight: 700;
|
||||
font-size: 20px;
|
||||
line-height: 1;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
margin: 6px 0 6px 10px;
|
||||
font-weight: 700;
|
||||
font-size: 20px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.storybook-header button + button {
|
||||
margin-left: 10px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.storybook-header .welcome {
|
||||
margin-right: 10px;
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
margin-right: 10px;
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
@@ -1,68 +1,68 @@
|
||||
.storybook-page {
|
||||
margin: 0 auto;
|
||||
padding: 48px 20px;
|
||||
max-width: 600px;
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
line-height: 24px;
|
||||
font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
margin: 0 auto;
|
||||
padding: 48px 20px;
|
||||
max-width: 600px;
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
line-height: 24px;
|
||||
font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
.storybook-page h2 {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
margin: 0 0 4px;
|
||||
font-weight: 700;
|
||||
font-size: 32px;
|
||||
line-height: 1;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
margin: 0 0 4px;
|
||||
font-weight: 700;
|
||||
font-size: 32px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.storybook-page p {
|
||||
margin: 1em 0;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.storybook-page a {
|
||||
color: inherit;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.storybook-page ul {
|
||||
margin: 1em 0;
|
||||
padding-left: 30px;
|
||||
margin: 1em 0;
|
||||
padding-left: 30px;
|
||||
}
|
||||
|
||||
.storybook-page li {
|
||||
margin-bottom: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.storybook-page .tip {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
margin-right: 10px;
|
||||
border-radius: 1em;
|
||||
background: #e7fdd8;
|
||||
padding: 4px 12px;
|
||||
color: #357a14;
|
||||
font-weight: 700;
|
||||
font-size: 11px;
|
||||
line-height: 12px;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
margin-right: 10px;
|
||||
border-radius: 1em;
|
||||
background: #e7fdd8;
|
||||
padding: 4px 12px;
|
||||
color: #357a14;
|
||||
font-weight: 700;
|
||||
font-size: 11px;
|
||||
line-height: 12px;
|
||||
}
|
||||
|
||||
.storybook-page .tip-wrapper {
|
||||
margin-top: 40px;
|
||||
margin-bottom: 40px;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
margin-top: 40px;
|
||||
margin-bottom: 40px;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.storybook-page .tip-wrapper svg {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
margin-top: 3px;
|
||||
margin-right: 4px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
margin-top: 3px;
|
||||
margin-right: 4px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.storybook-page .tip-wrapper svg path {
|
||||
fill: #1ea7fd;
|
||||
fill: #1ea7fd;
|
||||
}
|
||||
|
Reference in New Issue
Block a user