Merge branch 'main' into feature/rework-deployment-to-docker-compose

This commit is contained in:
2025-01-02 13:27:12 +01:00
82 changed files with 209 additions and 620 deletions

23
cmd/auth/auth.go Normal file
View File

@@ -0,0 +1,23 @@
package main
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/zitadel/zitadel-go/v3/pkg/authorization/oauth"
"github.com/zitadel/zitadel-go/v3/pkg/http/middleware"
)
func auth(mw *middleware.Interceptor[*oauth.IntrospectionContext]) gin.HandlerFunc {
return func(c *gin.Context) {
mw.RequireAuthorization()(http.HandlerFunc(authHTTPHandler(mw, c))).ServeHTTP(c.Writer, c.Request)
}
}
func authHTTPHandler(mw *middleware.Interceptor[*oauth.IntrospectionContext], c *gin.Context) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
authCtx := mw.Context(r.Context())
w.Header().Set("id", authCtx.UserID())
c.JSON(http.StatusOK, gin.H{"id": authCtx.UserID(), "status": "ok"})
}
}

21
cmd/auth/dockerfile Normal file
View File

@@ -0,0 +1,21 @@
FROM --platform=$BUILDPLATFORM golang:alpine AS build
ARG TARGETOS
ARG TARGETARCH
WORKDIR /app
COPY . .
RUN go get ./...
RUN GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -o auth-service
RUN apk update && apk add ca-certificates && update-ca-certificates
FROM --platform=$TARGETPLATFORM busybox:1.36.1
COPY --from=build /etc/ssl/certs /etc/ssl/certs
COPY --from=build /app/auth-service /app/
CMD [ "/app/auth-service" ]

84
cmd/auth/main.go Normal file
View File

@@ -0,0 +1,84 @@
package main
import (
"context"
"flag"
"log"
"os"
"os/signal"
"syscall"
"time"
"github.com/gin-gonic/gin"
utilities "github.com/vcscsvcscs/GenerationsHeritage/pkg"
"github.com/vcscsvcscs/GenerationsHeritage/pkg/gin/healthcheck"
"github.com/zitadel/zitadel-go/v3/pkg/authorization"
"github.com/zitadel/zitadel-go/v3/pkg/authorization/oauth"
"github.com/zitadel/zitadel-go/v3/pkg/http/middleware"
"github.com/zitadel/zitadel-go/v3/pkg/zitadel"
)
var (
cert = flag.String("cert", "/etc/gh-auth-service/ssl/tls.crt", "Specify the path of TLS cert")
key = flag.String("key", "/etc/gh-auth-service/ssl/tls.key", "Specify the path of TLS key")
zitadelAccessKey = flag.String("zitadel-access-key", "/etc/gh-auth-service/zitadel/api-key.json", "Specify the path of Zitadel access key")
httpsPort = flag.String("https", ":443", "Specify port for http secure hosting(example for format :443)")
httpPort = flag.String("http", ":80", "Specify port for http hosting(example for format :80)")
zitadelURI = flag.String("zitadel-uri", "zitadel.varghacsongor.hu", "Specify the Zitadel URI")
release = flag.Bool("release", false, "Set true to release build")
logToFile = flag.Bool("log-to-file", false, "Set true to log to file")
logToFileAndStd = flag.Bool("log-to-file-and-std", false, "Set true to log to file and std")
requestTimeout = time.Duration(*flag.Int("request-timeout", 20, "Set request timeout in seconds"))
)
func main() {
flag.Parse()
if *release {
gin.SetMode(gin.ReleaseMode)
}
utilities.SetupLogger(*logToFileAndStd, *logToFile)
hc := healthcheck.New()
router := gin.Default()
router.Use(gin.Recovery())
ctx := context.Background()
// Initiate the authorization by providing a zitadel configuration and a verifier.
// This example will use OAuth2 Introspection for this, therefore you will also need to provide the downloaded api key.json
authZ, err := authorization.New(ctx, zitadel.New(*zitadelURI), oauth.DefaultAuthorization(*zitadelAccessKey))
if err != nil {
log.Println("zitadel sdk could not initialize", "error", err)
os.Exit(1)
}
// Initialize the HTTP middleware by providing the authorization
mw := middleware.New(authZ)
router.GET("/health", hc.HealthCheckHandler())
router.GET("/auth", auth(mw))
server := utilities.SetupHttpsServer(router, *cert, *key, *httpsPort, *httpPort, requestTimeout)
// Wait for interrupt signal to gracefully shutdown the server with some time to finish requests.
quit := make(chan os.Signal, 1)
// kill (no param) default send syscall.SIGTERM
// kill -2 is syscall.SIGINT
// kill -9 is syscall.SIGKILL but can't be caught, so don't need to add it
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM, os.Interrupt)
<-quit
log.Println("Shutting down server...")
// The context is used to inform the server it has some seconds to finish
// the request it is currently handling
ctx, cancel := context.WithTimeout(context.Background(), requestTimeout*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
log.Fatal("Server forced to shutdown:", err)
}
log.Println("Server exiting")
}

21
cmd/backend/dockerfile Normal file
View File

@@ -0,0 +1,21 @@
FROM --platform=$BUILDPLATFORM golang:alpine AS build
ARG TARGETOS
ARG TARGETARCH
WORKDIR /app
COPY . .
RUN go get ./...
RUN GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -o backend
RUN apk update && apk add ca-certificates && update-ca-certificates
FROM --platform=$TARGETPLATFORM busybox:1.36.1
COPY --from=build /etc/ssl/certs /etc/ssl/certs
COPY --from=build /app/backend /app/
CMD [ "/app/backend" ]

View File

@@ -0,0 +1,49 @@
package handlers
import (
"encoding/json"
"log"
"net/http"
"github.com/gin-gonic/gin"
"github.com/neo4j/neo4j-go-driver/v5/neo4j"
"github.com/vcscsvcscs/GenerationsHeritage/pkg/memgraph"
)
func CreatePerson(driver neo4j.DriverWithContext) gin.HandlerFunc {
return func(c *gin.Context) {
if c.Request.Body == nil || c.ContentType() != "application/json" {
log.Printf("ip: %s error: request body is empty or content type is not application/json", c.ClientIP())
c.JSON(http.StatusBadRequest, gin.H{"error": "content type must be application/json and request body must not be empty"})
return
}
var person memgraph.Person
err := json.NewDecoder(c.Request.Body).Decode(&person)
if err != nil {
log.Printf("ip: %s error: %s", c.ClientIP(), err.Error())
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}
if err := person.Verify(); err != nil {
log.Printf("ip: %s error: %s", c.ClientIP(), err.Error())
c.JSON(http.StatusBadRequest, gin.H{"error": "contains-forbidden-characters"})
return
}
person.ID = c.GetString("id")
rec, err := person.CreatePerson(driver)
if err != nil {
log.Printf("ip: %s error: %s", c.ClientIP(), err.Error())
c.JSON(http.StatusBadRequest, gin.H{"error": "already-exists"})
return
}
c.JSON(http.StatusCreated, gin.H{"person": rec.AsMap()})
}
}

View File

