From ced66c5807575cd29f6aa5632e8ad02b38c8448a Mon Sep 17 00:00:00 2001 From: ivar Date: Sun, 28 Apr 2024 22:37:30 +0200 Subject: WIP new frontend --- code/frontend/.dockerignore | 7 + code/frontend/.env-example | 4 + code/frontend/.eslintignore | 13 + code/frontend/.eslintrc.cjs | 31 ++ code/frontend/.gitignore | 10 + code/frontend/.npmrc | 1 + code/frontend/.prettierignore | 4 + code/frontend/.prettierrc | 9 + code/frontend/.typesafe-i18n.json | 5 + code/frontend/.version | 1 + code/frontend/.version-dev | 1 + code/frontend/Dockerfile | 13 + code/frontend/build_and_push.sh | 89 ++++ code/frontend/bun.lockb | Bin 0 -> 314639 bytes code/frontend/components.json | 14 + code/frontend/package.json | 65 +++ code/frontend/playwright.config.ts | 12 + code/frontend/postcss.config.cjs | 13 + code/frontend/src/actions/pwKey.ts | 4 + code/frontend/src/app.d.ts | 13 + code/frontend/src/app.html | 12 + code/frontend/src/app.pcss | 108 +++++ .../frontend/src/components/locale-switcher.svelte | 56 +++ code/frontend/src/components/sonner.svelte | 21 + code/frontend/src/components/style-changer.svelte | 22 + .../src/components/ui/button/button.svelte | 25 ++ code/frontend/src/components/ui/button/index.ts | 48 +++ .../dropdown-menu-checkbox-item.svelte | 35 ++ .../ui/dropdown-menu/dropdown-menu-content.svelte | 26 ++ .../ui/dropdown-menu/dropdown-menu-item.svelte | 31 ++ .../ui/dropdown-menu/dropdown-menu-label.svelte | 19 + .../dropdown-menu/dropdown-menu-radio-group.svelte | 11 + .../dropdown-menu/dropdown-menu-radio-item.svelte | 35 ++ .../dropdown-menu/dropdown-menu-separator.svelte | 11 + .../ui/dropdown-menu/dropdown-menu-shortcut.svelte | 13 + .../dropdown-menu/dropdown-menu-sub-content.svelte | 29 ++ .../dropdown-menu/dropdown-menu-sub-trigger.svelte | 32 ++ .../src/components/ui/dropdown-menu/index.ts | 48 +++ code/frontend/src/configuration/index.ts | 38 ++ code/frontend/src/configuration/test.ts | 21 + code/frontend/src/global.d.ts | 11 + code/frontend/src/hooks.server.ts | 48 +++ code/frontend/src/i18n/en/app/index.ts | 7 + code/frontend/src/i18n/en/index.ts | 63 +++ code/frontend/src/i18n/formatters.ts | 13 + code/frontend/src/i18n/i18n-svelte.ts | 12 + code/frontend/src/i18n/i18n-types.ts | 461 +++++++++++++++++++++ code/frontend/src/i18n/i18n-util.async.ts | 42 ++ code/frontend/src/i18n/i18n-util.sync.ts | 35 ++ code/frontend/src/i18n/i18n-util.ts | 44 ++ code/frontend/src/i18n/nb/app/index.ts | 7 + code/frontend/src/i18n/nb/index.ts | 51 +++ code/frontend/src/index.test.ts | 7 + code/frontend/src/models/base/Customer.ts | 21 + code/frontend/src/models/base/CustomerContact.ts | 8 + code/frontend/src/models/base/CustomerEvent.ts | 6 + code/frontend/src/models/base/SessionData.ts | 5 + code/frontend/src/models/base/Tenant.ts | 8 + code/frontend/src/models/base/User.ts | 13 + code/frontend/src/models/base/UserRole.ts | 5 + code/frontend/src/models/internal/FormError.ts | 24 ++ code/frontend/src/models/internal/IForm.ts | 15 + code/frontend/src/models/internal/KnownProblem.ts | 10 + code/frontend/src/models/projects/Project.ts | 13 + code/frontend/src/models/projects/ProjectLabel.ts | 5 + code/frontend/src/models/projects/ProjectMember.ts | 10 + code/frontend/src/models/projects/ProjectMeta.ts | 7 + code/frontend/src/models/projects/ProjectRole.ts | 7 + code/frontend/src/models/projects/ProjectStatus.ts | 5 + code/frontend/src/models/work/WorkCategory.ts | 5 + code/frontend/src/models/work/WorkEntry.ts | 13 + .../src/models/work/WorkEntryQueryResponse.ts | 27 ++ code/frontend/src/models/work/WorkLabel.ts | 5 + code/frontend/src/models/work/WorkQuery.ts | 17 + .../src/routes/(main)/(app)/+layout.svelte | 379 +++++++++++++++++ .../src/routes/(main)/(app)/home/+page.svelte | 1 + .../src/routes/(main)/(app)/org/+page.svelte | 4 + .../src/routes/(main)/(app)/profile/+page.svelte | 4 + .../src/routes/(main)/(app)/projects/+page.svelte | 118 ++++++ .../routes/(main)/(app)/projects/[id]/+page.svelte | 5 + .../(main)/(app)/projects/create/+page.svelte | 59 +++ .../src/routes/(main)/(app)/settings/+page.svelte | 205 +++++++++ .../src/routes/(main)/(app)/tickets/+page.svelte | 4 + .../src/routes/(main)/(app)/todo/+page.svelte | 4 + .../src/routes/(main)/(app)/wiki/+page.svelte | 4 + .../src/routes/(main)/(public)/+layout.svelte | 18 + .../src/routes/(main)/(public)/portal/+page.svelte | 26 ++ .../src/routes/(main)/(public)/portal/+page.ts | 9 + .../(main)/(public)/reset-password/+page.svelte | 81 ++++ .../routes/(main)/(public)/reset-password/+page.ts | 11 + .../(public)/reset-password/[id]/+page.server.ts | 11 + .../(public)/reset-password/[id]/+page.svelte | 82 ++++ .../(main)/(public)/reset-password/[id]/+page.ts | 11 + .../routes/(main)/(public)/sign-in/+page.svelte | 155 +++++++ .../src/routes/(main)/(public)/sign-in/+page.ts | 11 + .../routes/(main)/(public)/sign-in/index.spec.js | 12 + .../src/routes/(main)/(public)/sign-in/index.ts | 20 + .../routes/(main)/(public)/sign-up/+page.svelte | 106 +++++ .../src/routes/(main)/(public)/sign-up/+page.ts | 11 + code/frontend/src/routes/(main)/+page.svelte | 1 + code/frontend/src/routes/+layout.server.ts | 44 ++ code/frontend/src/routes/+layout.svelte | 50 +++ code/frontend/src/routes/+layout.ts | 10 + .../src/services/abstractions/IAccountService.ts | 54 +++ .../src/services/abstractions/IApiTokenService.ts | 34 ++ .../services/abstractions/IPasswordResetService.ts | 21 + .../src/services/abstractions/ISettingsService.ts | 3 + code/frontend/src/services/account-service.ts | 123 ++++++ code/frontend/src/services/api-tokens-service.ts | 22 + .../src/services/password-reset-service.ts | 48 +++ code/frontend/src/services/settings-service.ts | 10 + code/frontend/src/utils/_fetch.ts | 93 +++++ code/frontend/src/utils/colors.ts | 47 +++ code/frontend/src/utils/crypto-helpers.ts | 49 +++ code/frontend/src/utils/global-state.ts | 22 + code/frontend/src/utils/misc-helpers.ts | 77 ++++ code/frontend/src/utils/persistent-store.ts | 110 +++++ code/frontend/src/utils/storage-helpers.ts | 26 ++ code/frontend/src/utils/testing-helpers.ts | 7 + code/frontend/src/utils/ui.ts | 56 +++ code/frontend/src/utils/validators.ts | 34 ++ code/frontend/static/favicon.png | Bin 0 -> 1571 bytes code/frontend/svelte.config.js | 23 + code/frontend/tailwind.config.js | 64 +++ code/frontend/tests/test.ts | 6 + code/frontend/tsconfig.json | 14 + code/frontend/vite.config.ts | 18 + 127 files changed, 4397 insertions(+) create mode 100644 code/frontend/.dockerignore create mode 100644 code/frontend/.env-example create mode 100644 code/frontend/.eslintignore create mode 100644 code/frontend/.eslintrc.cjs create mode 100644 code/frontend/.gitignore create mode 100644 code/frontend/.npmrc create mode 100644 code/frontend/.prettierignore create mode 100644 code/frontend/.prettierrc create mode 100644 code/frontend/.typesafe-i18n.json create mode 100644 code/frontend/.version create mode 100644 code/frontend/.version-dev create mode 100644 code/frontend/Dockerfile create mode 100644 code/frontend/build_and_push.sh create mode 100755 code/frontend/bun.lockb create mode 100644 code/frontend/components.json create mode 100644 code/frontend/package.json create mode 100644 code/frontend/playwright.config.ts create mode 100644 code/frontend/postcss.config.cjs create mode 100644 code/frontend/src/actions/pwKey.ts create mode 100644 code/frontend/src/app.d.ts create mode 100644 code/frontend/src/app.html create mode 100644 code/frontend/src/app.pcss create mode 100644 code/frontend/src/components/locale-switcher.svelte create mode 100644 code/frontend/src/components/sonner.svelte create mode 100644 code/frontend/src/components/style-changer.svelte create mode 100644 code/frontend/src/components/ui/button/button.svelte create mode 100644 code/frontend/src/components/ui/button/index.ts create mode 100644 code/frontend/src/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte create mode 100644 code/frontend/src/components/ui/dropdown-menu/dropdown-menu-content.svelte create mode 100644 code/frontend/src/components/ui/dropdown-menu/dropdown-menu-item.svelte create mode 100644 code/frontend/src/components/ui/dropdown-menu/dropdown-menu-label.svelte create mode 100644 code/frontend/src/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte create mode 100644 code/frontend/src/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte create mode 100644 code/frontend/src/components/ui/dropdown-menu/dropdown-menu-separator.svelte create mode 100644 code/frontend/src/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte create mode 100644 code/frontend/src/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte create mode 100644 code/frontend/src/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte create mode 100644 code/frontend/src/components/ui/dropdown-menu/index.ts create mode 100644 code/frontend/src/configuration/index.ts create mode 100644 code/frontend/src/configuration/test.ts create mode 100644 code/frontend/src/global.d.ts create mode 100644 code/frontend/src/hooks.server.ts create mode 100644 code/frontend/src/i18n/en/app/index.ts create mode 100644 code/frontend/src/i18n/en/index.ts create mode 100644 code/frontend/src/i18n/formatters.ts create mode 100644 code/frontend/src/i18n/i18n-svelte.ts create mode 100644 code/frontend/src/i18n/i18n-types.ts create mode 100644 code/frontend/src/i18n/i18n-util.async.ts create mode 100644 code/frontend/src/i18n/i18n-util.sync.ts create mode 100644 code/frontend/src/i18n/i18n-util.ts create mode 100644 code/frontend/src/i18n/nb/app/index.ts create mode 100644 code/frontend/src/i18n/nb/index.ts create mode 100644 code/frontend/src/index.test.ts create mode 100644 code/frontend/src/models/base/Customer.ts create mode 100644 code/frontend/src/models/base/CustomerContact.ts create mode 100644 code/frontend/src/models/base/CustomerEvent.ts create mode 100644 code/frontend/src/models/base/SessionData.ts create mode 100644 code/frontend/src/models/base/Tenant.ts create mode 100644 code/frontend/src/models/base/User.ts create mode 100644 code/frontend/src/models/base/UserRole.ts create mode 100644 code/frontend/src/models/internal/FormError.ts create mode 100644 code/frontend/src/models/internal/IForm.ts create mode 100644 code/frontend/src/models/internal/KnownProblem.ts create mode 100644 code/frontend/src/models/projects/Project.ts create mode 100644 code/frontend/src/models/projects/ProjectLabel.ts create mode 100644 code/frontend/src/models/projects/ProjectMember.ts create mode 100644 code/frontend/src/models/projects/ProjectMeta.ts create mode 100644 code/frontend/src/models/projects/ProjectRole.ts create mode 100644 code/frontend/src/models/projects/ProjectStatus.ts create mode 100644 code/frontend/src/models/work/WorkCategory.ts create mode 100644 code/frontend/src/models/work/WorkEntry.ts create mode 100644 code/frontend/src/models/work/WorkEntryQueryResponse.ts create mode 100644 code/frontend/src/models/work/WorkLabel.ts create mode 100644 code/frontend/src/models/work/WorkQuery.ts create mode 100644 code/frontend/src/routes/(main)/(app)/+layout.svelte create mode 100644 code/frontend/src/routes/(main)/(app)/home/+page.svelte create mode 100644 code/frontend/src/routes/(main)/(app)/org/+page.svelte create mode 100644 code/frontend/src/routes/(main)/(app)/profile/+page.svelte create mode 100644 code/frontend/src/routes/(main)/(app)/projects/+page.svelte create mode 100644 code/frontend/src/routes/(main)/(app)/projects/[id]/+page.svelte create mode 100644 code/frontend/src/routes/(main)/(app)/projects/create/+page.svelte create mode 100644 code/frontend/src/routes/(main)/(app)/settings/+page.svelte create mode 100644 code/frontend/src/routes/(main)/(app)/tickets/+page.svelte create mode 100644 code/frontend/src/routes/(main)/(app)/todo/+page.svelte create mode 100644 code/frontend/src/routes/(main)/(app)/wiki/+page.svelte create mode 100644 code/frontend/src/routes/(main)/(public)/+layout.svelte create mode 100644 code/frontend/src/routes/(main)/(public)/portal/+page.svelte create mode 100644 code/frontend/src/routes/(main)/(public)/portal/+page.ts create mode 100644 code/frontend/src/routes/(main)/(public)/reset-password/+page.svelte create mode 100644 code/frontend/src/routes/(main)/(public)/reset-password/+page.ts create mode 100644 code/frontend/src/routes/(main)/(public)/reset-password/[id]/+page.server.ts create mode 100644 code/frontend/src/routes/(main)/(public)/reset-password/[id]/+page.svelte create mode 100644 code/frontend/src/routes/(main)/(public)/reset-password/[id]/+page.ts create mode 100644 code/frontend/src/routes/(main)/(public)/sign-in/+page.svelte create mode 100644 code/frontend/src/routes/(main)/(public)/sign-in/+page.ts create mode 100644 code/frontend/src/routes/(main)/(public)/sign-in/index.spec.js create mode 100644 code/frontend/src/routes/(main)/(public)/sign-in/index.ts create mode 100644 code/frontend/src/routes/(main)/(public)/sign-up/+page.svelte create mode 100644 code/frontend/src/routes/(main)/(public)/sign-up/+page.ts create mode 100644 code/frontend/src/routes/(main)/+page.svelte create mode 100644 code/frontend/src/routes/+layout.server.ts create mode 100644 code/frontend/src/routes/+layout.svelte create mode 100644 code/frontend/src/routes/+layout.ts create mode 100644 code/frontend/src/services/abstractions/IAccountService.ts create mode 100644 code/frontend/src/services/abstractions/IApiTokenService.ts create mode 100644 code/frontend/src/services/abstractions/IPasswordResetService.ts create mode 100644 code/frontend/src/services/abstractions/ISettingsService.ts create mode 100644 code/frontend/src/services/account-service.ts create mode 100644 code/frontend/src/services/api-tokens-service.ts create mode 100644 code/frontend/src/services/password-reset-service.ts create mode 100644 code/frontend/src/services/settings-service.ts create mode 100644 code/frontend/src/utils/_fetch.ts create mode 100644 code/frontend/src/utils/colors.ts create mode 100644 code/frontend/src/utils/crypto-helpers.ts create mode 100644 code/frontend/src/utils/global-state.ts create mode 100644 code/frontend/src/utils/misc-helpers.ts create mode 100644 code/frontend/src/utils/persistent-store.ts create mode 100644 code/frontend/src/utils/storage-helpers.ts create mode 100644 code/frontend/src/utils/testing-helpers.ts create mode 100644 code/frontend/src/utils/ui.ts create mode 100644 code/frontend/src/utils/validators.ts create mode 100644 code/frontend/static/favicon.png create mode 100644 code/frontend/svelte.config.js create mode 100644 code/frontend/tailwind.config.js create mode 100644 code/frontend/tests/test.ts create mode 100644 code/frontend/tsconfig.json create mode 100644 code/frontend/vite.config.ts (limited to 'code/frontend') diff --git a/code/frontend/.dockerignore b/code/frontend/.dockerignore new file mode 100644 index 0000000..00774fa --- /dev/null +++ b/code/frontend/.dockerignore @@ -0,0 +1,7 @@ +.env +.env-example +.svelte-kit +.git +static +node_modules +build \ No newline at end of file diff --git a/code/frontend/.env-example b/code/frontend/.env-example new file mode 100644 index 0000000..270860f --- /dev/null +++ b/code/frontend/.env-example @@ -0,0 +1,4 @@ +VITE_LOG_LEVEL=DEBUG +VITE_TESTING=true +VITE_TEST_USERNAME="ms@test.tld" +VITE_TEST_PASSWORD="test123" \ No newline at end of file diff --git a/code/frontend/.eslintignore b/code/frontend/.eslintignore new file mode 100644 index 0000000..3897265 --- /dev/null +++ b/code/frontend/.eslintignore @@ -0,0 +1,13 @@ +.DS_Store +node_modules +/build +/.svelte-kit +/package +.env +.env.* +!.env.example + +# Ignore files for PNPM, NPM and YARN +pnpm-lock.yaml +package-lock.json +yarn.lock diff --git a/code/frontend/.eslintrc.cjs b/code/frontend/.eslintrc.cjs new file mode 100644 index 0000000..0b75758 --- /dev/null +++ b/code/frontend/.eslintrc.cjs @@ -0,0 +1,31 @@ +/** @type { import("eslint").Linter.Config } */ +module.exports = { + root: true, + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:svelte/recommended', + 'prettier' + ], + parser: '@typescript-eslint/parser', + plugins: ['@typescript-eslint'], + parserOptions: { + sourceType: 'module', + ecmaVersion: 2020, + extraFileExtensions: ['.svelte'] + }, + env: { + browser: true, + es2017: true, + node: true + }, + overrides: [ + { + files: ['*.svelte'], + parser: 'svelte-eslint-parser', + parserOptions: { + parser: '@typescript-eslint/parser' + } + } + ] +}; diff --git a/code/frontend/.gitignore b/code/frontend/.gitignore new file mode 100644 index 0000000..6635cf5 --- /dev/null +++ b/code/frontend/.gitignore @@ -0,0 +1,10 @@ +.DS_Store +node_modules +/build +/.svelte-kit +/package +.env +.env.* +!.env.example +vite.config.js.timestamp-* +vite.config.ts.timestamp-* diff --git a/code/frontend/.npmrc b/code/frontend/.npmrc new file mode 100644 index 0000000..b6f27f1 --- /dev/null +++ b/code/frontend/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/code/frontend/.prettierignore b/code/frontend/.prettierignore new file mode 100644 index 0000000..cc41cea --- /dev/null +++ b/code/frontend/.prettierignore @@ -0,0 +1,4 @@ +# Ignore files for PNPM, NPM and YARN +pnpm-lock.yaml +package-lock.json +yarn.lock diff --git a/code/frontend/.prettierrc b/code/frontend/.prettierrc new file mode 100644 index 0000000..2b9389a --- /dev/null +++ b/code/frontend/.prettierrc @@ -0,0 +1,9 @@ +{ + "useTabs": true, + "singleQuote": true, + "trailingComma": "none", + "semi": false, + "svelteAllowShorthand": true, + "printWidth": 120, + "plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"] +} diff --git a/code/frontend/.typesafe-i18n.json b/code/frontend/.typesafe-i18n.json new file mode 100644 index 0000000..4bb9efd --- /dev/null +++ b/code/frontend/.typesafe-i18n.json @@ -0,0 +1,5 @@ +{ + "adapter": "svelte", + "$schema": "https://unpkg.com/typesafe-i18n@5.26.2/schema/typesafe-i18n.json", + "outputPath": "src/i18n" +} \ No newline at end of file diff --git a/code/frontend/.version b/code/frontend/.version new file mode 100644 index 0000000..626799f --- /dev/null +++ b/code/frontend/.version @@ -0,0 +1 @@ +v1 diff --git a/code/frontend/.version-dev b/code/frontend/.version-dev new file mode 100644 index 0000000..c227083 --- /dev/null +++ b/code/frontend/.version-dev @@ -0,0 +1 @@ +0 \ No newline at end of file diff --git a/code/frontend/Dockerfile b/code/frontend/Dockerfile new file mode 100644 index 0000000..e381c8d --- /dev/null +++ b/code/frontend/Dockerfile @@ -0,0 +1,13 @@ +FROM registry.hub.docker.com/library/node:lts-buster-slim AS builder +WORKDIR . +COPY package.json . +RUN npm i +COPY . . +RUN npm run build +FROM registry.hub.docker.com/library/node:lts-buster-slim +USER node:node +WORKDIR . +COPY --from=builder --chown=node:node build build +COPY --from=builder --chown=node:node node_modules node_modules +COPY --chown=node:node package.json . +CMD ["node","build"] \ No newline at end of file diff --git a/code/frontend/build_and_push.sh b/code/frontend/build_and_push.sh new file mode 100644 index 0000000..6143419 --- /dev/null +++ b/code/frontend/build_and_push.sh @@ -0,0 +1,89 @@ +#!/usr/bin/env bash + +set -Eueo pipefail + +CURRENT_DEV_VERSION=$(cat .version-dev) +CURRENT_DEV_VERSION_INT=${CURRENT_DEV_VERSION//[!0-9]/} +CURRENT_VERSION=$(cat .version) +CURRENT_VERSION_INT=${CURRENT_VERSION//[!0-9]/} +if [ ${1-prod} == "dev" ]; then + NEW_VERSION="v$((CURRENT_DEV_VERSION_INT + 1))-dev" + OLD_VERSION=$CURRENT_DEV_VERSION +else + NEW_VERSION="v$((CURRENT_VERSION_INT + 1))" + OLD_VERSION=$CURRENT_VERSION +fi +IMAGE_NAME="greatoffice/app" +HUB_NAME="dr.ivar.systems/greatoffice/app" + +# Check for uncommited changes and optionally commit them +if [ "$(git status --untracked-files=no --porcelain)" ]; then + echo "Unclean git tree! press CTRL+C to exit or press ENTER to commit and push to the default branch" + read -n 1 + + read -p "Enter commit message: " COMMIT_MESSAGE + git add .. + git commit --quiet -m "$COMMIT_MESSAGE" +fi + +if [ ${1-prod} == "dev" ]; then + echo $NEW_VERSION >|.version-dev + git add .version-dev +else + echo $NEW_VERSION >|.version + git add .version +fi + +echo "Starting build of $HUB_NAME:$NEW_VERSION at $(date -u)..." +echo + +# Put version.txt inside of server +pushd static +echo "$NEW_VERSION" >version.txt +git add version.txt +popd + +git commit --quiet -m "chore(release): Bump version" + +read -p "Do you want to tag this build? (y/n) " -n 1 -r +echo +if [[ $REPLY =~ ^[Yy]$ ]]; then + read -p "Enter tag message (can be empty): " TAG_MESSAGE + git tag -am "$TAG_MESSAGE" $NEW_VERSION +fi + +read -p "Do you want to push the latest commits and tags to origin? (y/n) " -n 1 -r +echo +if [[ $REPLY =~ ^[Yy]$ ]]; then + echo "Pushing latest changes to remotes..." + echo + git push --quiet --follow-tags +fi + +# Build docker image +echo "Building docker image" +echo + +docker build -t $IMAGE_NAME:$NEW_VERSION . + +docker tag $IMAGE_NAME:$NEW_VERSION $HUB_NAME:$NEW_VERSION + +if [ ${1-prod} == "dev" ]; then + docker tag $IMAGE_NAME:$NEW_VERSION $HUB_NAME:latest-dev +fi +if [ ${1-prod} == "prod" ]; then + docker tag $IMAGE_NAME:$NEW_VERSION $HUB_NAME:latest +fi + +# Optionally push images to docker registry +echo "Press CTRL+C to exit or press ENTER to push docker image to registry" +read -n 1 +docker push $HUB_NAME:$NEW_VERSION + +if [ ${1-prod} == "dev" ]; then + docker push $HUB_NAME:latest-dev +fi + +if [ ${1-prod} == "prod" ]; then + docker push $HUB_NAME:latest +fi diff --git a/code/frontend/bun.lockb b/code/frontend/bun.lockb new file mode 100755 index 0000000..8da4b45 Binary files /dev/null and b/code/frontend/bun.lockb differ diff --git a/code/frontend/components.json b/code/frontend/components.json new file mode 100644 index 0000000..cb6136f --- /dev/null +++ b/code/frontend/components.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://shadcn-svelte.com/schema.json", + "style": "new-york", + "tailwind": { + "config": "tailwind.config.js", + "css": "src/app.pcss", + "baseColor": "stone" + }, + "aliases": { + "components": "$components", + "utils": "$utils/ui" + }, + "typescript": true +} diff --git a/code/frontend/package.json b/code/frontend/package.json new file mode 100644 index 0000000..71f27d3 --- /dev/null +++ b/code/frontend/package.json @@ -0,0 +1,65 @@ +{ + "name": "greatoffice-frontend", + "version": "0.0.1", + "private": true, + "scripts": { + "dev": "npm-run-all --parallel vite typesafe-i18n", + "typesafe-i18n": "typesafe-i18n", + "vite": "vite dev", + "build": "vite build", + "preview": "vite preview", + "test": "npm run test:integration && npm run test:unit", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + "lint": "prettier --check . && eslint .", + "format": "prettier --write .", + "test:integration": "playwright test", + "test:unit": "vitest" + }, + "devDependencies": { + "@playwright/test": "^1.28.1", + "@sveltejs/adapter-node": "^5.0.1", + "@sveltejs/kit": "^2.0.0", + "@sveltejs/vite-plugin-svelte": "^3.0.0", + "@tanstack/svelte-query": "^5.29.0", + "@tanstack/svelte-table": "^8.16.0", + "@types/eslint": "^8.56.0", + "@typescript-eslint/eslint-plugin": "^7.0.0", + "@faker-js/faker": "^8.4.1", + "@typescript-eslint/parser": "^7.0.0", + "@vite-pwa/sveltekit": "^0.4.0", + "npm-run-all": "^4.1.5", + "typesafe-i18n": "^5.26.2", + "autoprefixer": "^10.4.16", + "bits-ui": "^0.21.3", + "clsx": "^2.1.0", + "eslint": "^8.56.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-svelte": "^2.35.1", + "formsnap": "^1.0.0", + "mode-watcher": "^0.3.0", + "paneforge": "^0.0.4", + "postcss": "^8.4.32", + "postcss-load-config": "^5.0.2", + "prettier": "^3.1.1", + "prettier-plugin-svelte": "^3.1.2", + "prettier-plugin-tailwindcss": "^0.5.9", + "scheduler-polyfill": "^1.2.1", + "svelte": "^4.2.7", + "svelte-check": "^3.6.0", + "svelte-interactions": "^0.2.0", + "svelte-radix": "^1.1.0", + "svelte-sonner": "^0.3.22", + "sveltekit-superforms": "^2.12.4", + "tailwind-merge": "^2.2.2", + "tailwind-variants": "^0.2.1", + "tailwindcss": "^3.3.6", + "temporal-polyfill": "^0.2.4", + "tslib": "^2.4.1", + "typescript": "^5.0.0", + "vite": "^5.0.3", + "ofetch": "^1.3.4", + "vitest": "^1.2.0" + }, + "type": "module" +} diff --git a/code/frontend/playwright.config.ts b/code/frontend/playwright.config.ts new file mode 100644 index 0000000..30e57ee --- /dev/null +++ b/code/frontend/playwright.config.ts @@ -0,0 +1,12 @@ +import type { PlaywrightTestConfig } from '@playwright/test'; + +const config: PlaywrightTestConfig = { + webServer: { + command: 'bun run build && bun run preview', + port: 4173 + }, + testDir: 'tests', + testMatch: /(.+\.)?(test|spec)\.[jt]s/ +}; + +export default config; diff --git a/code/frontend/postcss.config.cjs b/code/frontend/postcss.config.cjs new file mode 100644 index 0000000..fe10e55 --- /dev/null +++ b/code/frontend/postcss.config.cjs @@ -0,0 +1,13 @@ +const tailwindcss = require('tailwindcss'); +const autoprefixer = require('autoprefixer'); + +const config = { + plugins: [ + //Some plugins, like tailwindcss/nesting, need to run before Tailwind, + tailwindcss(), + //But others, like autoprefixer, need to run after, + autoprefixer + ] +}; + +module.exports = config; diff --git a/code/frontend/src/actions/pwKey.ts b/code/frontend/src/actions/pwKey.ts new file mode 100644 index 0000000..e8f615c --- /dev/null +++ b/code/frontend/src/actions/pwKey.ts @@ -0,0 +1,4 @@ +export default function pwKey(node: HTMLElement, value: string | undefined) { + if (!value) return; + node.setAttribute("pw-key", value); +} \ No newline at end of file diff --git a/code/frontend/src/app.d.ts b/code/frontend/src/app.d.ts new file mode 100644 index 0000000..743f07b --- /dev/null +++ b/code/frontend/src/app.d.ts @@ -0,0 +1,13 @@ +// See https://kit.svelte.dev/docs/types#app +// for information about these interfaces +declare global { + namespace App { + // interface Error {} + // interface Locals {} + // interface PageData {} + // interface PageState {} + // interface Platform {} + } +} + +export {}; diff --git a/code/frontend/src/app.html b/code/frontend/src/app.html new file mode 100644 index 0000000..77a5ff5 --- /dev/null +++ b/code/frontend/src/app.html @@ -0,0 +1,12 @@ + + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/code/frontend/src/app.pcss b/code/frontend/src/app.pcss new file mode 100644 index 0000000..37c673f --- /dev/null +++ b/code/frontend/src/app.pcss @@ -0,0 +1,108 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 20 14.3% 4.1%; + + --muted: 60 4.8% 95.9%; + --muted-foreground: 25 5.3% 44.7%; + + --popover: 0 0% 100%; + --popover-foreground: 20 14.3% 4.1%; + + --card: 0 0% 100%; + --card-foreground: 20 14.3% 4.1%; + + --border: 20 5.9% 90%; + --input: 20 5.9% 90%; + + --primary: 24 9.8% 10%; + --primary-foreground: 60 9.1% 97.8%; + + --secondary: 60 4.8% 95.9%; + --secondary-foreground: 24 9.8% 10%; + + --accent: 60 4.8% 95.9%; + --accent-foreground: 24 9.8% 10%; + + --destructive: 0 72.2% 50.6%; + --destructive-foreground: 60 9.1% 97.8%; + + --ring: 20 14.3% 4.1%; + + --radius: 0.5rem; + } + + .dark { + --background: 20 14.3% 4.1%; + --foreground: 60 9.1% 97.8%; + + --muted: 12 6.5% 15.1%; + --muted-foreground: 24 5.4% 63.9%; + + --popover: 20 14.3% 4.1%; + --popover-foreground: 60 9.1% 97.8%; + + --card: 20 14.3% 4.1%; + --card-foreground: 60 9.1% 97.8%; + + --border: 12 6.5% 15.1%; + --input: 12 6.5% 15.1%; + + --primary: 60 9.1% 97.8%; + --primary-foreground: 24 9.8% 10%; + + --secondary: 12 6.5% 15.1%; + --secondary-foreground: 60 9.1% 97.8%; + + --accent: 12 6.5% 15.1%; + --accent-foreground: 60 9.1% 97.8%; + + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 60 9.1% 97.8%; + + --ring: 24 5.7% 82.9%; + } +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } +} + +pre { + font-family: monospace !important; +} + +*:focus-visible { + outline: 1px auto; +} + +.c-disabled { + cursor: not-allowed !important; + filter: opacity(.45); + pointer-events: none !important; +} + +.c-disabled.loading { + cursor: wait !important; +} + +.link { + @apply text-blue-600 hover:text-blue-700 transition duration-300 ease-in-out mb-4 cursor-pointer; + + &.danger { + @apply text-red-600 hover:text-red-700; + } + + &.active { + @apply underline + } +} diff --git a/code/frontend/src/components/locale-switcher.svelte b/code/frontend/src/components/locale-switcher.svelte new file mode 100644 index 0000000..fc03f39 --- /dev/null +++ b/code/frontend/src/components/locale-switcher.svelte @@ -0,0 +1,56 @@ + + + diff --git a/code/frontend/src/components/sonner.svelte b/code/frontend/src/components/sonner.svelte new file mode 100644 index 0000000..422e189 --- /dev/null +++ b/code/frontend/src/components/sonner.svelte @@ -0,0 +1,21 @@ + + + diff --git a/code/frontend/src/components/style-changer.svelte b/code/frontend/src/components/style-changer.svelte new file mode 100644 index 0000000..b219b10 --- /dev/null +++ b/code/frontend/src/components/style-changer.svelte @@ -0,0 +1,22 @@ + + + + + + + + setMode('light')}>Light + setMode('dark')}>Dark + resetMode()}>System + + diff --git a/code/frontend/src/components/ui/button/button.svelte b/code/frontend/src/components/ui/button/button.svelte new file mode 100644 index 0000000..196ae77 --- /dev/null +++ b/code/frontend/src/components/ui/button/button.svelte @@ -0,0 +1,25 @@ + + + + + diff --git a/code/frontend/src/components/ui/button/index.ts b/code/frontend/src/components/ui/button/index.ts new file mode 100644 index 0000000..9cfd91c --- /dev/null +++ b/code/frontend/src/components/ui/button/index.ts @@ -0,0 +1,48 @@ +import type { Button as ButtonPrimitive } from 'bits-ui' +import { type VariantProps, tv } from 'tailwind-variants' +import Root from './button.svelte' + +const buttonVariants = tv({ + base: 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50', + variants: { + variant: { + default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90', + destructive: 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90', + outline: 'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground', + secondary: 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80', + ghost: 'hover:bg-accent hover:text-accent-foreground', + link: 'text-primary underline-offset-4 hover:underline' + }, + size: { + default: 'h-9 px-4 py-2', + sm: 'h-8 rounded-md px-3 text-xs', + lg: 'h-10 rounded-md px-8', + icon: 'h-9 w-9' + } + }, + defaultVariants: { + variant: 'default', + size: 'default' + } +}) + +type Variant = VariantProps['variant'] +type Size = VariantProps['size'] + +type Props = ButtonPrimitive.Props & { + variant?: Variant + size?: Size +} + +type Events = ButtonPrimitive.Events + +export { + Root, + type Props, + type Events, + // + Root as Button, + type Props as ButtonProps, + type Events as ButtonEvents, + buttonVariants +} diff --git a/code/frontend/src/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte b/code/frontend/src/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte new file mode 100644 index 0000000..ea02af0 --- /dev/null +++ b/code/frontend/src/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte @@ -0,0 +1,35 @@ + + + + + + + + + + diff --git a/code/frontend/src/components/ui/dropdown-menu/dropdown-menu-content.svelte b/code/frontend/src/components/ui/dropdown-menu/dropdown-menu-content.svelte new file mode 100644 index 0000000..a2b8da7 --- /dev/null +++ b/code/frontend/src/components/ui/dropdown-menu/dropdown-menu-content.svelte @@ -0,0 +1,26 @@ + + + + + diff --git a/code/frontend/src/components/ui/dropdown-menu/dropdown-menu-item.svelte b/code/frontend/src/components/ui/dropdown-menu/dropdown-menu-item.svelte new file mode 100644 index 0000000..ed45da7 --- /dev/null +++ b/code/frontend/src/components/ui/dropdown-menu/dropdown-menu-item.svelte @@ -0,0 +1,31 @@ + + + + + diff --git a/code/frontend/src/components/ui/dropdown-menu/dropdown-menu-label.svelte b/code/frontend/src/components/ui/dropdown-menu/dropdown-menu-label.svelte new file mode 100644 index 0000000..69fddd1 --- /dev/null +++ b/code/frontend/src/components/ui/dropdown-menu/dropdown-menu-label.svelte @@ -0,0 +1,19 @@ + + + + + diff --git a/code/frontend/src/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte b/code/frontend/src/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte new file mode 100644 index 0000000..c07bd1a --- /dev/null +++ b/code/frontend/src/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte @@ -0,0 +1,11 @@ + + + + + diff --git a/code/frontend/src/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte b/code/frontend/src/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte new file mode 100644 index 0000000..c754953 --- /dev/null +++ b/code/frontend/src/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte @@ -0,0 +1,35 @@ + + + + + + + + + + diff --git a/code/frontend/src/components/ui/dropdown-menu/dropdown-menu-separator.svelte b/code/frontend/src/components/ui/dropdown-menu/dropdown-menu-separator.svelte new file mode 100644 index 0000000..b6c5798 --- /dev/null +++ b/code/frontend/src/components/ui/dropdown-menu/dropdown-menu-separator.svelte @@ -0,0 +1,11 @@ + + + diff --git a/code/frontend/src/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte b/code/frontend/src/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte new file mode 100644 index 0000000..f9e5953 --- /dev/null +++ b/code/frontend/src/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte @@ -0,0 +1,13 @@ + + + + + diff --git a/code/frontend/src/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte b/code/frontend/src/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte new file mode 100644 index 0000000..7c00a1b --- /dev/null +++ b/code/frontend/src/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte @@ -0,0 +1,29 @@ + + + + + diff --git a/code/frontend/src/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte b/code/frontend/src/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte new file mode 100644 index 0000000..4967d2b --- /dev/null +++ b/code/frontend/src/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte @@ -0,0 +1,32 @@ + + + + + + diff --git a/code/frontend/src/components/ui/dropdown-menu/index.ts b/code/frontend/src/components/ui/dropdown-menu/index.ts new file mode 100644 index 0000000..df959fa --- /dev/null +++ b/code/frontend/src/components/ui/dropdown-menu/index.ts @@ -0,0 +1,48 @@ +import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui' +import Item from './dropdown-menu-item.svelte' +import Label from './dropdown-menu-label.svelte' +import Content from './dropdown-menu-content.svelte' +import Shortcut from './dropdown-menu-shortcut.svelte' +import RadioItem from './dropdown-menu-radio-item.svelte' +import Separator from './dropdown-menu-separator.svelte' +import RadioGroup from './dropdown-menu-radio-group.svelte' +import SubContent from './dropdown-menu-sub-content.svelte' +import SubTrigger from './dropdown-menu-sub-trigger.svelte' +import CheckboxItem from './dropdown-menu-checkbox-item.svelte' + +const Sub = DropdownMenuPrimitive.Sub +const Root = DropdownMenuPrimitive.Root +const Trigger = DropdownMenuPrimitive.Trigger +const Group = DropdownMenuPrimitive.Group + +export { + Sub, + Root, + Item, + Label, + Group, + Trigger, + Content, + Shortcut, + Separator, + RadioItem, + SubContent, + SubTrigger, + RadioGroup, + CheckboxItem, + // + Root as DropdownMenu, + Sub as DropdownMenuSub, + Item as DropdownMenuItem, + Label as DropdownMenuLabel, + Group as DropdownMenuGroup, + Content as DropdownMenuContent, + Trigger as DropdownMenuTrigger, + Shortcut as DropdownMenuShortcut, + RadioItem as DropdownMenuRadioItem, + Separator as DropdownMenuSeparator, + RadioGroup as DropdownMenuRadioGroup, + SubContent as DropdownMenuSubContent, + SubTrigger as DropdownMenuSubTrigger, + CheckboxItem as DropdownMenuCheckboxItem +} diff --git a/code/frontend/src/configuration/index.ts b/code/frontend/src/configuration/index.ts new file mode 100644 index 0000000..1ffd67f --- /dev/null +++ b/code/frontend/src/configuration/index.ts @@ -0,0 +1,38 @@ +export const APP_ADDRESS = "https://stage.greatoffice.app"; +export const API_ADDRESS = "https://stage-api.greatoffice.app"; +export const DEV_APP_ADDRESS = "http://localhost"; +export const DEV_API_ADDRESS = "http://localhost:5000"; + +export function api_base(path: string = "", explicitVersion = 1): string { + if (path && !path.startsWith("_")) path = "v" + explicitVersion + path; + return (is_development() ? DEV_API_ADDRESS : API_ADDRESS) + (path !== "" ? "/" + path : ""); +} + +export function is_development(): boolean { + return import.meta.env.DEV; +} + +export const CookieNames = { + theme: "go_theme", + locale: "go_locale", + session: "go_session", +}; + +export const QueryKeys = { + labels: "labels", + categories: "categories", + entries: "entries", +}; + +export const StorageKeys = { + session: "sessionData", + theme: "theme", + debug: "debug", + categories: "categories", + labels: "labels", + entries: "entries", + stopwatch: "stopwatchState", + logLevel: "logLevel", +}; + +export type PortalMessage = "emailValidated"; \ No newline at end of file diff --git a/code/frontend/src/configuration/test.ts b/code/frontend/src/configuration/test.ts new file mode 100644 index 0000000..12392de --- /dev/null +++ b/code/frontend/src/configuration/test.ts @@ -0,0 +1,21 @@ +import {env} from "$env/dynamic/private"; + +export function get_test_context(): TestContext { + return { + user: { + username: env.TEST_USERNAME, + password: env.TEST_PASSWORD, + }, + }; +} + +export function is_testing(): boolean { + return env.TESTING == "true"; +} + +export interface TestContext { + user: { + username: string, + password: string + }; +} diff --git a/code/frontend/src/global.d.ts b/code/frontend/src/global.d.ts new file mode 100644 index 0000000..13f5e16 --- /dev/null +++ b/code/frontend/src/global.d.ts @@ -0,0 +1,11 @@ +/// + +type Locales = import('$lib/i18n/i18n-types').Locales +type TranslationFunctions = import('$lib/i18n/i18n-types').TranslationFunctions + +declare namespace App { + interface Locals { + locale: Locales + LL: TranslationFunctions + } +} \ No newline at end of file diff --git a/code/frontend/src/hooks.server.ts b/code/frontend/src/hooks.server.ts new file mode 100644 index 0000000..b636e31 --- /dev/null +++ b/code/frontend/src/hooks.server.ts @@ -0,0 +1,48 @@ +import {CookieNames} from "$configuration"; +import {detectLocale, i18n, isLocale, locales} from "$i18n/i18n-util"; +import type {Handle, RequestEvent} from "@sveltejs/kit"; +import {initAcceptLanguageHeaderDetector} from "typesafe-i18n/detectors"; +import type {Locales} from "$i18n/i18n-types"; +import {loadAllLocales} from "$i18n/i18n-util.sync"; + +loadAllLocales(); +const L = i18n(); + +export const handle: Handle = async ({event, resolve}) => { + const localeCookie = event.cookies.get(CookieNames.locale); + const preferredLocale = getPreferredLocale(event); + let finalLocale = localeCookie ?? preferredLocale; + let forceCookieSet = false; + + console.debug("Handling locale", { + locales, + localeCookie, + preferredLocale, + finalLocale, + }); + + if (!isLocale(finalLocale)) { + console.debug(finalLocale + " is not a valid locale or it does not exist, switching to default: en"); + finalLocale = "en"; + forceCookieSet = true; + } + + if (!localeCookie || forceCookieSet) { + // Set a locale cookie + event.cookies.set(CookieNames.locale, finalLocale, { + sameSite: "strict", + path: "/", + httpOnly: false, + }); + } + + event.locals.locale = finalLocale as Locales; + event.locals.LL = L[finalLocale as Locales]; + + return resolve(event, {transformPageChunk: ({html}) => html.replace("%lang%", finalLocale)}); +}; + +function getPreferredLocale(event: RequestEvent) { + const acceptLanguageDetector = initAcceptLanguageHeaderDetector(event.request); + return detectLocale(acceptLanguageDetector); +} diff --git a/code/frontend/src/i18n/en/app/index.ts b/code/frontend/src/i18n/en/app/index.ts new file mode 100644 index 0000000..7ccfc97 --- /dev/null +++ b/code/frontend/src/i18n/en/app/index.ts @@ -0,0 +1,7 @@ +import type { BaseTranslation } from '../../i18n-types' + +const en_app: BaseTranslation = { + members: "Members", +} + +export default en_app \ No newline at end of file diff --git a/code/frontend/src/i18n/en/index.ts b/code/frontend/src/i18n/en/index.ts new file mode 100644 index 0000000..b38eb48 --- /dev/null +++ b/code/frontend/src/i18n/en/index.ts @@ -0,0 +1,63 @@ +import type { BaseTranslation } from "../i18n-types"; + +const en: BaseTranslation = { + or: "Or", + name: "Name", + emailAddress: "Email address", + password: "Password", + pageNotFound: "Page not found", + noInternet: "It seems like your device does not have a internet connection, please check your connection.", + reset: "Reset", + of: "{0} of {1}", + isRequired: "{0} is required", + submit: "Submit", + success: "Success", + tryAgainSoon: "Try again soon", + createANewAccount: "Create a new account", + unexpectedError: "An unexpected error occured", + notFound: "Not found", + documentation: "Documentation", + tos: "Terms of service", + privacyPolicy: "Privacy policy", + signIntoYourAccount: "Sign into your account", + combobox: { + search: "Search", + noRecordsFound: "No records found", + createRecordHelpText: "Create a record by typing the name in the search bar and pressing enter", + createRecordButtonText: "Press enter or click here to create {0}" + }, + signInPage: { + title: "Sign in", + notMyComputer: "This is not my computer", + resetPassword: "Reset password", + yourPasswordIsUpdated: "Your password is updated", + signIn: "Sign In", + yourNewPasswordIsApplied: "Your new password is applied", + signInBelow: "Sign in below", + yourAccountIsDisabled: "Your account is disabled", + contactYourAdminIfDisabled: "Contact your administrator if this feels wrong", + youHaveReachedInactivityLimit: "You've reached the hidden inactivity limit", + feelFreeToSignInAgain: "Feel free to sign in again" + }, + signUpPage: { + title: "Sign up", + createYourNewAccount: "Create your new account", + }, + resetPasswordPage: { + title: "Reset password", + fulfillTitle: "Set new password", + setANewPassword: "Set a new password", + expired: "Expired", + requestHasExpired: "Your request has expired", + requestANewReset: "Request a new reset", + invalidRequestTitle: "Your request is invalid", + invalidRequestMessage: "This could be due to it being expired, nonexsistent or something else", + newPassword: "New password", + requestSentMessage: "If we find your email address in our systems, you will receive an email with instructions on how to set a new password for your account.", + requestAPasswordReset: "Request a password reset", + requestNotFound: "Your request was not found", + submitANewRequestBelow: "Submit a new reset request below" + } +}; + +export default en; diff --git a/code/frontend/src/i18n/formatters.ts b/code/frontend/src/i18n/formatters.ts new file mode 100644 index 0000000..f310eb7 --- /dev/null +++ b/code/frontend/src/i18n/formatters.ts @@ -0,0 +1,13 @@ +import { capitalise } from "$utils/misc-helpers"; +import type { FormattersInitializer } from "typesafe-i18n"; +import type { Locales, Formatters } from "./i18n-types"; + +export const initFormatters: FormattersInitializer = (locale: Locales) => { + + const formatters: Formatters = { + // add your formatter functions here + capitalise: (value: string) => capitalise(value), + }; + + return formatters; +}; diff --git a/code/frontend/src/i18n/i18n-svelte.ts b/code/frontend/src/i18n/i18n-svelte.ts new file mode 100644 index 0000000..6cdffb3 --- /dev/null +++ b/code/frontend/src/i18n/i18n-svelte.ts @@ -0,0 +1,12 @@ +// This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten. +/* eslint-disable */ + +import { initI18nSvelte } from 'typesafe-i18n/svelte' +import type { Formatters, Locales, TranslationFunctions, Translations } from './i18n-types' +import { loadedFormatters, loadedLocales } from './i18n-util' + +const { locale, LL, setLocale } = initI18nSvelte(loadedLocales, loadedFormatters) + +export { locale, LL, setLocale } + +export default LL diff --git a/code/frontend/src/i18n/i18n-types.ts b/code/frontend/src/i18n/i18n-types.ts new file mode 100644 index 0000000..ef1d664 --- /dev/null +++ b/code/frontend/src/i18n/i18n-types.ts @@ -0,0 +1,461 @@ +// This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten. +/* eslint-disable */ +import type { BaseTranslation as BaseTranslationType, LocalizedString, RequiredParams } from 'typesafe-i18n' + +export type BaseTranslation = BaseTranslationType & DisallowNamespaces +export type BaseLocale = 'en' + +export type Locales = + | 'en' + | 'nb' + +export type Translation = RootTranslation & DisallowNamespaces + +export type Translations = RootTranslation & +{ + app: NamespaceAppTranslation +} + +type RootTranslation = { + /** + * O​r + */ + or: string + /** + * N​a​m​e + */ + name: string + /** + * E​m​a​i​l​ ​a​d​d​r​e​s​s + */ + emailAddress: string + /** + * P​a​s​s​w​o​r​d + */ + password: string + /** + * P​a​g​e​ ​n​o​t​ ​f​o​u​n​d + */ + pageNotFound: string + /** + * I​t​ ​s​e​e​m​s​ ​l​i​k​e​ ​y​o​u​r​ ​d​e​v​i​c​e​ ​d​o​e​s​ ​n​o​t​ ​h​a​v​e​ ​a​ ​i​n​t​e​r​n​e​t​ ​c​o​n​n​e​c​t​i​o​n​,​ ​p​l​e​a​s​e​ ​c​h​e​c​k​ ​y​o​u​r​ ​c​o​n​n​e​c​t​i​o​n​. + */ + noInternet: string + /** + * R​e​s​e​t + */ + reset: string + /** + * {​0​}​ ​o​f​ ​{​1​} + * @param {unknown} 0 + * @param {unknown} 1 + */ + of: RequiredParams<'0' | '1'> + /** + * {​0​}​ ​i​s​ ​r​e​q​u​i​r​e​d + * @param {unknown} 0 + */ + isRequired: RequiredParams<'0'> + /** + * S​u​b​m​i​t + */ + submit: string + /** + * S​u​c​c​e​s​s + */ + success: string + /** + * T​r​y​ ​a​g​a​i​n​ ​s​o​o​n + */ + tryAgainSoon: string + /** + * C​r​e​a​t​e​ ​a​ ​n​e​w​ ​a​c​c​o​u​n​t + */ + createANewAccount: string + /** + * A​n​ ​u​n​e​x​p​e​c​t​e​d​ ​e​r​r​o​r​ ​o​c​c​u​r​e​d + */ + unexpectedError: string + /** + * N​o​t​ ​f​o​u​n​d + */ + notFound: string + /** + * D​o​c​u​m​e​n​t​a​t​i​o​n + */ + documentation: string + /** + * T​e​r​m​s​ ​o​f​ ​s​e​r​v​i​c​e + */ + tos: string + /** + * P​r​i​v​a​c​y​ ​p​o​l​i​c​y + */ + privacyPolicy: string + /** + * S​i​g​n​ ​i​n​t​o​ ​y​o​u​r​ ​a​c​c​o​u​n​t + */ + signIntoYourAccount: string + combobox: { + /** + * S​e​a​r​c​h + */ + search: string + /** + * N​o​ ​r​e​c​o​r​d​s​ ​f​o​u​n​d + */ + noRecordsFound: string + /** + * C​r​e​a​t​e​ ​a​ ​r​e​c​o​r​d​ ​b​y​ ​t​y​p​i​n​g​ ​t​h​e​ ​n​a​m​e​ ​i​n​ ​t​h​e​ ​s​e​a​r​c​h​ ​b​a​r​ ​a​n​d​ ​p​r​e​s​s​i​n​g​ ​e​n​t​e​r + */ + createRecordHelpText: string + /** + * P​r​e​s​s​ ​e​n​t​e​r​ ​o​r​ ​c​l​i​c​k​ ​h​e​r​e​ ​t​o​ ​c​r​e​a​t​e​ ​{​0​} + * @param {unknown} 0 + */ + createRecordButtonText: RequiredParams<'0'> + } + signInPage: { + /** + * S​i​g​n​ ​i​n + */ + title: string + /** + * T​h​i​s​ ​i​s​ ​n​o​t​ ​m​y​ ​c​o​m​p​u​t​e​r + */ + notMyComputer: string + /** + * R​e​s​e​t​ ​p​a​s​s​w​o​r​d + */ + resetPassword: string + /** + * Y​o​u​r​ ​p​a​s​s​w​o​r​d​ ​i​s​ ​u​p​d​a​t​e​d + */ + yourPasswordIsUpdated: string + /** + * S​i​g​n​ ​I​n + */ + signIn: string + /** + * Y​o​u​r​ ​n​e​w​ ​p​a​s​s​w​o​r​d​ ​i​s​ ​a​p​p​l​i​e​d + */ + yourNewPasswordIsApplied: string + /** + * S​i​g​n​ ​i​n​ ​b​e​l​o​w + */ + signInBelow: string + /** + * Y​o​u​r​ ​a​c​c​o​u​n​t​ ​i​s​ ​d​i​s​a​b​l​e​d + */ + yourAccountIsDisabled: string + /** + * C​o​n​t​a​c​t​ ​y​o​u​r​ ​a​d​m​i​n​i​s​t​r​a​t​o​r​ ​i​f​ ​t​h​i​s​ ​f​e​e​l​s​ ​w​r​o​n​g + */ + contactYourAdminIfDisabled: string + /** + * Y​o​u​'​v​e​ ​r​e​a​c​h​e​d​ ​t​h​e​ ​h​i​d​d​e​n​ ​i​n​a​c​t​i​v​i​t​y​ ​l​i​m​i​t + */ + youHaveReachedInactivityLimit: string + /** + * F​e​e​l​ ​f​r​e​e​ ​t​o​ ​s​i​g​n​ ​i​n​ ​a​g​a​i​n + */ + feelFreeToSignInAgain: string + } + signUpPage: { + /** + * S​i​g​n​ ​u​p + */ + title: string + /** + * C​r​e​a​t​e​ ​y​o​u​r​ ​n​e​w​ ​a​c​c​o​u​n​t + */ + createYourNewAccount: string + } + resetPasswordPage: { + /** + * R​e​s​e​t​ ​p​a​s​s​w​o​r​d + */ + title: string + /** + * S​e​t​ ​n​e​w​ ​p​a​s​s​w​o​r​d + */ + fulfillTitle: string + /** + * S​e​t​ ​a​ ​n​e​w​ ​p​a​s​s​w​o​r​d + */ + setANewPassword: string + /** + * E​x​p​i​r​e​d + */ + expired: string + /** + * Y​o​u​r​ ​r​e​q​u​e​s​t​ ​h​a​s​ ​e​x​p​i​r​e​d + */ + requestHasExpired: string + /** + * R​e​q​u​e​s​t​ ​a​ ​n​e​w​ ​r​e​s​e​t + */ + requestANewReset: string + /** + * Y​o​u​r​ ​r​e​q​u​e​s​t​ ​i​s​ ​i​n​v​a​l​i​d + */ + invalidRequestTitle: string + /** + * T​h​i​s​ ​c​o​u​l​d​ ​b​e​ ​d​u​e​ ​t​o​ ​i​t​ ​b​e​i​n​g​ ​e​x​p​i​r​e​d​,​ ​n​o​n​e​x​s​i​s​t​e​n​t​ ​o​r​ ​s​o​m​e​t​h​i​n​g​ ​e​l​s​e + */ + invalidRequestMessage: string + /** + * N​e​w​ ​p​a​s​s​w​o​r​d + */ + newPassword: string + /** + * I​f​ ​w​e​ ​f​i​n​d​ ​y​o​u​r​ ​e​m​a​i​l​ ​a​d​d​r​e​s​s​ ​i​n​ ​o​u​r​ ​s​y​s​t​e​m​s​,​ ​y​o​u​ ​w​i​l​l​ ​r​e​c​e​i​v​e​ ​a​n​ ​e​m​a​i​l​ ​w​i​t​h​ ​i​n​s​t​r​u​c​t​i​o​n​s​ ​o​n​ ​h​o​w​ ​t​o​ ​s​e​t​ ​a​ ​n​e​w​ ​p​a​s​s​w​o​r​d​ ​f​o​r​ ​y​o​u​r​ ​a​c​c​o​u​n​t​. + */ + requestSentMessage: string + /** + * R​e​q​u​e​s​t​ ​a​ ​p​a​s​s​w​o​r​d​ ​r​e​s​e​t + */ + requestAPasswordReset: string + /** + * Y​o​u​r​ ​r​e​q​u​e​s​t​ ​w​a​s​ ​n​o​t​ ​f​o​u​n​d + */ + requestNotFound: string + /** + * S​u​b​m​i​t​ ​a​ ​n​e​w​ ​r​e​s​e​t​ ​r​e​q​u​e​s​t​ ​b​e​l​o​w + */ + submitANewRequestBelow: string + } +} + +export type NamespaceAppTranslation = { + /** + * M​e​m​b​e​r​s + */ + members: string +} + +export type Namespaces = + | 'app' + +type DisallowNamespaces = { + /** + * reserved for 'app'-namespace\ + * you need to use the `./app/index.ts` file instead + */ + app?: "[typesafe-i18n] reserved for 'app'-namespace. You need to use the `./app/index.ts` file instead." +} + +export type TranslationFunctions = { + /** + * Or + */ + or: () => LocalizedString + /** + * Name + */ + name: () => LocalizedString + /** + * Email address + */ + emailAddress: () => LocalizedString + /** + * Password + */ + password: () => LocalizedString + /** + * Page not found + */ + pageNotFound: () => LocalizedString + /** + * It seems like your device does not have a internet connection, please check your connection. + */ + noInternet: () => LocalizedString + /** + * Reset + */ + reset: () => LocalizedString + /** + * {0} of {1} + */ + of: (arg0: unknown, arg1: unknown) => LocalizedString + /** + * {0} is required + */ + isRequired: (arg0: unknown) => LocalizedString + /** + * Submit + */ + submit: () => LocalizedString + /** + * Success + */ + success: () => LocalizedString + /** + * Try again soon + */ + tryAgainSoon: () => LocalizedString + /** + * Create a new account + */ + createANewAccount: () => LocalizedString + /** + * An unexpected error occured + */ + unexpectedError: () => LocalizedString + /** + * Not found + */ + notFound: () => LocalizedString + /** + * Documentation + */ + documentation: () => LocalizedString + /** + * Terms of service + */ + tos: () => LocalizedString + /** + * Privacy policy + */ + privacyPolicy: () => LocalizedString + /** + * Sign into your account + */ + signIntoYourAccount: () => LocalizedString + combobox: { + /** + * Search + */ + search: () => LocalizedString + /** + * No records found + */ + noRecordsFound: () => LocalizedString + /** + * Create a record by typing the name in the search bar and pressing enter + */ + createRecordHelpText: () => LocalizedString + /** + * Press enter or click here to create {0} + */ + createRecordButtonText: (arg0: unknown) => LocalizedString + } + signInPage: { + /** + * Sign in + */ + title: () => LocalizedString + /** + * This is not my computer + */ + notMyComputer: () => LocalizedString + /** + * Reset password + */ + resetPassword: () => LocalizedString + /** + * Your password is updated + */ + yourPasswordIsUpdated: () => LocalizedString + /** + * Sign In + */ + signIn: () => LocalizedString + /** + * Your new password is applied + */ + yourNewPasswordIsApplied: () => LocalizedString + /** + * Sign in below + */ + signInBelow: () => LocalizedString + /** + * Your account is disabled + */ + yourAccountIsDisabled: () => LocalizedString + /** + * Contact your administrator if this feels wrong + */ + contactYourAdminIfDisabled: () => LocalizedString + /** + * You've reached the hidden inactivity limit + */ + youHaveReachedInactivityLimit: () => LocalizedString + /** + * Feel free to sign in again + */ + feelFreeToSignInAgain: () => LocalizedString + } + signUpPage: { + /** + * Sign up + */ + title: () => LocalizedString + /** + * Create your new account + */ + createYourNewAccount: () => LocalizedString + } + resetPasswordPage: { + /** + * Reset password + */ + title: () => LocalizedString + /** + * Set new password + */ + fulfillTitle: () => LocalizedString + /** + * Set a new password + */ + setANewPassword: () => LocalizedString + /** + * Expired + */ + expired: () => LocalizedString + /** + * Your request has expired + */ + requestHasExpired: () => LocalizedString + /** + * Request a new reset + */ + requestANewReset: () => LocalizedString + /** + * Your request is invalid + */ + invalidRequestTitle: () => LocalizedString + /** + * This could be due to it being expired, nonexsistent or something else + */ + invalidRequestMessage: () => LocalizedString + /** + * New password + */ + newPassword: () => LocalizedString + /** + * If we find your email address in our systems, you will receive an email with instructions on how to set a new password for your account. + */ + requestSentMessage: () => LocalizedString + /** + * Request a password reset + */ + requestAPasswordReset: () => LocalizedString + /** + * Your request was not found + */ + requestNotFound: () => LocalizedString + /** + * Submit a new reset request below + */ + submitANewRequestBelow: () => LocalizedString + } + app: { + /** + * Members + */ + members: () => LocalizedString + } +} + +export type Formatters = {} diff --git a/code/frontend/src/i18n/i18n-util.async.ts b/code/frontend/src/i18n/i18n-util.async.ts new file mode 100644 index 0000000..2e6717e --- /dev/null +++ b/code/frontend/src/i18n/i18n-util.async.ts @@ -0,0 +1,42 @@ +// This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten. +/* eslint-disable */ + +import { initFormatters } from './formatters' +import type { Locales, Namespaces, Translations } from './i18n-types' +import { loadedFormatters, loadedLocales, locales } from './i18n-util' + +const localeTranslationLoaders = { + en: () => import('./en'), + nb: () => import('./nb'), +} + +const localeNamespaceLoaders = { + en: { + app: () => import('./en/app') + }, + nb: { + app: () => import('./nb/app') + } +} + +const updateDictionary = (locale: Locales, dictionary: Partial): Translations => + loadedLocales[locale] = { ...loadedLocales[locale], ...dictionary } + +export const importLocaleAsync = async (locale: Locales): Promise => + (await localeTranslationLoaders[locale]()).default as unknown as Translations + +export const loadLocaleAsync = async (locale: Locales): Promise => { + updateDictionary(locale, await importLocaleAsync(locale)) + loadFormatters(locale) +} + +export const loadAllLocalesAsync = (): Promise => Promise.all(locales.map(loadLocaleAsync)) + +export const loadFormatters = (locale: Locales): void => + void (loadedFormatters[locale] = initFormatters(locale)) + +export const importNamespaceAsync = async(locale: Locales, namespace: Namespace) => + (await localeNamespaceLoaders[locale][namespace]()).default as unknown as Translations[Namespace] + +export const loadNamespaceAsync = async (locale: Locales, namespace: Namespace): Promise => + void updateDictionary(locale, { [namespace]: await importNamespaceAsync(locale, namespace )}) diff --git a/code/frontend/src/i18n/i18n-util.sync.ts b/code/frontend/src/i18n/i18n-util.sync.ts new file mode 100644 index 0000000..8144fdc --- /dev/null +++ b/code/frontend/src/i18n/i18n-util.sync.ts @@ -0,0 +1,35 @@ +// This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten. +/* eslint-disable */ + +import { initFormatters } from './formatters' +import type { Locales, Translations } from './i18n-types' +import { loadedFormatters, loadedLocales, locales } from './i18n-util' + +import en from './en' +import nb from './nb' + +import en_app from './en/app' +import nb_app from './nb/app' + +const localeTranslations = { + en: { + ...en, + app: en_app + }, + nb: { + ...nb, + app: nb_app + }, +} + +export const loadLocale = (locale: Locales): void => { + if (loadedLocales[locale]) return + + loadedLocales[locale] = localeTranslations[locale] as unknown as Translations + loadFormatters(locale) +} + +export const loadAllLocales = (): void => locales.forEach(loadLocale) + +export const loadFormatters = (locale: Locales): void => + void (loadedFormatters[locale] = initFormatters(locale)) diff --git a/code/frontend/src/i18n/i18n-util.ts b/code/frontend/src/i18n/i18n-util.ts new file mode 100644 index 0000000..55b52bd --- /dev/null +++ b/code/frontend/src/i18n/i18n-util.ts @@ -0,0 +1,44 @@ +// This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten. +/* eslint-disable */ + +import { i18n as initI18n, i18nObject as initI18nObject, i18nString as initI18nString } from 'typesafe-i18n' +import type { LocaleDetector } from 'typesafe-i18n/detectors' +import type { LocaleTranslationFunctions, TranslateByString } from 'typesafe-i18n' +import { detectLocale as detectLocaleFn } from 'typesafe-i18n/detectors' +import { initExtendDictionary } from 'typesafe-i18n/utils' +import type { Formatters, Locales, Namespaces, Translations, TranslationFunctions } from './i18n-types' + +export const baseLocale: Locales = 'en' + +export const locales: Locales[] = [ + 'en', + 'nb' +] + +export const namespaces: Namespaces[] = [ + 'app' +] + +export const isLocale = (locale: string): locale is Locales => locales.includes(locale as Locales) + +export const isNamespace = (namespace: string): namespace is Namespaces => namespaces.includes(namespace as Namespaces) + +export const loadedLocales: Record = {} as Record + +export const loadedFormatters: Record = {} as Record + +export const extendDictionary = initExtendDictionary() + +export const i18nString = (locale: Locales): TranslateByString => initI18nString(locale, loadedFormatters[locale]) + +export const i18nObject = (locale: Locales): TranslationFunctions => + initI18nObject( + locale, + loadedLocales[locale], + loadedFormatters[locale] + ) + +export const i18n = (): LocaleTranslationFunctions => + initI18n(loadedLocales, loadedFormatters) + +export const detectLocale = (...detectors: LocaleDetector[]): Locales => detectLocaleFn(baseLocale, locales, ...detectors) diff --git a/code/frontend/src/i18n/nb/app/index.ts b/code/frontend/src/i18n/nb/app/index.ts new file mode 100644 index 0000000..6bf9ba6 --- /dev/null +++ b/code/frontend/src/i18n/nb/app/index.ts @@ -0,0 +1,7 @@ +import type { NamespaceAppTranslation } from '../../i18n-types' + +const nb_app: NamespaceAppTranslation = { + members: "Medlemmer" +} + +export default nb_app diff --git a/code/frontend/src/i18n/nb/index.ts b/code/frontend/src/i18n/nb/index.ts new file mode 100644 index 0000000..ef67504 --- /dev/null +++ b/code/frontend/src/i18n/nb/index.ts @@ -0,0 +1,51 @@ +import type { Translation } from "../i18n-types"; + +const nb: Translation = { + or: "Eller", + name: "Navn", + emailAddress: "E-postadresse", + password: "Passord", + pageNotFound: "Fant ikke siden", + noInternet: "Det ser ut som at du ikke tilkoblet internettet, sjekk tilkoblingen din for å fortsette", + reset: "Tilbakestill", + of: "{0} av {1}", + isRequired: "{0} er påkrevd", + submit: "Send", + success: "Suksess", + tryAgainSoon: "Prøv igjen snart", + createANewAccount: "Lag en ny konto", + unexpectedError: "En uventet feil oppstod", + notFound: "Ikke funnet", + documentation: "Dokumentasjon", + tos: "Vilkår", + privacyPolicy: "Personvernerklæring", + signIntoYourAccount: "Logg inn med din konto", + signInPage: { + notMyComputer: "Dette er ikke min datamaskin", + resetPassword: "Tilbakestill passord", + yourPasswordIsUpdated: "Ditt passord er oppdater", + signIn: "Logg inn", + yourNewPasswordIsApplied: "Ditt nye passord er satt", + signInBelow: "Logg inn nedenfor", + yourAccountIsDisabled: "Din konto er deaktivert", + contactYourAdminIfDisabled: "Ta kontakt med din administrator hvis dette føles feil", + youHaveReachedInactivityLimit: "Du har nådd den hemmelige inaktivitetsgrensen", + feelFreeToSignInAgain: "Logg gjerne inn igjen" + }, + signUpPage: { + createYourNewAccount: "Opprett din nye konto", + }, + resetPasswordPage: { + setANewPassword: "Skriv et nytt passord", + expired: "Utgått", + requestHasExpired: "Din forespørsel er utgått", + requestANewReset: "Spør om en ny tilbakestillingslenke", + newPassword: "Nytt passord", + requestSentMessage: "Hvis vi finner e-postadressen din i våre systemer, vil du få en e-post med instrukser for å sette ditt nye passord.", + requestAPasswordReset: "Forespør tilbakestilling av ditt passord", + requestNotFound: "Din forespørsel ble ikke funnet", + submitANewRequestBelow: "Spør om en ny tilbakestillingslenke nedenfor" + } +} + +export default nb; \ No newline at end of file diff --git a/code/frontend/src/index.test.ts b/code/frontend/src/index.test.ts new file mode 100644 index 0000000..e07cbbd --- /dev/null +++ b/code/frontend/src/index.test.ts @@ -0,0 +1,7 @@ +import { describe, it, expect } from 'vitest'; + +describe('sum test', () => { + it('adds 1 + 2 to equal 3', () => { + expect(1 + 2).toBe(3); + }); +}); diff --git a/code/frontend/src/models/base/Customer.ts b/code/frontend/src/models/base/Customer.ts new file mode 100644 index 0000000..ff52fbd --- /dev/null +++ b/code/frontend/src/models/base/Customer.ts @@ -0,0 +1,21 @@ +import type {CustomerContact} from "./CustomerContact"; +import type {User} from "./User"; + +export type Customer = { + /** + * Guid id for customer + */ + id: string, + /** + * The name of the company + */ + name: string, + /** + * Responsible contact in the current tenant + */ + tenantContact: User, + /** + * The customers main contact + */ + mainContact: CustomerContact, +} \ No newline at end of file diff --git a/code/frontend/src/models/base/CustomerContact.ts b/code/frontend/src/models/base/CustomerContact.ts new file mode 100644 index 0000000..e8abea5 --- /dev/null +++ b/code/frontend/src/models/base/CustomerContact.ts @@ -0,0 +1,8 @@ +export type CustomerContact = { + firstName: string, + lastname: string, + email: string, + phone: string, + workTitle: string, + note: string +} \ No newline at end of file diff --git a/code/frontend/src/models/base/CustomerEvent.ts b/code/frontend/src/models/base/CustomerEvent.ts new file mode 100644 index 0000000..af86511 --- /dev/null +++ b/code/frontend/src/models/base/CustomerEvent.ts @@ -0,0 +1,6 @@ +export type CustomerEvent = { + /** + * A descriptive name for the occured event + */ + name: string, +} \ No newline at end of file diff --git a/code/frontend/src/models/base/SessionData.ts b/code/frontend/src/models/base/SessionData.ts new file mode 100644 index 0000000..015cbf3 --- /dev/null +++ b/code/frontend/src/models/base/SessionData.ts @@ -0,0 +1,5 @@ +export type SessionData = { + id: string, + username: string, + displayName: string, +} \ No newline at end of file diff --git a/code/frontend/src/models/base/Tenant.ts b/code/frontend/src/models/base/Tenant.ts new file mode 100644 index 0000000..6307efc --- /dev/null +++ b/code/frontend/src/models/base/Tenant.ts @@ -0,0 +1,8 @@ +import type {User} from "./User"; + +export type Tenant = { + id: string, + name: string, + description: string, + masterUser: User, +} \ No newline at end of file diff --git a/code/frontend/src/models/base/User.ts b/code/frontend/src/models/base/User.ts new file mode 100644 index 0000000..2b74d0e --- /dev/null +++ b/code/frontend/src/models/base/User.ts @@ -0,0 +1,13 @@ +import type {UserRole} from "./UserRole"; + +export type User = { + /** + * Guid id for user + */ + id: string, + firstName: string, + lastName: string, + role: UserRole, + username: string, + email: string +} \ No newline at end of file diff --git a/code/frontend/src/models/base/UserRole.ts b/code/frontend/src/models/base/UserRole.ts new file mode 100644 index 0000000..ec32852 --- /dev/null +++ b/code/frontend/src/models/base/UserRole.ts @@ -0,0 +1,5 @@ +export enum UserRole { + REGULAR = "reg", + ADMINISTRATOR = "adm", + OWNER = "own" +} \ No newline at end of file diff --git a/code/frontend/src/models/internal/FormError.ts b/code/frontend/src/models/internal/FormError.ts new file mode 100644 index 0000000..f6d8978 --- /dev/null +++ b/code/frontend/src/models/internal/FormError.ts @@ -0,0 +1,24 @@ +import type { KnownProblem } from "./KnownProblem"; + +export class FormError { + title: string; + subtitle: string; + constructor(title: string = "", subtitle: string = "") { + this.title = title; + this.title = subtitle; + } + + set(title: string = "", subtitle: string = "") { + this.title = title; + this.subtitle = subtitle; + } + + set_from_known_problem(knownProblem: KnownProblem) { + this.title = knownProblem.title ?? ""; + this.subtitle = knownProblem.subtitle ?? ""; + } + + has_error() { + return this.title?.length > 0 || this.subtitle?.length > 0; + } +} \ No newline at end of file diff --git a/code/frontend/src/models/internal/IForm.ts b/code/frontend/src/models/internal/IForm.ts new file mode 100644 index 0000000..c14b770 --- /dev/null +++ b/code/frontend/src/models/internal/IForm.ts @@ -0,0 +1,15 @@ +import type { FormError } from "./FormError"; + +export interface IForm { + fields: Record; + error: FormError; + get_payload: Function; + submit_async: Function; + isLoading: boolean; + showError: boolean; +} + +export interface IFormField { + value: any; + errors: Array; +} diff --git a/code/frontend/src/models/internal/KnownProblem.ts b/code/frontend/src/models/internal/KnownProblem.ts new file mode 100644 index 0000000..b6923d9 --- /dev/null +++ b/code/frontend/src/models/internal/KnownProblem.ts @@ -0,0 +1,10 @@ +export type KnownProblem = { + title: string, + subtitle: string, + errors: Record, + traceId: string, +} + +export function is_known_problem(response: Response): boolean { + return response.headers.has("X-IsKnownProblem"); +} \ No newline at end of file diff --git a/code/frontend/src/models/projects/Project.ts b/code/frontend/src/models/projects/Project.ts new file mode 100644 index 0000000..f265e67 --- /dev/null +++ b/code/frontend/src/models/projects/Project.ts @@ -0,0 +1,13 @@ +import type { Temporal } from "temporal-polyfill" +import type { ProjectMember } from "./ProjectMember" +import type { ProjectStatus } from "./ProjectStatus" + +export type Project = { + id: string, + name: string, + description?: string, + start: Temporal.PlainDate, + stop?: Temporal.PlainDate, + members: Array, + status: ProjectStatus +} \ No newline at end of file diff --git a/code/frontend/src/models/projects/ProjectLabel.ts b/code/frontend/src/models/projects/ProjectLabel.ts new file mode 100644 index 0000000..59aa9d5 --- /dev/null +++ b/code/frontend/src/models/projects/ProjectLabel.ts @@ -0,0 +1,5 @@ +export type ProjectLabel = { + id: string, + name: string, + color: string +} \ No newline at end of file diff --git a/code/frontend/src/models/projects/ProjectMember.ts b/code/frontend/src/models/projects/ProjectMember.ts new file mode 100644 index 0000000..de348ef --- /dev/null +++ b/code/frontend/src/models/projects/ProjectMember.ts @@ -0,0 +1,10 @@ +import type { ProjectRole } from "./ProjectRole" + +export type ProjectMember = { + id: string, + name: string, + role: ProjectRole, + email: string, + userId?: string, + customerId?: string +} \ No newline at end of file diff --git a/code/frontend/src/models/projects/ProjectMeta.ts b/code/frontend/src/models/projects/ProjectMeta.ts new file mode 100644 index 0000000..c583b47 --- /dev/null +++ b/code/frontend/src/models/projects/ProjectMeta.ts @@ -0,0 +1,7 @@ +import type { Temporal } from "temporal-polyfill" +import type { User } from "../base/User" + +export type ProjectMeta = { + created: Temporal.PlainDateTime, + createdBy: User, +} \ No newline at end of file diff --git a/code/frontend/src/models/projects/ProjectRole.ts b/code/frontend/src/models/projects/ProjectRole.ts new file mode 100644 index 0000000..0fa2347 --- /dev/null +++ b/code/frontend/src/models/projects/ProjectRole.ts @@ -0,0 +1,7 @@ +export enum ProjectRole { + EXTERNAL = "ext", + INTERNAL = "int", + RESOURCE = "res", + MANAGER = "man", + OWNER = "own" +} \ No newline at end of file diff --git a/code/frontend/src/models/projects/ProjectStatus.ts b/code/frontend/src/models/projects/ProjectStatus.ts new file mode 100644 index 0000000..2df4b88 --- /dev/null +++ b/code/frontend/src/models/projects/ProjectStatus.ts @@ -0,0 +1,5 @@ +export enum ProjectStatus { + ACTIVE = "act", + EXPIRED = "exp", + IDLE = "idl" +} \ No newline at end of file diff --git a/code/frontend/src/models/work/WorkCategory.ts b/code/frontend/src/models/work/WorkCategory.ts new file mode 100644 index 0000000..7dd85d5 --- /dev/null +++ b/code/frontend/src/models/work/WorkCategory.ts @@ -0,0 +1,5 @@ +export type WorkCategory = { + id: string, + name: string, + color: string +} diff --git a/code/frontend/src/models/work/WorkEntry.ts b/code/frontend/src/models/work/WorkEntry.ts new file mode 100644 index 0000000..2108b88 --- /dev/null +++ b/code/frontend/src/models/work/WorkEntry.ts @@ -0,0 +1,13 @@ +import type { WorkLabel } from "./WorkLabel"; +import type { WorkCategory } from "./WorkCategory"; +import type { Project } from "../projects/Project"; + +export type WorkEntry = { + id: string, + start: string, + stop: string, + description: string, + labels?: Array, + category?: WorkCategory, + project?: Project +} diff --git a/code/frontend/src/models/work/WorkEntryQueryResponse.ts b/code/frontend/src/models/work/WorkEntryQueryResponse.ts new file mode 100644 index 0000000..a6974f1 --- /dev/null +++ b/code/frontend/src/models/work/WorkEntryQueryResponse.ts @@ -0,0 +1,27 @@ +import type { WorkCategory } from "./WorkCategory"; +import type { WorkLabel } from "./WorkLabel"; +import type { Temporal } from "temporal-polyfill"; + +export interface WorkEntryQueryResponse { + duration: WorkEntryQueryDuration, + categories?: Array, + labels?: Array, + dateRange?: WorkEntryQueryDateRange, + specificDate?: Temporal.PlainDateTime + page: number, + pageSize: number +} + +export interface WorkEntryQueryDateRange { + from: Temporal.PlainDateTime, + to: Temporal.PlainDateTime +} + +export enum WorkEntryQueryDuration { + TODAY = 0, + THIS_WEEK = 1, + THIS_MONTH = 2, + THIS_YEAR = 3, + SPECIFIC_DATE = 4, + DATE_RANGE = 5, +} diff --git a/code/frontend/src/models/work/WorkLabel.ts b/code/frontend/src/models/work/WorkLabel.ts new file mode 100644 index 0000000..f7e2795 --- /dev/null +++ b/code/frontend/src/models/work/WorkLabel.ts @@ -0,0 +1,5 @@ +export interface WorkLabel { + id?: string, + name?: string, + color?: string +} diff --git a/code/frontend/src/models/work/WorkQuery.ts b/code/frontend/src/models/work/WorkQuery.ts new file mode 100644 index 0000000..93b0aa4 --- /dev/null +++ b/code/frontend/src/models/work/WorkQuery.ts @@ -0,0 +1,17 @@ +import type {WorkEntry} from "./WorkEntry"; + +export interface IWorkQuery { + results: Array, + page: number, + pageSize: number, + totalRecords: number, + totalPageCount: number, +} + +export class WorkQuery implements IWorkQuery { + results: WorkEntry[]; + page: number; + pageSize: number; + totalRecords: number; + totalPageCount: number; +} diff --git a/code/frontend/src/routes/(main)/(app)/+layout.svelte b/code/frontend/src/routes/(main)/(app)/+layout.svelte new file mode 100644 index 0000000..3141931 --- /dev/null +++ b/code/frontend/src/routes/(main)/(app)/+layout.svelte @@ -0,0 +1,379 @@ + + +{#if showEmailValidatedNotif} + +{/if} + +
+ + + (sidebarOpen = false)} + > + +
+ + +
+ + + +
+ +
+
+ +
+
+ +
+
+
+ + + + + +
+ +
+ +
+
+
+ +
+ +
+
+
+
+ + +
+ + Open user menu + +
+ + +
+ + + View profile + + + + + Settings + + +
+ + sign_out()} + class="text-gray-700 block px-4 py-2 text-sm" + > + Sign out + + +
+
+
+
+
+
+
+
+
+ +
+
+
diff --git a/code/frontend/src/routes/(main)/(app)/home/+page.svelte b/code/frontend/src/routes/(main)/(app)/home/+page.svelte new file mode 100644 index 0000000..247ee47 --- /dev/null +++ b/code/frontend/src/routes/(main)/(app)/home/+page.svelte @@ -0,0 +1 @@ +

