mirror of
https://github.com/vcscsvcscs/GenerationsHeritage.git
synced 2025-08-13 22:39:06 +02:00
implement menus and person panels
This commit is contained in:
@@ -6,15 +6,15 @@ const config: StorybookConfig = {
|
||||
'@storybook/addon-svelte-csf',
|
||||
'@storybook/addon-essentials',
|
||||
'@chromatic-com/storybook',
|
||||
'@storybook/addon-interactions',
|
||||
'@storybook/addon-interactions'
|
||||
],
|
||||
framework: {
|
||||
name: '@storybook/sveltekit',
|
||||
options: {
|
||||
builder: {
|
||||
viteConfigPath: '../vite.config.ts',
|
||||
},
|
||||
viteConfigPath: '../vite.config.ts'
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
export default config;
|
||||
|
22
apps/app/.vscode/extensions.json
vendored
22
apps/app/.vscode/extensions.json
vendored
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"inlang.vs-code-extension",
|
||||
"42Crunch.vscode-openapi",
|
||||
"bruno-api-client.bruno",
|
||||
"svelte.svelte-vscode",
|
||||
"github.vscode-github-actions",
|
||||
"GitHub.copilot",
|
||||
"pixl-garden.BongoCat",
|
||||
"golang.go"
|
||||
]
|
||||
}
|
||||
"recommendations": [
|
||||
"inlang.vs-code-extension",
|
||||
"42Crunch.vscode-openapi",
|
||||
"bruno-api-client.bruno",
|
||||
"svelte.svelte-vscode",
|
||||
"github.vscode-github-actions",
|
||||
"GitHub.copilot",
|
||||
"pixl-garden.BongoCat",
|
||||
"golang.go"
|
||||
]
|
||||
}
|
||||
|
@@ -24,6 +24,7 @@
|
||||
"cancel": "Cancel",
|
||||
"city": "City",
|
||||
"child": "Child",
|
||||
"change_profile_picture": "Change Profile Picture",
|
||||
"citizenship": "Citizenship",
|
||||
"close": "Close",
|
||||
"coffee": "Coffee",
|
||||
@@ -43,7 +44,9 @@
|
||||
"deny": "Deny",
|
||||
"description": "Description",
|
||||
"details": "Details",
|
||||
"died": "Died",
|
||||
"directions": "Directions",
|
||||
"disclaimer": "Disclaimer",
|
||||
"document": "Document",
|
||||
"download": "Download",
|
||||
"edit": "Edit",
|
||||
|
@@ -24,6 +24,7 @@
|
||||
"cancel": "Mégse",
|
||||
"city": "Város",
|
||||
"child": "Gyermek",
|
||||
"change_profile_picture": "Profilkép megváltoztatása",
|
||||
"citizenship": "Állampolgárság",
|
||||
"close": "Bezár",
|
||||
"coffee": "Kávé",
|
||||
@@ -43,7 +44,9 @@
|
||||
"deny": "Elutasítás",
|
||||
"description": "Leírás",
|
||||
"details": "Részletek",
|
||||
"died": "Elhunyt",
|
||||
"directions": "Útvonalak",
|
||||
"disclaimer": "Felelősségkizárás",
|
||||
"document": "Dokumentum",
|
||||
"download": "Letöltés",
|
||||
"edit": "Szerkesztés",
|
||||
@@ -151,4 +154,4 @@
|
||||
"welcome": "Üdvözöljük a Generációk Öröksége oldalán",
|
||||
"yes": "Igen",
|
||||
"zip_code": "Irányítószám"
|
||||
}
|
||||
}
|
||||
|
@@ -7,6 +7,8 @@
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover" style="width: 100vw; height: 100vh" class="bg-base-200">
|
||||
<div style="display: contents;width: 100vw; height: 100vh" class="bg-base-200">%sveltekit.body%</div>
|
||||
<div style="display: contents; width: 100vw; height: 100vh" class="bg-base-200">
|
||||
%sveltekit.body%
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
@@ -28,7 +28,6 @@ const authHandle: Handle = async ({ event, resolve }) => {
|
||||
setSessionTokenCookie(event, token, session.expiresAt);
|
||||
} else {
|
||||
console.log('Session token is invalid');
|
||||
console.log(session, token);
|
||||
deleteSessionTokenCookie(event);
|
||||
}
|
||||
|
||||
|
@@ -41,7 +41,7 @@
|
||||
<select
|
||||
bind:value={current_theme}
|
||||
data-choose-theme
|
||||
class="btn btn-ghost btn-xs min-h-0 h-8 px-4 py-0 text-sm"
|
||||
class="btn btn-ghost btn-xs h-8 min-h-0 px-4 py-0 text-sm"
|
||||
onchange={set_theme}
|
||||
>
|
||||
<option value="" disabled={current_theme !== ''}>
|
||||
|
@@ -2,7 +2,7 @@ import type { Meta, StoryObj } from '@storybook/svelte';
|
||||
import PersonMenu from './PersonMenu.svelte';
|
||||
|
||||
const meta = {
|
||||
title: 'graph/PersonMenu',
|
||||
title: 'lib/graph/PersonMenu',
|
||||
component: PersonMenu,
|
||||
tags: ['autodocs'],
|
||||
argTypes: {
|
||||
@@ -14,8 +14,8 @@ const meta = {
|
||||
deleteNode: { action: 'deleteNode clicked' },
|
||||
createRelationshipAndNode: { action: 'createRelationshipAndNode clicked' },
|
||||
addRelationship: { action: 'addRelationship clicked' },
|
||||
addAdmin: { action: 'addAdmin clicked' },
|
||||
},
|
||||
addAdmin: { action: 'addAdmin clicked' }
|
||||
}
|
||||
} satisfies Meta<PersonMenu>;
|
||||
|
||||
export default meta;
|
||||
@@ -32,7 +32,7 @@ export const Default: Story = {
|
||||
deleteNode: () => console.log('delete node'),
|
||||
createRelationshipAndNode: () => console.log('create relationship and node'),
|
||||
addRelationship: () => console.log('add relationship'),
|
||||
addAdmin: () => console.log('add admin'),
|
||||
addAdmin: () => console.log('add admin')
|
||||
}
|
||||
};
|
||||
|
||||
@@ -49,7 +49,7 @@ export const TopLeftPosition: Story = {
|
||||
deleteNode: () => console.log('delete node'),
|
||||
createRelationshipAndNode: () => console.log('create relationship and node'),
|
||||
addRelationship: () => console.log('add relationship'),
|
||||
addAdmin: () => console.log('add admin'),
|
||||
addAdmin: () => console.log('add admin')
|
||||
}
|
||||
};
|
||||
|
||||
@@ -66,7 +66,7 @@ export const BottomRightPosition: Story = {
|
||||
deleteNode: () => console.log('delete node'),
|
||||
createRelationshipAndNode: () => console.log('create relationship and node'),
|
||||
addRelationship: () => console.log('add relationship'),
|
||||
addAdmin: () => console.log('add admin'),
|
||||
addAdmin: () => console.log('add admin')
|
||||
}
|
||||
};
|
||||
|
||||
@@ -83,6 +83,6 @@ export const MiddleRightPosition: Story = {
|
||||
deleteNode: () => console.log('delete node'),
|
||||
createRelationshipAndNode: () => console.log('create relationship and node'),
|
||||
addRelationship: () => console.log('add relationship'),
|
||||
addAdmin: () => console.log('add admin'),
|
||||
addAdmin: () => console.log('add admin')
|
||||
}
|
||||
};
|
||||
|
@@ -1,15 +1,14 @@
|
||||
<!-- <svelte:options immutable /> -->
|
||||
|
||||
<script lang="ts">
|
||||
import { death } from './../paraglide/messages.js';
|
||||
import { Handle, Position, useConnection, type NodeProps } from '@xyflow/svelte';
|
||||
import type { components } from '$lib/api/api.gen';
|
||||
import { isValidConnection } from './connection.js';
|
||||
type $$Props = NodeProps;
|
||||
|
||||
export let id: NodeProps['id'];
|
||||
export let id: NodeProps['id'];
|
||||
export let data: NodeProps['data'] & components['schemas']['PersonProperties'];
|
||||
console.log('data', data);
|
||||
const connection = useConnection();
|
||||
const connection = useConnection();
|
||||
let isConnecting = false;
|
||||
let isTarget = false;
|
||||
|
||||
@@ -17,27 +16,48 @@
|
||||
$: isTarget = connection.current.toHandle?.id !== id;
|
||||
</script>
|
||||
|
||||
<div class="card card-compact bg-primary-content text-primary rounded-full w-40 h-40 flex flex-col items-center justify-center shadow-lg">
|
||||
<div
|
||||
class="card card-compact bg-primary-content text-primary flex h-40 w-40 flex-col items-center justify-center rounded-full shadow-lg"
|
||||
>
|
||||
{#if !isConnecting}
|
||||
<Handle class="customHandle" position={Position.Right} type="source" style="z-index: 1;" />
|
||||
{/if}
|
||||
<Handle class="customHandle" position={Position.Left} type="target" isConnectableStart={false} />
|
||||
|
||||
<div class="avatar mb-2">
|
||||
{#if isConnecting && isTarget}
|
||||
<Handle position={Position.Left} type="target" isConnectableStart={false} style="z-index: 1;" />
|
||||
{/if}
|
||||
<div class="w-24 rounded-full ring ring-accent ring-offset-accent ring-offset-1 border-0 bg-accent">
|
||||
{#if isConnecting && isTarget}
|
||||
<Handle position={Position.Left} type="target" isConnectableStart={false} style="z-index: 1;" />
|
||||
{/if}
|
||||
<img src={data.profile_picture||'https://cdn-icons-png.flaticon.com/512/10628/10628885.png'} alt="Picture of {data.last_name} {data.first_name}" />
|
||||
{#if isConnecting && isTarget}
|
||||
<Handle
|
||||
isValidConnection={isValidConnection}
|
||||
position={Position.Left}
|
||||
type="target"
|
||||
isConnectableStart={false}
|
||||
style="z-index: 1;"
|
||||
/>
|
||||
{/if}
|
||||
<div
|
||||
class="ring-accent ring-offset-accent bg-accent w-24 rounded-full border-0 ring ring-offset-1"
|
||||
>
|
||||
{#if isConnecting && isTarget}
|
||||
<Handle
|
||||
isValidConnection={isValidConnection}
|
||||
position={Position.Left}
|
||||
type="target"
|
||||
isConnectableStart={false}
|
||||
style="z-index: 1;"
|
||||
/>
|
||||
{/if}
|
||||
<img
|
||||
src={data.profile_picture || 'https://cdn-icons-png.flaticon.com/512/10628/10628885.png'}
|
||||
alt="Picture of {data.last_name} {data.first_name}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-center px-2">
|
||||
<h2 class="font-semibold text-sm leading-tight">
|
||||
{data.first_name} {data.middle_name ? data.middle_name : ''} {data.last_name}
|
||||
<div class="px-2 text-center">
|
||||
<h2 class="text-sm leading-tight font-semibold">
|
||||
{data.first_name}
|
||||
{data.middle_name ? data.middle_name : ''}
|
||||
{data.last_name}
|
||||
</h2>
|
||||
<h3 class="text-xs opacity-70">
|
||||
{data.born}{data.death ? ' - ' + data.death : ''}
|
||||
@@ -46,16 +66,16 @@
|
||||
</div>
|
||||
|
||||
<style>
|
||||
:global(div.customHandle) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: blue;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
border-radius: 0;
|
||||
transform: none;
|
||||
border: none;
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
:global(div.customHandle) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: blue;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
border-radius: 0;
|
||||
transform: none;
|
||||
border: none;
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
|
10
apps/app/src/lib/graph/connection.ts
Normal file
10
apps/app/src/lib/graph/connection.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import type { Connection} from '@xyflow/svelte';
|
||||
import type { EdgeBase } from '@xyflow/system';
|
||||
|
||||
export function isValidConnection(edge: EdgeBase | Connection) {
|
||||
if (edge.source !== edge.target) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
22
apps/app/src/lib/graph/fetch_family_tree.ts
Normal file
22
apps/app/src/lib/graph/fetch_family_tree.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type {Layout} from '$lib/graph/model';
|
||||
|
||||
|
||||
export async function fetchFamilyTree(with_out_spouse: boolean): Promise<Layout> {
|
||||
const url = with_out_spouse
|
||||
? '/api/family_tree?with_out_spouse=true'
|
||||
: '/api/family_tree?with_out_spouse=false';
|
||||
const response = await fetch(url, {
|
||||
method: 'GET'
|
||||
})
|
||||
|
||||
if (response.status !== 200){
|
||||
throw new Error(await response.text());
|
||||
}
|
||||
|
||||
let layout: Layout = {
|
||||
Nodes: [],
|
||||
Edges: []
|
||||
}
|
||||
|
||||
return layout;
|
||||
}
|
@@ -1,12 +1,19 @@
|
||||
export interface NodeMenu {
|
||||
onClick: () => void;
|
||||
deleteNode: () => void;
|
||||
createRelationshipAndNode: () => void;
|
||||
addRelationship: () => void;
|
||||
addRecipe: (() => void) | undefined;
|
||||
addAdmin: (() => void) | undefined;
|
||||
top: number | undefined;
|
||||
left: number | undefined;
|
||||
right: number | undefined;
|
||||
bottom: number | undefined;
|
||||
import type { Node, Edge } from '@xyflow/svelte';
|
||||
|
||||
export type NodeMenu = {
|
||||
onClick: () => void;
|
||||
deleteNode: () => void;
|
||||
createRelationshipAndNode: () => void;
|
||||
addRelationship: () => void;
|
||||
addRecipe: (() => void) | undefined;
|
||||
addAdmin: (() => void) | undefined;
|
||||
top: number | undefined;
|
||||
left: number | undefined;
|
||||
right: number | undefined;
|
||||
bottom: number | undefined;
|
||||
};
|
||||
|
||||
export type Layout = {
|
||||
Nodes: Array<Node>;
|
||||
Edges: Array<Edge>;
|
||||
}
|
@@ -1,14 +1,12 @@
|
||||
import type { components } from '$lib/api/api.gen';
|
||||
import type { Node } from '@xyflow/svelte';
|
||||
import type { NodeEventWithPointer } from '@xyflow/svelte';
|
||||
|
||||
export function handleNodeClick(set_panel_options:(person:components['schemas']['PersonProperties']&{id:number})=>void): ({
|
||||
detail: { event, node }
|
||||
}: {
|
||||
detail: { event: MouseEvent; node: Node };
|
||||
}) => void {
|
||||
return ({ detail: { event, node } }) => {
|
||||
event.preventDefault();
|
||||
node.data.id = Number(node.id);
|
||||
set_panel_options(node.data as components['schemas']['PersonProperties']&{id:number});
|
||||
}
|
||||
}
|
||||
export function handleNodeClick(
|
||||
set_panel_options: (person: components['schemas']['PersonProperties'] & { id: number }) => void
|
||||
): NodeEventWithPointer<MouseEvent | TouchEvent> {
|
||||
return ({ event, node }) => {
|
||||
event.preventDefault();
|
||||
node.data.id = Number(node.id);
|
||||
set_panel_options(node.data as components['schemas']['PersonProperties'] & { id: number });
|
||||
};
|
||||
}
|
||||
|
@@ -2,3 +2,16 @@ import * as runtime from '$lib/paraglide/runtime';
|
||||
import { createI18n } from '@inlang/paraglide-sveltekit';
|
||||
|
||||
export const i18n = createI18n(runtime);
|
||||
|
||||
import * as messages from '$lib/paraglide/messages';
|
||||
|
||||
export type MessageKeys = keyof typeof messages;
|
||||
|
||||
export function callMessageFunction(name: MessageKeys): string {
|
||||
const fn = messages[name];
|
||||
if (typeof fn === 'function') {
|
||||
return fn({ thing: '', field: '', page: '', name: '' });
|
||||
} else {
|
||||
throw new Error(`Function ${name} is not callable`);
|
||||
}
|
||||
}
|
||||
|
@@ -4,5 +4,5 @@
|
||||
</script>
|
||||
|
||||
{#if show}
|
||||
<a class="btn btn-error btn-xs min-h-0 h-8 px-4 py-0 text-sm" href="/logout">{logout()}</a>
|
||||
{/if}
|
||||
<a class="btn btn-error btn-xs h-8 min-h-0 px-4 py-0 text-sm" href="/logout">{logout()}</a>
|
||||
{/if}
|
||||
|
@@ -1,22 +1,32 @@
|
||||
<script lang="ts">
|
||||
import type { components } from "$lib/api/api.gen";
|
||||
import { video, photos } from "$lib/paraglide/messages";
|
||||
import type { components } from '$lib/api/api.gen';
|
||||
import { video, photos, upload } from '$lib/paraglide/messages';
|
||||
|
||||
export let draftPerson: components['schemas']['PersonProperties'];
|
||||
|
||||
</script>
|
||||
|
||||
{#if draftPerson.photos?.length || draftPerson.videos?.length}
|
||||
<div class="divider">{photos()} & {video()}</div>
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
{#each draftPerson.photos ?? [] as picture}
|
||||
<img src={picture.url} alt={picture.description ?? photos()} class="rounded-lg shadow-md object-cover w-full h-32" />
|
||||
{/each}
|
||||
{#each draftPerson.videos ?? [] as video}
|
||||
<video src={video.url} controls class="rounded-lg shadow-md w-full h-32">
|
||||
<track kind="captions" src={video.description} srcLang="en" default />
|
||||
<track kind="descriptions" src={video.description} srcLang="en" default />
|
||||
</video>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
export let draftPerson: components['schemas']['PersonProperties'];
|
||||
export let editorMode = false;
|
||||
</script>
|
||||
|
||||
{#if editorMode}
|
||||
<button class="btn bg-neutral text-neutral-content btn-xs" on:click={() => {}}>
|
||||
{upload()}
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
{#if draftPerson.photos?.length || draftPerson.videos?.length}
|
||||
<div class="divider">{photos()} & {video()}</div>
|
||||
<div class="grid grid-cols-2 gap-4 md:grid-cols-4">
|
||||
{#each draftPerson.photos ?? [] as picture}
|
||||
<img
|
||||
src={picture.url}
|
||||
alt={picture.description ?? photos()}
|
||||
class="h-32 w-full rounded-lg object-cover shadow-md"
|
||||
/>
|
||||
{/each}
|
||||
{#each draftPerson.videos ?? [] as video}
|
||||
<video src={video.url} controls class="h-32 w-full rounded-lg shadow-md">
|
||||
<track kind="captions" src={video.description} srcLang="en" default />
|
||||
<track kind="descriptions" src={video.description} srcLang="en" default />
|
||||
</video>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
@@ -8,43 +8,43 @@
|
||||
import OtherDetails from './OtherDetails.svelte';
|
||||
import type { components } from '$lib/api/api.gen.js';
|
||||
let {
|
||||
open = false,
|
||||
closeModal = () => {},
|
||||
person = {}
|
||||
}: { open: boolean; person: components['schemas']['PersonProperties'] } = $props();
|
||||
}: { closeModal: () => void; person: components['schemas']['PersonProperties'] } = $props();
|
||||
|
||||
let editorMode = $state(false);
|
||||
let draftPerson = $state({});
|
||||
|
||||
$effect(() => {
|
||||
if (open) {
|
||||
draftPerson = structuredClone(person);
|
||||
editorMode = false;
|
||||
}
|
||||
});
|
||||
draftPerson = person;
|
||||
editorMode = false;
|
||||
|
||||
function close() {
|
||||
open = false;
|
||||
editorMode = false;
|
||||
draftPerson = {};
|
||||
closeModal();
|
||||
editorMode = false;
|
||||
draftPerson = {};
|
||||
}
|
||||
|
||||
function toggleEdit() {
|
||||
editorMode = !editorMode;
|
||||
}
|
||||
|
||||
function save() {
|
||||
// Save logic here
|
||||
editorMode = false;
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
{#if open}
|
||||
<div class="modal modal-open" transition:fade>
|
||||
<div class="modal-box max-h-screen w-full max-w-5xl overflow-y-auto">
|
||||
<div class="bg-base-100 sticky top-0 z-10">
|
||||
<ModalButtons {editorMode} onClose={close} onToggleEdit={toggleEdit} />
|
||||
<div class="divider"></div>
|
||||
</div>
|
||||
|
||||
<ProfileHeader {draftPerson} {editorMode} />
|
||||
<MediaGallery {draftPerson} />
|
||||
<LifeEventsTimeline {draftPerson} />
|
||||
<OtherDetails {draftPerson} {editorMode} />
|
||||
<div class="modal modal-open" transition:fade>
|
||||
<div class="modal-box max-h-screen w-full max-w-5xl overflow-y-auto">
|
||||
<div class="bg-base-100 sticky top-0 z-10">
|
||||
<ModalButtons {editorMode} onClose={close} onSave={save} onToggleEdit={toggleEdit} />
|
||||
<div class="divider"></div>
|
||||
</div>
|
||||
|
||||
<ProfileHeader {draftPerson} {editorMode} />
|
||||
<MediaGallery {draftPerson} />
|
||||
<LifeEventsTimeline {draftPerson} />
|
||||
<OtherDetails {draftPerson} {editorMode} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
@@ -1,20 +1,26 @@
|
||||
<script lang="ts">
|
||||
import { back, biography, close, edit } from '$lib/paraglide/messages';
|
||||
import { back, biography, close, edit, save } from '$lib/paraglide/messages';
|
||||
|
||||
export let editorMode = false;
|
||||
|
||||
export let onClose: () => void;
|
||||
export let onToggleEdit: () => void;
|
||||
export let onSave: () => void;
|
||||
</script>
|
||||
|
||||
<div class="flex items-center justify-between p-2">
|
||||
<h3 class="text-lg font-bold">{biography()}</h3>
|
||||
<div class="space-x-2">
|
||||
<button class="btn btn-outline btn-error btn-sm" on:click={onClose}>
|
||||
{close()}
|
||||
<button class="btn btn-secondary btn-sm" on:click={onToggleEdit}>
|
||||
{editorMode ? back() : edit()}
|
||||
</button>
|
||||
<button class="btn btn-outline btn-primary btn-sm" on:click={onToggleEdit}>
|
||||
{editorMode ? back() : edit}
|
||||
{#if editorMode}
|
||||
<button class="btn btn-accent btn-sm" on:click={onSave}>
|
||||
{save()}
|
||||
</button>
|
||||
{/if}
|
||||
<button class="btn btn-error btn-sm" on:click={onClose}>
|
||||
{close()}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -1,40 +1,42 @@
|
||||
<script lang="ts">
|
||||
import * as msg from '$lib/paraglide/messages';
|
||||
export let draftPerson: any;
|
||||
export let editorMode = false;
|
||||
import { callMessageFunction } from '$lib/i18n';
|
||||
import type { MessageKeys } from '$lib/i18n';
|
||||
export let draftPerson: any;
|
||||
export let editorMode = false;
|
||||
|
||||
const skipFields = [
|
||||
'first_name',
|
||||
'last_name',
|
||||
'born',
|
||||
'biological_sex',
|
||||
'email',
|
||||
'limit',
|
||||
'mothers_first_name',
|
||||
'mothers_last_name',
|
||||
'profile_picture',
|
||||
'photos',
|
||||
'videos',
|
||||
'life_events'
|
||||
];
|
||||
const skipFields = [
|
||||
'id',
|
||||
'first_name',
|
||||
'last_name',
|
||||
'born',
|
||||
'biological_sex',
|
||||
'email',
|
||||
'limit',
|
||||
'mothers_first_name',
|
||||
'mothers_last_name',
|
||||
'profile_picture',
|
||||
'photos',
|
||||
'videos',
|
||||
'life_events'
|
||||
];
|
||||
</script>
|
||||
|
||||
<div class="mt-6 grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
{#each Object.entries(draftPerson) as [key, value]}
|
||||
{#if !skipFields.includes(key)}
|
||||
<div>
|
||||
<p>
|
||||
<strong>{(msg as any)[key]() ?? key}:</strong>
|
||||
{#if editorMode}
|
||||
<textarea
|
||||
bind:value={draftPerson[key]}
|
||||
class="textarea textarea-bordered textarea-sm w-full"
|
||||
></textarea>
|
||||
{:else}
|
||||
{JSON.stringify(value) ?? '-'}
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
{#each Object.entries(draftPerson) as [key, value]}
|
||||
{#if !skipFields.includes(key) && value !== undefined && value !== null}
|
||||
<div>
|
||||
<p>
|
||||
<strong>{callMessageFunction(key as MessageKeys) || key}:</strong>
|
||||
{#if editorMode}
|
||||
<textarea
|
||||
bind:value={draftPerson[key]}
|
||||
class="textarea textarea-bordered textarea-sm w-full"
|
||||
></textarea>
|
||||
{:else}
|
||||
{JSON.stringify(value) ?? '-'}
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
|
@@ -1,30 +1,103 @@
|
||||
<script lang="ts">
|
||||
import { death } from './../paraglide/messages/en.js';
|
||||
import { onMount } from 'svelte';
|
||||
import type { components } from '$lib/api/api.gen';
|
||||
import { biological_sex, born, email, first_name, last_name, middle_name, mothers_first_name, mothers_last_name, profile_picture } from '$lib/paraglide/messages';
|
||||
export let draftPerson: components['schemas']['PersonProperties'];
|
||||
import { male,female,intersex,other,change_profile_picture, biological_sex, born,died, email, first_name, id, last_name, middle_name, mothers_first_name, mothers_last_name, profile_picture } from '$lib/paraglide/messages';
|
||||
import { callMessageFunction } from '$lib/i18n';
|
||||
import type { MessageKeys } from '$lib/i18n';
|
||||
|
||||
export let draftPerson: components['schemas']['PersonProperties'] & {
|
||||
id?: string,
|
||||
};
|
||||
export let editorMode = false;
|
||||
let birth_date: HTMLInputElement;
|
||||
let death_date: HTMLInputElement;
|
||||
onMount(() => {
|
||||
if (birth_date) {
|
||||
import('pikaday').then(({ default: Pikaday }) => {
|
||||
const picker = new Pikaday({
|
||||
format: 'YYYY-MM-DD',
|
||||
minDate: new Date(1900, 0, 1),
|
||||
field: birth_date,
|
||||
onSelect: function (date) {
|
||||
birth_date.value = date.toISOString();
|
||||
}
|
||||
});
|
||||
// Clean up when component unmounts
|
||||
return () => picker.destroy();
|
||||
});
|
||||
}
|
||||
if (death_date) {
|
||||
import('pikaday').then(({ default: Pikaday }) => {
|
||||
const picker = new Pikaday({
|
||||
format: 'YYYY-MM-DD',
|
||||
minDate: new Date(1900, 0, 1),
|
||||
field: death_date,
|
||||
onSelect: function (date) {
|
||||
death_date.value = date.toISOString();
|
||||
}
|
||||
});
|
||||
// Clean up when component unmounts
|
||||
return () => picker.destroy();
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col md:flex-row gap-6">
|
||||
{#if draftPerson.profile_picture}
|
||||
<div class="flex-shrink-0">
|
||||
<img src={draftPerson.profile_picture} alt={profile_picture()} class="rounded-lg shadow-md w-48 h-48 object-cover" />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex-shrink-0 flex flex-col items-center gap-2">
|
||||
<img src={draftPerson.profile_picture||'https://cdn-icons-png.flaticon.com/512/10628/10628885.png'} alt={profile_picture()} class="rounded-lg shadow-md w-48 h-48 object-cover" />
|
||||
{#if editorMode}
|
||||
<button class="btn bg-neutral text-neutral-content btn-xs" on:click={() => {}}>
|
||||
{change_profile_picture()}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex-1 grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<p><strong>{first_name()}:</strong> {#if editorMode}<input bind:value={draftPerson.first_name} class="input input-sm input-bordered w-full" />{:else}{draftPerson.first_name ?? '-'}{/if}</p>
|
||||
<p><strong>{last_name()}:</strong> {#if editorMode}<input bind:value={draftPerson.last_name} class="input input-sm input-bordered w-full" />{:else}{draftPerson.last_name ?? '-'}{/if}</p>
|
||||
<p><strong>{middle_name()}:</strong> {#if editorMode}<input bind:value={draftPerson.middle_name} class="input input-sm input-bordered w-full" />{:else}{draftPerson.middle_name ?? '-'}{/if}</p>
|
||||
<p><strong>{born()}:</strong> {#if editorMode}<input bind:value={draftPerson.born} class="input input-sm input-bordered w-full" />{:else}{draftPerson.born ?? '-'}{/if}</p>
|
||||
<p><strong>{biological_sex()}:</strong> {#if editorMode}<input bind:value={draftPerson.biological_sex} class="input input-sm input-bordered w-full" />{:else}{draftPerson.biological_sex ?? '-'}{/if}</p>
|
||||
<p><strong>{born()}:</strong>
|
||||
<input
|
||||
type="text"
|
||||
class="w-full pika-single"
|
||||
id="birth_date"
|
||||
bind:this={birth_date}
|
||||
bind:value={draftPerson.born}/>
|
||||
</p>
|
||||
<p><strong>{died()}:</strong>
|
||||
<input
|
||||
type="text"
|
||||
class="w-full pika-single"
|
||||
id="death_date"
|
||||
placeholder={died()}
|
||||
bind:this={death_date}
|
||||
bind:value={draftPerson.died}
|
||||
/>
|
||||
</p>
|
||||
<p><strong>{biological_sex()}:</strong>
|
||||
{#if editorMode}
|
||||
<select
|
||||
name="biological_sex"
|
||||
class="select select-bordered w-full select-sm"
|
||||
id="biological_sex"
|
||||
bind:value={draftPerson.biological_sex}
|
||||
placeholder={biological_sex()}
|
||||
>
|
||||
<option value="male">{male()} </option>
|
||||
<option value="female">{female()} </option>
|
||||
<option value="intersex">{intersex()} </option>
|
||||
<option value="other">{other()} </option>
|
||||
</select>
|
||||
{:else}{callMessageFunction(draftPerson.biological_sex as MessageKeys) ?? '-'}{/if}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p><strong>{email()}:</strong> {#if editorMode}<input bind:value={draftPerson.email} class="input input-sm input-bordered w-full" />{:else}{draftPerson.email ?? '-'}{/if}</p>
|
||||
<p><strong>Limit:</strong> {#if editorMode}<input bind:value={draftPerson.limit} class="input input-sm input-bordered w-full" />{:else}{draftPerson.limit ?? '-'}{/if}</p>
|
||||
<p><strong>{mothers_first_name()}:</strong> {#if editorMode}<input bind:value={draftPerson.mothers_first_name} class="input input-sm input-bordered w-full" />{:else}{draftPerson.mothers_first_name ?? '-'}{/if}</p>
|
||||
<p><strong>{mothers_last_name()}:</strong> {#if editorMode}<input bind:value={draftPerson.mothers_last_name} class="input input-sm input-bordered w-full" />{:else}{draftPerson.mothers_last_name ?? '-'}{/if}</p>
|
||||
<p><strong> {id()}:</strong>{draftPerson.id ?? '-'}</p>
|
||||
<p><strong> Limit:</strong>{draftPerson.limit ?? '-'}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
175
apps/app/src/lib/profile/create/Modal.svelte
Normal file
175
apps/app/src/lib/profile/create/Modal.svelte
Normal file
@@ -0,0 +1,175 @@
|
||||
<script lang="ts">
|
||||
import { fade } from 'svelte/transition';
|
||||
import {
|
||||
register,
|
||||
close,
|
||||
born,
|
||||
mothers_first_name,
|
||||
mothers_last_name,
|
||||
last_name,
|
||||
first_name,
|
||||
email,
|
||||
biological_sex,
|
||||
male,
|
||||
female,
|
||||
other,
|
||||
intersex
|
||||
} from '$lib/paraglide/messages';
|
||||
import { onMount } from 'svelte';
|
||||
import type { components } from '$lib/api/api.gen.js';
|
||||
import { validatePersonRegistration, validateFamilyRelationship } from './validate_fields';
|
||||
|
||||
let {
|
||||
closeModal = () => {},
|
||||
relationship = null,
|
||||
}: { closeModal : ()=>void,relationship: number | null } = $props();
|
||||
let birth_date: HTMLInputElement;
|
||||
let draftRelationship: components['schemas']['FamilyRelationship'] & {type: string} | null = {} as components['schemas']['FamilyRelationship'] & {type: string} | null;
|
||||
let draftPerson :components['schemas']['PersonRegistration'] = {} as components['schemas']['PersonRegistration'];
|
||||
let error: string | undefined | null = $state();
|
||||
|
||||
function onClose() {
|
||||
closeModal();
|
||||
}
|
||||
|
||||
async function create(event: SubmitEvent) {
|
||||
event.preventDefault();
|
||||
error = validatePersonRegistration(draftPerson);
|
||||
if (error) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (relationship !== null && draftRelationship !== null) {
|
||||
error = validateFamilyRelationship(draftRelationship);
|
||||
if (error) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const url = `/api/person`;
|
||||
const response = await fetch(url);
|
||||
result = await response.text();
|
||||
closeModal();
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (birth_date) {
|
||||
import('pikaday').then(({ default: Pikaday }) => {
|
||||
const picker = new Pikaday({
|
||||
format: 'YYYY-MM-DD',
|
||||
minDate: new Date(1900, 0, 1),
|
||||
field: birth_date,
|
||||
onOpen: function () {
|
||||
birth_date.placeholder = '';
|
||||
},
|
||||
onSelect: function (date) {
|
||||
birth_date.value = date.toISOString();
|
||||
}
|
||||
});
|
||||
// Clean up when component unmounts
|
||||
return () => picker.destroy();
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="modal modal-open" transition:fade>
|
||||
<div class="modal-box max-h-screen w-full max-w-5xl overflow-y-auto">
|
||||
<div class="bg-base-100 sticky top-0 z-10">
|
||||
<button class="btn btn-error btn-sm" onclick={onClose}>
|
||||
{close()}
|
||||
</button>
|
||||
<div class="divider"></div>
|
||||
</div>
|
||||
<form onsubmit={create}>
|
||||
<fieldset class="fieldset">
|
||||
{#if error}
|
||||
<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>{error}</span>
|
||||
</div>
|
||||
{/if}
|
||||
{#if relationship !== undefined}
|
||||
<input type="hidden" name="relationship" value={relationship} />
|
||||
{/if}
|
||||
<label class="fieldset-label" for="email">{email()}</label>
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
class="input"
|
||||
placeholder={email()}
|
||||
bind:value={draftPerson.email}
|
||||
/>
|
||||
<label class="fieldset-label" for="first_name">{first_name()}</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input"
|
||||
name="first_name"
|
||||
id="first_name"
|
||||
placeholder={first_name()}
|
||||
/>
|
||||
<label class="fieldset-label" for="last_name">{last_name()}</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input"
|
||||
name="last_name"
|
||||
id="last_name"
|
||||
placeholder={last_name()}
|
||||
/>
|
||||
<label class="fieldset-label" for="birth_date">{born()}</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input pika-single"
|
||||
id="birth_date"
|
||||
placeholder={born()}
|
||||
bind:value={draftPerson.born}
|
||||
bind:this={birth_date}
|
||||
/>
|
||||
<label class="fieldset-label" for="biological_sex">{biological_sex()}</label>
|
||||
<select
|
||||
name="biological_sex"
|
||||
class="select select-bordered w-full max-w-xs"
|
||||
id="biological_sex"
|
||||
placeholder={biological_sex()}
|
||||
bind:value={draftPerson.biological_sex}
|
||||
>
|
||||
<option value="male">{male()} </option>
|
||||
<option value="female">{female()} </option>
|
||||
<option value="intersex">{intersex()} </option>
|
||||
<option value="other">{other()} </option>
|
||||
</select>
|
||||
<label class="fieldset-label" for="mothers_last_name">{mothers_last_name()}</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input"
|
||||
name="mothers_last_name"
|
||||
id="mothers_last_name"
|
||||
placeholder={mothers_last_name()}
|
||||
bind:value={draftPerson.mothers_last_name}
|
||||
/>
|
||||
<label class="fieldset-label" for="mothers_first_name">{mothers_first_name()}</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input"
|
||||
name="mothers_first_name"
|
||||
id="mothers_first_name"
|
||||
placeholder={mothers_first_name()}
|
||||
bind:value={draftPerson.mothers_first_name}
|
||||
/>
|
||||
<button class="btn btn-neutral mt-4">{register()}</button>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
56
apps/app/src/lib/profile/create/validate_fields.ts
Normal file
56
apps/app/src/lib/profile/create/validate_fields.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import type { components } from '$lib/api/api.gen.js';
|
||||
|
||||
export function validatePersonRegistration(data: components['schemas']['PersonRegistration']): string | null {
|
||||
if (!data.first_name || data.first_name.trim() === "") {
|
||||
return "First name is required.";
|
||||
}
|
||||
|
||||
if (!data.last_name || data.last_name.trim() === "") {
|
||||
return "Last name is required.";
|
||||
}
|
||||
|
||||
if (data.email !== undefined && data.email !== null && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email)) {
|
||||
return "Invalid email format.";
|
||||
}
|
||||
|
||||
if (!data.born || isNaN(Date.parse(data.born))) {
|
||||
return "Valid birth date is required.";
|
||||
}
|
||||
|
||||
if (!data.biological_sex || !['male', 'female', 'intersex', 'unknown', 'other'].includes(data.biological_sex.toString())){
|
||||
return 'Invalid value for biological sex. Must be male female, intersex, unknown, or other.';
|
||||
}
|
||||
|
||||
if (!data.mothers_first_name || data.mothers_first_name.trim() === "") {
|
||||
return "Mother's first name is required.";
|
||||
}
|
||||
|
||||
if (!data.mothers_last_name || data.mothers_last_name.trim() === "") {
|
||||
return "Mother's last name is required.";
|
||||
}
|
||||
|
||||
return null; // No errors
|
||||
}
|
||||
|
||||
export function validateFamilyRelationship(relationship: components['schemas']['FamilyRelationship'] & {type:string}): string | null {
|
||||
const validRelationships = [
|
||||
"child",
|
||||
"parent",
|
||||
"spouse",
|
||||
"sibling"
|
||||
];
|
||||
|
||||
if (!validRelationships.includes(relationship.type)) {
|
||||
return `Invalid family relationship. Must be one of ${validRelationships.join(', ')}.`;
|
||||
}
|
||||
|
||||
if (relationship.from !== undefined && relationship.from !== null && isNaN(Date.parse(relationship.from))) {
|
||||
return "Valid date is required for 'from' field.";
|
||||
}
|
||||
|
||||
if (relationship.to !== undefined && relationship.to !== null && isNaN(Date.parse(relationship.to))) {
|
||||
return "Valid date is required for 'to' field.";
|
||||
}
|
||||
|
||||
return null; // No errors
|
||||
}
|
@@ -34,7 +34,11 @@ export async function invalidateUserSessions(userId: number, sessions: KVNamespa
|
||||
}
|
||||
}
|
||||
|
||||
export function setSessionTokenCookie(event: RequestEvent, token: string, expiresAt: EpochTimeStamp): void {
|
||||
export function setSessionTokenCookie(
|
||||
event: RequestEvent,
|
||||
token: string,
|
||||
expiresAt: EpochTimeStamp
|
||||
): void {
|
||||
event.cookies.set('session', token, {
|
||||
httpOnly: true,
|
||||
path: '/',
|
||||
|
@@ -10,8 +10,8 @@
|
||||
|
||||
<ParaglideJS {i18n}>
|
||||
{@render children()}
|
||||
<div class="flex flex-row absolute top-2 right-2 items-center gap-2">
|
||||
<div class="absolute top-2 right-2 flex flex-row items-center gap-2">
|
||||
<ThemeButton />
|
||||
<Logout show={!page.url.pathname.includes("login")}/>
|
||||
<Logout show={!page.url.pathname.includes('login')} />
|
||||
</div>
|
||||
</ParaglideJS>
|
||||
|
@@ -1,23 +1,28 @@
|
||||
<script lang="ts">
|
||||
import { title, family_tree, people } from '$lib/paraglide/messages.js';
|
||||
import type { PageProps } from './$types';
|
||||
import { title, family_tree } from '$lib/paraglide/messages.js';
|
||||
|
||||
import { SvelteFlowProvider, SvelteFlow, Controls, MiniMap } from '@xyflow/svelte';
|
||||
import '@xyflow/svelte/dist/style.css';
|
||||
import type { Node, Edge, NodeTypes, NodeProps } from '@xyflow/svelte';
|
||||
import type { Node, Edge, NodeTypes, NodeEventWithPointer } from '@xyflow/svelte';
|
||||
|
||||
import PersonNode from '$lib/graph/PersonNode.svelte';
|
||||
import PersonModal from '$lib/profile/Modal.svelte';
|
||||
import type { components } from '$lib/api/api.gen';
|
||||
import { handleNodeClick } from '$lib/graph/node_click';
|
||||
import PersonMenu from '$lib/graph/PersonMenu.svelte';
|
||||
import CreatePerson from '$lib/profile/create/Modal.svelte';
|
||||
|
||||
import type { components } from '$lib/api/api.gen';
|
||||
import type { NodeMenu } from '$lib/graph/model';
|
||||
|
||||
import { handleNodeClick } from '$lib/graph/node_click';
|
||||
|
||||
let { data, form }: PageProps = $props();
|
||||
const nodeTypes: NodeTypes = { personNode: PersonNode };
|
||||
let selectedPerson: components['schemas']['PersonProperties'] & { id: number | null } = $state({
|
||||
id: null
|
||||
});
|
||||
let openPersonPanel = $state(false);
|
||||
let openPersonMenu: NodeMenu | undefined = undefined;
|
||||
let openPersonMenu: NodeMenu | undefined = $state(undefined);
|
||||
|
||||
let ppl = data.people;
|
||||
if (ppl === undefined) {
|
||||
@@ -37,16 +42,61 @@
|
||||
]);
|
||||
let edges = $state.raw<Edge[]>([]);
|
||||
|
||||
handleNodeClick(
|
||||
let relationshipStart: number | null ;
|
||||
let createPerson = $state(false);
|
||||
|
||||
let clientWidth: number | undefined = $state();
|
||||
let clientHeight: number | undefined = $state();
|
||||
const handleContextMenu: NodeEventWithPointer<MouseEvent> = ({ event, node }) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (clientHeight === undefined || clientWidth === undefined) {
|
||||
clientHeight = window.innerHeight;
|
||||
clientWidth = window.innerWidth;
|
||||
}
|
||||
|
||||
openPersonMenu = {
|
||||
onClick: () => {
|
||||
openPersonMenu = undefined;
|
||||
},
|
||||
deleteNode: () => {
|
||||
openPersonMenu = undefined;
|
||||
},
|
||||
createRelationshipAndNode: () => {
|
||||
openPersonMenu = undefined;
|
||||
},
|
||||
addRelationship: () => {
|
||||
openPersonMenu = undefined;
|
||||
},
|
||||
addAdmin: () => {
|
||||
openPersonMenu = undefined;
|
||||
},
|
||||
addRecipe: () => {
|
||||
openPersonMenu = undefined;
|
||||
},
|
||||
top: event.clientY < clientHeight - 200 ? event.clientY : undefined,
|
||||
left: event.clientX < clientWidth - 200 ? event.clientX : undefined,
|
||||
right: event.clientX >= clientWidth - 200 ? clientWidth - event.clientX : undefined,
|
||||
bottom: event.clientY >= clientHeight - 200 ? clientHeight - event.clientY : undefined
|
||||
};
|
||||
};
|
||||
|
||||
let handleNodeClickFunc = handleNodeClick(
|
||||
(
|
||||
person: components['schemas']['PersonProperties'] & {
|
||||
id: number;
|
||||
}
|
||||
) => {
|
||||
openPersonPanel = true;
|
||||
console.log('person', person);
|
||||
selectedPerson = person;
|
||||
}
|
||||
);
|
||||
|
||||
let handlePaneClick = ({ event }: { event: MouseEvent }) => {
|
||||
openPersonPanel = false;
|
||||
openPersonMenu = undefined;
|
||||
};
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -58,6 +108,9 @@
|
||||
<SvelteFlow
|
||||
bind:nodes
|
||||
bind:edges
|
||||
onnodeclick={handleNodeClickFunc}
|
||||
onnodecontextmenu={handleContextMenu}
|
||||
onpaneclick={handlePaneClick}
|
||||
class="!bg-base-200"
|
||||
{nodeTypes}
|
||||
fitView
|
||||
@@ -65,7 +118,17 @@
|
||||
>
|
||||
<MiniMap class="!bg-base-300" />
|
||||
<Controls class="!bg-base-300" />
|
||||
<PersonModal person={selectedPerson} open={openPersonPanel} />
|
||||
{#if openPersonPanel}
|
||||
<PersonModal
|
||||
person={selectedPerson}
|
||||
closeModal={() => {
|
||||
openPersonPanel = false;
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
{#if createPerson}
|
||||
<CreatePerson relationship={relationshipStart}></CreatePerson>
|
||||
{/if}
|
||||
{#if openPersonMenu !== undefined}
|
||||
<PersonMenu {...openPersonMenu!} />
|
||||
{/if}
|
||||
|
44
apps/app/src/routes/api/family_tree/+page.server.ts
Normal file
44
apps/app/src/routes/api/family_tree/+page.server.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { error, redirect } from '@sveltejs/kit';
|
||||
import { client } from '$lib/api/client';
|
||||
import type { RequestEvent } from './$types';
|
||||
import type { components } from '$lib/api/api.gen';
|
||||
|
||||
|
||||
async function GET(event: RequestEvent): Promise<Response> {
|
||||
if (event.locals.session === null /*|| event.locals.familytree === nul*/) {
|
||||
return redirect(302, '/login');
|
||||
}
|
||||
|
||||
const response = await client.GET(event.url.searchParams.get('with_out_spouse') === 'true' ? '/family-tree-with-spouses':'/family-tree', {
|
||||
params: {
|
||||
header: { 'X-User-ID': event.locals.session.userId }
|
||||
}
|
||||
});
|
||||
|
||||
if (response.response.status !== 200) {
|
||||
return error(500, {
|
||||
message: response.error?.msg || 'Failed to fetch family tree'
|
||||
});
|
||||
}
|
||||
|
||||
if (response.data === null || response.data?.people === null || response.data?.people === undefined || response.data?.people.length === 0) {
|
||||
return error(500, {
|
||||
message: 'Family tree is empty'
|
||||
});
|
||||
}
|
||||
|
||||
var peopleToReturn : Array<components['schemas']['OptimizedPersonNode']> = [];
|
||||
for (const person of response.data.people) {
|
||||
let newPerson = person
|
||||
|
||||
if (newPerson.profile_picture!== null && newPerson.profile_picture !== undefined) {
|
||||
|
||||
}
|
||||
|
||||
peopleToReturn.push(newPerson);
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify(peopleToReturn), {
|
||||
status: 200,
|
||||
});
|
||||
}
|
0
apps/app/src/routes/api/person/+server.ts
Normal file
0
apps/app/src/routes/api/person/+server.ts
Normal file
@@ -71,7 +71,11 @@ export const load: PageServerLoad = async (event: RequestEvent) => {
|
||||
}
|
||||
|
||||
const sessionToken = generateSessionToken(String(response.data.Id));
|
||||
const session = await createSession(sessionToken, response.data.Id, event.platform.env.GH_SESSIONS)
|
||||
const session = await createSession(
|
||||
sessionToken,
|
||||
response.data.Id,
|
||||
event.platform.env.GH_SESSIONS
|
||||
);
|
||||
if (session === null) {
|
||||
return error(500, {
|
||||
message: 'Failed to create session'
|
||||
@@ -84,8 +88,6 @@ export const load: PageServerLoad = async (event: RequestEvent) => {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
let personP: PersonProperties = {
|
||||
google_id: sub,
|
||||
first_name: first_name,
|
||||
@@ -163,7 +165,9 @@ async function register(event: RequestEvent) {
|
||||
field: biological_sex()
|
||||
})
|
||||
});
|
||||
} else if (!['male', 'female', 'intersex', 'unknown', 'other'].includes(bbiological_sex.toString())) {
|
||||
} else if (
|
||||
!['male', 'female', 'intersex', 'unknown', 'other'].includes(bbiological_sex.toString())
|
||||
) {
|
||||
return fail(400, {
|
||||
message: `Invalid value for biological_sex. Must be one of "male", "female", "intersex", "unknown", or "other".`
|
||||
});
|
||||
@@ -194,17 +198,17 @@ async function register(event: RequestEvent) {
|
||||
born: parsed_date.toISOString().split('T')[0],
|
||||
mothers_first_name: mothers_first_name_f as string,
|
||||
mothers_last_name: mothers_last_name_f as string,
|
||||
biological_sex: bbiological_sex as components['schemas']['PersonRegistration']['biological_sex'],
|
||||
biological_sex:
|
||||
bbiological_sex as components['schemas']['PersonRegistration']['biological_sex'],
|
||||
limit: StorageLimit
|
||||
};
|
||||
|
||||
let response = await client
|
||||
.POST('/person/google/{google_id}', {
|
||||
params: {
|
||||
path: { google_id: google_id.toString() }
|
||||
},
|
||||
body: personP
|
||||
})
|
||||
let response = await client.POST('/person/google/{google_id}', {
|
||||
params: {
|
||||
path: { google_id: google_id.toString() }
|
||||
},
|
||||
body: personP
|
||||
});
|
||||
|
||||
if (response.response.status !== 200) {
|
||||
return fail(400, {
|
||||
@@ -212,9 +216,13 @@ async function register(event: RequestEvent) {
|
||||
});
|
||||
}
|
||||
|
||||
if (response.data === undefined) {
|
||||
return fail(400, {
|
||||
message: failed_to_create_user() + 'No user data returned'
|
||||
});
|
||||
}
|
||||
|
||||
if (!response.data?.Id) {
|
||||
console.log(response.data)
|
||||
if (response.data.Id === undefined) {
|
||||
return fail(400, {
|
||||
message: failed_to_create_user() + 'No user ID returned'
|
||||
});
|
||||
@@ -231,7 +239,7 @@ async function register(event: RequestEvent) {
|
||||
sessionToken,
|
||||
response.data.Id,
|
||||
event.platform.env.GH_SESSIONS
|
||||
)
|
||||
);
|
||||
if (session === null) {
|
||||
return fail(500, {
|
||||
message: failed_to_create_user() + 'Failed to create session'
|
||||
|
@@ -15,7 +15,8 @@
|
||||
biological_sex,
|
||||
male,
|
||||
female,
|
||||
other
|
||||
other,
|
||||
intersex
|
||||
} from '$lib/paraglide/messages';
|
||||
import { onMount } from 'svelte';
|
||||
import { enhance } from '$app/forms';
|
||||
@@ -129,7 +130,7 @@
|
||||
type="text"
|
||||
class="input pika-single"
|
||||
id="birth_date"
|
||||
value={born()}
|
||||
placeholder={born()}
|
||||
bind:this={birth_date}
|
||||
/>
|
||||
<input type="text" class="hidden" name="birth_date" bind:this={birth_date_value} />
|
||||
@@ -142,6 +143,7 @@
|
||||
>
|
||||
<option value="male">{male()} </option>
|
||||
<option value="female">{female()} </option>
|
||||
<option value="intersex">{intersex()} </option>
|
||||
<option value="other">{other()} </option>
|
||||
</select>
|
||||
<label class="fieldset-label" for="mothers_last_name">{mothers_last_name()}</label>
|
||||
|
20
apps/app/src/routes/login/page.stories.ts
Normal file
20
apps/app/src/routes/login/page.stories.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import type { Meta, StoryObj } from '@storybook/svelte';
|
||||
import SignInPage from './+page.svelte';
|
||||
|
||||
const meta = {
|
||||
title: 'login/+page',
|
||||
component: SignInPage,
|
||||
tags: ['autodocs']
|
||||
} satisfies Meta<typeof SignInPage>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => {
|
||||
return {
|
||||
Component: SignInPage
|
||||
};
|
||||
}
|
||||
};
|
@@ -17,4 +17,4 @@ export async function GET(event: RequestEvent): Promise<Response> {
|
||||
deleteSessionTokenCookie(event);
|
||||
|
||||
return redirect(302, '/login');
|
||||
}
|
||||
}
|
||||
|
@@ -5,9 +5,7 @@
|
||||
{
|
||||
"$schema": "node_modules/wrangler/config-schema.json",
|
||||
"name": "generations-heritage",
|
||||
"compatibility_flags": [
|
||||
"nodejs_compat"
|
||||
],
|
||||
"compatibility_flags": ["nodejs_compat"],
|
||||
"compatibility_date": "2025-02-14",
|
||||
"pages_build_output_dir": ".svelte-kit/cloudflare",
|
||||
"observability": {
|
||||
@@ -99,4 +97,4 @@
|
||||
* https://developers.cloudflare.com/workers/wrangler/configuration/#service-bindings
|
||||
*/
|
||||
// "services": [{ "binding": "MY_SERVICE", "service": "my-service" }]
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user