@@ -0,0 +1,32 @@
package handlers
import (
"log"
"net/http"
"github.com/gin-gonic/gin"
"github.com/neo4j/neo4j-go-driver/v5/neo4j"
"github.com/vcscsvcscs/GenerationsHeritage/pkg/memgraph"
)
func CreateRelationship(driver neo4j.DriverWithContext) gin.HandlerFunc {
return func(c *gin.Context) {
var relationship memgraph.Relationship
if err := c.ShouldBindJSON(&relationship); err != nil {
log.Printf("ip: %s error: %s", c.ClientIP(), err)
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
rec, err := relationship.CreateRelationship(driver)
if err != nil {
log.Printf("ip: %s error: %s", c.ClientIP(), err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "internal server error"})
return
}
c.JSON(http.StatusCreated, gin.H{"relationship": rec.AsMap()})
}
}

View File

@@ -0,0 +1,32 @@
package handlers
import (
"log"
"net/http"
"github.com/gin-gonic/gin"
"github.com/neo4j/neo4j-go-driver/v5/neo4j"
"github.com/vcscsvcscs/GenerationsHeritage/pkg/memgraph"
)
func CreateRelationshipAndPerson(driver neo4j.DriverWithContext) gin.HandlerFunc {
return func(c *gin.Context) {
var rp memgraph.RelationshipAndPerson
if err := c.ShouldBindJSON(&rp); err != nil {
log.Printf("ip: %s error: %s", c.ClientIP(), err)
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
rec, err := rp.CreateRelationshipAndPerson(driver)
if err != nil {
log.Printf("ip: %s error: %s", c.ClientIP(), err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "internal server error"})
return
}
c.JSON(http.StatusCreated, gin.H{"relationship": rec.AsMap()})
}
}

View File

@@ -0,0 +1,48 @@
package handlers
import (
"encoding/json"
"log"
"net/http"
"github.com/gin-gonic/gin"
"github.com/neo4j/neo4j-go-driver/v5/neo4j"
"github.com/vcscsvcscs/GenerationsHeritage/pkg/memgraph"
)
func DeletePerson(driver neo4j.DriverWithContext) gin.HandlerFunc {
return func(c *gin.Context) {
if c.Request.Body == nil || c.ContentType() != "application/json" {
log.Printf("ip: %s error: request body is empty or content type is not application/json", c.ClientIP())
c.JSON(http.StatusBadRequest, gin.H{"error": "content type must be application/json and request body must not be empty"})
return
}
var person memgraph.Person
err := json.NewDecoder(c.Request.Body).Decode(&person)
if err != nil {
log.Printf("ip: %s error: %s", c.ClientIP(), err)
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}
if person.ID != "" {
log.Printf("ip: %s error: %s", c.ClientIP(), err)
c.JSON(http.StatusBadRequest, gin.H{"error": "no person ID provided"})
return
}
err = person.DeletePerson(driver)
if err != nil {
log.Printf("ip: %s error: %s", c.ClientIP(), err)
c.JSON(http.StatusNotFound, gin.H{"error": "could not delete person with ID provided"})
return
}
c.JSON(http.StatusOK, gin.H{"status": "person deleted successfully"})
}
}

View File

@@ -0,0 +1,32 @@
package handlers
import (
"log"
"net/http"
"github.com/gin-gonic/gin"
"github.com/neo4j/neo4j-go-driver/v5/neo4j"
"github.com/vcscsvcscs/GenerationsHeritage/pkg/memgraph"
)
func DeleteRelationship(driver neo4j.DriverWithContext) gin.HandlerFunc {
return func(c *gin.Context) {
var relationship memgraph.Relationship
if err := c.ShouldBindJSON(&relationship); err != nil {
log.Printf("ip: %s error: %s", c.ClientIP(), err)
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
err := relationship.DeleteRelationship(driver)
if err != nil {
log.Printf("ip: %s error: %s", c.ClientIP(), err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "internal server error"})
return
}
c.JSON(http.StatusAccepted, gin.H{"status": "relationship deleted successfully"})
}
}

View File

@@ -0,0 +1,48 @@
package handlers
import (
"encoding/json"
"log"
"net/http"
"github.com/gin-gonic/gin"
"github.com/neo4j/neo4j-go-driver/v5/neo4j"
"github.com/vcscsvcscs/GenerationsHeritage/pkg/memgraph"
)
func UpdatePerson(driver neo4j.DriverWithContext) gin.HandlerFunc {
return func(c *gin.Context) {
if c.Request.Body == nil || c.ContentType() != "application/json" {
log.Printf("ip: %s error: request body is empty or content type is not application/json", c.ClientIP())
c.JSON(http.StatusBadRequest, gin.H{"error": "content type must be application/json and request body must not be empty"})
return
}
var person memgraph.Person
err := json.NewDecoder(c.Request.Body).Decode(&person)
if err != nil {
log.Printf("ip: %s error: %s", c.ClientIP(), err)
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}
if person.ID == "" {
log.Printf("ip: %s error: %s", c.ClientIP(), err)
c.JSON(http.StatusBadRequest, gin.H{"error": "No ID provided"})
return
}
rec, err := person.UpdatePerson(driver)
if err != nil {
log.Printf("ip: %s error: %s", c.ClientIP(), err)
c.JSON(http.StatusNotFound, gin.H{"error": "could not update person with information provided"})
return
}
c.JSON(http.StatusOK, gin.H{"person": rec.AsMap()})
}
}

View File

@@ -0,0 +1,32 @@
package handlers
import (
"log"
"net/http"
"github.com/gin-gonic/gin"
"github.com/neo4j/neo4j-go-driver/v5/neo4j"
"github.com/vcscsvcscs/GenerationsHeritage/pkg/memgraph"
)
func VerifyRelationship(driver neo4j.DriverWithContext) gin.HandlerFunc {
return func(c *gin.Context) {
var relationship memgraph.Relationship
if err := c.ShouldBindJSON(&relationship); err != nil {
log.Printf("ip: %s error: %s", c.ClientIP(), err)
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
rec, err := relationship.VerifyRelationship(driver)
if err != nil {
log.Printf("ip: %s error: %s", c.ClientIP(), err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "internal server error"})
return
}
c.JSON(http.StatusOK, gin.H{"relationship": rec.AsMap()})
}
}

View File

@@ -0,0 +1,55 @@
package handlers
import (
"context"
"log"
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/neo4j/neo4j-go-driver/v5/neo4j"
)
func ViewFamiliyTree(driver neo4j.DriverWithContext) gin.HandlerFunc {
return func(c *gin.Context) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
session := driver.NewSession(ctx, neo4j.SessionConfig{AccessMode: neo4j.AccessModeRead})
defer session.Close(ctx)
id := c.GetString("id")
if id == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "id is required"})
return
}
query := `
MATCH (n:Person {id: $person_id})-[p:Parent*1..]->(family:Person)
OPTIONAL MATCH (family)-[c:Child]->(children:Person)
WITH family, p, children, c, n
OPTIONAL MATCH (children)<-[p2:Parent]-(OtherParents:Person)
WITH family, p, children, c, OtherParents, p2,n
OPTIONAL MATCH (family)-[s:Spouse]-(spouse:Person)
RETURN family, p, children, c, OtherParents, p2, spouse, s, n;`
result, err := session.Run(ctx, query, map[string]any{"person_id": id})
if err != nil {
log.Printf("ip: %s error: %s", c.ClientIP(), err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "internal server error"})
return
}
rec, err := result.Collect(ctx)
if err != nil {
log.Printf("ip: %s error: %s", c.ClientIP(), err)
c.JSON(http.StatusNotFound, gin.H{"error": "could not find family tree for person with id: " + id})
return
}
c.JSON(200, rec)
}
}

