37 Commits

Author SHA1 Message Date
f87f680a83 authZ+N chained 2025-02-03 16:03:41 +01:00
8939577c15 change repo scope for argo and restructure deployment 2025-02-03 15:56:46 +01:00
66d25e03d1 add traefik oauth forward 2025-01-05 19:14:59 +01:00
d6a685cbcc Merge branch 'main' into feature/rework-deployment-to-docker-compose 2025-01-02 13:27:12 +01:00
cbb53d640d switch to main 2024-12-27 12:04:46 +01:00
462168d8ed delete zitadel relations 2024-12-27 11:59:52 +01:00
e4b27d7539 remove zitadel 2024-12-26 13:31:00 +01:00
30e236446a add favicon 2024-12-24 09:59:16 +01:00
62341abf09 remove quotes around admin in zitadel values.yml 2024-12-23 11:28:35 +01:00
cc623b71dc abc to password 2024-10-27 19:28:55 +01:00
9560c339a8 add memgraph password 2024-10-27 19:17:56 +01:00
154bb2de21 tls to memgraph 2024-10-27 19:17:34 +01:00
628140d6f6 fix admin password ref 2024-10-27 17:29:22 +01:00
ff4f4665c4 modify postgres start values 2024-10-27 16:51:10 +01:00
6947132f8d downgrade chart 2024-10-27 16:46:12 +01:00
e8b81e33c2 update postgres initadmin password 2024-10-27 16:40:49 +01:00
8a9d3a6091 fixup strings 2024-10-27 16:34:29 +01:00
17d308ce08 update postgress use 2024-10-27 15:28:49 +01:00
e8d065c3bd update username 2024-10-27 13:49:51 +01:00
65664e1a6e update values memgraph 2024-10-27 13:43:01 +01:00
c336cc09e3 update memgraph values 2024-10-27 13:34:27 +01:00
2124cd4fde add users volume claim to memgraph deployment 2024-10-27 13:30:47 +01:00
77b7249e83 fix cert dns for psql 2024-10-27 13:20:51 +01:00
ca4a8d7361 update memgraph storage size 2024-10-27 13:03:50 +01:00
95a6bf5a70 add secrets to memgraph init 2024-10-27 12:50:04 +01:00
9874dee333 change db host 2024-10-27 12:46:48 +01:00
ed0b9de12a change target revision for current branch 2024-10-27 12:42:45 +01:00
0dece52a55 update pvc for postgre 2024-10-27 12:42:10 +01:00
a64ec65096 update helm details 2024-10-27 12:35:44 +01:00
3ce6721740 rm manifest 2024-10-27 12:32:20 +01:00
d5cc3320ee change chart repo for psql 2024-10-27 12:31:35 +01:00
e14ed71766 update kustomize 2024-10-27 12:11:16 +01:00
b745b49d9b fixup deployment 2024-10-27 12:03:29 +01:00
6ee87d059e switch to psql 2024-10-27 09:53:00 +01:00
708f07a7ef add multiplatform img support 2024-09-21 23:39:10 +02:00
3944a0b8ef backend remove auth 2024-09-21 22:57:33 +02:00
6dc8facd67 Reworked deployment 2024-09-21 22:57:13 +02:00
342 changed files with 8830 additions and 40746 deletions

52
.github/workflows/auth-service-cd.yml vendored Normal file
View File

@@ -0,0 +1,52 @@
name: Release Auth service to Docker Hub
on:
pull_request:
paths:
- "cmd/auth/**"
- "pkg/**"
- "deployment/auth-service/**"
- ".github/workflows/auth-service-cd.yml"
jobs:
docker:
name: Build and Push Auth-service image to Docker Hub
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v6
id: create_image_tag
with:
script: |
if(github.ref == 'refs/heads/main') {
return 'latest';
} else if(context.issue.number) {
return "pr" + context.issue.number;
} else {
return "pr" + (
await github.rest.repos.listPullRequestsAssociatedWithCommit({
commit_sha: context.sha,
owner: context.repo.owner,
repo: context.repo.repo,
})
).data[0].number;
}
result-encoding: string
- name: Image tag
run: echo '${{steps.create_image_tag.outputs.result}}'
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v5
with:
push: true
context: "{{defaultContext}}:cmd/auth"
tags: vcscsvcscs/gheritage-auth-service:${{steps.create_image_tag.outputs.result}}
platforms: linux/amd64, linux/arm64

View File

@@ -1,18 +1,16 @@
name: Release to Docker Hub
name: Release Backend service to Docker Hub
on:
workflow_call:
inputs:
service-name:
required: true
type: string
working-directory:
required: true
type: string
pull_request:
paths:
- "cmd/backend/**"
- "pkg/**"
- "deployment/backend/**"
- ".github/workflows/backend-cd.yml"
jobs:
docker:
name: Build and Push ${{inputs.service-name}} image to Docker Hub
name: Build and Push Backend image to Docker Hub
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v6
@@ -49,6 +47,6 @@ jobs:
uses: docker/build-push-action@v5
with:
push: true
context: "{{defaultContext}}:${{ inputs.working-directory }}"
tags: vcscsvcscs/${{ inputs.service-name }}:${{steps.create_image_tag.outputs.result}}
platforms: linux/amd64, linux/arm64
context: "{{defaultContext}}:cmd/backend"
tags: vcscsvcscs/gheritage-backend-service:${{steps.create_image_tag.outputs.result}}
platforms: linux/amd64, linux/arm64

View File

@@ -1,53 +0,0 @@
name: Deploy Generation Heritage Svelte Kit App to Cloudflare
on:
workflow_call:
pull_request:
types: [closed]
paths:
- "apps/app**"
jobs:
ci:
uses: ./.github/workflows/svelte_ci.yml
deploy-to-production:
runs-on: ubuntu-latest
timeout-minutes: 60
needs: ci
if: github.actor_id == 'vcscsvcscs' || ( github.event_name == 'pull_request' && github.event.action == 'closed' && github.event.pull_request.merged == true )
steps:
- uses: actions/checkout@v4
- name: Build & Deploy Worker to Production
uses: cloudflare/wrangler-action@v3
with:
environment: production
workingDirectory: 'apps/app'
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
secrets: |
GOOGLE_CLIENT_ID
GOOGLE_CLIENT_SECRET
env:
GOOGLE_CLIENT_ID: ${{ secrets.PROD_GOOGLE_CLIENT_ID }}
GOOGLE_CLIENT_SECRET: ${{ secrets.PROD_GOOGLE_CLIENT_SECRET }}
deploy-to-stage:
runs-on: ubuntu-latest
timeout-minutes: 60
needs: ci
if: github.actor_id == 'vcscsvcscs' || ( github.ref == 'refs/heads/stage' && github.event_name == 'pull_request' && github.event.action == 'closed' && github.event.pull_request.merged == true )
steps:
- uses: actions/checkout@v4
- name: Build & Deploy Worker to Stage
uses: cloudflare/wrangler-action@v3
with:
environment: staging
workingDirectory: 'apps/app'
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
secrets: |
GOOGLE_CLIENT_ID
GOOGLE_CLIENT_SECRET
env:
GOOGLE_CLIENT_ID: ${{ secrets.STAGING_GOOGLE_CLIENT_ID }}
GOOGLE_CLIENT_SECRET: ${{ secrets.STAGING_GOOGLE_CLIENT_SECRET }}

View File

@@ -1,40 +0,0 @@
name: Database Adapter Continues Integration
on:
workflow_call:
pull_request:
types: [opened, synchronize, reopened]
paths:
- "apps/db-adapter**"
- ".github/workflows/db_adapter_ci.yml"
- ".github/workflows/go_test.yml"
- ".github/workflows/docker_build.yml"
jobs:
golangci:
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.24'
cache: false
- name: golangci-lint
uses: golangci/golangci-lint-action@v7
with:
version: latest
working-directory: 'apps/db-adapter'
test:
uses: ./.github/workflows/go_test.yml
needs: golangci
with:
working-directory: 'apps/db-adapter'
build:
needs: test
uses: ./.github/workflows/docker_build.yml
with:
working-directory: 'apps/db-adapter'
service-name: 'gheritage-db-adapter'

15
.github/workflows/frontend-ci.yml vendored Normal file
View File

@@ -0,0 +1,15 @@
name: Frontend Continuous Integration
on:
pull_request:
paths:
- "cmd/frontend/**"
jobs:
lint:
uses: ./.github/workflows/svelte_lint.yml
with:
working-directory: 'cmd/frontend'
build:
needs: lint
uses: ./.github/workflows/svelte_build.yml
with:
working-directory: 'cmd/frontend'

View File

@@ -1,40 +0,0 @@
name: Go Test
on:
workflow_call:
inputs:
working-directory:
required: true
type: string
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Go 1.24.x'
uses: actions/setup-go@v5
with:
cache-dependency-path: |
${{ inputs.working-directory }}/go.sum
go-version: '1.24.1'
- name: Display Go version
run: go version
- name: Install dependencies
run: |
cd ${{ inputs.working-directory }}
go get ./...
- name: Run tests
run: |
cd ${{ inputs.working-directory }}
go test ./... -json > ../../TestResults.json
- name: Upload Go test results
if: always()
uses: actions/upload-artifact@v4
with:
name: Go-Test-results
path: TestResults.json

23
.github/workflows/lint.yml vendored Normal file
View File

@@ -0,0 +1,23 @@
name: Go Lint
on:
pull_request:
paths:
- "cmd/**"
- "pkg/**"
jobs:
golangci:
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.23'
cache: false
- name: golangci-lint
uses: golangci/golangci-lint-action@v4
with:
version: latest
working-directory: .

24
.github/workflows/svelte_build.yml vendored Normal file
View File

@@ -0,0 +1,24 @@
on:
workflow_call:
inputs:
working-directory:
required: true
type: string
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '21.x'
- name: Install dependencies
run: |
cd ${{ inputs.working-directory }}
npm ci
- name: Build
run: |
cd ${{ inputs.working-directory }}
npm run build

View File

@@ -1,23 +0,0 @@
name: Frontend Continuous Integration
on:
workflow_call:
pull_request:
types: [opened, synchronize, reopened]
paths:
- "apps/app**"
- ".github/workflows/svelte_ci.yml"
- ".github/workflows/svelte_test.yml"
- ".github/workflows/svelte_lint.yml"
jobs:
lint:
uses: ./.github/workflows/svelte_lint.yml
with:
working-directory: 'apps/app'
build:
needs: lint
uses: ./.github/workflows/svelte_test.yml
with:
working-directory: 'apps/app'

View File

@@ -13,7 +13,7 @@ jobs:
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '22.x'
node-version: '21.x'
- name: Install dependencies
run: |
cd ${{ inputs.working-directory }}

View File

@@ -1,36 +0,0 @@
on:
workflow_call:
inputs:
working-directory:
required: true
type: string
jobs:
test:
runs-on: ubuntu-latest
permissions:
# Required to checkout the code
contents: read
# Required to put a comment into the pull-request
pull-requests: write
steps:
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '22.x'
- name: Install dependencies
run: |
cd ${{ inputs.working-directory }}
npm ci
- name: 'Test'
run: |
cd ${{ inputs.working-directory }}
npx vitest --coverage.enabled true
- name: 'Report Coverage'
# Set if: always() to also generate the report if tests are failing
# Only works if you set `reportOnFailure: true` in your vite config as specified above
if: always()
uses: davelosert/vitest-coverage-report-action@v2
with:
working-directory: ${{ inputs.working-directory }}

28
.github/workflows/test.yml vendored Normal file
View File

@@ -0,0 +1,28 @@
name: Go Test
on:
pull_request:
paths:
- "cmd/**"
- "pkg/**"
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Go 1.23.x'
uses: actions/setup-go@v5
with:
go-version: '1.23.x'
- name: Display Go version
run: go version
- name: Install dependencies
run: |
go get ./...
- name: Run tests
run: |
go test ./...

132
.golangci.yml Normal file
View File

@@ -0,0 +1,132 @@
linters-settings:
depguard:
rules:
logger:
deny:
# logging is allowed only by logutils.Log,
# logrus is allowed to use only in logutils package.
- pkg: "github.com/sirupsen/logrus"
desc: logging is allowed only by logutils.Log.
- pkg: "github.com/pkg/errors"
desc: Should be replaced by standard lib errors package.
- pkg: "github.com/instana/testify"
desc: It's a fork of github.com/stretchr/testify.
dupl:
threshold: 100
funlen:
lines: -1 # the number of lines (code + empty lines) is not a right metric and leads to code without empty line or one-liner.
statements: 50
goconst:
min-len: 2
min-occurrences: 3
gocritic:
enabled-tags:
- diagnostic
- experimental
- opinionated
- performance
- style
disabled-checks:
- dupImport # https://github.com/go-critic/go-critic/issues/845
- ifElseChain
- octalLiteral
- whyNoLint
gocyclo:
min-complexity: 15
gofmt:
rewrite-rules:
- pattern: 'interface{}'
replacement: 'any'
gomnd:
# don't include the "operation" and "assign"
checks:
- argument
- case
- condition
- return
ignored-numbers:
- '0'
- '1'
- '2'
- '3'
ignored-functions:
- strings.SplitN
govet:
settings:
printf:
funcs:
- (github.com/golangci/golangci-lint/pkg/logutils.Log).Infof
- (github.com/golangci/golangci-lint/pkg/logutils.Log).Warnf
- (github.com/golangci/golangci-lint/pkg/logutils.Log).Errorf
- (github.com/golangci/golangci-lint/pkg/logutils.Log).Fatalf
enable:
- nilness
- shadow
errorlint:
asserts: false
lll:
line-length: 140
misspell:
locale: US
nolintlint:
allow-unused: false # report any unused nolint directives
require-explanation: false # don't require an explanation for nolint directives
require-specific: false # don't require nolint directives to be specific about which linter is being skipped
revive:
rules:
- name: unexported-return
disabled: true
- name: unused-parameter
linters:
disable-all: true
enable:
- bodyclose
- depguard
- dogsled
- dupl
- errcheck
- errorlint
- exportloopref
- funlen
- gocheckcompilerdirectives
- gochecknoinits
- goconst
- gocritic
- gocyclo
- gofmt
- goimports
- gomnd
- goprintffuncname
- gosec
- gosimple
- govet
- ineffassign
- lll
- misspell
- nakedret
- noctx
- nolintlint
- revive
- staticcheck
- stylecheck
- typecheck
- unconvert
- unparam
- unused
- whitespace
# don't enable:
# - asciicheck
# - gochecknoglobals
# - gocognit
# - godot
# - godox
# - goerr113
# - nestif
# - prealloc
# - testpackage
# - wsl
run:
timeout: 5m

21
.vscode/launch.json vendored
View File

@@ -1,21 +0,0 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Launch db adapter",
"type": "go",
"request": "launch",
"mode": "debug",
"program": "${workspaceFolder}/apps/db-adapter/main.go",
"env": {
"HTTP_PORT": ":5237",
"MEMGRAPH_URI": "bolt://127.0.0.1:7687",
"MEMGRAPH_USER": "memgraph",
"MEMGRAPH_PASSWORD": "memgraph"
}
}
]
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,27 +0,0 @@
name: Playwright Tests
on:
push:
branches: [main, master]
pull_request:
branches: [main, master]
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: lts/*
- name: Install dependencies
run: npm ci
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Run Playwright tests
run: npx playwright test
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-report
path: playwright-report/
retention-days: 30

36
apps/app/.gitignore vendored
View File

@@ -1,36 +0,0 @@
node_modules
# Output
.output
.vercel
.netlify
.wrangler
/.svelte-kit
/build
# OS
.DS_Store
Thumbs.db
# Env
.env
.env.*
!.env.example
!.env.test
# Vite
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
# Paraglide
src/lib/paraglide
*storybook.log
.dev.vars
# Playwright
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/

View File

@@ -1,4 +0,0 @@
# Package Managers
package-lock.json
pnpm-lock.yaml
yarn.lock

View File

@@ -1,15 +0,0 @@
{
"useTabs": true,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
"overrides": [
{
"files": "*.svelte",
"options": {
"parser": "svelte"
}
}
]
}

View File

@@ -1,20 +0,0 @@
import type { StorybookConfig } from '@storybook/sveltekit';
const config: StorybookConfig = {
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|ts|svelte)'],
addons: [
'@storybook/addon-svelte-csf',
'@storybook/addon-essentials',
'@chromatic-com/storybook',
'@storybook/addon-interactions'
],
framework: {
name: '@storybook/sveltekit',
options: {
builder: {
viteConfigPath: '../vite.config.ts'
}
}
}
};
export default config;

View File

@@ -1,16 +0,0 @@
import type { Preview } from '@storybook/svelte';
import '../src/app.css';
const preview: Preview = {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i
}
}
}
};
export default preview;

View File

@@ -1,12 +0,0 @@
{
"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"
]
}

View File

@@ -1,5 +0,0 @@
{
"files.associations": {
"wrangler.json": "jsonc"
}
}

View File

@@ -1,18 +0,0 @@
import { test, expect } from '@playwright/test';
test('has title', async ({ page }) => {
await page.goto('https://playwright.dev/');
// Expect a title "to contain" a substring.
await expect(page).toHaveTitle(/Playwright/);
});
test('get started link', async ({ page }) => {
await page.goto('https://playwright.dev/');
// Click the get started link.
await page.getByRole('link', { name: 'Get started' }).click();
// Expects page to have a heading with the name of Installation.
await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible();
});

View File

@@ -1,9 +0,0 @@
GOOGLE_CLIENT_ID=""
GOOGLE_CLIENT_SECRET=""
GOOGLE_CALLBACK_URI="http://localhost:3000/login/google/callback"
DB_ADAPTER="http://localhost:5237"
CF_ACCESS_CLIENT_SECRET=""
CF_ACCESS_CLIENT_ID=""
NODE_ENV="development"
PORT="3000"
HOST="0.0.0.0"

View File

@@ -1,34 +0,0 @@
import prettier from 'eslint-config-prettier';
import js from '@eslint/js';
import { includeIgnoreFile } from '@eslint/compat';
import svelte from 'eslint-plugin-svelte';
import globals from 'globals';
import { fileURLToPath } from 'node:url';
import ts from 'typescript-eslint';
const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url));
export default ts.config(
includeIgnoreFile(gitignorePath),
js.configs.recommended,
...ts.configs.recommended,
...svelte.configs['flat/recommended'],
prettier,
...svelte.configs['flat/prettier'],
{
languageOptions: {
globals: {
...globals.browser,
...globals.node
}
}
},
{
files: ['**/*.svelte'],
languageOptions: {
parserOptions: {
parser: ts.parser
}
}
}
);