Welcome Home

\ No newline at end of file diff --git a/code/frontend/src/routes/(main)/(app)/org/+page.svelte b/code/frontend/src/routes/(main)/(app)/org/+page.svelte new file mode 100644 index 0000000..429ec25 --- /dev/null +++ b/code/frontend/src/routes/(main)/(app)/org/+page.svelte @@ -0,0 +1,4 @@ + + +

$ORGNAME

diff --git a/code/frontend/src/routes/(main)/(app)/profile/+page.svelte b/code/frontend/src/routes/(main)/(app)/profile/+page.svelte new file mode 100644 index 0000000..7c6eb3e --- /dev/null +++ b/code/frontend/src/routes/(main)/(app)/profile/+page.svelte @@ -0,0 +1,4 @@ + + +

Hi, Ivar

diff --git a/code/frontend/src/routes/(main)/(app)/projects/+page.svelte b/code/frontend/src/routes/(main)/(app)/projects/+page.svelte new file mode 100644 index 0000000..2585331 --- /dev/null +++ b/code/frontend/src/routes/(main)/(app)/projects/+page.svelte @@ -0,0 +1,118 @@ + + +
+
+

Projects

+

A list of all the projects in your organsation.

+
+
+ +
+
+
+ + + {#each $headerRows as headerRow (headerRow.id)} + + + {#each headerRow.cells as cell (cell.id)} + + + + {/each} + + + {/each} + + + {#each $rows as row (row.id)} + + + {#each row.cells as cell (cell.id)} + {@const materialisedCell = cell.render()} + + + + {/each} + + + {/each} + +
+
+ + + {#if props.sort.order === "asc"} + + {:else if props.sort.order === "desc"} + + {:else if !props.sort.disabled} + + {/if} + + {#if cell.id === "status"} + + {/if} +
+
+ {#if cell.id === "name"} + + + + {:else if cell.id === "status"} + + {:else} + + {/if} +
+
diff --git a/code/frontend/src/routes/(main)/(app)/projects/[id]/+page.svelte b/code/frontend/src/routes/(main)/(app)/projects/[id]/+page.svelte new file mode 100644 index 0000000..ca474e2 --- /dev/null +++ b/code/frontend/src/routes/(main)/(app)/projects/[id]/+page.svelte @@ -0,0 +1,5 @@ + + +

{$page.params.id}

diff --git a/code/frontend/src/routes/(main)/(app)/projects/create/+page.svelte b/code/frontend/src/routes/(main)/(app)/projects/create/+page.svelte new file mode 100644 index 0000000..d710edc --- /dev/null +++ b/code/frontend/src/routes/(main)/(app)/projects/create/+page.svelte @@ -0,0 +1,59 @@ + + +

Create a new project

+
+ +