View File

@@ -0,0 +1,50 @@
package handlers
import (
"context"
"log"
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/neo4j/neo4j-go-driver/v5/neo4j"
)
func ViewPerson(driver neo4j.DriverWithContext) gin.HandlerFunc {
return func(c *gin.Context) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
session := driver.NewSession(ctx, neo4j.SessionConfig{AccessMode: neo4j.AccessModeRead})
defer session.Close(ctx)
id := c.Query("id")
if id == "" {
id = c.GetString("id")
}
if id == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "id is required"})
return
}
result, err := session.Run(ctx, "MATCH (n:Person) WHERE n.id = $person_id RETURN n;", map[string]any{"person_id": id})
if err != nil {
log.Printf("ip: %s error: %s", c.ClientIP(), err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "internal server error"})
return
}
rec, err := result.Single(ctx)
if err != nil {
log.Printf("ip: %s error: %s", c.ClientIP(), err)
c.JSON(http.StatusNotFound, gin.H{"error": "could not find person with information provided"})
return
}
c.JSON(200, rec.AsMap()["n"])
}
}

106
cmd/backend/main.go Normal file
View File

@@ -0,0 +1,106 @@
package main
import (
"context"
"flag"
"log"
"os"
"os/signal"
"syscall"
"time"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
"github.com/vcscsvcscs/GenerationsHeritage/cmd/backend/handlers"
utilities "github.com/vcscsvcscs/GenerationsHeritage/pkg"
"github.com/vcscsvcscs/GenerationsHeritage/pkg/gin/healthcheck"
"github.com/vcscsvcscs/GenerationsHeritage/pkg/memgraph"
//"github.com/zitadel/zitadel-go/v3/pkg/authorization"
//"github.com/zitadel/zitadel-go/v3/pkg/authorization/oauth"
//"github.com/zitadel/zitadel-go/v3/pkg/zitadel"
)
var (
cert = flag.String("cert", "/etc/gh-backend/ssl/tls.crt", "Specify the path of TLS cert")
key = flag.String("key", "/etc/gh-backend/ssl/tls.key", "Specify the path of TLS key")
httpsPort = flag.String("https", ":443", "Specify port for http secure hosting(example for format :443)")
httpPort = flag.String("http", ":80", "Specify port for http hosting(example for format :80)")
// zitadelAccessKey = flag.String("zitadel-access-key", "/etc/gh-backend/zitadel/api-key.json", "Specify the path of Zitadel access key")
// zitadelURI = flag.String("zitadel-uri", "zitadel.varghacsongor.hu", "Specify the Zitadel URI")
memgraphURI = flag.String("memgraph", "bolt+ssc://memgraph:7687", "Specify the Memgraph database URI")
memgraphUser = flag.String("memgraph-user", "", "Specify the Memgraph database user")
memgraphPass = flag.String("memgraph-pass", "", "Specify the Memgraph database password")
release = flag.Bool("release", false, "Set true to release build")
logToFile = flag.Bool("log-to-file", false, "Set true to log to file")
logToFileAndStd = flag.Bool("log-to-file-and-std", false, "Set true to log to file and std")
requestTimeout = time.Duration(*flag.Int("request-timeout", 20, "Set request timeout in seconds"))
)
func main() {
flag.Parse()
if *release {
gin.SetMode(gin.ReleaseMode)
}
utilities.SetupLogger(*logToFileAndStd, *logToFile)
hc := healthcheck.New()
memgraphDriver := memgraph.InitDatabase(*memgraphURI, *memgraphUser, *memgraphPass)
router := gin.Default()
router.Use(cors.New(cors.Config{
AllowOrigins: []string{"http://localhost:5173", "http://localhost", "https://heritagebackend.varghacsongor.hu", "https://feature-add-frontend.generationsheritage.pages.dev/", "https://csalad.varghacsongor.hu/"},
AllowCredentials: true,
AllowHeaders: []string{"Authorization", "id", "Content-Type"},
MaxAge: 12 * time.Hour,
}))
router.Use(gin.Recovery())
//ctx := context.Background()
// Initiate the authorization by providing a zitadel configuration and a verifier.
// This example will use OAuth2 Introspection for this, therefore you will also need to provide the downloaded api key.json
//authZ, err := authorization.New(ctx, zitadel.New(*zitadelURI), oauth.DefaultAuthorization(*zitadelAccessKey))
//if err != nil {
// log.Println("zitadel sdk could not initialize", "error", err)
// os.Exit(1)
//}
// Initialize the HTTP middleware by providing the authorization
//mw := middleware.New(authZ)
//router.Use(auth(mw))
router.GET("/health", hc.HealthCheckHandler())
router.GET("/person", handlers.ViewPerson(memgraphDriver))
router.POST("/person", handlers.CreatePerson(memgraphDriver))
router.DELETE("/person", handlers.DeletePerson(memgraphDriver))
router.PUT("/person", handlers.UpdatePerson(memgraphDriver))
router.POST("/relationship", handlers.CreateRelationship(memgraphDriver))
router.DELETE("/relationship", handlers.DeleteRelationship(memgraphDriver))
router.PUT("/relationship", handlers.VerifyRelationship(memgraphDriver))
router.POST("/createRelationshipAndPerson", handlers.CreateRelationshipAndPerson(memgraphDriver))
router.GET("/familyTree", handlers.ViewFamiliyTree(memgraphDriver))
server := utilities.SetupHttpsServer(router, *cert, *key, *httpsPort, *httpPort, requestTimeout)
// Wait for interrupt signal to gracefully shutdown the server with some time to finish requests.
quit := make(chan os.Signal, 1)
// kill (no param) default send syscall.SIGTERM
// kill -2 is syscall.SIGINT
// kill -9 is syscall.SIGKILL but can't be caught, so don't need to add it
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM, os.Interrupt)
<-quit
log.Println("Shutting down server...")
// The context is used to inform the server it has some seconds to finish
// the request it is currently handling
ctx, cancel := context.WithTimeout(context.Background(), requestTimeout*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
log.Fatal("Server forced to shutdown:", err)
}
log.Println("Server exiting")
}

View File