View File

@@ -1,174 +0,0 @@
{
"$schema": "https://inlang.com/schema/inlang-message-format",
"about": "About",
"accept": "Accept",
"add": "Add",
"add_administrator": "Add administrator",
"add_note": "Add Note",
"add_relationship": "Add Relationship",
"address": "Address",
"admin": "Admin",
"alive": "Alive",
"allergies": "Allergies",
"allow_family_tree_admin_access": "Allow Family Tree Admin Access",
"animal": "Animal",
"audio": "Audio",
"back": "Back",
"baptized": "Baptized",
"belief": "Belief",
"biological_sex": "Biological Sex",
"biography": "Biography",
"birth_name": "Birth Name",
"blood_pressure": "Blood Pressure",
"blood_type": "Blood Type",
"born": "Born",
"cancel": "Cancel",
"city": "City",
"child": "Child",
"change_profile_picture": "Change Profile Picture",
"citizenship": "Citizenship",
"close": "Close",
"coffee": "Coffee",
"colour": "Colour",
"connection": "Connection",
"connection_type": "Connection Type",
"contact": "Contact",
"cookie_disclaimer": "This website uses cookies to ensure you get the best experience, including storing the theme and handling user sessions.",
"cookie_policy": "Cookie Policy",
"country": "Country",
"create": "Create",
"create_invite_code": "Create invite code",
"create_person": "Create person",
"create_relationship_and_person": "Create relationship and person",
"dark": "Dark",
"date": "Date",
"death": "Death",
"delete_profile": "Delete profile",
"deceased": "Deceased",
"deny": "Deny",
"description": "Description",
"details": "Details",
"died": "Died",
"directions": "Directions",
"disclaimer": "Disclaimer",
"document": "Document",
"download": "Download",
"edit": "Edit",
"email": "Email",
"export_something": "Export{thing}",
"extra_names": "Extra Names",
"faith": "Faith",
"failed_to_create_user": "Failed to create user",
"family_tree": "Family Tree",
"favourite": "Favourite",
"favourite_recipes": "Favourite Recipes",
"female": "Female",
"file": "File",
"first_name": "First Name",
"flower": "Flower",
"from_time": "From",
"fruit": "Fruit",
"hair_colour": "Hair Colour",
"hard_delete": "Delete permanently",
"have_invite_code": "I have invite code!",
"hello_world": "Hello, {name} from en!",
"height": "Height",
"hobby": "Hobby",
"home": "Home",
"id": "ID",
"ideology": "Ideology",
"illness": "Illness",
"image": "Image",
"ingridients": "Ingridients",
"interest": "Interest",
"intersex": "Intersex",
"invite_code": "Invite Code",
"language": "Language",
"last_name": "Last Name",
"life_events": "Life Events",
"light": "Light",
"loading": "Loading",
"login": "Login",
"logout": "Logout",
"male": "Male",
"media_title": "Media Title",
"medication": "Medication",
"message_for_future_generations": "Message for Future Generations",
"middle_name": "Middle Name",
"missing_field": "{field} is missing",
"mothers_first_name": "Mother's First Name",
"mothers_last_name": "Mother's Last Name",
"nation": "Nation",
"no": "No",
"no_data": "No Data",
"notes": "Notes",
"occupation": "Occupation",
"occupation_to_display": "Occupation to Display",
"optional_field": "Optional Field",
"other": "Other",
"others_said": "Others Said",
"parent": "Parent",
"people": "People",
"person": "Person",
"pet": "Pet",
"philosophy": "Philosophy",
"photos": "Photos",
"phone": "Phone",
"place_of_birth": "Place of Birth",
"place_of_death": "Place of Death",
"plant": "Plant",
"politics": "Politics",
"profile_id": "Profile ID",
"profiel_id_registration": "If someone already created a profile for you, ask them for your profiles ID and enter it here.",
"profile_picture": "Profile Picture",
"privacy_policy": "Privacy Policy",
"recipe": "Recipe",
"recipes": "Recipes",
"register": "Register",
"relation": "Relation",
"relation_type": "Relation Type",
"relationship": "Relationship",
"relationship_type": "Relationship Type",
"religion": "Religion",
"remove": "Remove",
"residence": "Residence",
"save": "Save",
"search": "Search",
"select": "Select",
"select_all": "Select All",
"settings": "Settings",
"sibling": "Sibling",
"sign_in": "Sign In",
"sign_out": "Sign Out",
"site_intro": "Welcome to Generations Heritage, a place to record your family tree and share your family history. Create a digital intellectual legacy for your descendants.",
"skill": "Skill",
"skin_colour": "Skin Colour",
"source": "Source",
"source_url": "Source URL",
"spouse": "Spouse",
"street": "Street",
"suffixes": "Suffixes",
"system": "System",
"talent": "Talent",
"terms_and_conditions": "Terms and Conditions",
"theme": "Theme",
"title": "Generations Heritage {page}",
"titles": "Titles",
"tree": "Tree",
"unselect_all": "Unselect All",
"unknown": "Unknown",
"upload": "Upload",
"until": "Until",
"vaccination": "Vaccination",
"vegetable": "Vegetable",
"verified": "Verified",
"video": "Video",
"website": "Website",
"weight": "Weight",
"welcome": "Welcome to Generations Heritage",
"yes": "Yes",
"zip_code": "Zip Code",
"add_life_event": "Add life-event",
"deleted_profiles": "Deleted profiles",
"managed_profiles": "Managed profiles"
}

View File

@@ -1,172 +0,0 @@
{
"$schema": "https://inlang.com/schema/inlang-message-format",
"about": "Rólunk",
"accept": "Elfogadás",
"add": "Hozzáadás",
"add_administrator": "Adminisztrátor hozzáadása",
"add_note": "Jegyzet hozzáadása",
"add_relationship": "Kapcsolat hozzáadása",
"address": "Cím",
"admin": "Adminisztrátor",
"alive": "Élő",
"allergies": "Allergiák",
"allow_family_tree_admin_access": "Családfa adminisztrátor hozzáférésének engedélyezése",
"animal": "Állat",
"audio": "Hang",
"back": "Vissza",
"baptized": "Megkeresztelve",
"belief": "Hit",
"biological_sex": "Biológiai nem",
"biography": "Életrajz",
"birth_name": "Születési név",
"blood_pressure": "Vérnyomás",
"blood_type": "Vércsoport",
"born": "Született",
"cancel": "Mégse",
"city": "Város",
"child": "Gyermek",
"change_profile_picture": "Profilkép megváltoztatása",
"citizenship": "Állampolgárság",
"close": "Bezár",
"coffee": "Kávé",
"colour": "Szín",
"connection": "Kapcsolat",
"connection_type": "Kapcsolat típusa",
"contact": "Kapcsolat",
"cookie_disclaimer": "Ez a weboldal sütiket használ annak érdekében, hogy a lehető legjobb élményt nyújtsa, beleértve a téma tárolását és a felhasználói munkamenetek kezelését.",
"cookie_policy": "Süti szabályzat",
"country": "Ország",
"create": "Létrehozás",
"create_invite_code": "Meghívó kód létrehozása",
"create_person": "Személy létrehozása",
"create_relationship_and_person": "Kapcsolat és személy létrehozása",
"dark": "Sötét",
"date": "Dátum",
"death": "Halál",
"delete_profile": "Delete profile",
"deceased": "Elhunyt",
"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",
"email": "Email",
"export_something": "{thing}Exportálás",
"extra_names": "Extra nevek",
"faith": "Vallás",
"failed_to_create_user": "Felhasználó létrehozása sikertelen",
"family_tree": "Családfa",
"favourite": "Kedvenc",
"favourite_recipes": "Kedvenc receptek",
"female": "Nő",
"file": "Fájl",
"first_name": "Keresztnév",
"flower": "Virág",
"from_time": "Tól",
"fruit": "Gyümölcs",
"hair_colour": "Hajszín",
"hard_delete": "Végleges Törlés",
"have_invite_code": "Rendelkezem meghívó kóddal!",
"hello_world": "Helló, {name} innen: hu!",
"height": "Magasság",
"hobby": "Hobbi",
"home": "Otthon",
"id": "Azonosító",
"ideology": "Ideológia",
"illness": "Betegség",
"image": "Kép",
"ingridients": "Hozzávalók",
"interest": "Érdeklődés",
"intersex": "Interszex",
"invite_code": "Meghívó kód",
"language": "Nyelv",
"last_name": "Vezetéknév",
"life_events": "Életesemények",
"light": "Világos",
"loading": "Betöltés",
"login": "Bejelentkezés",
"logout": "Kijelentkezés",
"male": "Férfi",
"media_title": "Média cím",
"medication": "Gyógyszer",
"message_for_future_generations": "Üzenet a jövő generációinak",
"middle_name": "Második név",
"missing_field": "{field} mező hiányzik",
"mothers_first_name": "Anyja keresztneve",
"mothers_last_name": "Anyja vezetékneve",
"nation": "Nemzet",
"no": "Nem",
"no_data": "Nincs adat",
"notes": "Jegyzetek",
"occupation": "Foglalkozás",
"occupation_to_display": "Megjelenítendő foglalkozás",
"optional_field": "Opcionális mező",
"other": "Más",
"others_said": "Mások mondták",
"parent": "Szülő",
"people": "Emberek",
"person": "Személy",
"pet": "Háziállat",
"philosophy": "Filozófia",
"photos": "Fotók",
"place_of_birth": "Születési hely",
"place_of_death": "Halálozási hely",
"plant": "Növény",
"politics": "Politika",
"profile_picture": "Profilkép",
"privacy_policy": "Adatvédelmi irányelvek",
"recipe": "Recept",
"recipes": "Receptek",
"register": "Regisztráció",
"relation": "Kapcsolat",
"relation_type": "Kapcsolat típusa",
"relationship": "Kapcsolat",
"relationship_type": "Kapcsolat típusa",
"religion": "Vallás",
"remove": "Eltávolítás",
"residence": "Lakóhely",
"save": "Mentés",
"search": "Keresés",
"select": "Kiválasztás",
"select_all": "Összes kiválasztása",
"settings": "Beállítások",
"sibling": "Testvér",
"sign_in": "Bejelentkezés",
"sign_out": "Kijelentkezés",
"site_intro": "Üdvözöljük a Generációk Öröksége oldalán, ahol rögzítheti családfáját és megoszthatja családtörténetét szereteivel. Hozon létre digitális szellemi hagyatékot leszármazottai számára.",
"skill": "Képesség",
"skin_colour": "Bőrszín",
"source": "Forrás",
"source_url": "Forrás URL",
"spouse": "Házastárs",
"street": "Utca",
"suffixes": "Utótagok",
"system": "Rendszer",
"talent": "Tehetség",
"terms_and_conditions": "Felhasználási feltételek",
"theme": "Téma",
"title": "Generációk Öröksége {page}",
"titles": "Címek",
"tree": "Fa",
"unselect_all": "Összes kiválasztásának megszüntetése",
"unknown": "Ismeretlen",
"upload": "Feltöltés",
"until": "-ig",
"vaccination": "Oltás",
"vegetable": "Zöldség",
"verified": "Igazolt",
"video": "Videó",
"website": "Weboldal",
"weight": "Súly",
"welcome": "Üdvözöljük a Generációk Öröksége oldalán",
"yes": "Igen",
"zip_code": "Irányítószám",
"add_life_event": "Életesemény hozzadása",
"deleted_profiles": "Törölt profilok",
"managed_profiles": "Adminisztrált profilok",
"phone": "Telefon"
}

11369
apps/app/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,73 +0,0 @@
{
"name": "generations-heritage",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "npm run build && wrangler pages dev --port 5173",
"prepare": "svelte-kit sync || echo ''",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"format": "prettier --write .",
"lint": "prettier --check . && eslint .",
"test:unit": "vitest",
"test": "npm run test:unit -- --run",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build",
"deploy-stage": "npm run build && wrangler pages deploy --env staging",
"deploy-prod": "npm run build && wrangler pages deploy --env production",
"cf-typegen": "wrangler types && mv worker-configuration.d.ts src/"
},
"devDependencies": {
"@chromatic-com/storybook": "^3.2.4",
"@eslint/compat": "^1.2.5",
"@eslint/js": "^9.18.0",
"@playwright/test": "^1.50.1",
"@storybook/addon-essentials": "^8.5.6",
"@storybook/addon-interactions": "^8.5.6",
"@storybook/addon-svelte-csf": "^5.0.0-next.23",
"@storybook/blocks": "^8.5.6",
"@storybook/svelte": "^8.5.6",
"@storybook/sveltekit": "^8.5.6",
"@storybook/test": "^8.5.6",
"@sveltejs/adapter-auto": "^4.0.0",
"@sveltejs/adapter-cloudflare": "^7.0.1",
"@sveltejs/kit": "^2.16.0",
"@sveltejs/vite-plugin-svelte": "^5.0.0",
"@tailwindcss/postcss": "^4.0.12",
"@types/node": "^22.13.9",
"@vitest/coverage-v8": "^3.0.5",
"daisyui": "^5.0.0",
"eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-svelte": "^2.46.1",
"globals": "^15.14.0",
"openapi-typescript": "^7.6.1",
"prettier": "^3.4.2",
"prettier-plugin-svelte": "^3.3.3",
"prettier-plugin-tailwindcss": "^0.6.11",
"storybook": "^8.5.6",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"tailwindcss": "^4.0.12",
"typescript": "^5.0.0",
"typescript-eslint": "^8.20.0",
"vite": "^6.0.0",
"vitest": "^3.0.0",
"wrangler": "^4.13.2"
},
"dependencies": {
"@dagrejs/dagre": "^1.1.4",
"@inlang/paraglide-sveltekit": "^0.15.0",
"@pilcrowjs/object-parser": "^0.0.4",
"@types/pikaday": "^1.7.9",
"@xyflow/svelte": "^1.0.0-next.11",
"arctic": "^3.3.0",
"neo4j-driver": "^5.28.1",
"openapi-fetch": "^0.13.5",
"pikaday": "^1.8.2",
"uuid": "^11.1.0"
}
}

View File

@@ -1,79 +0,0 @@
import { defineConfig, devices } from '@playwright/test';
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// import dotenv from 'dotenv';
// import path from 'path';
// dotenv.config({ path: path.resolve(__dirname, '.env') });
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: './e2e',
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
// baseURL: 'http://127.0.0.1:3000',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry'
},
/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] }
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] }
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] }
}
/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: { ...devices['Pixel 5'] },
// },
// {
// name: 'Mobile Safari',
// use: { ...devices['iPhone 12'] },
// },
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
// },
// {
// name: 'Google Chrome',
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
// },
]
/* Run your local dev server before starting the tests */
// webServer: {
// command: 'npm run start',
// url: 'http://127.0.0.1:3000',
// reuseExistingServer: !process.env.CI,
// },
});

View File

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

View File

@@ -1 +0,0 @@
cache

View File

@@ -1 +0,0 @@
3e148103694315c86d552d141b1e0996d919bde0a260527d1d9f4af226be7582

View File

@@ -1,17 +0,0 @@
{
"$schema": "https://inlang.com/schema/project-settings",
"modules": [
"https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-empty-pattern@1/dist/index.js",
"https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-identical-pattern@1/dist/index.js",
"https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-missing-translation@1/dist/index.js",
"https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-without-source@1/dist/index.js",
"https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-valid-js-identifier@1/dist/index.js",
"https://cdn.jsdelivr.net/npm/@inlang/plugin-message-format@2/dist/index.js",
"https://cdn.jsdelivr.net/npm/@inlang/plugin-m-function-matcher@0/dist/index.js"
],
"plugin.inlang.messageFormat": {
"pathPattern": "./messages/{languageTag}.json"
},
"sourceLanguageTag": "hu",
"languageTags": ["en", "hu"]
}

View File