@@ -0,0 +1,39 @@
/.nyc_output
/coverage
/lib
/node_modules
/*.config.js
!/.vscode
!/.github
!/.devcontainer
/prettier-playground
/tests/fixtures/rules/indent/invalid/ts
/tests/fixtures/rules/indent/invalid/ts-v5
/tests/fixtures/rules/indent/invalid/snippets01-input.svelte
/tests/fixtures/rules/indent/valid/
/tests/fixtures/rules/no-unused-class-name/valid/invalid-style01-input.svelte
/tests/fixtures/rules/no-unused-class-name/valid/unknown-lang01-input.svelte
/tests/fixtures/rules/valid-compile/invalid/ts
/tests/fixtures/rules/valid-compile/valid/babel
/tests/fixtures/rules/valid-compile/valid/ts
/tests/fixtures/rules/prefer-style-directive
/tests/fixtures/rules/@typescript-eslint
/tests/fixtures/rules/valid-compile/valid/svelte3-options-custom-element-input.svelte
/tests/fixtures/rules/mustache-spacing/valid/always/snippet-render01-input.svelte
/tests/fixtures/rules/mustache-spacing/invalid/snippet-render01-input.svelte
/.svelte-kit
/svelte.config-dist.js
/build
/docs-svelte-kit/shim/eslint.mjs
/docs-svelte-kit/shim/assert.mjs
!/.*.js
/docs-svelte-kit/src/routes/*.md
/docs-svelte-kit/src/routes/**/*.md
/docs-svelte-kit/src/app.html
# JSONSchema bug?
/.devcontainer/devcontainer.json
# Parser bug?
/tests/fixtures/rules/indent/invalid/const-tag01-input.svelte
/tests/fixtures/rules/indent/invalid/const-tag01-output.svelte

View File

@@ -0,0 +1,19 @@
module.exports = {
extends: ['plugin:svelte/base', 'plugin:svelte/recommended', 'plugin:svelte/prettier'],
overrides: [
{
files: ['*.svelte'],
parser: 'svelte-eslint-parser'
}
],
env: {
es6: true
},
parserOptions: {
ecmaFeatures: {
experimentalObjectRestSpread: true,
jsx: true
},
sourceType: 'module'
}
};

1
cmd/frontend/.npmrc Normal file
View File

@@ -0,0 +1 @@
engine-strict=true

View File

@@ -0,0 +1,8 @@
.svelte-kit
.type-coverage
build
/lib
.npmrc
.eslintignore
/tests/fixtures/rules/indent/valid/
.changeset

View File

@@ -0,0 +1,31 @@
'use strict';
module.exports = {
useTabs: true,
singleQuote: true,
trailingComma: 'none',
printWidth: 100,
plugins: ['prettier-plugin-svelte'],
overrides: [
{
files: ['.*rc'],
excludeFiles: ['.browserslistrc', '.npmrc', '.nvmrc'],
options: {
parser: 'json'
}
},
{
files: ['*.svelte'],
options: {
bracketSameLine: false
}
},
{
files: ['*.md', 'package.json', '**/package.json'],
options: {
useTabs: false,
tabWidth: 2
}
}
]
};

38
cmd/frontend/README.md Normal file
View File

@@ -0,0 +1,38 @@
# create-svelte
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/main/packages/create-svelte).
## Creating a project
If you're seeing this, you've probably already done this step. Congrats!
```bash
# create a new project in the current directory
npm create svelte@latest
# create a new project in my-app
npm create svelte@latest my-app
```
## Developing
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
```bash
npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
```
## Building
To create a production version of your app:
```bash
npm run build
```
You can preview the production build with `npm run preview`.
> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.

4954
cmd/frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

38
cmd/frontend/package.json Normal file
View File

@@ -0,0 +1,38 @@
{
"name": "frontend",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"format": "npx prettier --write --plugin prettier-plugin-svelte .",
"lint": "npx eslint --ext .svelte --ext .js ."
},
"devDependencies": {
"@babel/core": "^7.24.0",
"@babel/eslint-parser": "^7.23.10",
"@sveltejs/adapter-auto": "^3.0.0",
"@sveltejs/adapter-static": "^3.0.1",
"@sveltejs/kit": "^2.0.0",
"@sveltejs/vite-plugin-svelte": "^3.0.0",
"autoprefixer": "^10.4.19",
"daisyui": "^4.10.2",
"eslint": "^8.57.0",
"eslint-config-google": "^0.14.0",
"eslint-plugin-svelte": "^2.35.1",
"postcss": "^8.4.38",
"prettier": "^3.2.5",
"prettier-plugin-svelte": "^3.2.2",
"svelte": "^4.2.12",
"tailwindcss": "^3.4.3",
"vite": "^5.0.3"
},
"type": "module",
"dependencies": {
"@dagrejs/dagre": "github:dagrejs/dagre",
"@xyflow/svelte": "^0.0.41",
"oidc-client-ts": "^3.0.1",
"svelte-eslint-parser": "^0.33.1"
}
}

View File

@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {}
}
};

79
cmd/frontend/src/app.html Normal file
View File

@@ -0,0 +1,79 @@
<!doctype html>
<html lang="en" style="width: 100%; height: 100%">
<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">
<div style="display: contents; width: 100vw; height: 100vh" class="bg-base-100">
%sveltekit.body%
</div>
<div
class="dropdown mb-72"
style="position: absolute; left: auto; right: 3vw; top: 10px; bottom: auto"
>
<div tabindex="0" role="button" class="btn m-1">
Theme
<svg
width="12px"
height="12px"
class="h-2 w-2 fill-current opacity-60 inline-block"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 2048 2048"
>
<path d="M1799 349l242 241-1017 1017L7 590l242-241 775 775 775-775z"></path>
</svg>
</div>
<ul tabindex="0" class="dropdown-content z-[1] p-2 shadow-2xl bg-base-300 rounded-box w-36">
<li>
<input
type="radio"
name="theme-dropdown"
class="theme-controller btn btn-sm btn-block btn-ghost justify-start"
aria-label="Light"
value="light"
/>
</li>
<li>
<input
type="radio"
name="theme-dropdown"
class="theme-controller btn btn-sm btn-block btn-ghost justify-start"
aria-label="Dark"
value="dark"
/>
</li>
<li>
<input
type="radio"
name="theme-dropdown"
class="theme-controller btn btn-sm btn-block btn-ghost justify-start"
aria-label="Cyberpunk"
value="cyberpunk"
/>
</li>
<li>
<input
type="radio"
name="theme-dropdown"
class="theme-controller btn btn-sm btn-block btn-ghost justify-start"
aria-label="Synthwave"
value="synthwave"
/>
</li>
<li>
<input
type="radio"
name="theme-dropdown"
class="theme-controller btn btn-sm btn-block btn-ghost justify-start"
aria-label="Retro"
value="retro"
/>
</li>
</ul>
</div>
</body>
</html>

View File

@@ -0,0 +1,66 @@
import { UserManager, WebStorageStateStore, type User } from 'oidc-client-ts';
import { isAuthenticated, user } from './stores';
import { PUBLIC_ZITADEL_CLIENT_ID, PUBLIC_ISSUER } from '$env/static/public';
import { goto } from '$app/navigation';
import { browser } from '$app/environment';
let userManager: UserManager;
if (browser) {
const host_url = window.location.href.startsWith('http://')
? 'http://localhost:5173'
: 'https://' + window.location.hostname;
const config = {
authority: PUBLIC_ISSUER, // At Zitadel Project Console > [Your project] > [Your application] > URLs - Issuer
client_id: PUBLIC_ZITADEL_CLIENT_ID, // At Zitadel Project Console > [Your project] > [Your application] > Configuration - Client ID
redirect_uri: host_url + '/callback', // At Zitadel Project Console > [Your project] > [Your application] > URLs - Login Redirect URI
response_type: 'code',
scope: 'openid profile email',
post_logout_redirect_uri: host_url,
userStore: new WebStorageStateStore({ store: window.localStorage }),
automaticSilentRenew: true,
silent_redirect_uri: host_url + '/silent-refresh'
};
userManager = new UserManager(config);
userManager.events.addUserLoaded((loadedUser: User) => {
console.log('userManager.events.addUserLoaded');
user.set(loadedUser);
isAuthenticated.set(true);
});
userManager.events.addUserUnloaded(() => {
console.log('userManager.events.addUserUnloaded');
user.set(null);
isAuthenticated.set(false);
});
}
async function login(): Promise<void> {
console.log('UserManager.login()');
if (browser) {
await userManager.signinRedirect();
}
}
async function logout(): Promise<void> {
if (browser) {
await userManager.signoutRedirect();
}
}
async function handleCallback(): Promise<void> {
if (browser) {
await userManager.signinRedirectCallback();
goto('/');
}
}
async function handleSilentCallback(): Promise<void> {
if (browser) {
await userManager.signinSilentCallback();
goto('/');
}
}
export { login, logout, handleCallback, handleSilentCallback };

View File

@@ -0,0 +1,215 @@
<script lang="ts">
import { PUBLIC_API_URL } from '$env/static/public';
import { setFamilyTreeNodes } from './setFamilyTreeNodes';
import { user } from '../stores';
import { onMount } from 'svelte';
export let id = '';
let auth_token = '';
user.subscribe((value) => {
if (value) {
auth_token = value.access_token;
}
});
let relationship = '';
let dialog: HTMLDialogElement;
let payload = {
relationship: {
first_person_id: id,
relationship: relationship,
direction: '->'
},
person: {
first_name: '',
middle_name: '',
last_name: '',
mothers_first_name: '',
mothers_last_name: '',
born: '',
birthplace: '',
titles: [],
residence: '',
death: '',
deathplace: '',
life_events: [],
occupation: [],
occupation_to_display: '',
others_said: {},
photos: {},
profile_picture: 'https://cdn-icons-png.flaticon.com/512/3607/3607444.png'
}
};
function handleSubmit(event: Event) {
event.preventDefault();
if (
id &&
relationship &&
payload.person.first_name &&
payload.person.last_name &&
payload.person.born &&
payload.person.mothers_first_name &&
payload.person.mothers_last_name
) {
fetch(PUBLIC_API_URL + '/createRelationshipAndPerson', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: auth_token
},
body: JSON.stringify(payload)
}).then((response) => {
if (response.ok) {
setFamilyTreeNodes();
dialog.close();
} else {
alert('Failed to add family member! with status ' + response.status);
}
});
} else {
alert('You have to fill out all the required fields!');
}
}
onMount(() => {
if (dialog) dialog.showModal();
});
</script>
<dialog bind:this={dialog} class="modal bg-base-300">
<div class="modal-box w-11/12 max-w-5xl">
<form method="dialog">
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"></button>
</form>
<h1 class="font-bold text-lg">Create a family members profile</h1>
<p>
You can add a family member to your family tree by filling out the form below. You can update
the information later.
</p>
<form on:submit={handleSubmit} method="dialog" class="flex flex-col gap-4">
<div class="grid grid-cols-2 gap-4">
<label for="relationship" class="input input-bordered flex items-center gap-2">
Relationship type:
<select id="relationship" bind:value={relationship} required>
<option value="" disabled selected>Choose one ...</option>
<option value="Parent">Parent</option>
<option value="Child">Child</option>
<option value="Spouse">Spouse</option>
<option value="Sibling">Sibling</option>
</select>
</label>
<label for="firstName" class="input input-bordered flex items-center gap-2">
First Name:
<input
type="text"
id="firstName"
class="grow input-md"
bind:value={payload.person.first_name}
required
/>
</label>
<label for="middleName" class="input input-bordered flex items-center gap-2">
Middle Name:
<input
type="text"
id="middleName"
class="grow input-md"
bind:value={payload.person.middle_name}
/>
</label>
<label for="lastName" class="input input-bordered flex items-center gap-2">
Last Name:
<input
type="text"
id="lastName"
class="grow input-md"
bind:value={payload.person.last_name}
required
/>
</label>
<label for="born" class="input input-bordered flex items-center gap-2">
Born:
<input
type="date"
id="born"
class="grow input-md"
bind:value={payload.person.born}
required
/>
</label>
<label for="birthplace" class="input input-bordered flex items-center gap-2">
Birthplace:
<input
type="text"
id="birthplace"
class="grow input-md"
bind:value={payload.person.birthplace}
required
/>
</label>
<label for="mothersFirstName" class="input input-bordered flex items-center gap-2">
Mother's First Name:
<input
type="text"
id="mothersFirstName"
class="grow input-md"
bind:value={payload.person.mothers_first_name}
required
/>
</label>
<label for="mothersLastName" class="input input-bordered flex items-center gap-2">
Mother's Last Name:
<input
type="text"
id="mothersLastName"
class="grow input-md"
bind:value={payload.person.mothers_last_name}
required
/>
</label>
<label for="death" class="input input-bordered flex items-center gap-2">
Death:
<input type="date" id="death" class="grow input-md" bind:value={payload.person.death} />
</label>
<label for="deathplace" class="input input-bordered flex items-center gap-2">
Place of death:
<input
type="text"
id="deathplace"
class="grow input-md"
bind:value={payload.person.deathplace}
/>
</label>
<label for="residence" class="input input-bordered flex items-center gap-2">
Residence:
<input
type="text"
id="residence"
class="grow input-md"
bind:value={payload.person.residence}
/>
</label>
<label for="titles" class="input input-bordered flex items-center gap-2">
Titles:
<input type="text" id="titles" class="grow input-md" bind:value={payload.person.titles} />
</label>
</div>
<button type="submit" class="btn btn-primary w-40 place-self-end">Add</button>
</form>
</div>
</dialog>

View File

@@ -0,0 +1,80 @@
<script lang="ts">
import { PUBLIC_API_URL } from '$env/static/public';
import { setFamilyTreeNodes } from './setFamilyTreeNodes';
import { user } from '$lib/stores';
import { onMount } from 'svelte';
export let id = '';
let relationship = '';
let second_person_id = '';
let dialog: HTMLDialogElement; // HTMLDialogElement
let auth_token = '';
user.subscribe((value) => {
if (value) {
auth_token = value.access_token;
}
});
function handleSubmit(event: SubmitEvent) {
event.preventDefault();
if (second_person_id && id && relationship) {
const payload = {
first_person_id: id,
second_person_id: second_person_id,
relationship: relationship,
direction: '->'
};
fetch(PUBLIC_API_URL + '/relationship', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: auth_token
},
body: JSON.stringify(payload)
}).then((response) => {
if (response.ok) {
setFamilyTreeNodes();
dialog.close();
} else {
alert('Failed to add relationship! with status ' + response.status);
}
});
} else {
alert('You have to fill out all the fields!');
}
}
onMount(() => {
if (dialog) dialog.showModal();
});
</script>
<dialog bind:this={dialog} class="modal bg-base-300">
<div class="modal-box w-11/12 max-w-5xl">
<form method="dialog">
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"></button>
</form>
<h1 class="font-bold text-lg">Add a relationship</h1>
<p>
Ask your relative to give you their id and set what kind of relationship you have with them.
</p>
<form on:submit={handleSubmit} method="dialog">
<label for="id" class="input input-bordered flex items-center gap-2">
ID:
<input type="text" id="id" class="grow" bind:value={id} required />
</label>
<label for="relationship">Relationship type:</label>
<select id="relationship" bind:value={relationship} required>
<option value="" disabled selected>Choose one ...</option>
<option value="Parent">Parent</option>
<option value="Child">Child</option>
<option value="Spouse">Spouse</option>
<option value="Sibling">Sibling</option>
</select>
<button type="submit" class="btn btn-primary">Add</button>
</form>
</div>
</dialog>