@@ -1,32 +0,0 @@
@import 'tailwindcss';
/*
The default border color has changed to `currentColor` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still
looks the same as it did with Tailwind CSS v3.
If we ever want to remove these styles, we need to add an explicit border
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);
}
}
@plugin "daisyui" {
themes:
light --default,
dark --prefersdark,
light,
dark,
cyberpunk,
synthwave,
retro,
coffee,
dracula;
}

20
apps/app/src/app.d.ts vendored
View File

@@ -1,20 +0,0 @@
import { KVNamespace } from '@cloudflare/workers-types';
// See https://svelte.dev/docs/kit/types#app.d.ts
// for information about these interfaces
declare global {
namespace App {
interface Locals {
session: Session | null;
}
interface Platform {
env: {
GH_MEDIA: R2Bucket;
GH_SESSIONS: KVNamespace;
};
cf: CfProperties;
ctx: ExecutionContext;
}
}
}
export {};

View File

@@ -1,14 +0,0 @@
<!doctype html>
<html lang="%paraglide.lang%" dir="%paraglide.textDirection%" data-theme="" class="bg-base-200">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%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>
</body>
</html>

View File

@@ -1,52 +0,0 @@
import type { Handle } from '@sveltejs/kit';
import { themes } from '$lib/themes';
import { i18n } from '$lib/i18n';
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);
}
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 {
console.error('Session token is invalid');
deleteSessionTokenCookie(event);
}
event.locals.session = session;
return resolve(event);
};
const themeHandler: Handle = async ({ event, resolve }) => {
const theme = event.cookies.get('theme');
if (!theme || !themes.includes(theme)) {
return await resolve(event);
}
return await resolve(event, {
transformPageChunk: ({ html }) => {
return html.replace('data-theme=""', `data-theme="${theme}"`);
}
});
};
export const handle: Handle = sequence(handleParaglide, authHandle, themeHandler);

View File

@@ -1,2 +0,0 @@
import { i18n } from '$lib/i18n';
export const reroute = i18n.reroute();

View File

@@ -1,27 +0,0 @@
import type { Meta, StoryObj } from '@storybook/svelte';
import Logout from './Logout.svelte';
const meta = {
title: 'lib/Logout',
component: Logout,
tags: ['autodocs'],
argTypes: {
show: { control: { type: 'boolean' } }
}
} satisfies Meta<Logout>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Visible: Story = {
args: {
show: true
}
};
export const Hidden: Story = {
args: {
show: false
}
};

View File

@@ -1,8 +0,0 @@
<script lang="ts">
import { logout } from '$lib/paraglide/messages.js';
export let show = false;
</script>
{#if show}
<a class="btn btn-error btn-xs h-8 min-h-0 px-4 py-0 text-sm" href="/logout">{logout()}</a>
{/if}

View File

@@ -1,54 +0,0 @@
<script lang="ts">
import { themes } from './themes';
import { theme, light, dark, coffee } from '$lib/paraglide/messages.js';
let current_theme = $state('');
const themeMessages = new Map<string, string>([
['light', light()],
['dark', dark()],
['coffee', coffee()],
['cyberpunk', 'Cyberpunk'],
['synthwave', 'Synthwave'],
['retro', 'Retro'],
['dracula', 'Dracula']
]);
$effect(() => {
if (typeof window !== 'undefined') {
const theme = window.localStorage.getItem('theme');
if (theme && themes.includes(theme)) {
document.documentElement.setAttribute('data-theme', theme);
current_theme = theme;
}
}
});
function set_theme(event: Event) {
const select = event.target as HTMLSelectElement;
const theme = select.value;
if (themes.includes(theme)) {
const one_year = 60 * 60 * 24 * 365;
window.localStorage.setItem('theme', theme);
document.cookie = `theme=${theme}; max-age=${one_year}; path=/; SameSite=Lax`;
document.documentElement.setAttribute('data-theme', theme);
current_theme = theme;
}
}
</script>
<div class="dropdown dropdown-end">
<select
bind:value={current_theme}
data-choose-theme
class="btn btn-soft btn-xs h-8 min-h-0 px-4 py-0 text-sm"
onchange={set_theme}
>
<option value="" disabled={current_theme !== ''}>
{theme()}
</option>
{#each themes as theme}
<option value={theme} class="theme-controller capitalize">{themeMessages.get(theme)}</option>
{/each}
</select>
</div>

View File

@@ -1,174 +0,0 @@
<script lang="ts">
import {
hard_delete,
managed_profiles,
delete_profile,
edit,
from_time,
admin,
create_relationship_and_person,
add_relationship
} from '$lib/paraglide/messages';
import ModalButtons from './ModalButtons.svelte';
import type { components, operations } from '$lib/api/api.gen';
let {
closeModal,
editProfile = () => {},
removePersonFromGraph = () => {},
addRelationship = () => {},
createProfile = () => {},
createRelationshipAndProfile = () => {}
} = $props<{
closeModal: () => void;
removePersonFromGraph?: (id: any) => void;
addRelationship?: (id: number) => void;
createRelationshipAndProfile?: (id: number) => void;
editProfile?: (id: number) => void;
createProfile?: () => void;
}>();
let managed_profiles_list: components['schemas']['Admin'][] = $state([]);
async function fetchManagedProfiles() {
try {
const response = await fetch(`/api/managed_profiles`, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
});
if (!response.ok || response.status !== 200) {
console.log('Cannot get managed profiles, status: ' + response.status);
return;
}
const data = await response.json();
managed_profiles_list = [
...((
data as operations['getManagedProfiles']['responses']['200']['content']['application/json']
).managed ?? [])
];
} catch (error) {
console.error('Error fetching managed profiles:', error);
}
}
fetchManagedProfiles();
async function deleteProfile(id: number) {
fetch('/api/person/' + id, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
}
})
.then((response) => {
if (response.ok) {
removePersonFromGraph(id);
managed_profiles_list.forEach((profile) => {
if (profile.id === id) {
profile.label = ['DeletedPerson'];
}
});
return;
} else {
alert('Error deleting person');
}
})
.catch((error) => {
console.info('Error:', error);
});
}
async function hardDeleteProfile(id: number) {
fetch('/api/person/' + id + '/hard-delete', {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
}
})
.then((response) => {
if (response.ok) {
managed_profiles_list = managed_profiles_list.filter((profile) => profile.id !== id);
return;
} else {
alert('Error deleting person');
}
})
.catch((error) => {
console.error('Error:', error);
});
}
</script>
<div class="modal modal-open z-8">
<div class="modal-box w-full max-w-xl gap-4">
<div class="bg-base-100 z-5 sticky top-0">
<ModalButtons onClose={closeModal} createProfile={()=>{createProfile();closeModal()}} />
<div class="divider"></div>
</div>
<ul class="list bg-base-100 rounded-box shadow-md">
<li class="p-4 pb-2 text-xs tracking-wide opacity-60">{managed_profiles()}</li>
{#each managed_profiles_list as profile}
<li class="list-row">
<div>
<div class="text-xs font-semibold uppercase opacity-60">{profile.id}</div>
</div>
<div>
<div>{profile.first_name + ' ' + profile.last_name}</div>
</div>
{#if false}
<div>
<div class="text-xs font-semibold uppercase opacity-60">
{admin() + ' ' + from_time().toLowerCase() + ': ' + profile.adminSince}
</div>
<div class="text-xs font-semibold uppercase opacity-60">{profile.label![0]}</div>
</div>
<button
class="btn btn-success btn-soft"
onclick={() => {
addRelationship(profile.id!);
}}
>
{add_relationship()}
</button>
<button
class="btn btn-success btn-soft"
onclick={() => {
createRelationshipAndProfile(profile.id!);
}}
>
{create_relationship_and_person()}
</button>
{/if}
{#if profile.label?.includes('DeletedPerson')}
<button
class="btn btn-error btn-sm"
onclick={() => {
hardDeleteProfile(profile.id!);
}}
>
{hard_delete()}
</button>
{:else}
<button
class="btn btn-secondary btn-sm"
onclick={() => {
editProfile(profile.id!);
}}>
{edit()}
</button>
<button
class="btn btn-error btn-sm"
onclick={() => {
deleteProfile(profile.id!);
}}>
{delete_profile()}
</button>
{/if}
</li>
{/each}
</ul>
</div>
</div>

View File

@@ -1,28 +0,0 @@
<script lang="ts">
import {
add_relationship,
back,
biography,
close,
create,
create_person,
edit,
managed_profiles,
relation,
save
} from '$lib/paraglide/messages';
export let createProfile: () => void;
export let onClose: () => void;
</script>
<div class="flex items-center justify-between p-2">
<h3 class="text-lg font-bold">{managed_profiles()}</h3>
<div class="space-x-2">
<button class="btn btn-success btn-sm" on:click={createProfile}>
{'+ ' + create_person()}
</button>
<button class="btn btn-error btn-sm" on:click={onClose}>
{close()}
</button>
</div>
</div>

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +0,0 @@
import createClient from 'openapi-fetch';
import type { paths } from '$lib/api/api.gen'; // generated by openapi-typescript
import { DB_ADAPTER, CF_ACCESS_CLIENT_ID, CF_ACCESS_CLIENT_SECRET } from '$env/static/private';
export const client = createClient<paths>({
baseUrl: DB_ADAPTER || 'http://localhost:5237',
headers: {
'CF-Access-Client-Secret': CF_ACCESS_CLIENT_SECRET || '',
'CF-Access-Client-Id': CF_ACCESS_CLIENT_ID || ''
}
});

View File

@@ -1,20 +0,0 @@
<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>

View File

@@ -1,141 +0,0 @@
<script lang="ts">
import type { components } from '$lib/api/api.gen.ts';
import { child, spouse, parent, sibling } from '$lib/paraglide/messages';
import { getSmoothStepPath, BaseEdge, type EdgeProps, Position } from '@xyflow/svelte';
let {
sourceX,
sourceY,
source,
sourcePosition,
sourceHandleId,
target,
targetX,
targetY,
targetPosition,
targetHandleId,
markerEnd,
style,
data
}: EdgeProps = $props();
let edgeType = (
data as components['schemas']['FamilyRelationship'] & { type: string }
).type.toLowerCase();
let edgeLabel: string = $state(edgeType);
let edgeColor: string = $state('stroke: gray;');
let srcPos: Position = $state(sourcePosition || Position.Bottom);
let tgtPos: Position = $state(targetPosition || Position.Top);
// Determine edge styling and positioning based on relationship type and handles
if (edgeType === 'spouse') {
edgeColor = 'stroke: red;';
edgeLabel = spouse();
// Use handle-based positioning for spouses
if (sourceHandleId === 'spouse-right') {
srcPos = Position.Right;
tgtPos = Position.Left;
} else if (sourceHandleId === 'spouse-left') {
srcPos = Position.Left;
tgtPos = Position.Right;
} else {
// Fallback to position-based logic
if (sourceX < targetX) {
srcPos = Position.Right;
tgtPos = Position.Left;
} else {
srcPos = Position.Left;
tgtPos = Position.Right;
}
}
} else if (edgeType === 'child') {
edgeColor = 'stroke: blue;';
edgeLabel = child();
// Parent-child: from parent's bottom (child handle) to child's top (parent handle)
srcPos = Position.Bottom;
tgtPos = Position.Top;
} else if (edgeType === 'parent') {
edgeColor = 'stroke: blue;';
edgeLabel = parent();
// Parent relationship: from child (top) to parent (bottom)
srcPos = Position.Top;
tgtPos = Position.Bottom;
} else if (edgeType === 'sibling') {
edgeColor = 'stroke: orange;';
edgeLabel = sibling();
// Use handle-based positioning for siblings
if (sourceHandleId === 'spouse-right') {
srcPos = Position.Right;
tgtPos = Position.Left;
} else if (sourceHandleId === 'spouse-left') {
srcPos = Position.Left;
tgtPos = Position.Right;
} else {
// Fallback to position-based logic
if (sourceX < targetX) {
srcPos = Position.Right;
tgtPos = Position.Left;
} else {
srcPos = Position.Left;
tgtPos = Position.Right;
}
}
} else {
edgeColor = 'stroke: gray;';
edgeLabel = edgeType;
// Keep original positions for unknown types
srcPos = sourcePosition || Position.Bottom;
tgtPos = targetPosition || Position.Top;
}
// Explicit handle overrides (these should take precedence)
if (sourceHandleId === 'child') {
srcPos = Position.Bottom;
}
if (targetHandleId === 'parent') {
tgtPos = Position.Top;
}
if (sourceHandleId === 'spouse-left') {
srcPos = Position.Left;
}
if (sourceHandleId === 'spouse-right') {
srcPos = Position.Right;
}
if (targetHandleId === 'spouse-left') {
tgtPos = Position.Left;
}
if (targetHandleId === 'spouse-right') {
tgtPos = Position.Right;
}
let [path, labelX, labelY] = $derived(
getSmoothStepPath({
sourceX,
sourceY,
sourcePosition: srcPos,
targetX,
targetY,
targetPosition: tgtPos,
borderRadius: 20, // Add some rounding to make paths smoother
offset: 20 // Add offset to avoid overlapping with nodes
})
);
// Fix the style string formatting
const finalStyle = `${edgeColor} stroke-width: 3; stroke-opacity: 0.8; ${style ?? ''}`;
const onEdgeClick = () => {
window.dispatchEvent(
new CustomEvent('edge-click', {
detail: {
start: source,
end: target,
data: data as components['schemas']['FamilyRelationship'] & { type: string }
}
})
);
};
</script>
<BaseEdge {path} {labelX} {labelY} {markerEnd} style={finalStyle} onclick={onEdgeClick} />

View File

@@ -1,88 +0,0 @@
<script lang="ts">
import { onMount } from 'svelte';
import {
add_relationship,
remove,
create_relationship_and_person,
add_administrator
} from '$lib/paraglide/messages';
export let id: string;
export let XUserId: string;
export let top: number | undefined;
export let left: number | undefined;
export let right: number | undefined;
export let bottom: number | undefined;
export let onClick: () => void;
export let deleteNode: () => void;
export let createRelationshipAndNode: () => void;
export let addRelationship: () => void;
// export let addAdmin: (() => void) | undefined;
let contextMenu: HTMLDivElement;
let isAdmin: boolean = false;
onMount(() => {
if (top) {
contextMenu.style.top = `${top}px`;
}
if (left) {
contextMenu.style.left = `${left}px`;
}
if (right) {
contextMenu.style.right = `${right}px`;
}
if (bottom) {
contextMenu.style.bottom = `${bottom}px`;
}
fetch(`/api/admin/${id}/${XUserId}`)
.then((response) => {
if (response.status === 200) {
isAdmin = true;
} else {
isAdmin = false;
}
})
.catch((error) => {
console.error('Error fetching admin status:', error);
});
});
</script>
<div
role="menu"
tabindex="-1"
bind:this={contextMenu}
class="context-menu bg-primary-100 rounded-lg shadow-lg"
onclick={onClick}
onkeydown={(e) => {
if (e.key === 'Esc' || e.key === ' ' || e.key === 'Escape') {
onClick();
}
}}
>
<button onclick={createRelationshipAndNode} class="btn">
{create_relationship_and_person()}
</button>
<button onclick={addRelationship} class="btn">{add_relationship()}</button>
<!-- <button onclick={addAdmin} class="btn">{add_administrator()}</button> -->
{#if Number(XUserId) !== Number(id) && isAdmin}
<button onclick={deleteNode} class="btn">{remove()}</button>
{/if}
</div>
<style>
.context-menu {
border-style: solid;
box-shadow: 10px 19px 20px rgba(0, 0, 0, 10%);
position: absolute;
z-index: 10;
}
.context-menu button {
border: none;
display: block;
padding: 0.5em;
text-align: left;
width: 100%;
}
</style>

View File

@@ -1,149 +0,0 @@
<!-- <svelte:options immutable /> -->
<script lang="ts">
import { Handle, Position, type NodeProps } from '@xyflow/svelte';
import type { components } from '$lib/api/api.gen';
import { isValidConnection } from './connection.js';
type $$Props = NodeProps;
export let data: NodeProps['data'] & components['schemas']['PersonProperties'];
let nodeColor = ' bg-neutral text-neutral-content';
switch (data.biological_sex) {
case 'female':
nodeColor = ' bg-secondary text-secondary-content';
break;
case 'male':
nodeColor = ' bg-primary text-primary-content';
break;
case 'intersex':
nodeColor = ' bg-accent text-accent-content';
break;
}
</script>
<div
class={'card card-compact flex h-40 w-40 flex-col items-center justify-center rounded-full shadow-lg' +
nodeColor}
>
<Handle
class="customHandle"
id="spouse-left"
{isValidConnection}
position={Position.Left}
isConnectable={true}
type="source"
/>
<Handle
class="customHandle"
id="spouse-right"
{isValidConnection}
position={Position.Right}
isConnectable={true}
type="source"
/>
<Handle
class="customHandle"
id="spouse-left"
{isValidConnection}
position={Position.Left}
isConnectable={true}
type="target"
/>
<Handle
class="customHandle"
id="spouse-right"
{isValidConnection}
position={Position.Right}
isConnectable={true}
type="target"
/>
<Handle
class="customHandle"
id="child"
{isValidConnection}
isConnectable={true}
position={Position.Bottom}
type="source"
style="z-index: 1;"
/>
<Handle
class="customHandle"
{isValidConnection}
position={Position.Left}
isConnectable={true}
type="target"
isConnectableStart={false}
/>
<Handle
class="customHandle"
{isValidConnection}
position={Position.Right}
isConnectable={true}
type="target"
isConnectableStart={false}
/>
<Handle
class="customHandle"
{isValidConnection}
position={Position.Left}
isConnectable={true}
type="source"
isConnectableStart={true}
/>
<Handle
class="customHandle"
{isValidConnection}
position={Position.Right}
isConnectable={true}
type="source"
isConnectableStart={true}
/>
<Handle
class="customHandle"
id="parent"
{isValidConnection}
position={Position.Top}
isConnectable={true}
type="target"
isConnectableStart={false}
/>
<div class="avatar mb-2" style="z-index: 2; cursor: pointer;">
<div class={'w-24 rounded-full border-0 ring-offset-1' + nodeColor}>
<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="px-2 text-center" style="z-index: 2; cursor: pointer;">
<h2 class="text-sm font-semibold leading-tight">
{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 : ''}
</h3>
</div>
</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>

View File

@@ -1,10 +0,0 @@
import type { Connection } from '@xyflow/svelte';
import type { EdgeBase } from '@xyflow/system';
export function isValidConnection(edge: EdgeBase | Connection) {
if (Number(edge.source) !== Number(edge.target)) {
return true;
}
return false;
}

View File

@@ -1,232 +0,0 @@
import dagre from '@dagrejs/dagre';
import type { Layout } from './model';
import type { Edge, Node } from '@xyflow/svelte';
import { Position } from '@xyflow/svelte';
export class FamilyTree extends dagre.graphlib.Graph {
constructor() {
super();
}
getLayoutedElements(
nodes: Node[],
edges: Edge[],
nodeWidth: number,
nodeHeight: number,
direction = 'TB'
): Layout {
this.setGraph({
rankdir: direction,
nodesep: 80,
ranksep: 120,
marginx: 50,
marginy: 50
});
this.setDefaultEdgeLabel(() => ({}));
// Add nodes to dagre
nodes.forEach((node) => {
this.setNode(node.id, { width: nodeWidth, height: nodeHeight });
});
// Only add parent-child edges to dagre for hierarchical layout
edges.forEach((edge) => {
if (String(edge.data!.type).toLowerCase() === 'child') {
this.setEdge(edge.source, edge.target);
}
});
// Run dagre layout
dagre.layout(this);
// Create maps for relationship analysis
const spouseMap = new Map<string, string>();
const siblingMap = new Map<string, string[]>();
const childrenMap = new Map<string, string[]>();
const parentsMap = new Map<string, string[]>();
// Build relationship maps
edges.forEach((edge) => {
const type = String(edge.data?.type).toLowerCase();
if (type === 'spouse') {
spouseMap.set(edge.source, edge.target);
spouseMap.set(edge.target, edge.source);
} else if (type === 'sibling') {
if (!siblingMap.has(edge.source)) siblingMap.set(edge.source, []);
if (!siblingMap.has(edge.target)) siblingMap.set(edge.target, []);
siblingMap.get(edge.source)!.push(edge.target);
siblingMap.get(edge.target)!.push(edge.source);
} else if (type === 'child') {
if (!childrenMap.has(edge.source)) childrenMap.set(edge.source, []);
if (!parentsMap.has(edge.target)) parentsMap.set(edge.target, []);
childrenMap.get(edge.source)!.push(edge.target);
parentsMap.get(edge.target)!.push(edge.source);
}
});
// Helper function to check if position is occupied
const isPositionOccupied = (x: number, y: number, excludeId?: string): boolean => {
return nodes.some(node => {
if (excludeId && node.id === excludeId) return false;
const nodePos = this.node(node.id);
return Math.abs(nodePos.x - x) < nodeWidth && Math.abs(nodePos.y - y) < nodeHeight;
});
};
// Helper function to find free position near a reference point
const findFreePosition = (refX: number, refY: number, excludeId?: string): { x: number, y: number } => {
const padding = 30;
const positions = [
{ x: refX + nodeWidth + padding, y: refY }, // right
{ x: refX - nodeWidth - padding, y: refY }, // left
{ x: refX + (nodeWidth + padding) * 2, y: refY }, // far right
{ x: refX - (nodeWidth + padding) * 2, y: refY }, // far left
];
for (const pos of positions) {
if (!isPositionOccupied(pos.x, pos.y, excludeId)) {
return pos;
}
}
// If all positions are taken, just go further right
return { x: refX + (nodeWidth + padding) * 3, y: refY };
};
// Position spouses next to each other
const processedSpouses = new Set<string>();
spouseMap.forEach((spouse, person) => {
if (processedSpouses.has(person)) return;
const personNode = this.node(person);
const spouseNode = this.node(spouse);
// Determine who should be the anchor (prefer the one with children or hierarchically positioned)
const personHasChildren = childrenMap.has(person) && childrenMap.get(person)!.length > 0;
const spouseHasChildren = childrenMap.has(spouse) && childrenMap.get(spouse)!.length > 0;
let anchorNode, mobileNode, anchorId, mobileId;
if (personHasChildren && !spouseHasChildren) {
anchorNode = personNode;
mobileNode = spouseNode;
anchorId = person;
mobileId = spouse;
} else if (!personHasChildren && spouseHasChildren) {
anchorNode = spouseNode;
mobileNode = personNode;
anchorId = spouse;
mobileId = person;
} else {
// Both or neither have children, use alphabetical order
if (person < spouse) {
anchorNode = personNode;
mobileNode = spouseNode;
anchorId = person;
mobileId = spouse;
} else {
anchorNode = spouseNode;
mobileNode = personNode;
anchorId = spouse;
mobileId = person;
}
}
// Position mobile spouse next to anchor
const newPos = findFreePosition(anchorNode.x, anchorNode.y, mobileId);
mobileNode.x = newPos.x;
mobileNode.y = newPos.y;
processedSpouses.add(person);
processedSpouses.add(spouse);
});
// Position siblings
const processedSiblings = new Set<string>();
siblingMap.forEach((siblings, person) => {
if (processedSiblings.has(person)) return;
const personNode = this.node(person);
siblings.forEach((sibling) => {
if (processedSiblings.has(sibling)) return;
const siblingNode = this.node(sibling);
// If sibling is not a spouse of someone at the same level, position as sibling
const siblingSpouse = spouseMap.get(sibling);
if (!siblingSpouse || Math.abs(this.node(siblingSpouse).y - personNode.y) > nodeHeight) {
const newPos = findFreePosition(personNode.x, personNode.y, sibling);
siblingNode.x = newPos.x;
siblingNode.y = newPos.y;
}
processedSiblings.add(sibling);
});
processedSiblings.add(person);
});
// Create new edges
let newEdges: Edge[] = [];
const processedSpouseEdges = new Set<string>();
edges.forEach((edge) => {
let newEdge = { ...edge };
if (String(edge.data?.type).toLowerCase() === 'child') {
// Parent to child: source (parent) uses 'child' handle (bottom), target (child) uses 'parent' handle (top)
newEdge.sourceHandle = 'child';
newEdge.targetHandle = 'parent';
} else if (String(edge.data?.type).toLowerCase() === 'parent') {
return;
} else if (String(edge.data?.type).toLowerCase() === 'spouse') {
// Avoid duplicate spouse edges by creating a unique key
const spouseKey = [edge.source, edge.target].sort().join('-');
if (processedSpouseEdges.has(spouseKey)) {
return; // Skip this duplicate spouse edge
}
processedSpouseEdges.add(spouseKey);
// Set spouse handles based on position
const sourceNode = this.node(edge.source);
const targetNode = this.node(edge.target);
if (sourceNode.x < targetNode.x) {
newEdge.sourceHandle = 'spouse-right';
newEdge.targetHandle = 'spouse-left';
} else {
newEdge.sourceHandle = 'spouse-left';
newEdge.targetHandle = 'spouse-right';
}
} else if (String(edge.data?.type).toLowerCase() === 'sibling') {
// Set sibling handles based on position
const sourceNode = this.node(edge.source);
const targetNode = this.node(edge.target);
if (sourceNode.x < targetNode.x) {
newEdge.sourceHandle = 'spouse-right';
newEdge.targetHandle = 'spouse-left';
} else {
newEdge.sourceHandle = 'spouse-left';
newEdge.targetHandle = 'spouse-right';
}
}
newEdge.hidden = false;
newEdge.type = 'familyEdge';
newEdges.push(newEdge);
});
const layoutedNodes = nodes.map((node) => {
const nodeWithPosition = this.node(node.id);
return {
...node,
type: 'personNode',
position: {
x: nodeWithPosition.x - nodeWidth / 2,
y: nodeWithPosition.y - nodeHeight / 2
}
};
});
return { Nodes: layoutedNodes, Edges: newEdges };
}
}

View File

@@ -1,28 +0,0 @@
import type { Node, Edge, NodeTypes, EdgeTypes } from '@xyflow/svelte';
import FamilyEdge from './FamilyEdge.svelte';
import PersonNode from './PersonNode.svelte';
export const nodeTypes: NodeTypes = { personNode: PersonNode };
export const edgeTypes: EdgeTypes = {
familyEdge: FamilyEdge
};
export type NodeMenu = {
onClick: () => void;
deleteNode: () => void;
createRelationshipAndNode: () => void;
addRelationship: () => void;
addRecipe: (() => void) | undefined;
addAdmin: (() => void) | undefined;
id: string;
XUserId: string;
top: number | undefined;
left: number | undefined;
right: number | undefined;
bottom: number | undefined;
};
export type Layout = {
Nodes: Array<Node>;
Edges: Array<Edge>;
};

View File

@@ -1,11 +0,0 @@
import type { components } from '$lib/api/api.gen';
import type { NodeEventWithPointer } from '@xyflow/svelte';
export function handleNodeClick(
set_panel_options: (person: components['schemas']['PersonProperties'] & { id: number }) => void
): NodeEventWithPointer<MouseEvent | TouchEvent> {
return ({ event, node }) => {
event.preventDefault();
set_panel_options(node.data as components['schemas']['PersonProperties'] & { id: number });
};
}

View File

@@ -1,43 +0,0 @@
import type { components } from '$lib/api/api.gen';
import type { Layout } from '$lib/graph/model';
import type { Edge, Node } from '@xyflow/svelte';
export function parseFamilyTree(data: components['schemas']['FamilyTree']): Layout {
if (
data === null ||
data?.people === null ||
data?.people === undefined ||
data?.people.length === 0
) {
throw new Error('Family tree is empty');
}
const nodes: Node[] = data.people.map((person) => {
let newNode = { data: { ...person } } as Node;
if (person.id !== null && person.id !== undefined) {
newNode.id = 'person' + person.id.toString();
}
newNode.position = { x: 0, y: 0 };
newNode.data.id = person.id;
return newNode;
});
let relationships: Edge[] = [];
if (data.relationships) {
relationships = data.relationships.map((relationship) => {
const newEdge = { data: { ...relationship.Props } } as Edge;
newEdge.id = 'person' + relationship.ElementId;
newEdge.data!.type = relationship.Type?.toLowerCase();
if (relationship.StartElementId !== null && relationship.StartElementId !== undefined) {
newEdge.source = 'person' + relationship.StartId!.toString();
}
if (relationship.EndElementId !== null && relationship.EndElementId !== undefined) {
newEdge.target = 'person' + relationship.EndId!.toString();
}
return newEdge;
});
}
return { Nodes: nodes, Edges: relationships };
}

View File

@@ -1,22 +0,0 @@
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];
try {
if (typeof fn === 'function') {
return fn({ thing: '', field: '', page: '', name: '' });
} else {
throw new Error(`Function ${name} is not callable`);
}
} catch (error) {
console.error(`Error calling message function ${name}:`, error);
return '';
}
}

View File

@@ -1,7 +0,0 @@
export interface PersonProperties {
google_id: string;
first_name: string;
middle_name?: string;
last_name: string;
email: string;
}

View File

@@ -1,113 +0,0 @@
<script lang="ts">
import {
add_life_event,
description,
life_events,
unknown,
until,
remove
} from '$lib/paraglide/messages';
import type { components } from '$lib/api/api.gen';
export let person_life_events: components['schemas']['PersonProperties']['life_events'];
export let editorMode = false;
export let onChange: (field: keyof components['schemas']['PersonProperties'], value: any) => void;
function updateEvent(index: number, key: 'from' | 'to' | 'description', value: string) {
if (!person_life_events) return;
person_life_events = person_life_events.map((event, i) =>
i === index ? { ...event, [key]: value } : event
);
onChange('life_events', person_life_events);
}
function addEvent() {
const newEvent = { from: '', to: undefined, description: '' };
person_life_events = [...(person_life_events ?? []), newEvent];
onChange('life_events', person_life_events);
}
function removeEvent(index: number) {
if (!person_life_events) return;
person_life_events = person_life_events.filter((_, i) => i !== index);
onChange('life_events', person_life_events);
}
</script>
{#if person_life_events?.length}
<div class="divider">{life_events()}</div>
<ul class="timeline timeline-snap-start timeline-vertical">
{#each person_life_events as event, index}
<li>
<div class="timeline-start flex items-center">
{#if editorMode}
<input
type="date"
class="input input-xs input-bordered"
value={event.from ?? ''}
on:input={(e) => updateEvent(index, 'from', e.currentTarget.value)}
placeholder={unknown().toLowerCase()}
/>
<!-- Remove button -->
<button
type="button"
class="btn btn-xs btn-ghost text-error ml-2"
title={remove()}
on:click={() => removeEvent(index)}
aria-label={remove() + ' ' + life_events()}
>
&#10005;
</button>
{:else}
{event.from ?? unknown().toLowerCase()}
{/if}
</div>
<div class="timeline-middle">
<div class="badge badge-primary"></div>
</div>
<div class="timeline-end space-y-1">
{#if editorMode}
<textarea
class="textarea textarea-xs textarea-bordered w-full"
value={event.description ?? ''}
on:input={(e) => updateEvent(index, 'description', e.currentTarget.value)}
placeholder={description()}
></textarea>
{:else}
<p>{event.description}</p>
{/if}
{#if event.to || editorMode}
<p class="text-sm opacity-50">
{until()}
{#if editorMode}
<input
type="date"
class="input input-xs input-bordered ml-1"
value={event.to ?? ''}
on:input={(e) => updateEvent(index, 'to', e.currentTarget.value)}
placeholder={unknown().toLowerCase()}
/>
{:else}
{event.to ?? unknown().toLowerCase()}
{/if}
</p>
{/if}
</div>
<hr />
</li>
{/each}
</ul>
{/if}
{#if editorMode}
<div class="mt-4 flex justify-center">
<button class="btn btn-primary btn-sm" on:click={addEvent}>
{add_life_event()}
</button>
</div>
{/if}

View File

@@ -1,69 +0,0 @@
<script lang="ts">
import type { components } from '$lib/api/api.gen';
import { video, photos, upload } from '$lib/paraglide/messages';
import UploadMediaModal from '$lib/profile/editors/UploadMediaModal.svelte';
export let person: components['schemas']['PersonProperties'];
export let editorMode = false;
let uploadModal = false;
let mediaType: 'audio' | 'video' | 'photo' | undefined = undefined;
</script>
{#if person.photos?.length || person.videos?.length}
<div class="divider">{photos()} & {video()}</div>
<div class="grid grid-cols-2 gap-4 md:grid-cols-4">
{#each person.photos ?? [] as picture}
<img
src={picture.url}
alt={picture.description ?? photos()}
class="h-32 w-full rounded-lg object-cover shadow-md"
/>
{/each}
{#each person.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}
{#if false}
<div class="divider">{upload()}</div>
<div class="grid grid-cols-2 gap-4">
<button
class="btn btn-soft btn-xs"
on:click={() => {
uploadModal = true;
mediaType = 'photo';
}}
>
{'+ ' + photos()}
</button>
<button
class="btn btn-soft btn-xs"
on:click={() => {
uploadModal = true;
mediaType = 'video';
}}
>
{'+ ' + video()}
</button>
</div>
{/if}
{#if uploadModal}
<UploadMediaModal
closeModal={() => {
uploadModal = false;
}}
{mediaType}
onCreation={(newMedia: { url: string; name: string; description: string; date: string }) => {
if (mediaType === 'photo') {
person.photos = [...(person.photos ?? []), newMedia];
} else if (mediaType === 'video') {
person.videos = [...(person.videos ?? []), newMedia];
}
}}
/>
{/if}

View File

@@ -1,99 +0,0 @@
<script lang="ts">
import { died } from './../paraglide/messages/en.js';
import { fade } from 'svelte/transition';
import ModalButtons from './ModalButtons.svelte';
import ProfileHeader from './ProfileHeader.svelte';
import MediaGallery from './MediaGallery.svelte';
import LifeEventsTimeline from './LifeEventsTimeline.svelte';
import OtherDetails from './OtherDetails.svelte';
import type { components } from '$lib/api/api.gen.js';
let {
closeModal = () => {},
person = {}
}: {
closeModal: () => void;
person: components['schemas']['PersonProperties'] & {
id?: string;
};
} = $props();
let editorMode = $state(false);
let draftPerson = $state({} as components['schemas']['PersonProperties']);
editorMode = false;
function handleDraftPersonChange(
field: keyof components['schemas']['PersonProperties'],
value: any
) {
draftPerson[field] = value;
if (field === 'invite_code') {
save().then(() => {
editorMode = true;
});
return;
}
}
function close() {
closeModal();
editorMode = false;
draftPerson = {};
}
function toggleEdit() {
editorMode = !editorMode;
}
async function save() {
try {
console.debug('Saving person data:', draftPerson);
const response = await fetch(`/api/person/${person.id}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(draftPerson)
});
if (!response.ok) {
console.error('Error saving person data, status: ', response.status, (await response.json()));
alert('Error saving person data, status: ' + response.status + (await response.json()));
return;
}
if (response.status === 200) {
person = { ...person, ...draftPerson };
const data = (await response.json()) as {
person?: components['schemas']['Person'];
};
} else {
const errorDetails = await response.json();
console.error('Error details:', errorDetails);
alert(`Error saving person data, status: ${response.status} ${JSON.stringify(errorDetails)}`);
}
} catch (error) {
alert('An unexpected error occurred: ' + error);
}
editorMode = !editorMode;
}
</script>
<div class="modal modal-open" transition:fade>
<div class="modal-box max-h-80 max-h-screen w-full max-w-5xl overflow-y-auto">
<div class="bg-base-100 z-7 sticky top-0">
<ModalButtons {editorMode} onClose={close} onSave={save} onToggleEdit={toggleEdit} />
<div class="divider"></div>
</div>
<ProfileHeader {person} {editorMode} onChange={handleDraftPersonChange} />
<MediaGallery {person} {editorMode} />
<LifeEventsTimeline
person_life_events={person.life_events}
{editorMode}
onChange={handleDraftPersonChange}
/>
<OtherDetails {person} {editorMode} onChange={handleDraftPersonChange} />
</div>
</div>

View File

@@ -1,26 +0,0 @@
<script lang="ts">
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-secondary btn-sm" on:click={onToggleEdit}>
{editorMode ? back() : edit()}
</button>
{#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>

View File

@@ -1,155 +0,0 @@
<script lang="ts">
import { callMessageFunction } from '$lib/i18n';
import type { MessageKeys } from '$lib/i18n';
import { add_note, notes, theme } from '$lib/paraglide/messages';
import type { components } from '$lib/api/api.gen';
export let person: components['schemas']['PersonProperties'];
export let editorMode = false;
export let onChange: (field: keyof components['schemas']['PersonProperties'], value: any) => void;
const skipFields = [
'id',
'first_name',
'last_name',
'born',
'died',
'middle_name',
'biological_sex',
'email',
'limit',
'mothers_first_name',
'mothers_last_name',
'profile_picture',
'photos',
'videos',
'life_events',
'residence',
'medications',
'medical_conditions',
'languages',
'notes',
'phone',
'audios',
'google_id',
'invite_code'
];
let newNote = {
title: " ",
note: ""
};
</script>
<div class="mt-5 flex flex-col gap-2 justify-center items-center">
{#each person.notes ?? [] as note, i}
<div class="card bg-base-100 shadow-sm relative w-full max-w-xl">
<div class="card-body p-4 w-full">
{#if editorMode}
<input
type="text"
class="input input-bordered input-sm w-full mb-2"
placeholder={theme()}
bind:value={note.title}
oninput={() => onChange('notes', person.notes)}
/>
<textarea
class="textarea textarea-bordered textarea-sm w-full"
placeholder={notes()}
bind:value={note.note}
oninput={() => onChange('notes', person.notes)}
></textarea>
<button
type="button"
class="absolute top-2 right-2 btn btn-xs btn-ghost text-error ml-2"
aria-label="Remove note"
onclick={() => {
person.notes = (person.notes ?? []).filter((_, idx) => idx !== i);
onChange('notes', person.notes);
}}
>
&#10005;
</button>
{:else}
<h2 class="card-title">{note.title}</h2>
<p class="text-sm text-gray-500">{note.date}</p>
<p>{note.note}</p>
{/if}
</div>
</div>
{/each}
{#if editorMode}
<button
class="btn btn-accent btn-sm w-auto self-start"
onclick={() => {
const now = new Date();
const formattedDate = now.getFullYear() + '-' +
String(now.getMonth() + 1).padStart(2, '0') + '-' +
String(now.getDate()).padStart(2, '0');
person.notes = [...(person.notes ?? []), { title: '', note: '', date: formattedDate }];
onChange('notes', person.notes);
}}
>
{add_note()}
</button>
{/if}
</div>
<div class="mt-6 grid grid-cols-1 gap-4 md:grid-cols-2">
{#each Object.entries(person) as [key, value]}
{#if !skipFields.includes(key) && ((value !== undefined && value !== null) || editorMode)}
<div>
<label class="label font-semibold"
>{callMessageFunction(key as MessageKeys) || key}:
{#if editorMode}
{#if typeof value === 'string'}
{#if value.length > 100}
<textarea
bind:value={person[key as keyof components['schemas']['PersonProperties']]}
class="textarea textarea-bordered textarea-sm w-full"
oninput={(e) =>
onChange(
key as keyof components['schemas']['PersonProperties'],
String(person[key as keyof components['schemas']['PersonProperties']])
)}
></textarea>
{:else}
<input
type="text"
class="input input-bordered input-sm w-full"
bind:value={person[key as keyof components['schemas']['PersonProperties']]}
oninput={() =>
onChange(
key as keyof components['schemas']['PersonProperties'],
String(person[key as keyof components['schemas']['PersonProperties']])
)}
/>
{/if}
{:else if typeof value === 'boolean'}
<input
type="checkbox"
class="checkbox checkbox-primary"
bind:value={person[key as keyof components['schemas']['PersonProperties']]}
onchange={(e) =>
onChange(
key as keyof components['schemas']['PersonProperties'],
Boolean(person[key as keyof components['schemas']['PersonProperties']])
)}
/>
{:else if typeof value === 'number'}
<input
type="number"
class="input input-bordered input-sm w-full"
bind:value={person[key as keyof components['schemas']['PersonProperties']]}
oninput={(e) =>
onChange(
key as keyof components['schemas']['PersonProperties'],
Number(person[key as keyof components['schemas']['PersonProperties']])
)}
/>
{/if}
{:else}
<p>{value ?? '-'}</p>
{/if}
</label>
</div>
{/if}
{/each}
</div>

View File

@@ -1,205 +0,0 @@
<script lang="ts">
import { onMount } from 'svelte';
import type { components } from '$lib/api/api.gen';
import { v4 as uuidv4 } from 'uuid';
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,
create_invite_code,
invite_code,
phone
} from '$lib/paraglide/messages';
import { callMessageFunction } from '$lib/i18n';
import type { MessageKeys } from '$lib/i18n';
export let person: components['schemas']['PersonProperties'] & {
id?: string;
};
export let editorMode = false;
export let onChange: (field: keyof components['schemas']['PersonProperties'], value: any) => void;
let new_invite_code: string | undefined;
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().split('T')[0];
onChange('born', date.toISOString().split('T')[0]);
}
});
// 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().split('T')[0];
onChange('died', date.toISOString().split('T')[0]);
}
});
// Clean up when component unmounts
return () => picker.destroy();
});
}
});
</script>
<div class="flex flex-col gap-6 md:flex-row">
<div class="flex flex-shrink-0 flex-col items-center gap-2">
<img
src={person.profile_picture || 'https://cdn-icons-png.flaticon.com/512/10628/10628885.png'}
alt={profile_picture()}
class="h-48 w-48 rounded-lg object-cover shadow-md"
/>
{#if false}
<button class="btn btn-neutral btn-soft btn-xs" onclick={() => {}}>
{change_profile_picture()}
</button>
{/if}
</div>
<div class="grid flex-1 grid-cols-1 gap-4 md:grid-cols-2">
<div class="flex flex-col gap-2">
<p>
<strong>{first_name()}: </strong>
{#if editorMode}<input
bind:value={person.first_name}
onchange={() => onChange('first_name', person.first_name)}
class="input input-sm input-bordered w-full"
/>{:else}{person.first_name ?? '-'}{/if}
</p>
<p>
<strong>{last_name()}: </strong>
{#if editorMode}<input
bind:value={person.last_name}
onchange={() => onChange('last_name', person.last_name)}
class="input input-sm input-bordered w-full"
/>{:else}{person.last_name ?? '-'}{/if}
</p>
<p>
<strong>{middle_name()}:</strong>
{#if editorMode}<input
bind:value={person.middle_name}
onchange={() => onChange('middle_name', person.middle_name)}
class="input input-sm input-bordered w-full"
/>{:else}{person.middle_name ?? '-'}{/if}
</p>
<p>
<strong>{born()}: </strong>
{#if editorMode}<input
type="text"
class="pika-single w-full"
id="birth_date"
bind:this={birth_date}
placeholder={person.born}
onchange={() => onChange('born', birth_date.value)}
/>
{:else}{person.born ?? '-'}{/if}
</p>
<p>
<strong>{died()}: </strong>
{#if editorMode}<input
type="text"
class="pika-single w-full"
id="death_date"
placeholder={person.died ?? died()}
bind:this={death_date}
onchange={() => onChange('died', death_date.value)}
/>{:else}{person.died ?? '-'}{/if}
</p>
<p>
<strong>{biological_sex()}: </strong>
{#if editorMode}
<select
name="biological_sex"
class="select select-bordered select-sm w-full"
id="biological_sex"
bind:value={person.biological_sex}
onchange={() => onChange('biological_sex', person.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(person.biological_sex as MessageKeys) ?? '-'}{/if}
</p>
</div>
<div class="flex flex-col gap-2">
<p>
<strong>{email()}:</strong>
{#if editorMode}<input
bind:value={person.email}
onchange={() => onChange('email', person.email)}
class="input input-sm input-bordered w-full"
/>{:else}{person.email ?? '-'}{/if}
</p>
<p>
<strong>{phone()}:</strong>
{#if editorMode}<input
bind:value={person.phone}
onchange={() => onChange('phone', person.phone)}
class="input input-sm input-bordered w-full"
/>{:else}{person.phone ?? '-'}{/if}
<p>
<strong>{mothers_first_name()}:</strong>
{#if editorMode}<input
bind:value={person.mothers_first_name}
onchange={() => onChange('mothers_first_name', person.mothers_first_name)}
class="input input-sm input-bordered w-full"
/>{:else}{person.mothers_first_name ?? '-'}{/if}
</p>
<p>
<strong>{mothers_last_name()}:</strong>
{#if editorMode}<input
bind:value={person.mothers_last_name}
onchange={() => onChange('mothers_last_name', person.mothers_last_name)}
class="input input-sm input-bordered w-full"
/>{:else}{person.mothers_last_name ?? '-'}{/if}
</p>
<p><strong>{id()}: </strong>{' ' + (person.id ?? '-')}</p>
<p><strong>Limit: </strong>{' ' + (person.limit ?? '-')}</p>
{#if editorMode && (person.google_id === undefined || person.google_id === null || person.google_id === '')}
{#if new_invite_code === undefined}
<button
class="btn btn-soft btn-accent btn-m"
onclick={() => {
new_invite_code = uuidv4();
person.invite_code = new_invite_code;
onChange('invite_code', new_invite_code);
}}>{create_invite_code()}</button
>
{:else}
<p>
<strong>{invite_code()}:</strong>{person.invite_code}
</p>
{/if}
{/if}
</div>
</div>
</div>

View File

@@ -1,382 +0,0 @@
<script lang="ts">
import { fade } from 'svelte/transition';
import {
create,
create_person,
close,
born,
mothers_first_name,
mothers_last_name,
last_name,
first_name,
email,
biological_sex,
male,
female,
other,
intersex,
create_relationship_and_person,
child,
sibling,
parent,
spouse,
relation,
relation_type,
notes,
until,
optional_field,
from_time
} from '$lib/paraglide/messages';
import { onMount } from 'svelte';
import type { components } from '$lib/api/api.gen.js';
import { validatePersonRegistration, validateFamilyRelationship } from './validate_fields';
import type { Node, Edge } from '@xyflow/svelte';
let {
closeModal = () => {},
onCreation,
onOnlyPersonCreation = (person: components['schemas']['Person']) => {},
relationshipStartID
}: {
closeModal: () => void;
onCreation: (newNode: Node,newEdges: Edge[]) => void;
onOnlyPersonCreation: (person: components['schemas']['Person']) => void | undefined;
relationshipStartID: number | null;
} = $props();
let birth_date: HTMLInputElement;
let relationship_from_time: HTMLInputElement = $state({} as HTMLInputElement);
let relationship_until: HTMLInputElement = $state({} as HTMLInputElement);
let draftRelationship: (components['schemas']['FamilyRelationship'] & { type: string }) | null =
$state({} as components['schemas']['FamilyRelationship'] & { type: string });
let draftPerson: components['schemas']['PersonRegistration'] = $state(
{} as components['schemas']['PersonRegistration']
);
let error: string | undefined | null = $state();
function onClose() {
closeModal();
}
async function onCreate(event: SubmitEvent) {
event.preventDefault();
error = validatePersonRegistration(draftPerson);
if (error) {
return;
}
if (relationshipStartID !== null) {
if (draftRelationship !== null) {
error = validateFamilyRelationship(draftRelationship);
if (error) {
return;
}
}
let requestBody = {
relationship: draftRelationship,
type: draftRelationship!.type,
person: draftPerson
} as {
person: components['schemas']['PersonRegistration'];
type?: 'child' | 'parent' | 'spouse' | 'sibling';
relationship: components['schemas']['FamilyRelationship'];
};
let response = await fetch(`/api/person_and_relationship/${relationshipStartID}`, {
method: 'POST',
body: JSON.stringify(requestBody),
headers: {
'Content-Type': 'application/json'
}
});
if (!response.ok) {
error = 'Error creating person and relationship';
return;
}
let data = (await response.json()) as {
person?: components['schemas']['Person'];
relationships?: components['schemas']['dbtypeRelationship'][];
};
if (onCreation !== undefined) {
let edges: Array<Edge> = [];
data.relationships?.map((relationship) =>
edges.push({
id: "person"+String(relationship.Id),
source: "person"+String(relationship.StartElementId),
target: "person"+String(relationship.EndElementId),
data: {
...relationship.Props,
type: relationship.Type
}
})
);
let newNode = {
id: "person"+String(data.person?.Id),
data: {
...data.person?.Props,
id: data.person?.Id,
},
position: { x: 0, y: 0 },
type: 'personNode'
} as Node;
onCreation(newNode, edges);
}
} else {
let requestBody = draftPerson as components['schemas']['PersonRegistration'];
let response = await fetch(`/api/person`, {
method: 'POST',
body: JSON.stringify(requestBody),
headers: {
'Content-Type': 'application/json'
}
});
if (!response.ok) {
error = 'Error creating person';
return;
}
if (onOnlyPersonCreation !== undefined) {
onOnlyPersonCreation(await response.json());
}
}
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().split('T')[0];
draftPerson.born = date.toISOString().split('T')[0];
}
});
// Clean up when component unmounts
return () => picker.destroy();
});
}
if (relationship_from_time) {
import('pikaday').then(({ default: Pikaday }) => {
const picker = new Pikaday({
format: 'YYYY-MM-DD',
minDate: new Date(1900, 0, 1),
field: relationship_from_time,
onOpen: function () {
relationship_from_time.placeholder = '';
},
onSelect: function (date) {
relationship_from_time.value = date.toISOString().split('T')[0];
draftRelationship.from = date.toISOString().split('T')[0];
}
});
// Clean up when component unmounts
return () => picker.destroy();
});
}
if (relationship_until) {
import('pikaday').then(({ default: Pikaday }) => {
const picker = new Pikaday({
format: 'YYYY-MM-DD',
minDate: new Date(1900, 0, 1),
field: relationship_until,
onOpen: function () {
relationship_until.placeholder = '';
},
onSelect: function (date) {
relationship_until.value = date.toISOString().split('T')[0];
draftRelationship.to = date.toISOString().split('T')[0];
}
});
// Clean up when component unmounts
return () => picker.destroy();
});
}
});
</script>
<div class="modal modal-open max-h-screen" transition:fade>
<div class="modal-box flex w-full max-w-5xl flex-col items-center justify-center overflow-y-auto">
<div class="flex w-full max-w-5xl items-center justify-between p-2">
<h3 class="text-left text-lg font-bold">{relationshipStartID !== null?create_relationship_and_person():create_person()}</h3>
<div>
<button class="btn btn-error btn-sm" onclick={onClose}>
{close()}
</button>
</div>
</div>
<div class="divider"></div>
<form onsubmit={onCreate} class="w-full">
<fieldset
class="fieldset grid w-full grid-cols-1 items-center gap-y-4 md:grid-cols-2 md:gap-x-6"
>
{#if error}
<div role="alert" class="alert alert-error col-span-full">
<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 relationshipStartID !== null}
<input type="hidden" name="relationshipStartID" value={relationshipStartID} />
<div class="flex flex-col">
<label class="label" for="relationship_type">{relation_type()}</label>
<select
name="relationship_type"
class="select select-bordered"
id="relationship_type"
bind:value={draftRelationship.type}
>
<option value="child">{child()}</option>
<option value="parent">{parent()}</option>
<option value="sibling">{sibling()}</option>
<option value="spouse">{spouse()}</option>
</select>
</div>
<div class="flex flex-col">
<label class="label" for="relationship_notes"
>{relation() + ' ' + notes().toLowerCase()}:</label
>
<textarea
name="relationship_notes"
class="textarea"
bind:value={draftRelationship.notes}
placeholder={notes().toLowerCase() + ' ' + optional_field().toLowerCase()}
></textarea>
</div>
<div class="flex flex-col">
<label class="label" for="relationship_from_time">{from_time()}</label>
<input
type="text"
name="relationship_from_time"
id="relationship_from_time"
class="input input-bordered validator pika-single"
placeholder={optional_field()}
bind:this={relationship_from_time}
/>
</div>
<div class="flex flex-col">
<label class="label" for="relationship_until">{until()}</label>
<input
type="text"
name="relationship_until"
id="relationship_until"
class="input input-bordered validator pika-single"
placeholder={optional_field()}
bind:this={relationship_until}
/>
</div>
<div class="divider margin-t-2 col-span-full"></div>
{/if}
<!-- Inputs -->
<div class="flex flex-col">
<label class="label" for="first_name">{first_name()}</label>
<input
type="text"
name="first_name"
class="input input-bordered"
placeholder={first_name()}
bind:value={draftPerson.first_name}
/>
</div>
<div class="flex flex-col">
<label class="label" for="last_name">{last_name()}</label>
<input
type="text"
name="last_name"
class="input input-bordered"
placeholder={last_name()}
bind:value={draftPerson.last_name}
/>
</div>
<div class="flex flex-col">
<label class="label" for="email">{email()}</label>
<input
type="email"
name="email"
class="input input-bordered validator"
placeholder={email() + ' ' + optional_field().toLowerCase()}
bind:value={draftPerson.email}
/>
</div>
<div class="flex flex-col">
<label class="label" for="birth_date">{born()}</label>
<input
type="text"
name="birth_date"
class="input input-bordered validator pika-single"
placeholder={born()}
bind:this={birth_date}
/>
</div>
<div class="flex flex-col">
<label class="label" for="biological_sex">{biological_sex()}</label>
<select
name="biological_sex"
class="select select-bordered"
id="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>
</div>
<div class="flex flex-col">
<label class="label" for="mothers_last_name">{mothers_last_name()}</label>
<input
type="text"
name="mothers_last_name"
class="input input-bordered"
placeholder={mothers_last_name()}
bind:value={draftPerson.mothers_last_name}
/>
</div>
<div class="flex flex-col">
<label class="label" for="mothers_first_name">{mothers_first_name()}</label>
<input
type="text"
name="mothers_first_name"
class="input input-bordered"
placeholder={mothers_first_name()}
bind:value={draftPerson.mothers_first_name}
/>
</div>
<!-- Submit button spans full width -->
<div class="col-span-full mt-4 flex justify-center">
<button type="submit" class="btn btn-neutral mt-4">{create()}</button>
</div>
</fieldset>
</form>
</div>
</div>

View File

@@ -1,79 +0,0 @@
import type { components } from '$lib/api/api.gen.js';
import { first_name, last_name, missing_field, mothers_first_name } from '$lib/paraglide/messages';
export function validatePersonRegistration(
data: components['schemas']['PersonRegistration']
): string | null {
if (!data.first_name || data.first_name.trim() === '') {
return missing_field({
field: first_name()
});
}
if (!data.last_name || data.last_name.trim() === '') {
return missing_field({
field: last_name()
});
}
if (
data.email !== undefined &&
data.email !== null &&
!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email)
) {
return 'Invalid email format.';
}
if (!data.born || !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 missing_field({
field: mothers_first_name()
});
}
if (!data.mothers_last_name || data.mothers_last_name.trim() === '') {
return missing_field({
field: "Mother's last name"
});
}
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
}

View File

@@ -1,40 +0,0 @@
<script lang="ts">
import type { components } from '$lib/api/api.gen';
export let key: keyof components['schemas']['PersonProperties'];
export let value: any;
export let editorMode = false;
export let onChange: (field: keyof components['schemas']['PersonProperties'], value: any) => void;
let numberField: HTMLInputElement;
let textField: HTMLTextAreaElement;
let checkboxField: HTMLInputElement;
</script>
{#if editorMode}
{#if typeof value === 'boolean'}
<input
type="checkbox"
class="toggle toggle-primary"
checked={value}
bind:this={checkboxField}
oninput={() => onChange(key, checkboxField.value === 'true')}
/>
{:else if typeof value === 'number'}
<input
type="number"
class="input input-bordered input-sm w-full"
{value}
bind:this={numberField}
oninput={() => onChange(key, Number(numberField.value))}
/>
{:else}
<textarea
class="textarea textarea-bordered textarea-sm w-full"
{value}
oninput={() => onChange(key, textField.value)}
bind:this={textField}
></textarea>
{/if}
{:else}
<p class="text-sm text-gray-700">{value ?? '-'}</p>
{/if}

View File

@@ -1,101 +0,0 @@
<script lang="ts">
import { date, description, file, media_title, title, upload } from '$lib/paraglide/messages';
export let closeModal: () => void;
export let onCreation: (newMedia: {
url: string;
name: string;
description: string;
date: string;
}) => void = () => {};
export let mediaType: 'audio' | 'video' | 'photo' = 'photo';
let selectedFile: File | null = null;
let newMedia = {
url: '',
name: '',
description: '',
date: ''
};
// Determine accepted input types based on mediaType
$: acceptTypes =
mediaType === 'audio' ? 'audio/*' : mediaType === 'video' ? 'video/*' : 'image/*';
function handleFileChange(e: Event) {
const input = e.target as HTMLInputElement;
if (input.files && input.files.length > 0) {
selectedFile = input.files[0];
}
}
async function uploadMedia() {
if (!selectedFile) {
alert('Please select a file');
return;
}
// Simulate file upload (replace with actual upload logic)
newMedia.url = URL.createObjectURL(selectedFile);
// Emit event using custom dispatch
const uploadEvent = new CustomEvent('upload', {
detail: { ...newMedia }
});
dispatchEvent(uploadEvent);
// Clean up
selectedFile = null;
newMedia = { url: '', name: '', description: '', date: '' };
onCreation(newMedia);
closeModal();
}
</script>
<div class="modal modal-open z-8">
<div class="modal-box w-full max-w-xl">
<h3 class="text-lg font-bold">{upload() + mediaType}</h3>
<div class="form-control mt-4">
<label for="mfile" class="label">{upload() + ' ' + file()}</label>
<input
id="mfile"
type="file"
accept={acceptTypes}
class="file-input file-input-bordered w-full"
on:change={handleFileChange}
/>
</div>
<div class="form-control mt-4">
<label for="mtitle" class="label">{media_title()}</label>
<input id="mtitle" bind:value={newMedia.name} class="input input-bordered w-full" />
</div>
<div class="form-control mt-4">
<label for="mdesc" class="label">{description()}</label>
<textarea
id="mdesc"
bind:value={newMedia.description}
class="textarea textarea-bordered w-full"
>
</textarea>
</div>
<div class="form-control mt-4">
<label for="mdate" class="label">{date()}</label>
<input
id="mdate"
type="date"
bind:value={newMedia.date}
class="input input-bordered w-full"
/>
</div>
<div class="modal-action">
<button class="btn btn-outline" on:click={closeModal}>Cancel</button>
<button class="btn btn-primary" on:click={uploadMedia}>Upload</button>
</div>
</div>
</div>

View File

@@ -1,91 +0,0 @@
<script lang="ts">
import { onMount } from 'svelte';
import {
add_relationship,
remove,
create_relationship_and_person,
add_administrator
} from '$lib/paraglide/messages';
import type { Edge } from '@xyflow/svelte';
export let edge: Edge;
export let XUserId: string;
export let top: number | undefined;
export let left: number | undefined;
export let right: number | undefined;
export let bottom: number | undefined;
export let onClick: () => void;
export let deleteEdge: () => void;
let contextMenu: HTMLDivElement;
let isAdmin: boolean = false;
onMount(() => {
if (top) {
contextMenu.style.top = `${top}px`;
}
if (left) {
contextMenu.style.left = `${left}px`;
}
if (right) {
contextMenu.style.right = `${right}px`;
}
if (bottom) {
contextMenu.style.bottom = `${bottom}px`;
}
fetch(`/api/admin/${edge.source}/${XUserId}`)
.then((response) => {
if (response.status === 200) {
isAdmin = true;
} else {
isAdmin = false;
}
})
.catch((error) => {
console.error('Error fetching admin status:', error);
});
fetch(`/api/admin/${edge.target}/${XUserId}`)
.then((response) => {
if (response.status === 200) {
isAdmin = true;
}
})
.catch((error) => {
console.error('Error fetching admin status:', error);
});
});
</script>
<div
role="menu"
tabindex="-1"
bind:this={contextMenu}
class="context-menu bg-primary-100 rounded-lg shadow-lg"
onclick={onClick}
onkeydown={(e) => {
if (e.key === 'Esc' || e.key === ' ' || e.key === 'Escape') {
onClick();
}
}}
>
{#if isAdmin}
<button onclick={deleteEdge} class="btn">{remove()}</button>
{/if}
</div>
<style>
.context-menu {
border-style: solid;
box-shadow: 10px 19px 20px rgba(0, 0, 0, 10%);
position: absolute;
z-index: 10;
}
.context-menu button {
border: none;
display: block;
padding: 0.5em;
text-align: left;
width: 100%;
}
</style>

View File

@@ -1,300 +0,0 @@
<script lang="ts">
import {
child,
from_time,
id,
notes,
parent,
relation,
relation_type,
verified,
sibling,
spouse,
until
} from '$lib/paraglide/messages';
import type { Edge } from '@xyflow/svelte';
import ModalButtons from '$lib/relationship/ModalButtons.svelte';
import type { components, operations } from '$lib/api/api.gen';
let {
closeModal,
onCreation = (newEdges: Edge[]) => {},
editorMode = false,
createRelationship = false,
startNode = undefined,
endNode = undefined
} = $props<{
closeModal: () => void;
onCreation?: (newEdges: Edge[]) => void;
editorMode?: boolean;
createRelationship?: boolean;
startNode?: string;
endNode?: string;
}>();
let relationships: components['schemas']['dbtypeRelationship'][] = $state([]);
let newRelationship: components['schemas']['FamilyRelationship'] = $state({
verified: false,
notes: '',
from: '',
to: ''
});
let relationshiptype: 'sibling' | 'child' | 'parent' | 'spouse' | undefined = $state('sibling');
async function getRelationships(startId: string, endId: string) {
if (
startId === undefined ||
endId === undefined ||
startId === '' ||
endId === '' ||
startId === endId
) {
return;
}
const response = await fetch(`/api/relationship/${startId}/${endId}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
});
if (!response.ok) {
console.log('Cannot get relationships, status: ' + response.status);
return;
}
relationships.push((await response.json()) as components['schemas']['dbtypeRelationship']);
}
if (!createRelationship) {
getRelationships(startNode, endNode);
getRelationships(endNode, startNode);
}
async function save() {
for (const r of relationships) {
if (!r.Props) {
console.log('No properties found for relationship', r);
continue;
}
if (r.Props.verified === undefined) {
r.Props.verified = false;
}
console.debug('Saving relationship', r.StartId, r.EndId, r.Props);
const patchBody: components['schemas']['FamilyRelationship'] = r.Props!;
const response = await fetch(`/api/relationship/${r.StartId}/${r.EndId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(patchBody)
});
if (!response.ok) {
console.error(`Failed to save relationship ${r.StartId}${r.EndId}`);
}
if (response.status === 200) {
console.debug(`Relationship ${r.StartId}${r.EndId} saved successfully`);
} else {
console.error(`Failed to save relationship ${r.StartId}${r.EndId}`);
}
}
editorMode = !editorMode;
}
async function createNewRelationship() {
if (relationships.length > 0) {
alert('Relationship already exists');
createRelationship = false;
return;
}
if (!startNode || !endNode) {
alert('Please select nodes');
return;
}
let body: operations['createRelationship']['requestBody']['content']['application/json'] = {
id1: Number(startNode),
id2: Number(endNode),
type: relationshiptype,
relationship: newRelationship
};
console.log('Creating relationship', body);
const response = await fetch(`/api/relationship`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
if (!response.ok) {
console.error('Cannot create relationship' + ', status: ' + response.status + (await response.json()));
return;
}
const created = (await response.json()) as components['schemas']['dbtypeRelationship'][];
console.debug('Relationship created successfully',created);
relationships.push(...created);
let newEdges: Edge[] = [];
for (const r of created) {
newEdges.push({
id: r.ElementId!,
source: r.StartElementId!,
target: r.EndElementId!,
type: 'relationship',
data: { ...r.Props, type: r.Type }
});
}
onCreation(newEdges);
closeModal();
}
</script>
<div class="modal modal-open z-8">
<div class="modal-box w-full max-w-xl gap-4">
<div class="bg-base-100 sticky top-0 z-7">
<ModalButtons
{editorMode}
createMode={createRelationship}
onCreate={createNewRelationship}
onClose={closeModal}
onSave={save}
onToggleEdit={() => {
editorMode = !editorMode;
}}
/>
<div class="divider"></div>
</div>
{#if createRelationship}
<!-- Relationship type selector -->
<div class="form-control mt-4">
<label for="relationshiptype" class="label">{relation_type()}</label>
<select id="relationshiptype" bind:value={relationshiptype} class="select select-bordered">
<option value="sibling">{sibling()}</option>
<option value="child">{child()}</option>
<option value="parent">{parent()}</option>
<option value="spouse">{spouse()}</option>
</select>
</div>
<div class="form-control mt-1">
<p><strong>{id().toLowerCase()}:</strong>{startNode}</p>
</div>
<div class="form-control mt-1">
<label for="endNode" class="label">{relation() + ' ' + id().toLowerCase()}:</label>
<input id="endNode" type="text" bind:value={endNode} class="input input-bordered w-full" />
</div>
{/if}
{#if !createRelationship}
<div class="form-control mt-2">
<p>
<strong>{id()} 1:</strong> {startNode}
</p>
<p>
<strong>{id()} 2:</strong> {endNode}
</p>
</div>
<!-- Editor mode: show all existing relationships -->
{#each relationships as r, index}
<div class="border-base-300 mt-4 rounded border p-4">
<div class="form-control">
<p><strong>{relation_type()}:</strong> {r.Type}</p>
</div>
<div class="form-control mt-2">
{#if editorMode}
<label for={`verified-${index}`} class="label">Verified</label>
<input
id={`verified-${index}`}
type="checkbox"
bind:checked={relationships[index].Props!.verified}
class="checkbox"
/>
{:else}
<p><strong>{verified()}:</strong>{r.Props?.verified}</p>
{/if}
</div>
<div class="form-control mt-2">
{#if editorMode}
<label for={`notes-${index}`} class="label">{notes()}</label>
<textarea
id={`notes-${index}`}
bind:value={relationships[index].Props!.notes}
class="textarea textarea-bordered w-full"
></textarea>
{:else}
<p><strong>{notes()}:</strong> {relationships[index].Props?.notes}</p>
{/if}
</div>
<div class="form-control mt-2">
{#if editorMode}
<label for={`from-${index}`} class="label">{from_time()}</label>
<input
id={`from-${index}`}
type="date"
bind:value={relationships[index].Props!.from}
class="input input-bordered w-full"
/>
{:else}
<p><strong>{from_time()}:</strong> {r.Props?.from}</p>
{/if}
</div>
<div class="form-control mt-2">
{#if editorMode}
<label for={`to-${index}`} class="label">{until()}</label>
<input
id={`to-${index}`}
type="date"
bind:value={relationships[index].Props!.to}
class="input input-bordered w-full"
/>
{:else}
<p><strong>{until()}:</strong> {r.Props?.to}</p>
{/if}
</div>
</div>
{/each}
{:else}
<!-- Creator mode: only one relationship -->
<div class="border-base-300 mt-4 rounded border p-4">
<div class="form-control">
<label for="verified" class="label">Verified</label>
<input
id="verified"
type="checkbox"
bind:checked={newRelationship.verified}
class="checkbox"
/>
</div>
<div class="form-control mt-2">
<label for="notes" class="label">{notes()}</label>
<textarea
id="notes"
bind:value={newRelationship.notes}
class="textarea textarea-bordered w-full"
></textarea>
</div>
<div class="form-control mt-2">
<label for="from" class="label">{from_time()}</label>
<input
id="from"
type="date"
bind:value={newRelationship.from}
class="input input-bordered w-full"
/>
</div>
<div class="form-control mt-2">
<label for="to" class="label">{until()}</label>
<input
id="to"
type="date"
bind:value={newRelationship.to}
class="input input-bordered w-full"
/>
</div>
</div>
{/if}
</div>
</div>

View File

@@ -1,41 +0,0 @@
<script lang="ts">
import {
add_relationship,
back,
close,
edit,
relation,
save
} from '$lib/paraglide/messages';
export let editorMode = false;
export let createMode = false;
export let onClose: () => void;
export let onToggleEdit: () => void;
export let onSave: () => void;
export let onCreate: () => void;
</script>
<div class="flex items-center justify-between p-2">
<h3 class="text-lg font-bold">{relation()}</h3>
<div class="space-x-2">
{#if !createMode}
<button class="btn btn-secondary btn-sm" on:click={onToggleEdit}>
{editorMode ? back() : edit()}
</button>
{/if}
{#if createMode}
<button class="btn btn-accent btn-sm" on:click={onCreate}>
{add_relationship()}
</button>
{:else 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>

View File

@@ -1,12 +0,0 @@
import type { Edge } from '@xyflow/svelte';
export interface RelationshipMenu {
edge: Edge;
XUserId: string;
top: number | undefined;
left: number | undefined;
right: number | undefined;
bottom: number | undefined;
onClick: () => void;
deleteEdge: () => void;
}

View File

@@ -1,8 +0,0 @@
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'
);

View File

@@ -1,89 +0,0 @@
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';
// in seconds
const EXPIRATION_TTL: number = 60 * 60 * 24 * 7;
export async function validateSessionToken(
token: string,
sessions: KVNamespace
): Promise<SessionValidationResult> {
const session: Session | null = await sessions.get(token, { type: 'json' });
if (!session) {
return null;
}
if (Date.now() >= session.expiresAt - 1000 * 60 * 60 * 24 * 15) {
await sessions.put(token, JSON.stringify(session), { expirationTtl: EXPIRATION_TTL });
}
return session;
}
export async function invalidateSession(sessionId: string, sessions: KVNamespace): Promise<void> {
await sessions.delete(sessionId);
}
export async function invalidateUserSessions(userId: number, sessions: KVNamespace): Promise<void> {
const keys = await sessions.list({ prefix: `${userId}:` });
for (const key of keys.keys) {
await sessions.delete(key.name);
}
}
export function setSessionTokenCookie(
event: RequestEvent,
token: string,
expiresAt: EpochTimeStamp
): void {
event.cookies.set('session', token, {
httpOnly: true,
path: '/',
secure: import.meta.env.PROD,
sameSite: 'lax',
expires: new Date(expiresAt)
});
}
export function deleteSessionTokenCookie(event: RequestEvent): void {
event.cookies.set('session', '', {
httpOnly: true,
path: '/',
secure: import.meta.env.PROD,
sameSite: 'lax',
maxAge: 0
});
}
export function generateSessionToken(userId: string): string {
const tokenBytes = new Uint8Array(20);
crypto.getRandomValues(tokenBytes);
const token = encodeBase32(tokenBytes).toLowerCase();
return `${userId}:${encodeHexLowerCase(sha256(new TextEncoder().encode(token)))}`;
}
export async function createSession(
token: string,
userId: number,
sessions: KVNamespace
): Promise<Session> {
const session: Session = {
id: token,
userId,
expiresAt: Date.now() + 1000 * EXPIRATION_TTL
};
await sessions.put(token, JSON.stringify(session), { expirationTtl: EXPIRATION_TTL });
return session;
}
export interface Session {
id: string;
expiresAt: EpochTimeStamp;
userId: number;
}
type SessionValidationResult = Session | null;

View File

@@ -1,54 +0,0 @@
<script lang="ts">
import { managed_profiles } from '$lib/paraglide/messages';
let clicked = $state(false);
let {
open_admin_panel = () => {
console.log('admin panel opened');
}
}: { open_admin_panel: () => void } = $props();
</script>
<div class="dropdown">
<button
tabindex="0"
class={'btn btn-circle swap swap-rotate' + (clicked ? ' swap-active' : '')}
onclick={() => (clicked = !clicked)}
>
<input type="checkbox" />
<!-- hamburger icon -->
<svg
class="swap-off fill-current"
xmlns="http://www.w3.org/2000/svg"
width="32"
height="32"
viewBox="0 0 512 512"
>
<path d="M64,384H448V341.33H64Zm0-106.67H448V234.67H64ZM64,128v42.67H448V128Z" />
</svg>
<!-- close icon -->
<svg
class="swap-on fill-current"
xmlns="http://www.w3.org/2000/svg"
width="32"
height="32"
viewBox="0 0 512 512"
>
<polygon
points="400 145.49 366.51 112 256 222.51 145.49 112 112 145.49 222.51 256 112 366.51 145.49 400 256 289.49 366.51 400 400 366.51 289.49 256 400 145.49"
/>
</svg>
</button>
<ul class="menu menu-sm dropdown-content bg-base-100 rounded-box z-1 mt-3 w-52 p-2 shadow">
<li>
<button
tabindex="0"
class="btn btn-primary"
aria-label="close sidebar"
onclick={open_admin_panel}
>
{managed_profiles()}
</button>
</li>
</ul>
</div>

View File

@@ -1,13 +0,0 @@
<script>
import { managed_profiles } from '$lib/paraglide/messages';
</script>
<div class="drawer-side">
<label for="my-drawer" aria-label="close sidebar" class="drawer-overlay"></label>
<ul class="menu bg-base-200 text-base-content min-h-full w-80 gap-4 p-4 pt-16">
<!-- Sidebar content here -->
<li>
<button class="btn btn-primary" aria-label="close sidebar">{managed_profiles()}</button>
</li>
</ul>
</div>

View File

@@ -1,41 +0,0 @@
import { describe, it, expect, vi } from 'vitest';
import type { Mock } from 'vitest';
import { switchToLanguage } from './switchToLanguage';
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) => '')
}
}));
vi.mock('$app/state', () => ({
page: {
url: {
pathname: '/current-path'
}
}
}));
vi.mock('$app/navigation', () => ({
goto: vi.fn()
}));
describe('switchToLanguage', () => {
it('should switch to the new language', () => {
const newLanguage = 'en';
const canonicalPath = '/canonical-path';
const localisedPath = '/en/canonical-path';
(i18n.route as Mock).mockReturnValue(canonicalPath);
(i18n.resolveRoute as Mock).mockReturnValue(localisedPath);
switchToLanguage(newLanguage);
expect(i18n.route).toHaveBeenCalledWith('/current-path');
expect(i18n.resolveRoute).toHaveBeenCalledWith(canonicalPath, newLanguage);
expect(goto).toHaveBeenCalledWith(localisedPath);
});
});

View File

@@ -1,10 +0,0 @@
import type { AvailableLanguageTag } from '$lib/paraglide/runtime';
import { i18n } from '$lib/i18n';
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);
}

View File

@@ -1,19 +0,0 @@
export function tailwindClassToPixels(className: string): number | null {
const remSize = getRemInPixels(); // <-- real rem size at runtime
const regex = /^(w|h)-(\d+)$/;
const match = className.match(regex);
if (!match) return null;
const value = parseInt(match[2], 10);
return (value / 4) * remSize;
}
export function getRemInPixels(): number {
try {
const fontSize = getComputedStyle(document.documentElement).fontSize;
return parseFloat(fontSize);
} catch (e) {
return 16; // Default to 16px if unable to get computed style
}
}

View File

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

View File

@@ -1,17 +0,0 @@
<script lang="ts">
import '../app.css';
import { i18n } from '$lib/i18n';
import { ParaglideJS } from '@inlang/paraglide-sveltekit';
let { children } = $props();
import ThemeButton from '$lib/ThemeSelect.svelte';
import Logout from '$lib/Logout.svelte';
import { page } from '$app/state';
</script>
<ParaglideJS {i18n}>
{@render children()}
<div class="absolute top-2 right-2 flex flex-row items-center gap-2">
<ThemeButton />
<Logout show={!page.url.pathname.includes('login')} />
</div>
</ParaglideJS>

View File

@@ -1,31 +0,0 @@
import { redirect } from '@sveltejs/kit';
import { parseFamilyTree } from '$lib/graph/parse_family_tree';
import type { components } from '$lib/api/api.gen';
import type { RequestEvent } from './$types';
import { browser } from '$app/environment';
import type { Layout } from '$lib/graph/model';
export async function load(event: RequestEvent) {
if (event.locals.session === null /*|| event.locals.familytree === nul*/) {
return redirect(302, '/login');
}
//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 {};
}
const response = await event.fetch('/api/family_tree?with_out_spouse=false', {
method: 'GET'
});
if (response.status !== 200) {
console.error(await response.text());
}
const data = (await response.json()) as components['schemas']['FamilyTree'];
const layout = parseFamilyTree(data) as Layout & { id: string };
layout.id = event.locals.session.userId;
return layout;
}