View File

@@ -0,0 +1,152 @@
<script lang="ts">
import { PUBLIC_API_URL } from '$env/static/public';
import { onMount } from 'svelte';
import { user } from '../stores';
let auth_token = '';
user.subscribe((value) => {
if (value) {
auth_token = value.access_token;
}
});
let dialog: HTMLDialogElement;
let payload = {
first_name: '',
middle_name: '',
last_name: '',
mothers_first_name: '',
mothers_last_name: '',
born: '',
birthplace: '',
titles: [],
residence: '',
life_events: [],
occupation: [],
occupation_to_display: '',
photos: {},
profile_picture: 'https://cdn-icons-png.flaticon.com/512/3607/3607444.png'
};
function handleSubmit(event: Event) {
event.preventDefault();
if (
payload.first_name &&
payload.last_name &&
payload.born &&
payload.mothers_first_name &&
payload.mothers_last_name
) {
payload.born = new Date(payload.born).toISOString();
fetch(PUBLIC_API_URL + '/person', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer' + auth_token
},
body: JSON.stringify(payload)
}).then((response) => {
if (response.ok) {
dialog.close();
} else {
alert('Failed to create profile! with status ' + response.status);
}
});
} else {
alert('You have to fill out all the required fields!');
}
}
onMount(() => {
if (dialog) dialog.showModal();
});
</script>
<dialog bind:this={dialog} class="modal bg-base-300">
<div class="modal-box w-11/12 max-w-5xl flex flex-col gap-4">
<h1 class="font-bold text-lg">Create your profile</h1>
<p>To start building your family tree fill this form. You can add more information later.</p>
<form on:submit={handleSubmit} method="dialog">
<div class="grid grid-cols-2 gap-4">
<label class="input input-bordered flex items-center gap-2" for="firstName">
First Name:
<input
type="text"
id="firstName"
class="grow input-md"
bind:value={payload.first_name}
required
/>
</label>
<label class="input input-bordered flex items-center gap-2" for="middleName">
Middle Name:
<input
type="text"
id="middleName"
class="grow input-md"
bind:value={payload.middle_name}
/>
</label>
<label class="input input-bordered flex items-center gap-2" for="lastName">
Last Name:
<input
type="text"
id="lastName"
class="grow input-md"
bind:value={payload.last_name}
required
/>
</label>
<label class="input input-bordered flex items-center gap-2" for="born">
Born:
<input type="date" id="born" class="grow input-md" bind:value={payload.born} required />
</label>
<label class="input input-bordered flex items-center gap-2" for="birthplace">
Birthplace:
<input
type="text"
id="birthplace"
class="grow input-md"
bind:value={payload.birthplace}
required
/>
</label>
<label class="input input-bordered flex items-center gap-2" for="mothersFirstName">
Mother's First Name:
<input
type="text"
id="mothersFirstName"
class="grow input-md"
bind:value={payload.mothers_first_name}
required
/>
</label>
<label class="input input-bordered flex items-center gap-2" for="mothersLastName">
Mother's Last Name:
<input
type="text"
id="mothersLastName"
class="grow input-md"
bind:value={payload.mothers_last_name}
required
/>
</label>
<label class="input input-bordered flex items-center gap-2" for="residence">
Residence:
<input type="text" id="residence" class="grow input-md" bind:value={payload.residence} />
</label>
<label class="input input-bordered flex items-center gap-2" for="titles">
Titles:
<input type="text" id="titles" class="grow input-md" bind:value={payload.titles} />
</label>
<button type="submit" class="btn btn-primary w-40 place-self-end">Add</button>
</div>
</form>
</div>
</dialog>

View File