View File

@@ -1,382 +0,0 @@
<script lang="ts">
import CreateRelationship from '$lib/relationship/Modal.svelte';
import { onMount } from 'svelte';
import { nodeTypes, edgeTypes } from '$lib/graph/model';
import { title, family_tree, select } from '$lib/paraglide/messages.js';
import type { RelationshipMenu } from '$lib/relationship/model.ts';
import AdminMenu from '$lib/admin/Modal.svelte';
import { SvelteFlowProvider, SvelteFlow, Controls, MiniMap } from '@xyflow/svelte';
import '@xyflow/svelte/dist/style.css';
import type { OnConnectEnd, Node, Edge, NodeEventWithPointer } from '@xyflow/svelte';
import PersonModal from '$lib/profile/Modal.svelte';
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';
import { FamilyTree } from '$lib/graph/layout';
import { tailwindClassToPixels } from '$lib/tailwindSizeToPx';
import type { Layout } from '$lib/graph/model';
import HamburgerIcon from '$lib/sidebar/hamburgerIcon.svelte';
let { data }: { data: Layout & { id: string } } = $props();
let selectedPerson: components['schemas']['PersonProperties'] & { id: string | undefined } =
$state({
id: undefined
});
let selectedRelationship: Edge | undefined = $state(undefined);
let openPersonPanel = $state(false);
let openPersonMenu: NodeMenu | undefined = $state(undefined);
let with_out_spouse = $state(false);
let createRelationship = $state(false);
let adminMenu = $state(false);
let familyTreeDAG = new FamilyTree();
let layout = familyTreeDAG.getLayoutedElements(
data.Nodes,
data.Edges,
tailwindClassToPixels('w-40') || 160,
tailwindClassToPixels('h-40') || 160,
'TB'
);
let nodes = $state.raw<Node[]>([] as Node[]);
let edges = $state.raw<Edge[]>([] as Edge[]);
let relationshipStart: number | null = $state(null);
let relationshipMenu = $state(undefined as RelationshipMenu | undefined);
let createPerson = $state(false);
let clientWidth: number | undefined = $state();
let clientHeight: number | undefined = $state();
let removePersonFromGraph = (id: any) => {
nodes = nodes.filter((n) => n.data.id !== id);
edges = edges.filter((e) => e.source !== 'person' + id && e.target !== 'person' + id);
};
let delete_profile = (id: any) => {
fetch('/api/person/' + id, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
}
})
.then((response) => {
if (response.ok) {
removePersonFromGraph(id);
} else {
alert('Error deleting person');
}
})
.catch((error) => {
console.error('Error:', error);
});
};
const handleContextMenu: NodeEventWithPointer<MouseEvent> = ({ event, node }) => {
event.preventDefault();
if (clientHeight === undefined || clientWidth === undefined) {
clientHeight = window.innerHeight;
clientWidth = window.innerWidth;
}
if (openPersonMenu !== undefined) {
openPersonMenu.onClick();
}
openPersonMenu = {
XUserId: data.id,
onClick: () => {
openPersonMenu = undefined;
},
deleteNode: () => {
if (Number(data.id) === Number(node.data.id)) {
relationshipStart = null;
openPersonMenu = undefined;
return;
}
delete_profile(node.data.id);
openPersonMenu = undefined;
},
createRelationshipAndNode: () => {
relationshipStart = Number(node.data.id);
createPerson = true;
openPersonMenu = undefined;
},
addRelationship: () => {
relationshipStart = Number(node.data.id);
createRelationship = true;
selectedRelationship = {
id: 'relationship' + node.data.id,
source: String(relationshipStart),
target: String(node.data.id)
};
openPersonMenu = undefined;
},
addAdmin: () => {
relationshipStart = Number(node.data.id);
openPersonMenu = undefined;
},
addRecipe: () => {
relationshipStart = Number(node.data.id);
openPersonMenu = undefined;
},
id: String(node.data.id),
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
};
};
function onCreation(newNodes: Array<Node> | null, newEdges: Array<Edge> | null): void {
if (newNodes !== null) {
nodes = [...nodes, ...newNodes];
}
if (newEdges !== null) {
edges = [...edges, ...newEdges];
}
let newLayout = familyTreeDAG.getLayoutedElements(
nodes,
edges,
tailwindClassToPixels('w-40') || 160,
tailwindClassToPixels('h-40') || 160,
'TB'
);
edges = [...newLayout.Edges];
nodes = [...newLayout.Nodes];
}
let handleNodeClickFunc = handleNodeClick(
(
person: components['schemas']['PersonProperties'] & {
id: number | undefined;
}
) => {
selectedPerson = { ...person, id: String(person.id) };
openPersonPanel = true;
fetch('/api/person/' + person.id, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
})
.then((response) => {
if (response.ok) {
return response.json() as Promise<components['schemas']['Person']>;
} else {
alert('Error fetching person data');
return null;
}
})
.then((data) => {
if (data) {
selectedPerson = data.Props as components['schemas']['PersonProperties'] & {
id: string | undefined;
};
selectedPerson.id = String(person.id);
}
console.debug('Fetched person data:', data);
});
}
);
let handlePaneClick = ({ event }: { event: MouseEvent }) => {
openPersonPanel = false;
openPersonMenu = undefined;
};
const handleConnectEnd: OnConnectEnd = (event, connectionState) => {
event.preventDefault();
const sourceNodeId = connectionState.fromNode?.data.id;
if (sourceNodeId === undefined) return;
relationshipStart = Number(sourceNodeId);
if (connectionState.isValid) {
createRelationship = true;
selectedRelationship = {
id: 'relationship' + connectionState.toNode?.data.id,
source: String(relationshipStart),
target: String(connectionState.toNode?.data.id)
};
return;
}
createPerson = true;
};
onMount(() => {
nodes = [...layout.Nodes];
edges = [...layout.Edges];
});
</script>
<svelte:head>
<title>{title({ page: family_tree() })}</title>
</svelte:head>
<div style="height:100vh;" class="!bg-base-200 flex flex-col">
<SvelteFlowProvider>
<SvelteFlow
bind:nodes
bind:edges
onconnectend={handleConnectEnd}
onedgeclick={({ edge, event }: { edge: Edge; event: MouseEvent }) => {
selectedRelationship = edge;
selectedRelationship.source = String(edge.source.replace('person', ''));
selectedRelationship.target = String(edge.target.replace('person', ''));
}}
onnodeclick={handleNodeClickFunc}
onnodecontextmenu={handleContextMenu}
onedgecontextmenu={({ edge, event }: { edge: Edge; event: MouseEvent }) => {
selectedRelationship = edge;
selectedRelationship.source = String(edge.source.replace('person', ''));
selectedRelationship.target = String(edge.target.replace('person', ''));
if (clientHeight === undefined || clientWidth === undefined) {
clientHeight = window.innerHeight;
clientWidth = window.innerWidth;
}
relationshipMenu = {
XUserId: data.id,
edge: selectedRelationship,
onClick: () => {
relationshipMenu = undefined;
},
deleteEdge: () => {
edges = edges.filter((e) => e.id !== edge.id);
relationshipMenu = 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
};
}}
onpaneclick={handlePaneClick}
class="!bg-base-200"
{nodeTypes}
{edgeTypes}
fitView={true}
>
<MiniMap class="!bg-base-300" />
<Controls class="!bg-base-300" />
{#if openPersonPanel}
<PersonModal
person={selectedPerson}
closeModal={() => {
openPersonPanel = false;
}}
/>
{/if}
{#if createPerson}
<CreatePerson
onOnlyPersonCreation={() => {
createPerson = false;
}}
onCreation={(node, edges) => {
onCreation([node], edges);
createPerson = false;
}}
closeModal={() => {
createPerson = false;
}}
relationshipStartID={relationshipStart}
></CreatePerson>
{/if}
{#if selectedRelationship}
<CreateRelationship
{createRelationship}
onCreation={(newEdges: Array<Edge>) => {
onCreation(null, newEdges);
createRelationship = false;
}}
closeModal={() => {
createRelationship = false;
selectedRelationship = undefined;
relationshipStart = null;
layout = familyTreeDAG.getLayoutedElements(
nodes,
edges,
tailwindClassToPixels('w-40') || 160,
tailwindClassToPixels('h-40') || 160,
'TB'
);
edges = [...layout.Edges];
nodes = [...layout.Nodes];
}}
startNode={String(selectedRelationship.source)}
endNode={String(selectedRelationship.target)}
/>
{/if}
{#if openPersonMenu !== undefined}
<PersonMenu {...openPersonMenu!} />
{/if}
{#if adminMenu}
<AdminMenu
createProfile={() => {
createPerson = true;
relationshipStart = null;
}}
createRelationshipAndProfile={(id: number) => {
createPerson = true;
relationshipStart = id;
}}
addRelationship={(id: number) => {
createRelationship = true;
selectedRelationship = {
id: 'relationship' + id,
source: String(id),
target: String(id)
};
}}
closeModal={() => {
adminMenu = false;
}}
editProfile={(id: number) => {
selectedPerson = { id: String(id) };
fetch('/api/person/' + id, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
})
.then((response) => {
if (response.ok) {
return response.json() as Promise<components['schemas']['Person']>;
} else {
alert('Error fetching person data');
return null;
}
})
.then((data) => {
if (data) {
selectedPerson = data.Props as components['schemas']['PersonProperties'] & {
id: string | undefined;
};
selectedPerson.id = String(id);
openPersonPanel = true;
} else {
alert('Error fetching person data');
}
});
}}
removePersonFromGraph={removePersonFromGraph}
/>
{/if}
</SvelteFlow>
</SvelteFlowProvider>
</div>
<div class="absolute left-2 top-2 flex flex-row items-center gap-2">
<HamburgerIcon
open_admin_panel={() => {
adminMenu = !adminMenu;
}}
/>
</div>

View File

@@ -1,26 +0,0 @@
import { client } from '$lib/api/client';
import { redirect } from '@sveltejs/kit';
import type { RequestEvent } from './$types';
export async function GET(event: RequestEvent): Promise<Response> {
if (event.locals.session === null) {
return redirect(302, '/login');
}
const response = await client.GET('/admin/{id1}', {
params: {
path: { id1: Number(event.params.ID1) },
header: { 'X-User-ID': event.locals.session.userId }
}
});
if (response.response.ok) {
return new Response(null, {
status: response.response.status
});
} else {
return new Response(JSON.stringify(response.error), {
status: response.response.status
});
}
}

View File

@@ -1,72 +0,0 @@
import { client } from '$lib/api/client';
import { redirect } from '@sveltejs/kit';
import type { RequestEvent } from './$types';
export async function GET(event: RequestEvent): Promise<Response> {
if (event.locals.session === null) {
return redirect(302, '/login');
}
const response = await client.GET('/admin/{id1}/{id2}', {
params: {
path: { id1: Number(event.params.ID1), id2: Number(event.params.ID2) },
header: { 'X-User-ID': event.locals.session.userId }
}
});
if (response.response.ok) {
return new Response(JSON.stringify(response.data), {
status: response.response.status
});
} else {
return new Response(JSON.stringify(response.error), {
status: response.response.status
});
}
}
export async function POST(event: RequestEvent): Promise<Response> {
if (event.locals.session === null) {
return redirect(302, '/login');
}
const response = await client.POST('/admin/{id1}/{id2}', {
params: {
path: { id1: Number(event.params.ID1), id2: Number(event.params.ID2) },
header: { 'X-User-ID': event.locals.session.userId }
}
});
if (response.response.ok) {
return new Response(JSON.stringify(response.data), {
status: response.response.status
});
} else {
return new Response(JSON.stringify(response.error), {
status: response.response.status
});
}
}
export async function DELETE(event: RequestEvent): Promise<Response> {
if (event.locals.session === null) {
return redirect(302, '/login');
}
const response = await client.DELETE('/admin/{id1}/{id2}', {
params: {
path: { id1: Number(event.params.ID1), id2: Number(event.params.ID2) },
header: { 'X-User-ID': event.locals.session.userId }
}
});
if (response.response.ok) {
return new Response(null, {
status: response.response.status
});
} else {
return new Response(JSON.stringify(response.error), {
status: response.response.status
});
}
}

View File

@@ -1,99 +0,0 @@
import { redirect } from '@sveltejs/kit';
import { client } from '$lib/api/client';
import type { RequestEvent } from './$types';
import type { components } from '$lib/api/api.gen';
export async function POST(event: RequestEvent): Promise<Response> {
if (event.locals.session === null) {
return redirect(302, '/login');
}
let message = (await event.request.json()) as components['schemas']['Message'];
message.edited = null;
message.sent_at = new Date(Date.now()).toISOString();
const response = await client.POST('/comment/{id}', {
params: {
path: { id: Number(event.params.ID) },
header: { 'X-User-ID': event.locals.session.userId }
},
body: message
});
return new Response(await response.response.json(), {
status: response.response.status
});
}
export async function GET(event: RequestEvent): Promise<Response> {
if (event.locals.session === null) {
return redirect(302, '/login');
}
const response = await client.GET('/comment/{id}', {
params: {
path: { id: Number(event.params.ID) },
header: { 'X-User-ID': event.locals.session.userId }
}
});
if (response.response.ok) {
return new Response(JSON.stringify(response.data), {
status: response.response.status
});
} else {
return new Response(JSON.stringify(response.error), {
status: response.response.status
});
}
}
export async function DELETE(event: RequestEvent): Promise<Response> {
if (event.locals.session === null) {
return redirect(302, '/login');
}
const response = await client.DELETE('/comment/{id}', {
params: {
path: { id: Number(event.params.ID) },
header: { 'X-User-ID': event.locals.session.userId }
}
});
if (response.response.ok) {
return new Response(null, {
status: response.response.status
});
} else {
return new Response(JSON.stringify(response.error), {
status: response.response.status
});
}
}
export async function PATCH(event: RequestEvent): Promise<Response> {
if (event.locals.session === null) {
return redirect(302, '/login');
}
let message = (await event.request.json()) as components['schemas']['Message'];
message.edited = new Date(Date.now()).toISOString();
const response = await client.PATCH('/comment/{id}', {
params: {
path: { id: Number(event.params.ID) },
header: { 'X-User-ID': event.locals.session.userId }
},
body: message
});
if (response.response.ok) {
return new Response(JSON.stringify(response.data), {
status: response.response.status
});
} else {
return new Response(JSON.stringify(response.error), {
status: response.response.status
});
}
}

View File

@@ -1,57 +0,0 @@
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';
export async function GET(event: RequestEvent): Promise<Response> {
if (event.locals.session === null) {
return redirect(302, '/login');
}
const response = await client.GET(
event.url.searchParams.get('with_out_spouse') === 'false'
? '/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 graphToReturn: components['schemas']['FamilyTree'] = {
people: [],
relationships: response.data.relationships
};
for (const person of response.data.people) {
let newPerson = person;
if (newPerson.profile_picture !== null && newPerson.profile_picture !== undefined) {
}
if (graphToReturn.people !== undefined) {
graphToReturn.people.push(newPerson);
}
}
return new Response(JSON.stringify(graphToReturn), {
status: 200
});
}

View File

@@ -1,25 +0,0 @@
import { client } from '$lib/api/client';
import { redirect } from '@sveltejs/kit';
import type { RequestEvent } from './$types';
export async function GET(event: RequestEvent): Promise<Response> {
if (event.locals.session === null) {
return redirect(302, '/login');
}
const response = await client.GET('/managed_profiles', {
params: {
header: { 'X-User-ID': event.locals.session.userId }
}
});
if (response.response.ok) {
return new Response(JSON.stringify(response.data), {
status: response.response.status
});
} else {
return new Response(JSON.stringify(response.error), {
status: response.response.status
});
}
}

View File

@@ -1,27 +0,0 @@
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';
export async function POST(event: RequestEvent): Promise<Response> {
if (event.locals.session === null) {
return redirect(302, '/login');
}
const response = await client.POST('/person', {
params: {
header: { 'X-User-ID': event.locals.session.userId }
},
body: (await event.request.json()) as components['schemas']['PersonRegistration']
});
if (response.response.ok) {
return new Response(JSON.stringify(response.data), {
status: response.response.status
});
} else {
return new Response(JSON.stringify(response.error), {
status: response.response.status
});
}
}

View File

@@ -1,74 +0,0 @@
import { redirect } from '@sveltejs/kit';
import { client } from '$lib/api/client';
import type { RequestEvent } from './$types';
import type { components } from '$lib/api/api.gen';
export async function GET(event: RequestEvent): Promise<Response> {
if (event.locals.session === null) {
return redirect(302, '/login');
}
const response = await client.GET('/person/{id}', {
params: {
path: { id: Number(event.params.ID) },
header: { 'X-User-ID': event.locals.session.userId }
}
});
if (response.response.ok) {
return new Response(JSON.stringify(response.data), {
status: response.response.status
});
} else {
return new Response(JSON.stringify(response.error), {
status: response.response.status
});
}
}
export async function DELETE(event: RequestEvent): Promise<Response> {
if (event.locals.session === null) {
return redirect(302, '/login');
}
const response = await client.DELETE('/person/{id}', {
params: {
path: { id: Number(event.params.ID) },
header: { 'X-User-ID': event.locals.session.userId }
}
});
if (response.response.ok) {
return new Response(null, {
status: response.response.status
});
} else {
return new Response(JSON.stringify(response.error), {
status: response.response.status
});
}
}
export async function PATCH(event: RequestEvent): Promise<Response> {
if (event.locals.session === null) {
return redirect(302, '/login');
}
const response = await client.PATCH('/person/{id}', {
params: {
path: { id: Number(event.params.ID) },
header: { 'X-User-ID': event.locals.session.userId }
},
body: (await event.request.json()) as components['schemas']['PersonProperties']
});
if (response.response.ok) {
return new Response(JSON.stringify(response.data), {
status: response.response.status
});
} else {
return new Response(JSON.stringify(response.error), {
status: response.response.status
});
}
}

View File

@@ -1,26 +0,0 @@
import { error, redirect } from '@sveltejs/kit';
import { client } from '$lib/api/client';
import type { RequestEvent } from './$types';
export async function DELETE(event: RequestEvent): Promise<Response> {
if (event.locals.session === null) {
return redirect(302, '/login');
}
const response = await client.DELETE('/person/{id}/hard-delete', {
params: {
path: { id: Number(event.params.ID) },
header: { 'X-User-ID': event.locals.session.userId }
}
});
if (response.response.ok) {
return new Response(null, {
status: response.response.status
});
} else {
return new Response(JSON.stringify(response.error), {
status: response.response.status
});
}
}

View File

@@ -1,32 +0,0 @@
import { redirect } from '@sveltejs/kit';
import { client } from '$lib/api/client';
import type { RequestEvent } from './$types';
import type { components } from '$lib/api/api.gen';
export async function POST(event: RequestEvent): Promise<Response> {
if (event.locals.session === null) {
return redirect(302, '/login');
}
const response = await client.POST('/person_and_relationship/{id}', {
params: {
path: { id: Number(event.params.ID) },
header: { 'X-User-ID': event.locals.session.userId }
},
body: (await event.request.json()) as {
person: components['schemas']['PersonRegistration'];
type?: 'child' | 'parent' | 'spouse' | 'sibling';
relationship: components['schemas']['FamilyRelationship'];
}
});
if (response.response.ok && response.response.status === 200) {
return new Response(JSON.stringify(response.data), {
status: response.response.status
});
} else {
return new Response(JSON.stringify(response.error), {
status: response.response.status
});
}
}

View File

@@ -1,32 +0,0 @@
import { redirect } from '@sveltejs/kit';
import { client } from '$lib/api/client';
import type { RequestEvent } from './$types';
import type { components } from '$lib/api/api.gen';
export async function POST(event: RequestEvent): Promise<Response> {
if (event.locals.session === null) {
return redirect(302, '/login');
}
const response = await client.POST('/relationship', {
params: {
header: { 'X-User-ID': event.locals.session.userId }
},
body: (await event.request.json()) as {
id1?: number;
id2?: number;
type?: 'child' | 'parent' | 'spouse' | 'sibling';
relationship?: components['schemas']['FamilyRelationship'];
}
});
if (response.response.ok) {
return new Response(JSON.stringify(response.data), {
status: response.response.status
});
} else {
return new Response(JSON.stringify(response.error), {
status: response.response.status
});
}
}

View File

@@ -1,76 +0,0 @@
import { redirect } from '@sveltejs/kit';
import { client } from '$lib/api/client';
import type { RequestEvent } from './$types';
import type { components } from '$lib/api/api.gen';
export async function GET(event: RequestEvent): Promise<Response> {
if (event.locals.session === null) {
return redirect(302, '/login');
}
const response = await client.GET('/relationship/{id1}/{id2}', {
params: {
path: { id1: Number(event.params.ID1), id2: Number(event.params.ID2) },
header: { 'X-User-ID': event.locals.session.userId }
}
});
if (response.response.ok) {
return new Response(JSON.stringify(response.data), {
status: response.response.status
});
} else {
return new Response(JSON.stringify(response.error), {
status: response.response.status
});
}
}
export async function PATCH(event: RequestEvent): Promise<Response> {
if (event.locals.session === null) {
return redirect(302, '/login');
}
const response = await client.PATCH('/relationship/{id1}/{id2}', {
params: {
path: { id1: Number(event.params.ID1), id2: Number(event.params.ID2) },
header: { 'X-User-ID': event.locals.session.userId }
},
body: {
relationship: (await event.request.json()) as components['schemas']['FamilyRelationship']
}
});
if (response.response.ok) {
return new Response(JSON.stringify(response.data), {
status: response.response.status
});
} else {
return new Response(JSON.stringify(response.error), {
status: response.response.status
});
}
}
export async function DELETE(event: RequestEvent): Promise<Response> {
if (event.locals.session === null) {
return redirect(302, '/login');
}
const response = await client.DELETE('/relationship/{id1}/{id2}', {
params: {
path: { id1: Number(event.params.ID1), id2: Number(event.params.ID2) },
header: { 'X-User-ID': event.locals.session.userId }
}
});
if (response.response.ok) {
return new Response(null, {
status: response.response.status
});
} else {
return new Response(JSON.stringify(response.error), {
status: response.response.status
});
}
}

View File

@@ -1,11 +0,0 @@
import { redirect } from '@sveltejs/kit';
import type { RequestEvent } from './$types';
export async function load(event: RequestEvent) {
if (event.locals.session !== null) {
return redirect(302, '/');
}
return {};
}

View File

@@ -1,42 +0,0 @@
<script lang="ts">
import { sign_in, site_intro, title, family_tree, welcome } from '$lib/paraglide/messages.js';
import FamilyTree from './highresolution_icon_no_background_croped.png';
</script>
<svelte:head>
<title>{title({ page: sign_in() })}</title>
</svelte:head>
<div class="hero bg-base-200 min-h-screen">
<div class="hero-content flex flex-col items-center justify-center text-center">
<div class="max-w-lg">
<figure class="top-margin-10 px-10 pt-10">
<img src={FamilyTree} alt={family_tree()} class="rounded-xl" />
</figure>
<h1 class="text-3xl font-bold">{welcome()}</h1>
<p class="py-6">
{site_intro()}
</p>
<a href="/login/google" class="btn rounded-full border-[#e5e5e5] bg-white text-black">
<!-- Google -->
<svg
aria-label="Google logo"
width="16"
height="16"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512"
><g
><path d="m0 0H512V512H0" fill="none"></path><path
fill="#34a853"
d="M153 292c30 82 118 95 171 60h62v48A192 192 0 0190 341"
></path><path fill="#4285f4" d="m386 400a140 175 0 0053-179H260v74h102q-7 37-38 57"
></path><path fill="#fbbc02" d="m90 341a208 200 0 010-171l63 49q-12 37 0 73"></path>
<path fill="#ea4335" d="m153 219c22-69 116-109 179-50l55-54c-78-75-230-72-297 55"
></path></g
></svg
>
Google {sign_in()}
</a>
</div>
</div>
</div>

View File

@@ -1,32 +0,0 @@
import { google } from '$lib/server/oauth';
import { generateCodeVerifier, generateState } from 'arctic';
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']);
event.cookies.set('google_oauth_state', state, {
httpOnly: true,
maxAge: 60 * 10,
secure: import.meta.env.PROD,
path: '/',
sameSite: 'lax'
});
event.cookies.set('google_code_verifier', codeVerifier, {
httpOnly: true,
maxAge: 60 * 10,
secure: import.meta.env.PROD,
path: '/',
sameSite: 'lax'
});
return new Response(null, {
status: 302,
headers: {
Location: url.toString()
}
});
}

View File

@@ -1,358 +0,0 @@
import { google } from '$lib/server/oauth';
import { ObjectParser } from '@pilcrowjs/object-parser';
import { browser } from '$app/environment';
import { client } from '$lib/api/client';
import { type components } from '$lib/api/api.gen';
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,
biological_sex
} from '$lib/paraglide/messages';
import type { PageServerLoad, Actions, RequestEvent } from './$types';
import type { OAuth2Tokens } from 'arctic';
import type { PersonProperties } from '$lib/model';
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 {};
}
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.' });
}
if (storedState !== state) {
return error(400, { message: 'Please restart the process.' });
}
let tokens: OAuth2Tokens;
try {
tokens = await google.validateAuthorizationCode(code, codeVerifier);
} catch (e) {
let already_loaded = event.cookies.get('already_loaded') ?? null;
if (already_loaded !== null) {
event.cookies.delete('already_loaded', {
path: '/login/google/callback',
sameSite: 'lax',
httpOnly: true,
maxAge: 0,
secure: import.meta.env.PROD
});
return {};
}
return error(400, { message: 'Failed to validate authorization code with ' + e });
}
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 response = await client.GET('/person/google/{google_id}', {
params: {
path: { google_id: sub }
}
});
if (response.response.status === 200) {
if (response.data?.Id !== undefined) {
if (!event.platform || !event.platform.env || !event.platform.env.GH_SESSIONS) {
return error(500, {
message: 'Server configuration error. GH_SESSIONS KeyValue store missing'
});
}
const sessionToken = generateSessionToken(String(response.data.Id));
const session = await createSession(
sessionToken,
response.data.Id,
event.platform.env.GH_SESSIONS
);
if (session === null) {
return error(500, {
message: 'Failed to create session'
});
}
event.cookies.delete('already_loaded', {
path: '/login/google/callback',
sameSite: 'lax',
httpOnly: true,
maxAge: 0,
secure: import.meta.env.PROD
});
setSessionTokenCookie(event, sessionToken, session.expiresAt);
return redirect(302, '/');
}
}
let personP: PersonProperties = {
google_id: sub,
first_name: first_name,
last_name: family_name,
email: email
};
event.cookies.set('already_loaded', 'true', {
path: '/login/google/callback',
sameSite: 'lax',
httpOnly: true,
maxAge: 60 * 10,
secure: import.meta.env.PROD
});
return {
props: personP
};
};
export const actions: Actions = {
register: register
};
async function register(event: RequestEvent) {
if (browser) {
return {};
}
const data = await event.request.formData();
let parsedData: components['schemas']['PersonRegistration'] = {
first_name: data.get('first_name'),
last_name: data.get('last_name'),
email: data.get('email'),
biological_sex: data.get('biological_sex'),
born: data.get('birth_date'),
mothers_first_name: data.get('mothers_first_name'),
mothers_last_name: data.get('mothers_last_name'),
google_id: data.get('google_id'),
limit: StorageLimit
} as components['schemas']['PersonRegistration'];
if (!event.platform || !event.platform.env || !event.platform.env.GH_SESSIONS) {
return fail(500, {
data: parsedData,
message: 'Server configuration error. GH_SESSIONS KeyValue store missing'
});
}
const first_name_f = data.get('first_name');
if (first_name_f === null || first_name_f === '') {
return fail(400, {
data: parsedData,
message: missing_field({
field: first_name()
})
});
}
const google_id = data.get('google_id');
if (google_id === null || google_id === '') {
return fail(400, {
data: parsedData,
message: missing_field({
field: 'google_id'
})
});
}
const last_name_f = data.get('last_name');
if (last_name_f === null || last_name_f === '') {
return fail(400, {
data: parsedData,
message: missing_field({
field: last_name()
})
});
}
const email = data.get('email');
if (email === null || email === '') {
return fail(400, {
message: missing_field({
field: 'Email'
})
});
}
let birth_date = data.get('birth_date');
if (birth_date === null || birth_date === '') {
return fail(400, {
message: missing_field({
field: born()
})
});
} else {
birth_date = birth_date.toString();
}
const bbiological_sex = data.get('biological_sex');
if (bbiological_sex === null || bbiological_sex === '') {
return fail(400, {
data: parsedData,
message: missing_field({
field: biological_sex()
})
});
} else if (
!['male', 'female', 'intersex', 'unknown', 'other'].includes(bbiological_sex.toString())
) {
return fail(400, {
data: parsedData,
message: `Invalid value for biological_sex. Must be one of "male", "female", "intersex", "unknown", or "other".`
});
}
const mothers_first_name_f = data.get('mothers_first_name');
if (mothers_first_name_f === null || mothers_first_name_f === '') {
return fail(400, {
data: parsedData,
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, {
data: parsedData,
message: missing_field({
field: mothers_last_name()
})
});
}
const parsed_date = new Date(birth_date as string);
let personP: components['schemas']['PersonRegistration'] = {
first_name: first_name_f as string,
last_name: last_name_f as string,
email: email as string,
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'],
limit: StorageLimit
};
let invite_code = data.get('invite_code');
if (invite_code !== null) {
invite_code = invite_code.toString();
} else {
invite_code = '';
}
let responseData:
| {
Id?: number;
ElementId?: string;
Labels?: string[];
Props?: components['schemas']['PersonProperties'];
}
| undefined = undefined;
if (!(invite_code.length > 0)) {
let response = await client.POST('/person/google/{google_id}', {
params: {
data: parsedData,
path: { google_id: google_id.toString() }
},
body: personP
});
if (response.response.status !== 200) {
return fail(400, {
data: parsedData,
message: failed_to_create_user() + response.error?.msg
});
}
if (response.data === undefined) {
return fail(400, {
data: parsedData,
message: failed_to_create_user() + 'No user data returned'
});
}
responseData = response.data;
} else {
let response = await client.PATCH('/person/google/{google_id}', {
params: {
path: { google_id: google_id.toString() }
},
body: {
invite_code: invite_code,
person: personP
}
});
if (response.response.status !== 200) {
return fail(400, {
data: parsedData,
message: failed_to_create_user() + response.error?.msg
});
}
if (response.data === undefined) {
return fail(400, {
data: parsedData,
message: failed_to_create_user() + 'No user data returned'
});
}
responseData = response.data;
}
if (responseData.Id === undefined) {
return fail(400, {
data: parsedData,
message: failed_to_create_user() + 'No user ID returned'
});
}
if (!event.platform) {
return fail(500, {
data: parsedData,
message: 'Server configuration error. GH_SESSIONS KeyValue store missing'
});
}
const sessionToken = generateSessionToken(String(responseData.Id));
const session = await createSession(
sessionToken,
responseData.Id,
event.platform.env.GH_SESSIONS
);
if (session === null) {
return fail(500, {
data: parsedData,
message: failed_to_create_user() + 'Failed to create session'
});
}
setSessionTokenCookie(event, sessionToken, session.expiresAt);
event.cookies.delete('already_loaded', {
path: '/login/google/callback',
sameSite: 'lax',
httpOnly: true,
maxAge: 0,
secure: import.meta.env.PROD
});
return redirect(302, '/');
}

View File

@@ -1,194 +0,0 @@
<script lang="ts">
import type { PageData } from './$types';
import {
register,
title,
family_tree,
welcome,
site_intro,
born,
mothers_first_name,
mothers_last_name,
last_name,
first_name,
email,
biological_sex,
male,
female,
other,
intersex,
invite_code,
have_invite_code
} from '$lib/paraglide/messages';
import { onMount } from 'svelte';
import { enhance } from '$app/forms';
import FamilyTree from '../../highresolution_icon_no_background_croped.png';
let {
data,
form
}: {
data: PageData;
form: {
message: string;
};
} = $props();
let birth_date: HTMLInputElement;
let birth_date_value: 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,
onOpen: function () {
birth_date_value.placeholder = '';
},
onSelect: function (date) {
birth_date_value.value = date.toISOString().split('T')[0];
}
});
// Clean up when component unmounts
return () => picker.destroy();
});
}
});
let showInviteInput = $state(false);
function toggleInviteInput() {
showInviteInput = !showInviteInput;
}
</script>
<svelte:head>
<title>{title({ page: register() })}</title>
</svelte:head>
<div class="hero bg-base-200 min-h-screen">
<div class="hero-content flex-col lg:flex-row-reverse">
<div class="max-w-xxl flex flex-col items-center justify-center text-center">
<figure class="top-margin-10 max-w-sm px-10 pt-10">
<img src={FamilyTree} alt={family_tree()} class="rounded-xl" />
</figure>
<h1 class="text-5xl font-bold">{welcome()}</h1>
<p class="py-6">
{site_intro()}
</p>
</div>
<div class="card bg-base-100 w-full max-w-sm shrink-0 shadow-2xl">
<div class="card-body">
<form method="POST" action="?/register" use:enhance>
<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}
<label class="fieldset-label" for="email">{email()}</label>
<input
type="email"
name="email"
class="input"
placeholder={email()}
value={data.props?.email}
/>
<input
type="text"
name="google_id"
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"
name="first_name"
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"
name="last_name"
id="last_name"
placeholder={last_name()}
value={data.props?.last_name}
/>
<label class="fieldset-label" for="birth_date">{born()}</label>
<input
type="text"
class="input pika-single"
id="birth_date"
placeholder={born()}
bind:this={birth_date}
/>
<input type="text" class="hidden" name="birth_date" bind:this={birth_date_value} />
<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()}
>
<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()}
/>
<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()}
/>
<button type="button" class="btn btn-soft mt-4 max-w-xs" onclick={toggleInviteInput}>
{have_invite_code()}
</button>
{#if showInviteInput}
<div class="mt-4">
<label class="fieldset-label" for="invite_code">Meghívókód</label>
<input
type="text"
class="input input-bordered w-full"
name="invite_code"
id="invite_code"
placeholder={invite_code()}
/>
</div>
{/if}
<button class="btn btn-neutral mt-4 max-w-xs">{register()}</button>
</fieldset>
</form>
</div>
</div>
</div>
</div>

Some files were not shown because too many files have changed in this diff Show More