@@ -0,0 +1,61 @@
<script lang="ts">
import { onMount } from 'svelte';
export let onClick: () => void;
export let deleteNode: () => void;
export let addRelationship: () => void;
export let addFamilymember: () => void;
export let id: string;
export let top: number | undefined;
export let left: number | undefined;
export let right: number | undefined;
export let bottom: number | undefined;
let contextMenu: HTMLDivElement;
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`;
}
});
</script>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
bind:this={contextMenu}
class="context-menu bg-primary-100 rounded-lg shadow-lg"
on:click={onClick}
>
<p style="margin: 0.5em;">
<small>node: {id}</small>
</p>
<button on:click={addRelationship} class="btn">Add Relationship</button>
<button on:click={addFamilymember} class="btn">Add familymember</button>
<button on:click={deleteNode} class="btn">Delete</button>
</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

@@ -0,0 +1,70 @@
<script lang="ts">
import { Handle, Position, type NodeProps } from '@xyflow/svelte';
export let id: NodeProps['id'];
id;
export let dragHandle: NodeProps['dragHandle'] = undefined;
dragHandle;
export let type: NodeProps['type'] = undefined;
type;
export let selected: NodeProps['selected'] = undefined;
selected;
export let width: NodeProps['width'] = undefined;
width;
export let height: NodeProps['height'] = undefined;
height;
export let dragging: NodeProps['dragging'];
dragging;
export let targetPosition: NodeProps['targetPosition'] = undefined;
targetPosition;
export let sourcePosition: NodeProps['sourcePosition'] = undefined;
sourcePosition;
export let data = {
id: '',
last_name: 'Nem',
first_name: 'Ismert',
middle_name: '',
profile_picture: 'https://cdn-icons-png.flaticon.com/512/3607/3607444.png'
};
</script>
<div class="rounded-badge card card-compact bg-base-300">
<div class="card-body items-center text-center w-30">
<div class="avatar">
<figure class="w-24 mask mask-squircle">
<img src={'https://cdn-icons-png.flaticon.com/512/3607/3607444.png'} alt="Picture of {data.last_name} {data.first_name}" />
</figure>
</div>
<h2 class="card-title text-base-content">
{data.first_name}
{data.middle_name ? data.middle_name : ''}
{data.last_name}
</h2>
</div>
</div>
<Handle
type="target"
position={Position.Top}
id="spouse"
style="transform: translate(10px, 50%); left: 0;"
/>
<Handle
type="source"
position={Position.Top}
id="spouse"
style="transform: translate(0, 50%); left: auto; right: 10px"
/>
<Handle type="target" position={Position.Top} id="parent" style="transform: translate(0, 50%);" />
<Handle
type="source"
position={Position.Bottom}
id="child"
style="transform: translate(0, -3px);"
/>
<Handle type="target" position={Position.Top} id="child" style="transform: translate(0, 50%);" />
<Handle
type="source"
position={Position.Bottom}
id="parent"
style="transform: translate(0, -3px);"
/>

View File

@@ -0,0 +1,53 @@
<script lang="ts">
import { onMount } from 'svelte';
import { fetch_profile } from './getProfile';
export let data = {
id: '',
last_name: 'Nem',
first_name: 'Ismert',
middle_name: '',
profile_picture: 'https://cdn-icons-png.flaticon.com/512/3607/3607444.png'
};
let dialog: HTMLDialogElement; // HTMLDialogElement
onMount(() => {
if (dialog) dialog.showModal();
fetch_profile(data.id).then((data) => {
data = data
});
data.profile_picture = 'https://cdn-icons-png.flaticon.com/512/3607/3607444.png';
});
</script>
<dialog bind:this={dialog} class="modal bg-base-300">
<div class="modal-box w-11/12 max-w-5xl">
<form method="dialog">
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"></button>
</form>
<div class="hero min-h-screen bg-base-200">
<div class="hero-content flex-col lg:flex-row">
<div class="avatar max-w-sm rounded-lg shadow-2xl">
<figure class="w-24 mask mask-squircle">
<img src={data.profile_picture} alt="Picture of {data.last_name} {data.first_name}" />
</figure>
</div>
<div>
<h1 class="text-5xl font-bold">
{data.first_name}
{data.middle_name ? data.middle_name : ''}
{data.last_name}
</h1>
<p class="py-6">
Provident cupiditate voluptatem et in. Quaerat fugiat ut assumenda excepturi
exercitationem quasi. In deleniti eaque aut repudiandae et a id nisi.
</p>
</div>
</div>
</div>
<form method="dialog" class="modal-backdrop">
<button>close</button>
</form>
</div>
</dialog>

View File

@@ -0,0 +1,39 @@
import dagre from '@dagrejs/dagre';
import { Position, type Node, type Edge } from '@xyflow/svelte';
const dagreGraph = new dagre.graphlib.Graph();
const nodeWidth = 250;
const nodeHeight = 250;
function getLayoutedElements(nodes: Node[], edges: Edge[], direction = 'TB') {
dagreGraph.setDefaultEdgeLabel(() => ({}));
const isHorizontal = direction === 'LR';
dagreGraph.setGraph({ rankdir: direction });
nodes.forEach((node) => {
dagreGraph.setNode(node.id, { width: nodeWidth, height: nodeHeight });
});
edges.forEach((edge) => {
dagreGraph.setEdge(edge.source, edge.target);
});
dagre.layout(dagreGraph);
nodes.forEach((node) => {
const nodeWithPosition = dagreGraph.node(node.id);
node.targetPosition = isHorizontal ? Position.Left : Position.Top;
node.sourcePosition = isHorizontal ? Position.Right : Position.Bottom;
// We are shifting the dagre node position (anchor=center center) to the top left
// so it matches the React Flow node anchor point (top left).
node.position = {
x: nodeWithPosition.x - nodeWidth / 2,
y: nodeWithPosition.y - nodeHeight / 2
};
});
return { nodes, edges };
}
export { getLayoutedElements };

View File

@@ -0,0 +1,77 @@
import { type Node, type Edge } from '@xyflow/svelte';
function AddToNodesData(data: any, i: number, pushToNodesCallback: (arg: Node) => void) {
if (data[0].Values[i] != null) {
if (Object.prototype.toString.call(data[0].Values[i]) === '[object Array]') {
data[0].Values[i].forEach((person: { ElementId: string; Props: {} }) => {
pushToNodesCallback({
id: person.ElementId,
type: 'custom',
data: person.Props,
position: { x: 0, y: 0 }
});
});
} else {
console.log(data[0].Values[i]);
pushToNodesCallback({
id: data[0].Values[i].ElementId,
type: 'custom',
data: data[0].Values[i].Props,
position: { x: 0, y: 0 }
});
}
}
}
function AddToEdgesData(data: any, i: number, pushToEdgesCallback: (arg: Edge) => void) {
if (data[0].Values[i] != null) {
if (Object.prototype.toString.call(data[0].Values[i]) === '[object Array]') {
data[0].Values[i].forEach(
(edge: {
ElementId: string;
StartElementId: string;
EndElementId: string;
Type: string;
Props: {};
}) => {
pushToEdgesCallback({
id: edge.ElementId,
source: edge.StartElementId,
sourceHandle:
edge.Type === 'Parent' ? 'parent' : edge.Type === 'Child' ? 'child' : 'spouse',
target: edge.EndElementId,
targetHandle:
edge.Type === 'Parent' ? 'child' : edge.Type === 'Child' ? 'parent' : 'spouse',
label: edge.Type,
data: edge.Props,
zIndex: edge.Type === 'Spouse' ? 1 : 0
});
}
);
} else {
console.log(data[0].Values[i]);
pushToEdgesCallback({
id: data[0].Values[i].ElementId,
source: data[0].Values[i].StartElementId,
sourceHandle:
data[0].Values[i].Type === 'Parent'
? 'parent'
: data[0].Values[i].Type === 'Child'
? 'child'
: 'spouse',
target: data[0].Values[i].EndElementId,
targetHandle:
data[0].Values[i].Type === 'Parent'
? 'child'
: data[0].Values[i].Type === 'Child'
? 'parent'
: 'spouse',
label: data[0].Values[i].Type,
data: data[0].Values[i].Props,
zIndex: data[0].Values[i].Type === 'Spouse' ? 1 : 0
});
}
}
}
export { AddToNodesData, AddToEdgesData };

View File

@@ -0,0 +1,24 @@
import { PUBLIC_API_URL } from '$env/static/public';
import { user } from '$lib/stores';
let auth_token: string;
user.subscribe((value) => {
if (value) {
auth_token = value.access_token;
}
});
async function fetch_family_tree() {
const response = await fetch(PUBLIC_API_URL + '/familyTree', {
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: 'Bearer ' + auth_token
}
});
const data = await response.json();
return data;
}
export { fetch_family_tree };

View File

@@ -0,0 +1,24 @@
import { PUBLIC_API_URL } from '$env/static/public';
import { user } from '$lib/stores';
let auth_token: string;
user.subscribe((value) => {
if (value) {
auth_token = value.access_token;
}
});
async function fetch_profile(id: string = '') {
const response = await fetch(PUBLIC_API_URL + '/person' + (id != '' ? '?id=' + id : ''), {
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: 'Bearer ' + auth_token
}
});
const data = await response.json();
return data;
}
export { fetch_profile };

View File

@@ -0,0 +1,61 @@
import { fetch_family_tree } from '$lib/family_tree/getFamilyTree';
import { getLayoutedElements } from '$lib/family_tree/dagreLayout';
import { AddToNodesData, AddToEdgesData } from '$lib/family_tree/dataAdapter';
import type { Node, Edge } from '@xyflow/svelte';
import { useEdges, useNodes } from '@xyflow/svelte';
import { fetch_profile } from './getProfile';
export async function setFamilyTreeNodes(): Promise<{
nodes: Node[];
edges: Edge[];
}> {
console.log('fetching nodes');
let layoutedElements: {
nodes: Node[];
edges: Edge[];
} = { nodes: [], edges: [] };
let nodes_data: Node[] = [];
let edges_data: Edge[] = [];
await fetch_profile().then((data) => {
nodes_data.push({
id: data.ElementId,
type: 'custom',
data: data.Props,
position: { x: 0, y: 0 }
});
});
await fetch_family_tree().then((data: []) => {
if (data.length == 0) {
return layoutedElements;
}
function pushNodeToData(node: Node) {
nodes_data.push(node);
}
AddToNodesData(data, 0, pushNodeToData);
AddToNodesData(data, 2, pushNodeToData);
AddToNodesData(data, 4, pushNodeToData);
AddToNodesData(data, 6, pushNodeToData);
AddToNodesData(data, 8, pushNodeToData);
function pushEdgeToData(edge: Edge) {
edges_data.push(edge);
}
AddToEdgesData(data, 1, pushEdgeToData);
AddToEdgesData(data, 3, pushEdgeToData);
AddToEdgesData(data, 5, pushEdgeToData);
AddToEdgesData(data, 7, pushEdgeToData);
});
console.log('Fetched nodes and edges data.');
// Remove duplicate nodes
nodes_data = nodes_data.filter(
(node, index, self) => index === self.findIndex((n) => n.id === node.id)
);
return getLayoutedElements(nodes_data, edges_data, 'TB');
}

View File

@@ -0,0 +1 @@
// place files you want to import through the `$lib` alias in this folder.

View File

@@ -0,0 +1,5 @@
import { writable } from 'svelte/store';
import type { User } from 'oidc-client-ts';
export const isAuthenticated = writable<boolean>(false);
export const user = writable<User | null>(null);

View File

@@ -0,0 +1,3 @@
export const prerender = true;
import 'tailwindcss/tailwind.css';
import '@xyflow/svelte/dist/style.css';

View File

@@ -0,0 +1,139 @@
<script lang="ts">
import { writable } from 'svelte/store';
import { onMount } from 'svelte';
import { SvelteFlowProvider, SvelteFlow, Controls, MiniMap } from '@xyflow/svelte';
import type { Node, Edge, NodeTypes } from '@xyflow/svelte';
import { isAuthenticated } from '../lib/stores';
import PersonNode from './../lib/family_tree/PersonNode.svelte';
import { setFamilyTreeNodes } from '../lib/family_tree/setFamilyTreeNodes';
import { login } from '../lib/auth';
import PersonMenu from '$lib/family_tree/PersonMenu.svelte';
import PersonPanel from '$lib/family_tree/PersonPanel.svelte';
import AddRelationship from '$lib/family_tree/AddRelationship.svelte';
import AddFamilyMember from '$lib/family_tree/AddFamilyMember.svelte';
import CreateProfile from '$lib/family_tree/CreateProfile.svelte';
let relationshipPanel = '';
let AddFamilyMemberPanel = '';
let CreateProfilePanel = false;
const nodes = writable<Node[]>([]);
const edges = writable<Edge[]>([]);
onMount(() => {
if (!$isAuthenticated) {
console.log('user is not authenticated');
login();
} else {
console.log('user is authenticated');
setFamilyTreeNodes().then((layout) => {
if (layout.nodes.length < 1) {
CreateProfilePanel = true;
} else {
nodes.set(layout.nodes);
edges.set(layout.edges);
}
});
}
});
const nodeTypes: NodeTypes = {
custom: PersonNode
};
let personPanel:
| {
id: string;
last_name: string;
first_name: string;
middle_name: string;
profile_picture: string;
}
| undefined;
let menu: { id: string; top?: number; left?: number; right?: number; bottom?: number } | null;
let width: number;
let height: number;
function handleContextMenu({
detail: { event, node }
}: {
detail: { event: MouseEvent; node: Node };
}) {
event.preventDefault();
menu = {
id: node.data.id,
top: event.clientY < height - 200 ? event.clientY : undefined,
left: event.clientX < width - 200 ? event.clientX : undefined,
right: event.clientX >= width - 200 ? width - event.clientX : undefined,
bottom: event.clientY >= height - 200 ? height - event.clientY : undefined
};
}
// Close the context menu if it's open whenever the window is clicked.
function handlePaneClick() {
menu = null;
relationshipPanel = '';
}
function handleNodeClick({
detail: { event, node }
}: {
detail: { event: MouseEvent; node: Node };
}) {
event.preventDefault();
personPanel = node.data;
}
</script>
<div style="height:100vh;">
<SvelteFlowProvider>
<SvelteFlow
{nodes}
{nodeTypes}
{edges}
on:nodecontextmenu={handleContextMenu}
on:nodeclick={handleNodeClick}
on:paneclick={handlePaneClick}
class="bg-base-100"
fitView
onlyRenderVisibleElements
>
<Controls class="bg-base-300 text-base-content" />
<MiniMap class="bg-base-200" />
{#if menu != null}
<PersonMenu
onClick={handlePaneClick}
deleteNode={() => {
console.log('delete node');
}}
addRelationship={() => {
if (menu) relationshipPanel = menu.id;
}}
addFamilymember={() => {
if (menu) AddFamilyMemberPanel = menu.id;
}}
id={menu.id}
top={menu.top}
left={menu.left}
right={menu.right}
bottom={menu.bottom}
/>
{/if}
{#if personPanel != null}
<PersonPanel data={personPanel} />
{/if}
{#if relationshipPanel != ''}
<AddRelationship id={relationshipPanel} />
{/if}
{#if AddFamilyMemberPanel != ''}
<AddFamilyMember id={AddFamilyMemberPanel} />
{/if}
{#if CreateProfilePanel}
<CreateProfile />
{/if}
</SvelteFlow>
</SvelteFlowProvider>
</div>

View File

@@ -0,0 +1,11 @@
<!-- src/routes/callback.svelte -->
<script lang="ts">
import { onMount } from 'svelte';
import { handleCallback } from '../../lib/auth';
onMount(() => {
handleCallback();
});
</script>
<div>Logging in...</div>

View File

@@ -0,0 +1,10 @@
<script lang="ts">
import { onMount } from 'svelte';
import { handleSilentCallback } from '../../lib/auth';
onMount(() => {
handleSilentCallback();
});
</script>
<div>Refreshing...</div>

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 266 KiB

View File

@@ -0,0 +1,18 @@
import adapter from '@sveltejs/adapter-static';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */
const config = {
preprocess: vitePreprocess(),
kit: {
adapter: adapter({
pages: 'build',
assets: 'build',
fallback: undefined,
precompress: false,
strict: true
})
}
};
export default config;

View File

@@ -0,0 +1,12 @@
/** @type {import('tailwindcss').Config} */
export default {
content: ['./src/**/*.{html,svelte,js,ts}'],
important: true,
theme: {
extend: {}
},
daisyui: {
themes: ['light', 'dark', 'cyberpunk', 'synthwave', 'retro', 'roboto', 'dracula']
},
plugins: [require('daisyui')]
};

View File

@@ -0,0 +1,14 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true
},
"include": ["src/**/*", "src/node_modules", ".svelte-kit/ambient.d.ts"] // see last element
}

View File

@@ -0,0 +1,6 @@
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [sveltekit()]
});