aboutsummaryrefslogtreecommitdiffstats
path: root/old-apps/projects
diff options
context:
space:
mode:
Diffstat (limited to 'old-apps/projects')
-rw-r--r--old-apps/projects/.version1
-rw-r--r--old-apps/projects/.version-dev1
-rw-r--r--old-apps/projects/CHANGELOG.md118
-rwxr-xr-xold-apps/projects/build_and_push.sh76
-rw-r--r--old-apps/projects/cliff.toml62
-rw-r--r--old-apps/projects/src/.typesafe-i18n.json5
l---------old-apps/projects/src/_assets/preload.css1
l---------old-apps/projects/src/_assets/preload.js1
-rw-r--r--old-apps/projects/src/_assets/projects.pngbin0 -> 7951 bytes
-rw-r--r--old-apps/projects/src/_assets/pwa/android-chrome-192x192.pngbin0 -> 3291 bytes
-rw-r--r--old-apps/projects/src/_assets/pwa/android-chrome-512x512.pngbin0 -> 9687 bytes
-rw-r--r--old-apps/projects/src/_assets/pwa/apple-touch-icon.pngbin0 -> 2769 bytes
-rw-r--r--old-apps/projects/src/_assets/pwa/browserconfig.xml9
-rw-r--r--old-apps/projects/src/_assets/pwa/favicon-16x16.pngbin0 -> 636 bytes
-rw-r--r--old-apps/projects/src/_assets/pwa/favicon-32x32.pngbin0 -> 907 bytes
-rw-r--r--old-apps/projects/src/_assets/pwa/favicon.icobin0 -> 15086 bytes
-rw-r--r--old-apps/projects/src/_assets/pwa/favicon.svg4
-rw-r--r--old-apps/projects/src/_assets/pwa/manifest.json28
-rw-r--r--old-apps/projects/src/_assets/pwa/mstile-144x144.pngbin0 -> 3109 bytes
-rw-r--r--old-apps/projects/src/_assets/pwa/mstile-150x150.pngbin0 -> 3238 bytes
-rw-r--r--old-apps/projects/src/_assets/pwa/mstile-310x150.pngbin0 -> 3501 bytes
-rw-r--r--old-apps/projects/src/_assets/pwa/mstile-310x310.pngbin0 -> 6823 bytes
-rw-r--r--old-apps/projects/src/_assets/pwa/mstile-70x70.pngbin0 -> 2238 bytes
-rw-r--r--old-apps/projects/src/_assets/pwa/safari-pinned-tab.svg50
-rw-r--r--old-apps/projects/src/app/index.d.ts48
-rw-r--r--old-apps/projects/src/app/index.html63
-rw-r--r--old-apps/projects/src/app/index.scss40
-rw-r--r--old-apps/projects/src/app/index.svelte96
-rw-r--r--old-apps/projects/src/app/index.ts16
-rw-r--r--old-apps/projects/src/app/lib/i18n/en/index.ts126
-rw-r--r--old-apps/projects/src/app/lib/i18n/formatters.ts11
-rw-r--r--old-apps/projects/src/app/lib/i18n/i18n-svelte.ts12
-rw-r--r--old-apps/projects/src/app/lib/i18n/i18n-types.ts822
-rw-r--r--old-apps/projects/src/app/lib/i18n/i18n-util.async.ts27
-rw-r--r--old-apps/projects/src/app/lib/i18n/i18n-util.sync.ts26
-rw-r--r--old-apps/projects/src/app/lib/i18n/i18n-util.ts31
-rw-r--r--old-apps/projects/src/app/lib/i18n/nb/index.ts126
-rw-r--r--old-apps/projects/src/app/lib/services/user-service.ts14
-rw-r--r--old-apps/projects/src/app/lib/stores/categories.ts44
-rw-r--r--old-apps/projects/src/app/lib/stores/entries.ts74
-rw-r--r--old-apps/projects/src/app/lib/stores/labels.ts44
-rw-r--r--old-apps/projects/src/app/pages/_layout.svelte66
-rw-r--r--old-apps/projects/src/app/pages/data.svelte396
-rw-r--r--old-apps/projects/src/app/pages/home.svelte178
-rw-r--r--old-apps/projects/src/app/pages/nav/css/1_responsive-sidebar.css179
-rw-r--r--old-apps/projects/src/app/pages/nav/css/2_side-navigation-v4.css213
-rw-r--r--old-apps/projects/src/app/pages/nav/html/side-navigation-v4.html211
-rw-r--r--old-apps/projects/src/app/pages/nav/index.ts6
-rw-r--r--old-apps/projects/src/app/pages/nav/js/_1_diagonal-movement.js296
-rw-r--r--old-apps/projects/src/app/pages/nav/js/_1_responsive-sidebar.js215
-rw-r--r--old-apps/projects/src/app/pages/nav/js/_2_side-navigation-v4.js73
-rw-r--r--old-apps/projects/src/app/pages/nav/nav-item.svelte18
-rw-r--r--old-apps/projects/src/app/pages/nav/nav-wrapper.svelte20
-rw-r--r--old-apps/projects/src/app/pages/nav/scss/_1_responsive-sidebar.scss147
-rw-r--r--old-apps/projects/src/app/pages/nav/scss/_2_side-navigation-v4.scss237
-rw-r--r--old-apps/projects/src/app/pages/nav/side-navigation-v4.zipbin0 -> 13503 bytes
-rw-r--r--old-apps/projects/src/app/pages/not-found.svelte25
-rw-r--r--old-apps/projects/src/app/pages/settings.svelte12
-rw-r--r--old-apps/projects/src/app/pages/ui-workbench.svelte7
-rw-r--r--old-apps/projects/src/app/pages/views/category-form/index.svelte144
-rw-r--r--old-apps/projects/src/app/pages/views/data-table-paginator.svelte101
-rw-r--r--old-apps/projects/src/app/pages/views/entry-form/index.svelte199
-rw-r--r--old-apps/projects/src/app/pages/views/entry-form/sections/category.svelte76
-rw-r--r--old-apps/projects/src/app/pages/views/entry-form/sections/date-time.svelte167
-rw-r--r--old-apps/projects/src/app/pages/views/entry-form/sections/labels.svelte66
-rw-r--r--old-apps/projects/src/app/pages/views/profile-modal.svelte156
-rw-r--r--old-apps/projects/src/app/pages/views/settings-categories-tile.svelte126
-rw-r--r--old-apps/projects/src/app/pages/views/settings-labels-tile.svelte111
-rw-r--r--old-apps/projects/src/index.html55
-rw-r--r--old-apps/projects/src/package.json28
-rw-r--r--old-apps/projects/src/pnpm-lock.yaml1374
-rw-r--r--old-apps/projects/src/tsconfig.json27
-rw-r--r--old-apps/projects/src/vite.config.ts32
73 files changed, 6937 insertions, 0 deletions
diff --git a/old-apps/projects/.version b/old-apps/projects/.version
new file mode 100644
index 0000000..85aca46
--- /dev/null
+++ b/old-apps/projects/.version
@@ -0,0 +1 @@
+v2-projects
diff --git a/old-apps/projects/.version-dev b/old-apps/projects/.version-dev
new file mode 100644
index 0000000..91e061c
--- /dev/null
+++ b/old-apps/projects/.version-dev
@@ -0,0 +1 @@
+v13-projects-dev
diff --git a/old-apps/projects/CHANGELOG.md b/old-apps/projects/CHANGELOG.md
new file mode 100644
index 0000000..a3af953
--- /dev/null
+++ b/old-apps/projects/CHANGELOG.md
@@ -0,0 +1,118 @@
+# Changelog
+
+## [unreleased]
+
+### Bug Fixes
+
+- Correct path to BASE_DOMAIN
+
+### Miscellaneous Tasks
+
+- Bump version
+- Update CHANGELOG.md for v19-portal-dev
+- Bump version
+- Update CHANGELOG.md for v18-portal-dev
+- Bump version
+- Update CHANGELOG.md for v11-projects-dev
+
+## [unreleased]
+
+### Bug Fixes
+
+- Inherit radius on svg to align styling with other theme figure icons
+
+### Features
+
+- Work in progress more module data models
+- Add inital translation support
+- Add link to BASE_DOMAIN on every public page in portal
+- Centre guarded portal pages
+- Add link to BASE_DOMAIN on every public page in portal
+- Set a max width on the portal layout
+
+### Miscellaneous Tasks
+
+- Bump version
+- Update CHANGELOG.md for v8-frontpage-dev
+- Bump version
+- Update CHANGELOG.md for v7-frontpage-dev
+- Bump version
+- Update CHANGELOG.md for v10-projects-dev
+
+### Refactor
+
+- Use dev.greatoffice.life as BASE_DOMAIN while in development-phase
+- Remove all transitions on theme-switcher
+- Put pre.css inside of style tags so that we dont have to wait for the second request on pre.css to show the loader and theme
+
+## [unreleased]
+
+### Miscellaneous Tasks
+
+- Bump version
+- Update CHANGELOG.md for v9-projects-dev
+
+### Refactor
+
+- Temporarily disable user deletion from within projects
+
+## [unreleased]
+
+### Features
+
+- More work on portal
+- Implement new theme switcher component and backend
+
+### Miscellaneous Tasks
+
+- Bump version
+- Update CHANGELOG.md for v17-portal-dev
+- Bump version
+- Update CHANGELOG.md for v8-projects-dev
+
+## [unreleased]
+
+### Features
+
+- Seperate layout for docs
+- New frontpage
+- !WIP start implementation of svelte-query
+
+### Miscellaneous Tasks
+
+- Bump version
+- Update CHANGELOG.md for v16-portal-dev
+- Bump version
+- Update CHANGELOG.md for v6-frontpage-dev
+- Bump version
+- Update CHANGELOG.md for v5-frontpage-dev
+- Bump version
+- Update CHANGELOG.md for v4-frontpage-dev
+- Bump version
+- Bump version
+- Bump version
+- Update CHANGELOG.md for v41-server-dev
+- Bump version
+- Update CHANGELOG.md for v40-server-dev
+- Bump version
+- Update CHANGELOG.md for v39-server-dev
+- Bump version
+- Remove logging of quartz db host
+- Update CHANGELOG.md for v7-projects-dev
+
+### Refactor
+
+- Update style on portal forms
+- Implement caching in VaultService and use VaultService instead of IOptions
+- Use Vault to get configuration
+- Use Vault to get configuration
+- Small changes on button style
+- Add a small box-shadow
+
+## [unreleased]
+
+### Miscellaneous Tasks
+
+- Bump version
+- Update CHANGELOG.md for v6-projects-dev
+
diff --git a/old-apps/projects/build_and_push.sh b/old-apps/projects/build_and_push.sh
new file mode 100755
index 0000000..abc8ea9
--- /dev/null
+++ b/old-apps/projects/build_and_push.sh
@@ -0,0 +1,76 @@
+#!/usr/bin/env bash
+
+set -Eueo pipefail
+
+APP_NAME="projects"
+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))-$APP_NAME-dev"
+ OLD_VERSION=$CURRENT_DEV_VERSION
+else
+ NEW_VERSION="v$((CURRENT_VERSION_INT+1))-$APP_NAME"
+ OLD_VERSION=$CURRENT_VERSION
+fi
+# 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 $APP_NAME@$NEW_VERSION at $(date -u)..."
+echo
+
+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
+ commit_msg="chore(release): Update CHANGELOG.md for $NEW_VERSION"
+ git cliff -r ../../ $OLD_VERSION..HEAD --with-commit "$commit_msg" --prepend CHANGELOG.md
+ git add CHANGELOG.md
+ git commit --quiet -m "$commit_msg";
+ 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
+
+pushd src
+pnpm run build
+
+cd build
+echo "$NEW_VERSION" >version.txt
+
+
+if [ ${1-prod} == "dev" ]; then
+ scp -r * contabo-fast-1:services/public/projects.dev.greatoffice.life/www
+else
+ echo "Pushing to production in 10 sec, press CTRL+C to cancel"
+ sleep 10
+ scp -r * contabo-fast-1:services/public/projects.greatoffice.life/www
+fi
+
+popd
diff --git a/old-apps/projects/cliff.toml b/old-apps/projects/cliff.toml
new file mode 100644
index 0000000..7299951
--- /dev/null
+++ b/old-apps/projects/cliff.toml
@@ -0,0 +1,62 @@
+# configuration file for git-cliff (0.1.0)
+
+[changelog]
+# changelog header
+header = """
+# Changelog\n
+"""
+# template for the changelog body
+# https://tera.netlify.app/docs/#introduction
+body = """
+{% if version %}\
+ ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
+{% else %}\
+ ## [unreleased]
+{% endif %}\
+{% for group, commits in commits | group_by(attribute="group") %}
+ ### {{ group | upper_first }}
+ {% for commit in commits %}
+ - {% if commit.breaking %}[**breaking**] {% endif %}{{ commit.message | upper_first }}\
+ {% endfor %}
+{% endfor %}\n
+"""
+# remove the leading and trailing whitespace from the template
+trim = true
+# changelog footer
+footer = """
+<!-- generated by git-cliff -->
+"""
+
+[git]
+# parse the commits based on https://www.conventionalcommits.org
+conventional_commits = true
+# filter out the commits that are not conventional
+filter_unconventional = true
+# regex for preprocessing the commit messages
+commit_preprocessors = [
+ { pattern = "([ \\n])(([a-f0-9]{7})[a-f0-9]*)", replace = "${1}commit # [${3}](https://git.ivar.systems/greatoffice/commit/${2})" },
+ { pattern = "https://git.ivar.systems/greatoffice/commit/([a-f0-9]{7})[a-f0-9]*", replace = "commit # [${1}](${0})" },
+]
+# regex for parsing and grouping commits
+commit_parsers = [
+ { message = "^feat", group = "Features" },
+ { message = "^fix", group = "Bug Fixes" },
+ { message = "^doc", group = "Documentation" },
+ { message = "^perf", group = "Performance" },
+ { message = "^refactor", group = "Refactor" },
+ { message = "^style", group = "Styling" },
+ { message = "^test", group = "Testing" },
+ { message = "^chore", group = "Miscellaneous Tasks" },
+]
+# filter out the commits that are not matched by commit parsers
+filter_commits = true
+# glob pattern for matching git tags
+tag_pattern = "v.*"
+# regex for skipping tags
+skip_tags = "v0.1.0-beta.1"
+# regex for ignoring tags
+ignore_tags = ""
+# sort the tags chronologically
+date_order = true
+# sort the commits inside sections by oldest/newest order
+sort_commits = "newest"
diff --git a/old-apps/projects/src/.typesafe-i18n.json b/old-apps/projects/src/.typesafe-i18n.json
new file mode 100644
index 0000000..74cca10
--- /dev/null
+++ b/old-apps/projects/src/.typesafe-i18n.json
@@ -0,0 +1,5 @@
+{
+ "adapter": "svelte",
+ "$schema": "https://unpkg.com/typesafe-i18n@5.11.0/schema/typesafe-i18n.json",
+ "outputPath": "app/lib/i18n"
+} \ No newline at end of file
diff --git a/old-apps/projects/src/_assets/preload.css b/old-apps/projects/src/_assets/preload.css
new file mode 120000
index 0000000..e248c5b
--- /dev/null
+++ b/old-apps/projects/src/_assets/preload.css
@@ -0,0 +1 @@
+/Users/ivarlovlie/i2r/greatoffice/apps/web-shared/src/assets/preload.css \ No newline at end of file
diff --git a/old-apps/projects/src/_assets/preload.js b/old-apps/projects/src/_assets/preload.js
new file mode 120000
index 0000000..3fa1cc7
--- /dev/null
+++ b/old-apps/projects/src/_assets/preload.js
@@ -0,0 +1 @@
+/Users/ivarlovlie/i2r/greatoffice/apps/web-shared/src/assets/preload.js \ No newline at end of file
diff --git a/old-apps/projects/src/_assets/projects.png b/old-apps/projects/src/_assets/projects.png
new file mode 100644
index 0000000..e49191f
--- /dev/null
+++ b/old-apps/projects/src/_assets/projects.png
Binary files differ
diff --git a/old-apps/projects/src/_assets/pwa/android-chrome-192x192.png b/old-apps/projects/src/_assets/pwa/android-chrome-192x192.png
new file mode 100644
index 0000000..5c098bc
--- /dev/null
+++ b/old-apps/projects/src/_assets/pwa/android-chrome-192x192.png
Binary files differ
diff --git a/old-apps/projects/src/_assets/pwa/android-chrome-512x512.png b/old-apps/projects/src/_assets/pwa/android-chrome-512x512.png
new file mode 100644
index 0000000..973a1c3
--- /dev/null
+++ b/old-apps/projects/src/_assets/pwa/android-chrome-512x512.png
Binary files differ
diff --git a/old-apps/projects/src/_assets/pwa/apple-touch-icon.png b/old-apps/projects/src/_assets/pwa/apple-touch-icon.png
new file mode 100644
index 0000000..b4d9773
--- /dev/null
+++ b/old-apps/projects/src/_assets/pwa/apple-touch-icon.png
Binary files differ
diff --git a/old-apps/projects/src/_assets/pwa/browserconfig.xml b/old-apps/projects/src/_assets/pwa/browserconfig.xml
new file mode 100644
index 0000000..b3930d0
--- /dev/null
+++ b/old-apps/projects/src/_assets/pwa/browserconfig.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<browserconfig>
+ <msapplication>
+ <tile>
+ <square150x150logo src="/mstile-150x150.png"/>
+ <TileColor>#da532c</TileColor>
+ </tile>
+ </msapplication>
+</browserconfig>
diff --git a/old-apps/projects/src/_assets/pwa/favicon-16x16.png b/old-apps/projects/src/_assets/pwa/favicon-16x16.png
new file mode 100644
index 0000000..5dde9f9
--- /dev/null
+++ b/old-apps/projects/src/_assets/pwa/favicon-16x16.png
Binary files differ
diff --git a/old-apps/projects/src/_assets/pwa/favicon-32x32.png b/old-apps/projects/src/_assets/pwa/favicon-32x32.png
new file mode 100644
index 0000000..9cef4c4
--- /dev/null
+++ b/old-apps/projects/src/_assets/pwa/favicon-32x32.png
Binary files differ
diff --git a/old-apps/projects/src/_assets/pwa/favicon.ico b/old-apps/projects/src/_assets/pwa/favicon.ico
new file mode 100644
index 0000000..89c7542
--- /dev/null
+++ b/old-apps/projects/src/_assets/pwa/favicon.ico
Binary files differ
diff --git a/old-apps/projects/src/_assets/pwa/favicon.svg b/old-apps/projects/src/_assets/pwa/favicon.svg
new file mode 100644
index 0000000..964dbb8
--- /dev/null
+++ b/old-apps/projects/src/_assets/pwa/favicon.svg
@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-stopwatch" viewBox="0 0 16 16">
+ <path d="M8.5 5.6a.5.5 0 1 0-1 0v2.9h-3a.5.5 0 0 0 0 1H8a.5.5 0 0 0 .5-.5V5.6z"/>
+ <path d="M6.5 1A.5.5 0 0 1 7 .5h2a.5.5 0 0 1 0 1v.57c1.36.196 2.594.78 3.584 1.64a.715.715 0 0 1 .012-.013l.354-.354-.354-.353a.5.5 0 0 1 .707-.708l1.414 1.415a.5.5 0 1 1-.707.707l-.353-.354-.354.354a.512.512 0 0 1-.013.012A7 7 0 1 1 7 2.071V1.5a.5.5 0 0 1-.5-.5zM8 3a6 6 0 1 0 .001 12A6 6 0 0 0 8 3z"/>
+</svg> \ No newline at end of file
diff --git a/old-apps/projects/src/_assets/pwa/manifest.json b/old-apps/projects/src/_assets/pwa/manifest.json
new file mode 100644
index 0000000..4c550fe
--- /dev/null
+++ b/old-apps/projects/src/_assets/pwa/manifest.json
@@ -0,0 +1,28 @@
+{
+ "manifest_version": 2,
+ "version": "0.1",
+ "name": "Time Tracker",
+ "short_name": "Time Tracker",
+ "display": "standalone",
+ "background_color": "#fff",
+ "theme_color": "#4D3DF7",
+ "start_url": ".",
+ "orientation": "portrait",
+ "icons": [
+ {
+ "src": "/favicon.svg",
+ "purpose": "maskable any",
+ "sizes": "any"
+ },
+ {
+ "src": "/pwa/android-chrome-192x192.png",
+ "sizes": "192x192",
+ "type": "image/png"
+ },
+ {
+ "src": "/pwa/android-chrome-512x512.png",
+ "sizes": "512x512",
+ "type": "image/png"
+ }
+ ]
+}
diff --git a/old-apps/projects/src/_assets/pwa/mstile-144x144.png b/old-apps/projects/src/_assets/pwa/mstile-144x144.png
new file mode 100644
index 0000000..84d94cb
--- /dev/null
+++ b/old-apps/projects/src/_assets/pwa/mstile-144x144.png
Binary files differ
diff --git a/old-apps/projects/src/_assets/pwa/mstile-150x150.png b/old-apps/projects/src/_assets/pwa/mstile-150x150.png
new file mode 100644
index 0000000..b1398ae
--- /dev/null
+++ b/old-apps/projects/src/_assets/pwa/mstile-150x150.png
Binary files differ
diff --git a/old-apps/projects/src/_assets/pwa/mstile-310x150.png b/old-apps/projects/src/_assets/pwa/mstile-310x150.png
new file mode 100644
index 0000000..76b16a0
--- /dev/null
+++ b/old-apps/projects/src/_assets/pwa/mstile-310x150.png
Binary files differ
diff --git a/old-apps/projects/src/_assets/pwa/mstile-310x310.png b/old-apps/projects/src/_assets/pwa/mstile-310x310.png
new file mode 100644
index 0000000..d8e4097
--- /dev/null
+++ b/old-apps/projects/src/_assets/pwa/mstile-310x310.png
Binary files differ
diff --git a/old-apps/projects/src/_assets/pwa/mstile-70x70.png b/old-apps/projects/src/_assets/pwa/mstile-70x70.png
new file mode 100644
index 0000000..0df1e8c
--- /dev/null
+++ b/old-apps/projects/src/_assets/pwa/mstile-70x70.png
Binary files differ
diff --git a/old-apps/projects/src/_assets/pwa/safari-pinned-tab.svg b/old-apps/projects/src/_assets/pwa/safari-pinned-tab.svg
new file mode 100644
index 0000000..ba2220c
--- /dev/null
+++ b/old-apps/projects/src/_assets/pwa/safari-pinned-tab.svg
@@ -0,0 +1,50 @@
+<?xml version="1.0" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
+ "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
+<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
+ width="700.000000pt" height="700.000000pt" viewBox="0 0 700.000000 700.000000"
+ preserveAspectRatio="xMidYMid meet">
+<metadata>
+Created by potrace 1.14, written by Peter Selinger 2001-2017
+</metadata>
+<g transform="translate(0.000000,700.000000) scale(0.100000,-0.100000)"
+fill="#000000" stroke="none">
+<path d="M3195 6780 c-116 -3 -211 -10 -226 -17 -39 -17 -105 -98 -116 -142
+-19 -72 -2 -146 45 -202 26 -31 96 -69 131 -72 25 -2 31 -6 32 -27 1 -27 1
+-198 0 -216 -1 -6 -47 -19 -103 -28 -160 -28 -451 -107 -533 -146 -11 -5 -51
+-21 -90 -36 -60 -23 -246 -112 -325 -155 -431 -236 -834 -619 -1101 -1045
+-207 -328 -364 -733 -423 -1089 -51 -307 -61 -583 -31 -875 26 -261 119 -615
+225 -861 185 -430 432 -773 800 -1108 75 -69 387 -301 405 -301 1 0 33 -18 70
+-40 209 -128 602 -288 796 -325 12 -2 29 -7 39 -10 72 -23 273 -56 435 -73
+144 -14 601 -5 658 13 7 2 37 7 67 10 273 33 616 141 904 283 725 357 1275
+982 1542 1754 55 159 113 395 129 523 4 28 8 57 10 65 2 8 7 47 10 85 3 39 8
+93 10 120 6 66 6 327 0 390 -2 28 -7 82 -10 120 -3 39 -11 99 -16 135 -6 36
+-13 79 -16 95 -15 98 -61 279 -103 405 -121 372 -298 694 -542 993 -27 32 -48
+61 -48 65 0 4 35 41 78 84 l77 76 90 -90 c53 -54 108 -99 134 -110 62 -28 130
+-25 191 8 95 52 135 151 103 257 -13 46 -44 79 -362 397 -322 323 -351 349
+-398 363 -148 44 -287 -61 -285 -215 1 -62 35 -118 126 -208 47 -47 86 -87 86
+-90 0 -6 -91 -101 -132 -138 l-25 -23 -46 38 c-264 223 -584 405 -924 528 -92
+34 -320 100 -376 109 -15 3 -35 7 -45 10 -9 3 -34 7 -54 10 -86 13 -113 18
+-117 22 -2 2 -4 56 -4 121 l0 118 29 9 c66 19 114 47 139 80 72 95 65 215 -18
+296 -58 56 -83 60 -402 63 -159 1 -380 0 -490 -3z m560 -1104 c224 -24 547
+-99 670 -156 11 -5 56 -24 100 -41 90 -37 282 -134 306 -155 8 -8 19 -14 23
+-14 13 0 192 -124 286 -199 97 -77 297 -270 364 -351 237 -288 405 -598 509
+-941 30 -98 44 -157 72 -299 3 -14 8 -47 11 -75 3 -27 7 -56 10 -63 22 -69 21
+-519 -1 -642 -1 -8 -6 -40 -10 -70 -4 -30 -9 -64 -11 -75 -2 -11 -7 -33 -10
+-50 -3 -16 -14 -66 -26 -110 -11 -44 -21 -84 -22 -90 -18 -79 -93 -275 -154
+-408 -39 -83 -158 -296 -171 -307 -3 -3 -26 -34 -50 -70 -116 -169 -312 -384
+-466 -508 -38 -32 -78 -65 -89 -74 -25 -22 -229 -160 -281 -189 -177 -99 -405
+-197 -570 -244 -126 -36 -305 -74 -375 -81 -19 -2 -48 -5 -65 -8 -121 -22
+-509 -22 -618 0 -12 2 -42 6 -67 10 -369 45 -795 215 -1125 448 -192 135 -399
+326 -517 476 -23 30 -48 61 -55 67 -57 60 -227 336 -291 473 -64 135 -150 365
+-167 444 -2 12 -6 30 -9 41 -28 120 -36 156 -41 193 -3 24 -7 53 -10 65 -32
+148 -38 552 -10 707 2 14 7 45 10 70 33 274 160 643 313 910 60 106 201 312
+232 340 3 3 23 28 45 55 22 28 85 96 140 151 347 352 768 590 1252 709 56 14
+118 27 137 30 20 2 61 9 93 14 32 6 92 13 133 17 41 3 76 7 77 8 6 5 368 -2
+428 -8z"/>
+<path d="M3423 4754 c-45 -17 -95 -61 -121 -109 -15 -27 -17 -93 -19 -650 -1
+-341 -2 -641 -3 -666 l0 -47 -679 0 -679 -1 -49 -24 c-59 -30 -76 -49 -104
+-112 -54 -122 23 -270 154 -293 23 -5 400 -8 837 -7 l795 1 42 22 c52 27 98
+82 112 136 8 29 10 273 9 826 -3 854 1 796 -59 867 -53 63 -153 87 -236 57z"/>
+</g>
+</svg>
diff --git a/old-apps/projects/src/app/index.d.ts b/old-apps/projects/src/app/index.d.ts
new file mode 100644
index 0000000..c044583
--- /dev/null
+++ b/old-apps/projects/src/app/index.d.ts
@@ -0,0 +1,48 @@
+/* Use this file to declare any custom file extensions for importing */
+/* Use this folder to also add/extend a package d.ts file, if needed. */
+
+/* CSS MODULES */
+declare module "*.module.css" {
+ const classes: { [key: string]: string };
+ export default classes;
+}
+declare module "*.module.scss" {
+ const classes: { [key: string]: string };
+ export default classes;
+}
+
+/* CSS */
+declare module "*.css";
+declare module "*.scss";
+
+/* IMAGES */
+declare module "*.svg" {
+ const ref: string;
+ export default ref;
+}
+declare module "*.bmp" {
+ const ref: string;
+ export default ref;
+}
+declare module "*.gif" {
+ const ref: string;
+ export default ref;
+}
+declare module "*.jpg" {
+ const ref: string;
+ export default ref;
+}
+declare module "*.jpeg" {
+ const ref: string;
+ export default ref;
+}
+declare module "*.png" {
+ const ref: string;
+ export default ref;
+}
+
+/* CUSTOM: ADD YOUR OWN HERE */
+declare module "*.svelte" {
+ const value: any;
+ export default value;
+}
diff --git a/old-apps/projects/src/app/index.html b/old-apps/projects/src/app/index.html
new file mode 100644
index 0000000..7e0b0e1
--- /dev/null
+++ b/old-apps/projects/src/app/index.html
@@ -0,0 +1,63 @@
+<!doctype html>
+<html lang="en">
+
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport"
+ content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
+ <link rel="apple-touch-icon"
+ sizes="180x180"
+ href="../_assets/pwa/apple-touch-icon.png">
+ <link rel="icon"
+ type="image/png"
+ sizes="32x32"
+ href="../_assets/pwa/favicon-32x32.png">
+ <link rel="icon"
+ type="image/png"
+ sizes="16x16"
+ href="../_assets/pwa/favicon-16x16.png">
+ <link rel="manifest"
+ href="../_assets/pwa/manifest.json">
+ <link rel="mask-icon"
+ href="../_assets/pwa/safari-pinned-tab.svg"
+ color="#5bbad5">
+ <meta name="msapplication-TileColor"
+ content="#da532c">
+ <link rel="icon"
+ href="../_assets/pwa/favicon.svg">
+ <script>
+ const currentTheme = localStorage.getItem("theme");
+ if (currentTheme === "light") {
+ document.querySelector("html").dataset.theme = "light";
+ } else {
+ document.querySelector("html").dataset.theme = "dark";
+ }
+ </script>
+ <link rel="stylesheet"
+ href="../_assets/pre.css">
+ <title>Time Tracker</title>
+</head>
+
+<body>
+
+<noscript>
+ This page is built with javascript. Allow it and try again.
+</noscript>
+
+<div class="fill-loader fill-loader--v4"
+ id="loader"
+ role="alert">
+ <p class="fill-loader__label">Loading Time Tracker...</p>
+ <div aria-hidden="true">
+ <div class="fill-loader__base"></div>
+ <div class="fill-loader__fill"></div>
+ </div>
+</div>
+
+<div id="root"></div>
+
+<script type="module"
+ src="./index.ts"></script>
+</body>
+
+</html>
diff --git a/old-apps/projects/src/app/index.scss b/old-apps/projects/src/app/index.scss
new file mode 100644
index 0000000..f83b1a1
--- /dev/null
+++ b/old-apps/projects/src/app/index.scss
@@ -0,0 +1,40 @@
+@use '../../web-shared/src/styles/base'as * with ($breakpoints: ('xs': "768px",
+ 'sm': "768px",
+ 'md': "1200px",
+ 'lg': "1200px",
+ 'xl': "1600px",
+ ),
+ $grid-columns: 12);
+
+@use '../../web-shared/src/styles/custom-style/colors';
+@use '../../web-shared/src/styles/custom-style/spacing';
+@use '../../web-shared/src/styles/custom-style/shared-styles';
+@use '../../web-shared/src/styles/custom-style/typography';
+@use '../../web-shared/src/styles/custom-style/icons';
+@use '../../web-shared/src/styles/custom-style/buttons';
+@use '../../web-shared/src/styles/custom-style/forms';
+@use '../../web-shared/src/styles/custom-style/util';
+
+@use '../../web-shared/src/styles/components/radios-checkboxes';
+@use '../../web-shared/src/styles/components/circle-loader';
+@use '../../web-shared/src/styles/components/list';
+@use '../../web-shared/src/styles/components/form-validator';
+@use '../../web-shared/src/styles/components/btn-states';
+@use '../../web-shared/src/styles/components/alert';
+@use '../../web-shared/src/styles/components/details';
+@use '../../web-shared/src/styles/components/tabbed-navigation';
+@use '../../web-shared/src/styles/components/dropdown';
+@use '../../web-shared/src/styles/components/modal';
+@use '../../web-shared/src/styles/components/chip';
+@use '../../web-shared/src/styles/components/autocomplete';
+@use '../../web-shared/src/styles/components/select-autocomplete';
+@use '../../web-shared/src/styles/components/interactive-table';
+@use '../../web-shared/src/styles/components/pagination';
+@use '../../web-shared/src/styles/components/custom-select';
+@use '../../web-shared/src/styles/components/pre-header';
+@use '../../web-shared/src/styles/components/table';
+@use '../../web-shared/src/styles/components/custom-checkbox';
+@use '../../web-shared/src/styles/components/menu';
+@use '../../web-shared/src/styles/components/user-menu';
+@use '../../web-shared/src/styles/components/light-dark-switch';
+@use '../../web-shared/src/styles/components/side-navigation-v4';
diff --git a/old-apps/projects/src/app/index.svelte b/old-apps/projects/src/app/index.svelte
new file mode 100644
index 0000000..c121a32
--- /dev/null
+++ b/old-apps/projects/src/app/index.svelte
@@ -0,0 +1,96 @@
+<svelte:options immutable={true}/>
+<svelte:window bind:online={online}/>
+
+<script lang="ts">
+ import { Locales } from "$app/lib/i18n/i18n-types";
+ import { logout_user } from "$app/lib/services/user-service";
+ import { currentLocale, preffered_or_default } from "$shared/lib/locale";
+ import { CookieNames } from "$shared/lib/configuration";
+ import { get_cookie } from "$shared/lib/helpers";
+ import { Temporal } from "@js-temporal/polyfill";
+ import { onMount } from "svelte";
+ import Router from "svelte-spa-router";
+ import { wrap } from "svelte-spa-router/wrap";
+ import { QueryClient, QueryClientProvider } from "@sveltestack/svelte-query";
+ import { is_active } from "$shared/lib/session";
+ import UiWorkbench from "$app/pages/ui-workbench.svelte";
+ import NotFound from "$app/pages/not-found.svelte";
+ import Home from "$app/pages/home.svelte";
+ import Settings from "$app/pages/settings.svelte";
+ import Data from "$app/pages/data.svelte";
+ import PreHeader from "$shared/components/pre-header.svelte";
+ import { setLocale } from "$app/lib/i18n/i18n-svelte";
+ import { loadLocaleAsync } from "$app/lib/i18n/i18n-util.async";
+ import { i18nObject } from "$app/lib/i18n/i18n-util";
+
+ let online = true;
+ let notOnlineText;
+ let LL;
+
+ console.log("Projects Startup Report", {
+ prefferedLocale: navigator.language,
+ timeZone: Temporal.Now.timeZone().id,
+ themeCookie: {name: CookieNames.theme, value: get_cookie(CookieNames.theme)},
+ localeCookie: {name: CookieNames.locale, value: get_cookie(CookieNames.locale)},
+ prefersColorScheme: window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"
+ });
+
+ currentLocale.subscribe(async locale => {
+ if (locale === "preffered") locale = preffered_or_default();
+ await loadLocaleAsync(locale as Locales);
+ LL = i18nObject(locale as Locales);
+ setLocale(locale as Locales);
+ });
+
+ onMount(async () => {
+ await loadLocaleAsync($currentLocale);
+ LL = i18nObject($currentLocale);
+ setLocale($currentLocale);
+ notOnlineText = LL.messages.noInternet();
+ });
+
+ async function user_is_logged_in() {
+ if (!await is_active()) {
+ await logout_user("expired");
+ }
+ return true;
+ }
+
+ const queryClient = new QueryClient();
+
+ const routes = {
+ "/home": wrap({
+ component: Home,
+ conditions: [user_is_logged_in],
+ }),
+ "/": wrap({
+ component: Home,
+ conditions: [user_is_logged_in],
+ }),
+ "/settings": wrap({
+ component: Settings,
+ conditions: [user_is_logged_in],
+ }),
+ "/data": wrap({
+ component: Data,
+ conditions: [user_is_logged_in],
+ }),
+ "/ui-workbench": UiWorkbench,
+ "*": NotFound,
+ };
+</script>
+
+<PreHeader show="{!online}">{notOnlineText}</PreHeader>
+
+<QueryClientProvider client={queryClient}>
+ <Router
+ {routes}
+ restoreScrollState={true}
+ on:routeLoading={() => {
+ document.getElementById("loader").style.display = "inline-block";
+ }}
+ on:routeLoaded={() => {
+ document.getElementById("loader").style.display = "none";
+ }}
+ />
+</QueryClientProvider>
diff --git a/old-apps/projects/src/app/index.ts b/old-apps/projects/src/app/index.ts
new file mode 100644
index 0000000..febb583
--- /dev/null
+++ b/old-apps/projects/src/app/index.ts
@@ -0,0 +1,16 @@
+// @ts-ignore
+import App from "./index.svelte";
+import "./index.scss";
+import {is_debug, is_development} from "$shared/lib/configuration";
+import {noop} from "$shared/lib/helpers";
+
+if (is_development() || is_debug()) {
+ console.log("%c Debug", "background-color:yellow;color:black;font-size:18px;");
+} else {
+ console.log("%c Production; Suppressing logs", "background-color:yellow;color:black;font-size:18px;");
+ console.log = noop;
+}
+
+export default new App({
+ target: document.getElementById("root"),
+});
diff --git a/old-apps/projects/src/app/lib/i18n/en/index.ts b/old-apps/projects/src/app/lib/i18n/en/index.ts
new file mode 100644
index 0000000..a85af7b
--- /dev/null
+++ b/old-apps/projects/src/app/lib/i18n/en/index.ts
@@ -0,0 +1,126 @@
+import type {BaseTranslation} from "../i18n-types";
+
+const en: BaseTranslation = {
+ nav: {
+ home: "Home",
+ data: "Data",
+ settings: "Settings",
+ usermenu: {
+ logout: "Log out",
+ logoutTitle: "Log out of your profile",
+ profile: "Profile",
+ profileTitle: "Administrate your profile",
+ toggleTitle: "Toggle user menu",
+ }
+ },
+ views: {
+ dataTablePaginator: {
+ goToPrevPage: "Go to previous page",
+ goToNextPage: "Go to next page",
+ of: "of",
+ },
+ categoryForm: {
+ name: "Name",
+ color: "Color",
+ defaultLabels: "Default labels",
+ labelsPlaceholder: "Search or create"
+ },
+ settingsCategoriesTile: {
+ deleteAllConfirm: "Are you sure you want to delete this category?\nThis will delete all relating entries!",
+ active: "Active",
+ archived: "Archived",
+ name: "Name",
+ color: "Color",
+ editEntry: "Edit entry",
+ deleteEntry: "Delete entry",
+ noCategories: "No categories",
+ categories: "Categories"
+ },
+ settingsLabelsTile: {
+ deleteAllConfirm: "Are you sure you want to delete this label?\nIt will be removed from all related entries!",
+ active: "Active",
+ archived: "Archived",
+ name: "Name",
+ color: "Color",
+ editEntry: "Edit label",
+ deleteEntry: "Delete label",
+ noLabels: "No labels",
+ labels: "Labels"
+ },
+ entryForm: {
+ entryUpdateError: "An error occured while updating the entry, try again soon.",
+ entryCreateError: "An error occured while creating the entry, try again soon.",
+ errDescriptionReq: "Description is required",
+ reset: "Reset",
+ description: "Description",
+ save: "Save",
+ create: "Create",
+ category: {
+ category: "Category",
+ placeholder: "Search or create",
+ noResults: "No categories available (Create a new one by searching for it)",
+ errisRequired: "Category is required",
+ _logReset: "Reset category section"
+ },
+ labels: {
+ placeholder: "Search or create",
+ noResults: "No labels available (Create a new one by searching for it)",
+ labels: "Labels",
+ _logReset: "Reset labels section"
+ },
+ dateTime: {
+ errDateIsRequired: "Date is required",
+ errFromIsRequired: "From is required",
+ errFromAfterTo: "From can not be after To",
+ errFromEqTo: "From and To can not be equal",
+ errToIsRequired: "To is required",
+ errToBeforeFrom: "To can not be before From",
+ from: "From",
+ to: "To",
+ date: "Date",
+ _logReset: "Reset date time section"
+ }
+ }
+ },
+ data: {
+ durationSummary: "Showing {entryCountString:string}, totalling in {totalHourMin:string}",
+ hourSingleChar: "h",
+ minSingleChar: "m",
+ entry: "entry",
+ entries: "entries",
+ confirmDeleteEntry: "Are you sure you want to delete this entry?",
+ editEntry: "Edit entry",
+ date: "Date",
+ from: "From",
+ duration: "Duration",
+ category: "Category",
+ description: "Description",
+ loading: "Loading",
+ noEntries: "No entries",
+ to: "to",
+ use: "Use",
+ },
+ home: {
+ confirmDeleteEntry: "Are you sure you want to delete this entry?",
+ newEntry: "New entry",
+ editEntry: "Edit entry",
+ deleteEntry: "Delete entry",
+ loggedTimeToday: "Logged time today",
+ loggedTimeTodayString: "{hours}h{minutes}m",
+ currentTime: "Current time",
+ loading: "Loading",
+ stopwatch: "Stopwatch",
+ todayEntries: "Today's entries",
+ noEntriesToday: "No entries today",
+ refreshTodayEntries: "Refresh today's entries",
+ category: "Category",
+ timespan: "Timespan",
+ },
+ messages: {
+ pageNotFound: "Page not found",
+ goToFrontpage: "Go to frontpage",
+ noInternet: "It seems like your device does not have a internet connection, please check your connection."
+ }
+};
+
+export default en;
diff --git a/old-apps/projects/src/app/lib/i18n/formatters.ts b/old-apps/projects/src/app/lib/i18n/formatters.ts
new file mode 100644
index 0000000..78734f9
--- /dev/null
+++ b/old-apps/projects/src/app/lib/i18n/formatters.ts
@@ -0,0 +1,11 @@
+import type { FormattersInitializer } from 'typesafe-i18n'
+import type { Locales, Formatters } from './i18n-types'
+
+export const initFormatters: FormattersInitializer<Locales, Formatters> = (locale: Locales) => {
+
+ const formatters: Formatters = {
+ // add your formatter functions here
+ }
+
+ return formatters
+}
diff --git a/old-apps/projects/src/app/lib/i18n/i18n-svelte.ts b/old-apps/projects/src/app/lib/i18n/i18n-svelte.ts
new file mode 100644
index 0000000..6cdffb3
--- /dev/null
+++ b/old-apps/projects/src/app/lib/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<Locales, Translations, TranslationFunctions, Formatters>(loadedLocales, loadedFormatters)
+
+export { locale, LL, setLocale }
+
+export default LL
diff --git a/old-apps/projects/src/app/lib/i18n/i18n-types.ts b/old-apps/projects/src/app/lib/i18n/i18n-types.ts
new file mode 100644
index 0000000..acba223
--- /dev/null
+++ b/old-apps/projects/src/app/lib/i18n/i18n-types.ts
@@ -0,0 +1,822 @@
+// 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
+export type BaseLocale = 'en'
+
+export type Locales =
+ | 'en'
+ | 'nb'
+
+export type Translation = RootTranslation
+
+export type Translations = RootTranslation
+
+type RootTranslation = {
+ nav: {
+ /**
+ * Home
+ */
+ home: string
+ /**
+ * Data
+ */
+ data: string
+ /**
+ * Settings
+ */
+ settings: string
+ usermenu: {
+ /**
+ * Log out
+ */
+ logout: string
+ /**
+ * Log out of your profile
+ */
+ logoutTitle: string
+ /**
+ * Profile
+ */
+ profile: string
+ /**
+ * Administrate your profile
+ */
+ profileTitle: string
+ /**
+ * Toggle user menu
+ */
+ toggleTitle: string
+ }
+ }
+ views: {
+ dataTablePaginator: {
+ /**
+ * Go to previous page
+ */
+ goToPrevPage: string
+ /**
+ * Go to next page
+ */
+ goToNextPage: string
+ /**
+ * of
+ */
+ of: string
+ }
+ categoryForm: {
+ /**
+ * Name
+ */
+ name: string
+ /**
+ * Color
+ */
+ color: string
+ /**
+ * Default labels
+ */
+ defaultLabels: string
+ /**
+ * Search or create
+ */
+ labelsPlaceholder: string
+ }
+ settingsCategoriesTile: {
+ /**
+ * Are you sure you want to delete this category?
+ This will delete all relating entries!
+ */
+ deleteAllConfirm: string
+ /**
+ * Active
+ */
+ active: string
+ /**
+ * Archived
+ */
+ archived: string
+ /**
+ * Name
+ */
+ name: string
+ /**
+ * Color
+ */
+ color: string
+ /**
+ * Edit entry
+ */
+ editEntry: string
+ /**
+ * Delete entry
+ */
+ deleteEntry: string
+ /**
+ * No categories
+ */
+ noCategories: string
+ /**
+ * Categories
+ */
+ categories: string
+ }
+ settingsLabelsTile: {
+ /**
+ * Are you sure you want to delete this label?
+ It will be removed from all related entries!
+ */
+ deleteAllConfirm: string
+ /**
+ * Active
+ */
+ active: string
+ /**
+ * Archived
+ */
+ archived: string
+ /**
+ * Name
+ */
+ name: string
+ /**
+ * Color
+ */
+ color: string
+ /**
+ * Edit label
+ */
+ editEntry: string
+ /**
+ * Delete label
+ */
+ deleteEntry: string
+ /**
+ * No labels
+ */
+ noLabels: string
+ /**
+ * Labels
+ */
+ labels: string
+ }
+ entryForm: {
+ /**
+ * An error occured while updating the entry, try again soon.
+ */
+ entryUpdateError: string
+ /**
+ * An error occured while creating the entry, try again soon.
+ */
+ entryCreateError: string
+ /**
+ * Description is required
+ */
+ errDescriptionReq: string
+ /**
+ * Reset
+ */
+ reset: string
+ /**
+ * Description
+ */
+ description: string
+ /**
+ * Save
+ */
+ save: string
+ /**
+ * Create
+ */
+ create: string
+ category: {
+ /**
+ * Category
+ */
+ category: string
+ /**
+ * Search or create
+ */
+ placeholder: string
+ /**
+ * No categories available (Create a new one by searching for it)
+ */
+ noResults: string
+ /**
+ * Category is required
+ */
+ errisRequired: string
+ /**
+ * Reset category section
+ */
+ _logReset: string
+ }
+ labels: {
+ /**
+ * Search or create
+ */
+ placeholder: string
+ /**
+ * No labels available (Create a new one by searching for it)
+ */
+ noResults: string
+ /**
+ * Labels
+ */
+ labels: string
+ /**
+ * Reset labels section
+ */
+ _logReset: string
+ }
+ dateTime: {
+ /**
+ * Date is required
+ */
+ errDateIsRequired: string
+ /**
+ * From is required
+ */
+ errFromIsRequired: string
+ /**
+ * From can not be after To
+ */
+ errFromAfterTo: string
+ /**
+ * From and To can not be equal
+ */
+ errFromEqTo: string
+ /**
+ * To is required
+ */
+ errToIsRequired: string
+ /**
+ * To can not be before From
+ */
+ errToBeforeFrom: string
+ /**
+ * From
+ */
+ from: string
+ /**
+ * To
+ */
+ to: string
+ /**
+ * Date
+ */
+ date: string
+ /**
+ * Reset date time section
+ */
+ _logReset: string
+ }
+ }
+ }
+ data: {
+ /**
+ * Showing {entryCountString}, totalling in {totalHourMin}
+ * @param {string} entryCountString
+ * @param {string} totalHourMin
+ */
+ durationSummary: RequiredParams<'entryCountString' | 'totalHourMin'>
+ /**
+ * h
+ */
+ hourSingleChar: string
+ /**
+ * m
+ */
+ minSingleChar: string
+ /**
+ * entry
+ */
+ entry: string
+ /**
+ * entries
+ */
+ entries: string
+ /**
+ * Are you sure you want to delete this entry?
+ */
+ confirmDeleteEntry: string
+ /**
+ * Edit entry
+ */
+ editEntry: string
+ /**
+ * Date
+ */
+ date: string
+ /**
+ * From
+ */
+ from: string
+ /**
+ * Duration
+ */
+ duration: string
+ /**
+ * Category
+ */
+ category: string
+ /**
+ * Description
+ */
+ description: string
+ /**
+ * Loading
+ */
+ loading: string
+ /**
+ * No entries
+ */
+ noEntries: string
+ /**
+ * to
+ */
+ to: string
+ /**
+ * Use
+ */
+ use: string
+ }
+ home: {
+ /**
+ * Are you sure you want to delete this entry?
+ */
+ confirmDeleteEntry: string
+ /**
+ * New entry
+ */
+ newEntry: string
+ /**
+ * Edit entry
+ */
+ editEntry: string
+ /**
+ * Delete entry
+ */
+ deleteEntry: string
+ /**
+ * Logged time today
+ */
+ loggedTimeToday: string
+ /**
+ * {hours}h{minutes}m
+ * @param {unknown} hours
+ * @param {unknown} minutes
+ */
+ loggedTimeTodayString: RequiredParams<'hours' | 'minutes'>
+ /**
+ * Current time
+ */
+ currentTime: string
+ /**
+ * Loading
+ */
+ loading: string
+ /**
+ * Stopwatch
+ */
+ stopwatch: string
+ /**
+ * Today's entries
+ */
+ todayEntries: string
+ /**
+ * No entries today
+ */
+ noEntriesToday: string
+ /**
+ * Refresh today's entries
+ */
+ refreshTodayEntries: string
+ /**
+ * Category
+ */
+ category: string
+ /**
+ * Timespan
+ */
+ timespan: string
+ }
+ messages: {
+ /**
+ * Page not found
+ */
+ pageNotFound: string
+ /**
+ * Go to frontpage
+ */
+ goToFrontpage: string
+ /**
+ * It seems like your device does not have a internet connection, please check your connection.
+ */
+ noInternet: string
+ }
+}
+
+export type TranslationFunctions = {
+ nav: {
+ /**
+ * Home
+ */
+ home: () => LocalizedString
+ /**
+ * Data
+ */
+ data: () => LocalizedString
+ /**
+ * Settings
+ */
+ settings: () => LocalizedString
+ usermenu: {
+ /**
+ * Log out
+ */
+ logout: () => LocalizedString
+ /**
+ * Log out of your profile
+ */
+ logoutTitle: () => LocalizedString
+ /**
+ * Profile
+ */
+ profile: () => LocalizedString
+ /**
+ * Administrate your profile
+ */
+ profileTitle: () => LocalizedString
+ /**
+ * Toggle user menu
+ */
+ toggleTitle: () => LocalizedString
+ }
+ }
+ views: {
+ dataTablePaginator: {
+ /**
+ * Go to previous page
+ */
+ goToPrevPage: () => LocalizedString
+ /**
+ * Go to next page
+ */
+ goToNextPage: () => LocalizedString
+ /**
+ * of
+ */
+ of: () => LocalizedString
+ }
+ categoryForm: {
+ /**
+ * Name
+ */
+ name: () => LocalizedString
+ /**
+ * Color
+ */
+ color: () => LocalizedString
+ /**
+ * Default labels
+ */
+ defaultLabels: () => LocalizedString
+ /**
+ * Search or create
+ */
+ labelsPlaceholder: () => LocalizedString
+ }
+ settingsCategoriesTile: {
+ /**
+ * Are you sure you want to delete this category?
+ This will delete all relating entries!
+ */
+ deleteAllConfirm: () => LocalizedString
+ /**
+ * Active
+ */
+ active: () => LocalizedString
+ /**
+ * Archived
+ */
+ archived: () => LocalizedString
+ /**
+ * Name
+ */
+ name: () => LocalizedString
+ /**
+ * Color
+ */
+ color: () => LocalizedString
+ /**
+ * Edit entry
+ */
+ editEntry: () => LocalizedString
+ /**
+ * Delete entry
+ */
+ deleteEntry: () => LocalizedString
+ /**
+ * No categories
+ */
+ noCategories: () => LocalizedString
+ /**
+ * Categories
+ */
+ categories: () => LocalizedString
+ }
+ settingsLabelsTile: {
+ /**
+ * Are you sure you want to delete this label?
+ It will be removed from all related entries!
+ */
+ deleteAllConfirm: () => LocalizedString
+ /**
+ * Active
+ */
+ active: () => LocalizedString
+ /**
+ * Archived
+ */
+ archived: () => LocalizedString
+ /**
+ * Name
+ */
+ name: () => LocalizedString
+ /**
+ * Color
+ */
+ color: () => LocalizedString
+ /**
+ * Edit label
+ */
+ editEntry: () => LocalizedString
+ /**
+ * Delete label
+ */
+ deleteEntry: () => LocalizedString
+ /**
+ * No labels
+ */
+ noLabels: () => LocalizedString
+ /**
+ * Labels
+ */
+ labels: () => LocalizedString
+ }
+ entryForm: {
+ /**
+ * An error occured while updating the entry, try again soon.
+ */
+ entryUpdateError: () => LocalizedString
+ /**
+ * An error occured while creating the entry, try again soon.
+ */
+ entryCreateError: () => LocalizedString
+ /**
+ * Description is required
+ */
+ errDescriptionReq: () => LocalizedString
+ /**
+ * Reset
+ */
+ reset: () => LocalizedString
+ /**
+ * Description
+ */
+ description: () => LocalizedString
+ /**
+ * Save
+ */
+ save: () => LocalizedString
+ /**
+ * Create
+ */
+ create: () => LocalizedString
+ category: {
+ /**
+ * Category
+ */
+ category: () => LocalizedString
+ /**
+ * Search or create
+ */
+ placeholder: () => LocalizedString
+ /**
+ * No categories available (Create a new one by searching for it)
+ */
+ noResults: () => LocalizedString
+ /**
+ * Category is required
+ */
+ errisRequired: () => LocalizedString
+ /**
+ * Reset category section
+ */
+ _logReset: () => LocalizedString
+ }
+ labels: {
+ /**
+ * Search or create
+ */
+ placeholder: () => LocalizedString
+ /**
+ * No labels available (Create a new one by searching for it)
+ */
+ noResults: () => LocalizedString
+ /**
+ * Labels
+ */
+ labels: () => LocalizedString
+ /**
+ * Reset labels section
+ */
+ _logReset: () => LocalizedString
+ }
+ dateTime: {
+ /**
+ * Date is required
+ */
+ errDateIsRequired: () => LocalizedString
+ /**
+ * From is required
+ */
+ errFromIsRequired: () => LocalizedString
+ /**
+ * From can not be after To
+ */
+ errFromAfterTo: () => LocalizedString
+ /**
+ * From and To can not be equal
+ */
+ errFromEqTo: () => LocalizedString
+ /**
+ * To is required
+ */
+ errToIsRequired: () => LocalizedString
+ /**
+ * To can not be before From
+ */
+ errToBeforeFrom: () => LocalizedString
+ /**
+ * From
+ */
+ from: () => LocalizedString
+ /**
+ * To
+ */
+ to: () => LocalizedString
+ /**
+ * Date
+ */
+ date: () => LocalizedString
+ /**
+ * Reset date time section
+ */
+ _logReset: () => LocalizedString
+ }
+ }
+ }
+ data: {
+ /**
+ * Showing {entryCountString}, totalling in {totalHourMin}
+ */
+ durationSummary: (arg: { entryCountString: string, totalHourMin: string }) => LocalizedString
+ /**
+ * h
+ */
+ hourSingleChar: () => LocalizedString
+ /**
+ * m
+ */
+ minSingleChar: () => LocalizedString
+ /**
+ * entry
+ */
+ entry: () => LocalizedString
+ /**
+ * entries
+ */
+ entries: () => LocalizedString
+ /**
+ * Are you sure you want to delete this entry?
+ */
+ confirmDeleteEntry: () => LocalizedString
+ /**
+ * Edit entry
+ */
+ editEntry: () => LocalizedString
+ /**
+ * Date
+ */
+ date: () => LocalizedString
+ /**
+ * From
+ */
+ from: () => LocalizedString
+ /**
+ * Duration
+ */
+ duration: () => LocalizedString
+ /**
+ * Category
+ */
+ category: () => LocalizedString
+ /**
+ * Description
+ */
+ description: () => LocalizedString
+ /**
+ * Loading
+ */
+ loading: () => LocalizedString
+ /**
+ * No entries
+ */
+ noEntries: () => LocalizedString
+ /**
+ * to
+ */
+ to: () => LocalizedString
+ /**
+ * Use
+ */
+ use: () => LocalizedString
+ }
+ home: {
+ /**
+ * Are you sure you want to delete this entry?
+ */
+ confirmDeleteEntry: () => LocalizedString
+ /**
+ * New entry
+ */
+ newEntry: () => LocalizedString
+ /**
+ * Edit entry
+ */
+ editEntry: () => LocalizedString
+ /**
+ * Delete entry
+ */
+ deleteEntry: () => LocalizedString
+ /**
+ * Logged time today
+ */
+ loggedTimeToday: () => LocalizedString
+ /**
+ * {hours}h{minutes}m
+ */
+ loggedTimeTodayString: (arg: { hours: unknown, minutes: unknown }) => LocalizedString
+ /**
+ * Current time
+ */
+ currentTime: () => LocalizedString
+ /**
+ * Loading
+ */
+ loading: () => LocalizedString
+ /**
+ * Stopwatch
+ */
+ stopwatch: () => LocalizedString
+ /**
+ * Today's entries
+ */
+ todayEntries: () => LocalizedString
+ /**
+ * No entries today
+ */
+ noEntriesToday: () => LocalizedString
+ /**
+ * Refresh today's entries
+ */
+ refreshTodayEntries: () => LocalizedString
+ /**
+ * Category
+ */
+ category: () => LocalizedString
+ /**
+ * Timespan
+ */
+ timespan: () => LocalizedString
+ }
+ messages: {
+ /**
+ * Page not found
+ */
+ pageNotFound: () => LocalizedString
+ /**
+ * Go to frontpage
+ */
+ goToFrontpage: () => LocalizedString
+ /**
+ * It seems like your device does not have a internet connection, please check your connection.
+ */
+ noInternet: () => LocalizedString
+ }
+}
+
+export type Formatters = {}
diff --git a/old-apps/projects/src/app/lib/i18n/i18n-util.async.ts b/old-apps/projects/src/app/lib/i18n/i18n-util.async.ts
new file mode 100644
index 0000000..3ccef5f
--- /dev/null
+++ b/old-apps/projects/src/app/lib/i18n/i18n-util.async.ts
@@ -0,0 +1,27 @@
+// 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'
+
+const localeTranslationLoaders = {
+ en: () => import('./en'),
+ nb: () => import('./nb'),
+}
+
+const updateDictionary = (locale: Locales, dictionary: Partial<Translations>) =>
+ loadedLocales[locale] = { ...loadedLocales[locale], ...dictionary }
+
+export const importLocaleAsync = async (locale: Locales) =>
+ (await localeTranslationLoaders[locale]()).default as unknown as Translations
+
+export const loadLocaleAsync = async (locale: Locales): Promise<void> => {
+ updateDictionary(locale, await importLocaleAsync(locale))
+ loadFormatters(locale)
+}
+
+export const loadAllLocalesAsync = (): Promise<void[]> => Promise.all(locales.map(loadLocaleAsync))
+
+export const loadFormatters = (locale: Locales): void =>
+ void (loadedFormatters[locale] = initFormatters(locale))
diff --git a/old-apps/projects/src/app/lib/i18n/i18n-util.sync.ts b/old-apps/projects/src/app/lib/i18n/i18n-util.sync.ts
new file mode 100644
index 0000000..f1a8e9e
--- /dev/null
+++ b/old-apps/projects/src/app/lib/i18n/i18n-util.sync.ts
@@ -0,0 +1,26 @@
+// 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'
+
+const localeTranslations = {
+ en,
+ nb,
+}
+
+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/old-apps/projects/src/app/lib/i18n/i18n-util.ts b/old-apps/projects/src/app/lib/i18n/i18n-util.ts
new file mode 100644
index 0000000..cad1e7a
--- /dev/null
+++ b/old-apps/projects/src/app/lib/i18n/i18n-util.ts
@@ -0,0 +1,31 @@
+// 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 { detectLocale as detectLocaleFn } from 'typesafe-i18n/detectors'
+import type { Formatters, Locales, Translations, TranslationFunctions } from './i18n-types'
+
+export const baseLocale: Locales = 'en'
+
+export const locales: Locales[] = [
+ 'en',
+ 'nb'
+]
+
+export const loadedLocales = {} as Record<Locales, Translations>
+
+export const loadedFormatters = {} as Record<Locales, Formatters>
+
+export const i18nString = (locale: Locales) => initI18nString<Locales, Formatters>(locale, loadedFormatters[locale])
+
+export const i18nObject = (locale: Locales) =>
+ initI18nObject<Locales, Translations, TranslationFunctions, Formatters>(
+ locale,
+ loadedLocales[locale],
+ loadedFormatters[locale]
+ )
+
+export const i18n = () => initI18n<Locales, Translations, TranslationFunctions, Formatters>(loadedLocales, loadedFormatters)
+
+export const detectLocale = (...detectors: LocaleDetector[]) => detectLocaleFn<Locales>(baseLocale, locales, ...detectors)
diff --git a/old-apps/projects/src/app/lib/i18n/nb/index.ts b/old-apps/projects/src/app/lib/i18n/nb/index.ts
new file mode 100644
index 0000000..1638345
--- /dev/null
+++ b/old-apps/projects/src/app/lib/i18n/nb/index.ts
@@ -0,0 +1,126 @@
+import type {Translation} from "../i18n-types";
+
+const nb: Translation = {
+ nav: {
+ home: "Hjem",
+ data: "Data",
+ settings: "Innstillinger",
+ usermenu: {
+ logout: "Logg ut",
+ logoutTitle: "Logg ut av din profil",
+ profile: "Profil",
+ profileTitle: "Administrer din profil",
+ toggleTitle: "Vis brukermeny"
+ }
+ },
+ views: {
+ categoryForm: {
+ name: "Navn",
+ color: "Farge",
+ defaultLabels: "Standard merknader",
+ labelsPlaceholder: "Søk eller opprett"
+ },
+ dataTablePaginator: {
+ goToPrevPage: "GÃ¥ til forrige side",
+ goToNextPage: "GÃ¥ til neste side",
+ of: "av",
+ },
+ settingsCategoriesTile: {
+ deleteAllConfirm: "Er du sikker på at du vil slette denne kategorien?\nDette vil slette alle tilhørende rader!",
+ active: "Aktive",
+ archived: "Arkiverte",
+ name: "Navn",
+ color: "Farge",
+ editEntry: "Rediger kategori",
+ deleteEntry: "Slett kategori",
+ noCategories: "Ingen kategorier",
+ categories: "Kategorier"
+ },
+ settingsLabelsTile: {
+ deleteAllConfirm: "Er du sikker på at du vil slette denne merknaden?\nDen vil bli slette fra alle relaterte rader!",
+ active: "Aktive",
+ archived: "Arkiverte",
+ name: "Navn",
+ color: "Farge",
+ editEntry: "Rediger merknad",
+ deleteEntry: "Slett merknad",
+ noLabels: "Ingen merknader",
+ labels: "Merknader"
+ },
+ entryForm: {
+ entryUpdateError: "En feil oppstod med lagringen av din rad, prøv igjen snart.",
+ entryCreateError: "En feil oppstod med opprettelsen av din rad, prøv igjen snart.",
+ errDescriptionReq: "Beskrivelse er påkrevd",
+ reset: "Tilbakestill",
+ description: "Beskrivelse",
+ save: "Lagre",
+ create: "Opprett",
+ category: {
+ category: "Kategori",
+ placeholder: "Søk eller opprett",
+ noResults: "Ingen kategorier tilgjengelig (Opprett en ny ved å skrive navnet i søkefeltet).",
+ errisRequired: "Kategori er påkrevd",
+ _logReset: "Tilbakestilte kategori-seksjonen"
+ },
+ labels: {
+ placeholder: "Søk eller opprett",
+ noResults: "Ingen merkander tilgjengelig (Opprett en ny ved å skrive navnet i søkefeltet).",
+ labels: "Merknader",
+ _logReset: "Tilbakestilte merknader-seksjonen"
+ },
+ dateTime: {
+ errDateIsRequired: "Dato er påkrevd",
+ errFromIsRequired: "Fra er påkrevd",
+ errFromAfterTo: "Fra kan ikke være etter Til",
+ errFromEqTo: "Fra og Til kan ikke ha lik verdi",
+ errToIsRequired: "Til er påkrevd",
+ errToBeforeFrom: "Til kan ikke være før Fra",
+ from: "Fra",
+ to: "Til",
+ date: "Dato",
+ _logReset: "Tilbakestilte dato-seksjonen"
+ }
+ }
+ },
+ data: {
+ durationSummary: "Viser {entryCountString:string}, Tilsammen {totalHourMin:string}",
+ hourSingleChar: "t",
+ minSingleChar: "m",
+ entry: "rad",
+ entries: "rader",
+ confirmDeleteEntry: "Er du sikker på at du vil slette denne raden?",
+ editEntry: "Rediger rad",
+ date: "Dato",
+ from: "Fra",
+ duration: "Tidsrom",
+ category: "Kategori",
+ description: "Beskrivelse",
+ loading: "Laster",
+ noEntries: "Ingen rader",
+ to: "til",
+ use: "Bruk",
+ },
+ home: {
+ loggedTimeTodayString: "{hours}t{minutes}m",
+ confirmDeleteEntry: "Er du sikker på at du vil slette denne raden?",
+ newEntry: "Ny tidsoppføring",
+ editEntry: "Rediger rad",
+ deleteEntry: "Slett rad",
+ loggedTimeToday: "Registrert tid hittil idag",
+ currentTime: "Klokken",
+ loading: "Laster",
+ stopwatch: "Stoppeklokke",
+ todayEntries: "Dagens tidsoppføringer",
+ noEntriesToday: "Ingen oppføringer i dag",
+ refreshTodayEntries: "Last inn dagens tidsoppføringer på nytt",
+ category: "Kategori",
+ timespan: "Tidsrom",
+ },
+ messages: {
+ pageNotFound: "Fant ikke siden",
+ goToFrontpage: "GÃ¥ til forsiden",
+ noInternet: "Det ser ut som at du er uten internettilgang, vennligst sjekk tilkoblingen din."
+ }
+};
+
+export default nb;
diff --git a/old-apps/projects/src/app/lib/services/user-service.ts b/old-apps/projects/src/app/lib/services/user-service.ts
new file mode 100644
index 0000000..4155819
--- /dev/null
+++ b/old-apps/projects/src/app/lib/services/user-service.ts
@@ -0,0 +1,14 @@
+import {portal_base} from "$shared/lib/configuration";
+import {end_session} from "$shared/lib/session";
+import {clear_categories} from "$app/lib/stores/categories";
+import {clear_entries} from "$app/lib/stores/entries";
+import {clear_labels} from "$app/lib/stores/labels";
+
+export async function logout_user(reason: string = "") {
+ await end_session(() => {
+ clear_categories();
+ clear_labels();
+ clear_entries();
+ location.replace(portal_base("#/login" + (reason ? "?" + reason : "")));
+ });
+}
diff --git a/old-apps/projects/src/app/lib/stores/categories.ts b/old-apps/projects/src/app/lib/stores/categories.ts
new file mode 100644
index 0000000..2a63c42
--- /dev/null
+++ b/old-apps/projects/src/app/lib/stores/categories.ts
@@ -0,0 +1,44 @@
+import {writable, get} from "svelte/store";
+import {create_time_category, delete_time_category, get_time_categories} from "$shared/lib/api/time-entry";
+import type {TimeCategoryDto} from "$shared/lib/models/TimeCategoryDto";
+import type {IInternalFetchResponse} from "$shared/lib/models/IInternalFetchResponse";
+
+const categories = writable<Array<TimeCategoryDto>>([]);
+
+export async function reload_categories() {
+ const get_categories_response = await get_time_categories();
+ if (!get_categories_response.ok) {
+ clear_categories();
+ return;
+ }
+ categories.set(get_categories_response.data ?? []);
+}
+
+export function clear_categories() {
+ categories.set([]);
+}
+
+export async function create_category_async(request: TimeCategoryDto): Promise<IInternalFetchResponse> {
+ const create_entry_response = await create_time_category(request);
+ if (create_entry_response.ok) {
+ const stored_entries = get(categories);
+ stored_entries.push(create_entry_response.data);
+ categories.set(stored_entries);
+ }
+ return create_entry_response;
+}
+
+export async function edit_category_async(entry: TimeCategoryDto) {
+ if (!entry.id) return;
+}
+
+export async function delete_category_async(entry: TimeCategoryDto) {
+ if (!entry.id) return;
+ const http_request = await delete_time_category(entry.id);
+ if (http_request.ok) {
+ const stored_entries = get(categories);
+ categories.set(stored_entries.filter(e => e.id !== entry.id));
+ }
+}
+
+export default categories;
diff --git a/old-apps/projects/src/app/lib/stores/entries.ts b/old-apps/projects/src/app/lib/stores/entries.ts
new file mode 100644
index 0000000..e933568
--- /dev/null
+++ b/old-apps/projects/src/app/lib/stores/entries.ts
@@ -0,0 +1,74 @@
+import {Temporal} from "@js-temporal/polyfill";
+import {writable, get} from "svelte/store";
+import {get_time_entries, create_time_entry, delete_time_entry, update_time_entry} from "$shared/lib/api/time-entry";
+import type {TimeEntryDto} from "$shared/lib/models/TimeEntryDto";
+import type {IInternalFetchResponse} from "$shared/lib/models/IInternalFetchResponse";
+import type {TimeEntryQuery} from "$shared/lib/models/TimeEntryQuery";
+
+const entries = writable<Array<TimeEntryDto>>([]);
+
+export function get_time_entry(id: string): TimeEntryDto {
+ return get(entries).find(c => c.id === id);
+}
+
+export async function reload_entries(query: TimeEntryQuery): Promise<void> {
+ const get_entries_response = await get_time_entries(query);
+ if (!get_entries_response.ok) {
+ clear_entries();
+ return;
+ }
+ entries.set(get_default_sorted(get_entries_response.data?.results ?? []));
+}
+
+export function clear_entries() {
+ entries.set([]);
+}
+
+function get_default_sorted(unsorted: Array<TimeEntryDto>): Array<TimeEntryDto> {
+ if (unsorted.length < 1) return unsorted;
+ const byStart = unsorted.sort((a, b) => {
+ return Temporal.Instant.compare(Temporal.Instant.from(b.start), Temporal.Instant.from(a.start));
+ });
+
+ return byStart.sort((a, b) => {
+ return Temporal.Instant.compare(Temporal.Instant.from(b.stop), Temporal.Instant.from(a.stop));
+ });
+}
+
+export async function create_entry_async(request: TimeEntryDto): Promise<IInternalFetchResponse> {
+ const create_entry_response = await create_time_entry(request);
+ if (create_entry_response.ok) {
+ const stored_entries = get(entries) ?? [];
+ stored_entries.push(create_entry_response.data);
+ entries.set(get_default_sorted(stored_entries));
+ }
+ return create_entry_response;
+}
+
+export async function edit_entry_async(request: TimeEntryDto): Promise<IInternalFetchResponse> {
+ if (!request.id) return;
+ const edit_entry_response = await update_time_entry(request);
+ if (edit_entry_response.ok) {
+ const stored_entries = get(entries) ?? [];
+ const index = stored_entries.findIndex(c => c.id === request.id);
+ if (index === -1) {
+ stored_entries.push(edit_entry_response.data);
+ } else {
+ stored_entries[index] = edit_entry_response.data;
+ }
+ entries.set(get_default_sorted(stored_entries));
+ }
+ return edit_entry_response;
+}
+
+export async function delete_entry_async(entry_id: string): Promise<void> {
+ if (!entry_id) throw new Error("No id was supplied when deleting query");
+ const delete_entry_response = await delete_time_entry(entry_id);
+ if (delete_entry_response.ok) {
+ const stored_entries = get(entries) ?? [];
+ entries.set(get_default_sorted(stored_entries.filter((e) => e.id !== entry_id) ?? []));
+ }
+}
+
+
+export default entries;
diff --git a/old-apps/projects/src/app/lib/stores/labels.ts b/old-apps/projects/src/app/lib/stores/labels.ts
new file mode 100644
index 0000000..d5ffaa9
--- /dev/null
+++ b/old-apps/projects/src/app/lib/stores/labels.ts
@@ -0,0 +1,44 @@
+import {writable, get} from "svelte/store";
+import {create_time_label, delete_time_label, get_time_labels} from "$shared/lib/api/time-entry";
+import type {IInternalFetchResponse} from "$shared/lib/models/IInternalFetchResponse";
+import type {TimeLabelDto} from "$shared/lib/models/TimeLabelDto";
+
+const labels = writable<Array<TimeLabelDto>>([]);
+
+export async function reload_labels() {
+ const get_labels_response = await get_time_labels();
+ if (!get_labels_response.ok) {
+ clear_labels();
+ return;
+ }
+ labels.set(get_labels_response.data ?? []);
+}
+
+export function clear_labels() {
+ labels.set([]);
+}
+
+export async function create_label_async(request: TimeLabelDto): Promise<IInternalFetchResponse> {
+ const create_label_response = await create_time_label(request);
+ if (create_label_response.ok) {
+ const stored_entries = get(labels) ?? [];
+ stored_entries.push(create_label_response.data);
+ labels.set(stored_entries);
+ }
+ return create_label_response;
+}
+
+export async function edit_label_async(entry: TimeLabelDto) {
+ if (!entry.id) throw new Error("Label id is required");
+}
+
+export async function delete_label_async(entry: TimeLabelDto) {
+ if (!entry.id) return;
+ const http_request = await delete_time_label(entry.id);
+ if (http_request.ok) {
+ const stored_entries = get(labels) ?? [];
+ labels.set(stored_entries.filter(e => e.id !== entry.id));
+ }
+}
+
+export default labels;
diff --git a/old-apps/projects/src/app/pages/_layout.svelte b/old-apps/projects/src/app/pages/_layout.svelte
new file mode 100644
index 0000000..07a4a25
--- /dev/null
+++ b/old-apps/projects/src/app/pages/_layout.svelte
@@ -0,0 +1,66 @@
+<script>
+ import {onMount} from "svelte";
+ import {location, link} from "svelte-spa-router";
+ import {logout_user} from "$app/lib/services/user-service";
+ import {random_string} from "$shared/lib/helpers";
+ import {get_session_data} from "$shared/lib/session";
+ import ProfileModal from "$app/pages/views/profile-modal.svelte";
+ import {Menu, MenuItem, MenuItemSeparator} from "$shared/components/menu";
+ import Button from "$shared/components/button.svelte";
+ import {IconNames} from "$shared/lib/configuration";
+ import LL from "$app/lib/i18n/i18n-svelte";
+ import BlowoutToolbelt from "$shared/components/blowout-toolbelt.svelte";
+ import {NavWrapper, NavItem} from "./nav";
+
+ let ProfileModalFunctions = {};
+ let showUserMenu = false;
+ let userMenuTriggerNode;
+ const userMenuId = "__menu_" + random_string(3);
+ const username = get_session_data()?.profile.username;
+
+ onMount(() => {
+ userMenuTriggerNode = document.getElementById("open-user-menu");
+ });
+</script>
+
+<ProfileModal bind:functions={ProfileModalFunctions}/>
+<BlowoutToolbelt/>
+
+<NavWrapper>
+ <ul slot="navigation-items">
+ <NavItem to="/home" text="{$LL.nav.home()}"/>
+ <NavItem to="/data" text="{$LL.nav.data()}"/>
+ <NavItem to="/settings" text="{$LL.nav.settings()}"/>
+ </ul>
+ <div slot="navigation-footer" class="tabs-nav-v2 justify-between">
+ <div class="tab-v2 padding-x-sm">
+ <Button class="user-menu-control"
+ variant="reset"
+ id="open-user-menu"
+ on:click={() => showUserMenu = !showUserMenu}
+ text={username}
+ icon={IconNames.chevronDown}
+ icon_width="2rem"
+ icon_height="2rem"
+ icon_right_aligned="true"
+ title="{$LL.nav.usermenu.toggleTitle()}"
+ aria-controls="{userMenuId}"
+ />
+ <Menu bind:show="{showUserMenu}"
+ trigger={userMenuTriggerNode}
+ id="{userMenuId}">
+ <div slot="options">
+ <MenuItem on:click={() => ProfileModalFunctions.open()}>
+ <span title="{$LL.nav.usermenu.profileTitle()}">{$LL.nav.usermenu.profile()}</span>
+ </MenuItem>
+ <MenuItemSeparator/>
+ <MenuItem danger="true"
+ on:click={() => logout_user()}>
+ <span title="{$LL.nav.usermenu.logoutTitle()}">{$LL.nav.usermenu.logout()}</span>
+ </MenuItem>
+ </div>
+ </Menu>
+ </div>
+ </div>
+ <slot slot="main-content"/>
+</NavWrapper> \ No newline at end of file
diff --git a/old-apps/projects/src/app/pages/data.svelte b/old-apps/projects/src/app/pages/data.svelte
new file mode 100644
index 0000000..190c641
--- /dev/null
+++ b/old-apps/projects/src/app/pages/data.svelte
@@ -0,0 +1,396 @@
+<script>
+ import {IconNames} from "$shared/lib/configuration";
+ import {onMount} from "svelte";
+ import {Temporal} from "@js-temporal/polyfill";
+ import Layout from "./_layout.svelte";
+ import Modal from "$shared/components/modal.svelte";
+ import Tile from "$shared/components/tile.svelte";
+ import Icon from "$shared/components/icon.svelte";
+ import EntryForm from "$app/pages/views/entry-form/index.svelte";
+ import {Table, THead, TBody, TCell, TRow, TablePaginator} from "$shared/components/table";
+ import {TimeEntryQueryDuration} from "$shared/lib/models/TimeEntryQuery";
+ import {delete_time_entry, get_time_entries, get_time_entry} from "$shared/lib/api/time-entry";
+ import {seconds_to_hour_minute_string, is_guid, move_focus, unwrap_date_time_from_entry} from "$shared/lib/helpers";
+ import Button from "$shared/components/button.svelte";
+ import LL from "$app/lib/i18n/i18n-svelte";
+
+ let pageCount = 1;
+ let page = 1;
+
+ const defaultQuery = {
+ duration: TimeEntryQueryDuration.THIS_YEAR,
+ categories: [],
+ labels: [],
+ page: page,
+ pageSize: 50,
+ };
+
+ let isLoading;
+ let categories = [];
+ let labels = [];
+ let entries = [];
+ let durationSummary = false;
+ let EditEntryModal;
+ let EditEntryForm;
+ let currentTimespanFilter = TimeEntryQueryDuration.THIS_YEAR;
+ let currentSpecificDateFilter = Temporal.Now.plainDateTimeISO().subtract({days: 1}).toString().substring(0, 10);
+ let currentDateRangeFilter = {};
+ let currentCategoryFilter = "all";
+ let currentLabelFilter = "all";
+ let showDateFilterOptions = false;
+ let secondsLogged = 0;
+
+ function set_duration_summary_string() {
+ if (entries.length > 0) {
+ durationSummary = $LL.data.durationSummary({
+ entryCountString: `${entries.length} ${entries.length === 1 ? $LL.data.entry() : $LL.data.entries()}`,
+ totalHourMin: seconds_to_hour_minute_string(secondsLogged)
+ });
+ } else {
+ durationSummary = "";
+ }
+ }
+
+ async function load_entries(query = defaultQuery) {
+ isLoading = true;
+ const response = await get_time_entries(query);
+ if (response.status === 200) {
+ const responseEntries = [];
+ secondsLogged = 0;
+ for (const entry of response.data.results) {
+ const date_time = unwrap_date_time_from_entry(entry);
+ const seconds = (date_time.duration.hours * 60 * 60) + (date_time.duration.minutes * 60);
+ responseEntries.push({
+ id: entry.id,
+ date: date_time.start_date,
+ start: date_time.start_time,
+ stop: date_time.stop_time,
+ durationString: date_time.duration.hours + $LL.data.hourSingleChar() + date_time.duration.minutes + $LL.data.minSingleChar(),
+ seconds: seconds,
+ category: entry.category,
+ labels: entry.labels,
+ description: entry.description,
+ });
+ secondsLogged += seconds;
+ }
+ entries = responseEntries;
+ page = response.data.page;
+ pageCount = response.data.totalPageCount;
+ } else {
+ entries = [];
+ page = 0;
+ pageCount = 0;
+ }
+ isLoading = false;
+ set_duration_summary_string();
+ }
+
+ function load_entries_with_filter(page = 1) {
+ let query = defaultQuery;
+ query.duration = currentTimespanFilter;
+ query.labels = [];
+ query.categories = [];
+ query.page = page;
+
+ if (currentTimespanFilter === TimeEntryQueryDuration.SPECIFIC_DATE) {
+ query.specificDate = currentSpecificDateFilter;
+ } else {
+ delete query.specificDate;
+ }
+
+ if (currentTimespanFilter === TimeEntryQueryDuration.DATE_RANGE) {
+ query.dateRange = currentDateRangeFilter;
+ } else {
+ delete query.dateRange;
+ }
+
+ if ((currentCategoryFilter !== "all" && currentCategoryFilter?.length > 0) ?? false) {
+ for (const chosenCategoryId of currentCategoryFilter) {
+ if (chosenCategoryId === "all") {
+ continue;
+ }
+ query.categories.push({
+ id: chosenCategoryId,
+ });
+ }
+ }
+
+ if ((currentLabelFilter !== "all" && currentLabelFilter?.length > 0) ?? false) {
+ for (const chosenLabelId of currentLabelFilter) {
+ if (chosenLabelId === "all") {
+ continue;
+ }
+ query.labels.push({
+ id: chosenLabelId,
+ });
+ }
+ }
+
+ load_entries(query);
+ }
+
+ async function handle_delete_entry_button_click(e, entryId) {
+ if (confirm($LL.data.confirmDeleteEntry())) {
+ const response = await delete_time_entry(entryId);
+ if (response.ok) {
+ const indexOfEntry = entries.findIndex((c) => c.id === entryId);
+ if (indexOfEntry !== -1) {
+ secondsLogged -= entries[indexOfEntry].seconds;
+ entries.splice(indexOfEntry, 1);
+ entries = entries;
+ set_duration_summary_string();
+ }
+ }
+ }
+ }
+
+ function handle_edit_entry_form_updated() {
+ load_entries_with_filter(page);
+ EditEntryModal.close();
+ }
+
+ async function handle_edit_entry_button_click(event, entryId) {
+ const response = await get_time_entry(entryId);
+ if (response.status === 200) {
+ if (is_guid(response.data.id)) {
+ EditEntryForm.set_values(response.data);
+ EditEntryModal.open();
+ move_focus(document.querySelector("input[id='date']"));
+ }
+ }
+ }
+
+ function close_date_filter_box(event) {
+ if (!event.target.closest(".date_filter_box_el")) {
+ showDateFilterOptions = false;
+ window.removeEventListener("click", close_date_filter_box);
+ }
+ }
+
+ function toggle_date_filter_box(event) {
+ const box = document.getElementById("date_filter_box");
+ const rect = event.target.getBoundingClientRect();
+ box.style.top = rect.y + "px";
+ box.style.left = rect.x - 50 + "px";
+ showDateFilterOptions = true;
+ window.addEventListener("click", close_date_filter_box);
+ }
+
+ onMount(() => {
+ isLoading = true;
+ Promise.all([load_entries()]).then(() => {
+ isLoading = false;
+ });
+ });
+</script>
+
+<Modal title="{$LL.data.editEntry()}"
+ bind:functions={EditEntryModal}
+ on:closed={() => EditEntryForm.reset()}>
+ <EntryForm bind:functions={EditEntryForm}
+ on:updated={handle_edit_entry_form_updated}/>
+</Modal>
+
+<div id="date_filter_box"
+ style="margin-top:25px"
+ class="padding-xs z-index-overlay bg shadow-sm position-absolute date_filter_box_el border {showDateFilterOptions ? '' : 'hide'}">
+ <div class="flex items-baseline margin-bottom-xxxxs">
+ <label class="text-sm color-contrast-medium margin-right-xs"
+ for="durationSelect">Timespan:</label>
+ <div class="select inline-block js-select">
+ <select name="durationSelect"
+ bind:value={currentTimespanFilter}
+ id="durationSelect">
+ <option value={TimeEntryQueryDuration.TODAY}
+ selected> Today
+ </option>
+ <option value={TimeEntryQueryDuration.THIS_WEEK}>This week</option>
+ <option value={TimeEntryQueryDuration.THIS_MONTH}>This month</option>
+ <option value={TimeEntryQueryDuration.THIS_YEAR}>This year</option>
+ <option value={TimeEntryQueryDuration.SPECIFIC_DATE}>Spesific date</option>
+ <option value={TimeEntryQueryDuration.DATE_RANGE}>Date range</option>
+ </select>
+
+ <svg class="icon icon--xxxs margin-left-xxs"
+ viewBox="0 0 8 8">
+ <path d="M7.934,1.251A.5.5,0,0,0,7.5,1H.5a.5.5,0,0,0-.432.752l3.5,6a.5.5,0,0,0,.864,0l3.5-6A.5.5,0,0,0,7.934,1.251Z"/>
+ </svg>
+ </div>
+ </div>
+
+ {#if currentTimespanFilter === TimeEntryQueryDuration.SPECIFIC_DATE}
+ <div class="flex items-baseline margin-bottom-xxxxs justify-between">
+ <span class="text-sm color-contrast-medium margin-right-xs">{$LL.data.date()}:</span>
+ <span class="text-sm">
+ <input type="date"
+ class="border-none padding-0 color-inherit bg-transparent"
+ bind:value={currentSpecificDateFilter}/>
+ </span>
+ </div>
+ {/if}
+
+ {#if currentTimespanFilter === TimeEntryQueryDuration.DATE_RANGE}
+ <div class="flex items-baseline margin-bottom-xxxxs justify-between">
+ <span class="text-sm color-contrast-medium margin-right-xs">{$LL.data.from()}:</span>
+ <span class="text-sm">
+ <input type="date"
+ class="border-none padding-0 color-inherit bg-transparent"
+ on:change={(e) => (currentDateRangeFilter.from = e.target.value)}/>
+ </span>
+ </div>
+
+ <div class="flex items-baseline margin-bottom-xxxxs justify-between">
+ <span class="text-sm color-contrast-medium margin-right-xs">{$LL.data.to()}:</span>
+ <span class="text-sm">
+ <input type="date"
+ class="border-none padding-0 color-inherit bg-transparent"
+ on:change={(e) => (currentDateRangeFilter.to = e.target.value)}/>
+ </span>
+ </div>
+ {/if}
+
+ <div class="flex items-baseline justify-end">
+ <Button variant="subtle"
+ on:click={() => load_entries_with_filter(page)}
+ class="text-sm"
+ text="{$LL.data.use()}"/>
+ </div>
+</div>
+
+<Layout>
+ <Tile class="{isLoading ? 'c-disabled loading' : ''}">
+ <nav class="s-tabs text-sm hide">
+ <ul class="s-tabs__list">
+ <li><span class="s-tabs__link s-tabs__link--current">All (21)</span></li>
+ <li><span class="s-tabs__link">Published (19)</span></li>
+ <li><span class="s-tabs__link">Draft (2)</span></li>
+ </ul>
+ </nav>
+ <div class="max-width-100% overflow-auto"
+ style="max-height: 82.5vh">
+ <Table class="text-sm width-100% int-table--sticky-header">
+ <THead>
+ <TCell type="th"
+ style="width: 30px;">
+ <div class="custom-checkbox int-table__checkbox">
+ <input class="custom-checkbox__input"
+ type="checkbox"
+ aria-label="Select all rows"/>
+ <div class="custom-checkbox__control"
+ aria-hidden="true"></div>
+ </div>
+ </TCell>
+
+ <TCell type="th"
+ style="width: 100px">
+ <div class="flex items-center justify-between">
+ <span>{$LL.data.date()}</span>
+ <div class="date_filter_box_el cursor-pointer"
+ on:click={toggle_date_filter_box}>
+ <Icon name="{IconNames.funnel}"/>
+ </div>
+ </div>
+ </TCell>
+
+ <TCell type="th"
+ style="width: 100px">
+ <div class="flex items-center">
+ <span>{$LL.data.duration()}</span>
+ </div>
+ </TCell>
+
+ <TCell type="th"
+ style="width: 100px;">
+ <div class="flex items-center">
+ <span>{$LL.data.category()}</span>
+ </div>
+ </TCell>
+
+ <TCell type="th"
+ style="width: 300px;">
+ <div class="flex items-center">
+ <span>{$LL.data.description()}</span>
+ </div>
+ </TCell>
+ <TCell type="th"
+ style="width: 50px"></TCell>
+ </THead>
+ <TBody>
+ {#if entries.length > 0}
+ {#each entries as entry}
+ <TRow class="text-nowrap"
+ data-id={entry.id}>
+ <TCell type="th"
+ thScope="row">
+ <div class="custom-checkbox int-table__checkbox">
+ <input class="custom-checkbox__input"
+ type="checkbox"
+ aria-label="Select this row"/>
+ <div class="custom-checkbox__control"
+ aria-hidden="true"></div>
+ </div>
+ </TCell>
+ <TCell>
+ <pre>{entry.date.toLocaleString()}</pre>
+ </TCell>
+ <TCell>
+ <pre class="flex justify-between">
+ <div class="flex justify-between">
+ <span>{entry.start.toLocaleString(undefined, {timeStyle: "short"})}</span>
+ <span> - </span>
+ <span>{entry.stop.toLocaleString(undefined, {timeStyle: "short"})}</span>
+ </div>
+ </pre>
+ </TCell>
+ <TCell>
+ <span data-id={entry.category.id}>{entry.category.name}</span>
+ </TCell>
+ <TCell class="text-truncate max-width-xxxxs"
+ title="{entry.description}">
+ {entry.description ?? ""}
+ </TCell>
+ <TCell class="flex flex-row justify-end items-center">
+ <Button icon="{IconNames.pencilSquare}"
+ variant="reset"
+ icon_width="1.2rem"
+ icon_height="1.2rem"
+ on:click={(e) => handle_edit_entry_button_click(e, entry.id)}
+ title="Edit entry"/>
+ <Button icon="{IconNames.trash}"
+ variant="reset"
+ icon_width="1.2rem"
+ icon_height="1.2rem"
+ on:click={(e) => handle_delete_entry_button_click(e, entry.id)}
+ title="Delete entry"/>
+ </TCell>
+ </TRow>
+ {/each}
+ {:else}
+ <TRow class="text-nowrap">
+ <TCell type="th"
+ thScope="row"
+ colspan="7">
+ {isLoading ? $LL.data.loading() + "..." : $LL.data.noEntries()}
+ </TCell>
+ </TRow>
+ {/if}
+ </TBody>
+ </Table>
+ </div>
+ <div class="flex items-center justify-between">
+ <p class="text-sm">
+ {#if durationSummary}
+ <small class={isLoading ? "c-disabled loading" : ""}>{durationSummary}</small>
+ {:else}
+ <small class={isLoading ? "c-disabled loading" : ""}>{$LL.data.noEntries()}</small>
+ {/if}
+ </p>
+
+ <nav class="grid padding-y-sm {isLoading ? 'c-disabled loading' : ''}">
+ <TablePaginator {page}
+ on:value_change={(e) => load_entries_with_filter(e.detail.newValue)}
+ {pageCount}/>
+ </nav>
+ </div>
+ </Tile>
+</Layout>
diff --git a/old-apps/projects/src/app/pages/home.svelte b/old-apps/projects/src/app/pages/home.svelte
new file mode 100644
index 0000000..1f398b5
--- /dev/null
+++ b/old-apps/projects/src/app/pages/home.svelte
@@ -0,0 +1,178 @@
+<script lang="ts">
+ import LL from "$app/lib/i18n/i18n-svelte";
+ import { delete_time_entry, get_time_entries, get_time_entry } from "$shared/lib/api/time-entry";
+ import { IconNames, QueryKeys } from "$shared/lib/configuration";
+ import { TimeEntryDto } from "$shared/lib/models/TimeEntryDto";
+ import { Temporal } from "@js-temporal/polyfill";
+ import { useMutation, useQuery, useQueryClient } from "@sveltestack/svelte-query";
+ import { onMount } from "svelte";
+ import Tile from "$shared/components/tile.svelte";
+ import Button from "$shared/components/button.svelte";
+ import Stopwatch from "$shared/components/stopwatch.svelte";
+ import { Table, THead, TBody, TCell, TRow } from "$shared/components/table";
+ import Layout from "./_layout.svelte";
+ import EntryFrom from "$app/pages/views/entry-form/index.svelte";
+ import { seconds_to_hour_minute, unwrap_date_time_from_entry } from "$shared/lib/helpers";
+ import { TimeEntryQueryDuration } from "$shared/lib/models/TimeEntryQuery";
+
+ let currentTime = "";
+ let isLoading = false;
+ let EditEntryForm: any;
+ let timeEntries = [] as Array<TimeEntryDto>;
+ let timeLoggedTodayString = $LL.home.loggedTimeTodayString({hours: 0, minutes: 0});
+ let loggedSecondsToday = 0;
+
+ const queryClient = useQueryClient();
+ const queryResult = useQuery(QueryKeys.entries, async () => await get_time_entries({
+ duration: TimeEntryQueryDuration.TODAY,
+ page: 1,
+ pageSize: 100,
+ })?.data ?? []
+ );
+
+ function set_current_time() {
+ currentTime = Temporal.Now.plainTimeISO().toLocaleString(undefined, {
+ timeStyle: "short",
+ });
+ }
+
+ const delete_entry_mutation = useMutation(delete_time_entry, {
+ onSuccess: (data) => {
+ queryClient.invalidateQueries([QueryKeys.entries, data.data.id]);
+ },
+ });
+
+ async function on_edit_entry_button_click(event, entryId: string) {
+ const response = useQuery([QueryKeys.entries, entryId], () => {
+ return get_time_entry(entryId);
+ });
+
+ EditEntryForm.set_values(response);
+ }
+
+ async function on_delete_entry_button_click(event, entryId: string) {
+ if (confirm($LL.home.confirmDeleteEntry())) {
+ $delete_entry_mutation.mutate(entryId);
+ }
+ }
+
+ function on_create_from_stopwatch(event) {
+ EditEntryForm.set_time({to: event.detail.to, from: event.detail.from});
+ if (event.detail.description) {
+ EditEntryForm.set_description(event.detail.description);
+ }
+ }
+
+ onMount(async () => {
+ set_current_time();
+ setInterval(() => {
+ set_current_time();
+ }, 1e4);
+ queryResult.subscribe((result) => {
+ const newEntries = [];
+ loggedSecondsToday = 0;
+ for (const entry of result.data?.results ?? []) {
+ const date_time = unwrap_date_time_from_entry(entry);
+ newEntries.push({
+ id: entry.id,
+ start: date_time.start_time,
+ stop: date_time.stop_time,
+ category: entry.category,
+ });
+ loggedSecondsToday += (date_time.duration.hours * 60 * 60) + (date_time.duration.minutes * 60);
+ }
+ timeLoggedTodayString = $LL.home.loggedTimeTodayString(seconds_to_hour_minute(loggedSecondsToday));
+ timeEntries = newEntries;
+ });
+ });
+</script>
+
+<Layout>
+ <div class="grid gap-md margin-top-xs flex-row@md items-start flex-column-reverse">
+ <Tile class="col">
+ <h3 class="text-md padding-bottom-xxxs">{$LL.home.newEntry()}</h3>
+ <EntryFrom bind:functions={EditEntryForm}/>
+ </Tile>
+ <div class="col grid gap-sm">
+ <Tile class="col-6@md col-12">
+ <p class="text-xxl">{timeLoggedTodayString}</p>
+ <p class="text-xs margin-bottom-xxs">{$LL.home.loggedTimeToday()}</p>
+ <pre class="text-xxl">{currentTime}</pre>
+ <p class="text-xs">{$LL.home.currentTime()}</p>
+ </Tile>
+ <Tile class="col-6@md col-12">
+ <Stopwatch on:create={on_create_from_stopwatch}>
+ <h3 slot="header"
+ class="text-md">{$LL.home.stopwatch()}</h3>
+ </Stopwatch>
+ </Tile>
+ <Tile class="col-12">
+ <h3 class="text-md padding-bottom-xxxs">{$LL.home.todayEntries()}</h3>
+ <div class="max-width-100% overflow-auto">
+ <Table class="width-100% text-sm">
+ <THead>
+ <TCell type="th"
+ class="text-left">
+ <span>{$LL.home.category()}</span>
+ </TCell>
+ <TCell type="th"
+ class="text-left">
+ <span>{$LL.home.timespan()}</span>
+ </TCell>
+ <TCell type="th"
+ class="text-right">
+ <Button icon="{IconNames.refresh}"
+ variant="reset"
+ icon_width="1.2rem"
+ icon_height="1.2rem"
+ title="{$LL.home.refreshTodayEntries()}"
+ on:click={() => queryClient.invalidateQueries(QueryKeys.entries)}/>
+ </TCell>
+ </THead>
+ <TBody>
+ {#if timeEntries.length > 0}
+ {#each timeEntries as entry}
+ <TRow class="text-nowrap text-left"
+ data-id={entry.id}>
+ <TCell>
+ <span data-id={entry.category?.id}>
+ {entry.category?.name}
+ </span>
+ </TCell>
+ <TCell>
+ {entry.start.toLocaleString(undefined, {timeStyle: "short"})}
+ <span>-</span>
+ {entry.stop.toLocaleString(undefined, {timeStyle: "short"})}
+ </TCell>
+ <TCell class="flex flex-row justify-end items-center">
+ <Button icon="{IconNames.pencilSquare}"
+ variant="reset"
+ icon_width="1.2rem"
+ icon_height="1.2rem"
+ on:click={(e) => on_edit_entry_button_click(e, entry.id)}
+ title="{$LL.home.editEntry()}"/>
+ <Button icon="{IconNames.trash}"
+ variant="reset"
+ icon_width="1.2rem"
+ icon_height="1.2rem"
+ on:click={(e) => on_delete_entry_button_click(e, entry.id)}
+ title="{$LL.home.deleteEntry()}"/>
+ </TCell>
+ </TRow>
+ {/each}
+ {:else}
+ <TRow class="text-nowrap">
+ <TCell type="th"
+ thScope="row"
+ colspan="7">
+ {isLoading ? $LL.home.loading() + "..." : $LL.home.noEntriesToday()}
+ </TCell>
+ </TRow>
+ {/if}
+ </TBody>
+ </Table>
+ </div>
+ </Tile>
+ </div>
+ </div>
+</Layout>
diff --git a/old-apps/projects/src/app/pages/nav/css/1_responsive-sidebar.css b/old-apps/projects/src/app/pages/nav/css/1_responsive-sidebar.css
new file mode 100644
index 0000000..515a9f2
--- /dev/null
+++ b/old-apps/projects/src/app/pages/nav/css/1_responsive-sidebar.css
@@ -0,0 +1,179 @@
+/* --------------------------------
+
+File#: _1_responsive-sidebar
+Title: Responsive Sidebar
+Descr: Responsive sidebar container
+Usage: codyhouse.co/license
+
+-------------------------------- */
+/* mobile version only (--default) 👇 */
+.sidebar:not(.sidebar--static) {
+ position: fixed;
+ top: 0;
+ left: 0;
+ z-index: var(--z-index-fixed-element, 10);
+ width: 100%;
+ height: 100%;
+ visibility: hidden;
+ transition: visibility 0s 0.3s;
+}
+.sidebar:not(.sidebar--static)::after {
+ /* overlay layer */
+ content: "";
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background-color: hsla(var(--color-black-h), var(--color-black-s), var(--color-black-l), 0);
+ transition: background-color 0.3s;
+ z-index: 1;
+}
+.sidebar:not(.sidebar--static) .sidebar__panel {
+ /* content */
+ position: absolute;
+ top: 0;
+ left: 0;
+ z-index: 2;
+ width: 100%;
+ max-width: 380px;
+ height: 100%;
+ overflow: auto;
+ -webkit-overflow-scrolling: touch;
+ background-color: var(--color-bg);
+ -webkit-transform: translateX(-100%);
+ transform: translateX(-100%);
+ transition: box-shadow 0.3s, -webkit-transform 0.3s;
+ transition: box-shadow 0.3s, transform 0.3s;
+ transition: box-shadow 0.3s, transform 0.3s, -webkit-transform 0.3s;
+}
+.sidebar:not(.sidebar--static).sidebar--right-on-mobile .sidebar__panel {
+ left: auto;
+ right: 0;
+ -webkit-transform: translateX(100%);
+ transform: translateX(100%);
+}
+.sidebar:not(.sidebar--static).sidebar--is-visible {
+ visibility: visible;
+ transition: none;
+}
+.sidebar:not(.sidebar--static).sidebar--is-visible::after {
+ background-color: hsla(var(--color-black-h), var(--color-black-s), var(--color-black-l), 0.85);
+}
+.sidebar:not(.sidebar--static).sidebar--is-visible .sidebar__panel {
+ -webkit-transform: translateX(0);
+ transform: translateX(0);
+ box-shadow: var(--shadow-md);
+}
+
+/* end mobile version */
+.sidebar__header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ position: -webkit-sticky;
+ position: sticky;
+ top: 0;
+}
+
+.sidebar__close-btn {
+ --size: 32px;
+ width: var(--size);
+ height: var(--size);
+ display: flex;
+ border-radius: 50%;
+ background-color: var(--color-bg-light);
+ box-shadow: var(--inner-glow), var(--shadow-sm);
+ transition: 0.2s;
+ flex-shrink: 0;
+}
+.sidebar__close-btn .icon {
+ display: block;
+ margin: auto;
+}
+.sidebar__close-btn:hover {
+ background-color: var(--color-bg-lighter);
+ box-shadow: var(--inner-glow), var(--shadow-md);
+}
+
+/* desktop version only (--static) 👇 */
+.sidebar--static {
+ flex-shrink: 0;
+ flex-grow: 1;
+}
+.sidebar--static .sidebar__header {
+ display: none;
+}
+
+.sidebar--sticky-on-desktop {
+ position: -webkit-sticky;
+ position: sticky;
+ top: var(--space-sm);
+ max-height: calc(100vh - var(--space-sm));
+ overflow: auto;
+ -webkit-overflow-scrolling: touch;
+}
+
+/* end desktop version */
+.sidebar, .sidebar-loaded\:show {
+ opacity: 0;
+ /* hide sidebar - or other elements using the .sidebar-loaded:show class - while it is initialized in JS */
+}
+
+.sidebar--loaded {
+ opacity: 1;
+}
+
+/* detect when the sidebar needs to switch from the mobile layout to a static one - used in JS */
+[class*=sidebar--static]::before {
+ display: none;
+}
+
+.sidebar--static::before {
+ content: "static";
+}
+
+.sidebar--static\@xs::before {
+ content: "mobile";
+}
+@media (min-width: 32rem) {
+ .sidebar--static\@xs::before {
+ content: "static";
+ }
+}
+
+.sidebar--static\@sm::before {
+ content: "mobile";
+}
+@media (min-width: 48rem) {
+ .sidebar--static\@sm::before {
+ content: "static";
+ }
+}
+
+.sidebar--static\@md::before {
+ content: "mobile";
+}
+@media (min-width: 64rem) {
+ .sidebar--static\@md::before {
+ content: "static";
+ }
+}
+
+.sidebar--static\@lg::before {
+ content: "mobile";
+}
+@media (min-width: 80rem) {
+ .sidebar--static\@lg::before {
+ content: "static";
+ }
+}
+
+.sidebar--static\@xl::before {
+ content: "mobile";
+}
+@media (min-width: 90rem) {
+ .sidebar--static\@xl::before {
+ content: "static";
+ }
+} \ No newline at end of file
diff --git a/old-apps/projects/src/app/pages/nav/css/2_side-navigation-v4.css b/old-apps/projects/src/app/pages/nav/css/2_side-navigation-v4.css
new file mode 100644
index 0000000..ec5fcdf
--- /dev/null
+++ b/old-apps/projects/src/app/pages/nav/css/2_side-navigation-v4.css
@@ -0,0 +1,213 @@
+/* --------------------------------
+
+File#: _2_side-navigation-v4
+Title: Side Navigation v4
+Descr: Main, side navigation
+Usage: codyhouse.co/license
+
+-------------------------------- */
+.sidenav-v4 {
+ --sidenav-v4-icon-size: 20px;
+ --sidenav-v4-icon-margin-right: var(--space-xxs);
+}
+
+.sidenav-v4__item {
+ position: relative;
+}
+
+.sidenav-v4__link,
+.sidenav-v4__sub-link,
+.sidenav-v4__separator {
+ padding: var(--space-sm);
+}
+
+.sidenav-v4__link, .sidenav-v4__sub-link {
+ display: flex;
+ align-items: center;
+ width: 100%;
+ border-radius: var(--radius-md);
+ text-decoration: none;
+ color: inherit;
+ line-height: 1;
+ font-size: var(--text-md);
+ transition: 0.2s;
+}
+.sidenav-v4__link:hover, .sidenav-v4__sub-link:hover {
+ color: var(--color-primary);
+ background-color: hsla(var(--color-contrast-higher-h), var(--color-contrast-higher-s), var(--color-contrast-higher-l), 0.075);
+}
+.sidenav-v4__link[aria-current=page], .sidenav-v4__sub-link[aria-current=page] {
+ color: var(--color-primary);
+}
+
+.sidenav-v4__sub-link {
+ position: relative;
+ color: var(--color-contrast-medium);
+ /* dot indicator */
+}
+.sidenav-v4__sub-link::before {
+ content: "";
+ display: block;
+ --size: 6px;
+ width: var(--size);
+ height: var(--size);
+ background: currentColor;
+ border-radius: 50%;
+ margin-left: calc(var(--sidenav-v4-icon-size)/2 - var(--size)/2);
+ margin-right: calc(var(--sidenav-v4-icon-size)/2 - var(--size)/2 + var(--sidenav-v4-icon-margin-right));
+ opacity: 0;
+ /* visible only if current */
+}
+.sidenav-v4__sub-link[aria-current=page]::before {
+ /* show dot indicator */
+ opacity: 1;
+}
+
+.sidenav-v4__notification-marker {
+ margin-left: auto;
+ background-color: var(--color-accent);
+ border-radius: var(--radius-md);
+ height: 16px;
+ line-height: 16px;
+ padding: 0 4px;
+ color: var(--color-white);
+ font-size: 12px;
+ /* hide - visible only on desktop */
+ display: none;
+}
+
+/* label icon */
+.sidenav-v4__icon {
+ --size: var(--sidenav-v4-icon-size);
+ margin-right: var(--sidenav-v4-icon-margin-right);
+}
+
+/* arrow icon - visible on mobile if item is expandable */
+.sidenav-v4__arrow-icon {
+ --size: 20px;
+ /* hide icon for links - show only for buttons created in JS */
+}
+.sidenav-v4__arrow-icon .icon__group {
+ will-change: transform;
+ -webkit-transform-origin: 50% 50%;
+ transform-origin: 50% 50%;
+ -webkit-transform: rotate(-90deg);
+ transform: rotate(-90deg);
+ transition: -webkit-transform 0.3s var(--ease-out);
+ transition: transform 0.3s var(--ease-out);
+ transition: transform 0.3s var(--ease-out), -webkit-transform 0.3s var(--ease-out);
+}
+.sidenav-v4__arrow-icon .icon__group > * {
+ -webkit-transform-origin: 50% 50%;
+ transform-origin: 50% 50%;
+ stroke-dasharray: 20;
+ stroke-dashoffset: 0;
+ -webkit-transform: translateY(0px);
+ transform: translateY(0px);
+ transition: stroke-dashoffset 0.3s, -webkit-transform 0.3s;
+ transition: transform 0.3s, stroke-dashoffset 0.3s;
+ transition: transform 0.3s, stroke-dashoffset 0.3s, -webkit-transform 0.3s;
+ transition-timing-function: var(--ease-out);
+}
+.sidenav-v4__item--collapsed .sidenav-v4__arrow-icon .icon__group {
+ -webkit-transform: rotate(0deg);
+ transform: rotate(0deg);
+}
+.sidenav-v4__item--collapsed .sidenav-v4__arrow-icon .icon__group > * {
+ -webkit-transform: translateY(4px);
+ transform: translateY(4px);
+}
+.sidenav-v4__item--collapsed .sidenav-v4__arrow-icon .icon__group > *:first-child {
+ stroke-dashoffset: 10.15;
+}
+.sidenav-v4__item--collapsed .sidenav-v4__arrow-icon .icon__group > *:last-child {
+ stroke-dashoffset: 10.15;
+}
+.sidenav-v4__link--href .sidenav-v4__arrow-icon {
+ display: none;
+}
+
+/* current item */
+.sidenav-v4__item--current .sidenav-v4__sub-list {
+ display: block;
+ /* show sublist */
+}
+
+/* separator */
+.sidenav-v4__separator span {
+ display: block;
+ width: var(--sidenav-v4-icon-size);
+ height: 1px;
+ background-color: var(--color-contrast-lower);
+}
+
+/* mobile only */
+@media not all and (min-width: 64rem) {
+ .sidenav-v4__item--collapsed .sidenav-v4__sub-list {
+ display: none;
+ }
+
+ .sidenav-v4__link--href {
+ display: none;
+ /* hide link -> show button */
+ }
+}
+/* desktop */
+@media (min-width: 64rem) {
+ .sidenav-v4__sub-list {
+ display: none;
+ }
+
+ .sidenav-v4__link,
+.sidenav-v4__sub-link,
+.sidenav-v4__separator {
+ padding: var(--space-xs);
+ }
+
+ .sidenav-v4__link,
+.sidenav-v4__sub-link {
+ font-size: var(--text-sm);
+ }
+
+ .sidenav-v4__link--btn {
+ display: none;
+ /* hide button -> show link */
+ }
+
+ /* tooltip */
+ .sidenav-v4__item:not(.sidenav-v4__item--current) .sidenav-v4__sub-list {
+ width: 220px;
+ position: absolute;
+ z-index: var(--z-index-overlay);
+ left: 100%;
+ top: 0;
+ background-color: var(--color-bg-light);
+ box-shadow: var(--inner-glow), var(--shadow-md);
+ border-radius: var(--radius-md);
+ overflow: hidden;
+ }
+ .sidenav-v4__item:not(.sidenav-v4__item--current) .sidenav-v4__sub-link {
+ border-radius: 0;
+ color: var(--color-contrast-high);
+ }
+ .sidenav-v4__item:not(.sidenav-v4__item--current) .sidenav-v4__sub-link::before {
+ display: none;
+ /* remove dot indicator */
+ }
+ .sidenav-v4__item:not(.sidenav-v4__item--current) .sidenav-v4__sub-link:hover {
+ color: var(--color-primary);
+ }
+ .sidenav-v4__item:not(.sidenav-v4__item--current).sidenav-v4__item--hover .sidenav-v4__sub-list, .sidenav-v4__item:not(.sidenav-v4__item--current):focus-within .sidenav-v4__sub-list {
+ display: block;
+ }
+ .sidenav-v4__item:not(.sidenav-v4__item--current):hover .sidenav-v4__link {
+ /* highlight main link if tooltip is visible */
+ color: var(--color-primary);
+ background-color: hsla(var(--color-contrast-higher-h), var(--color-contrast-higher-s), var(--color-contrast-higher-l), 0.075);
+ }
+
+ /* notification marker */
+ .sidenav-v4__notification-marker {
+ display: block;
+ }
+} \ No newline at end of file
diff --git a/old-apps/projects/src/app/pages/nav/html/side-navigation-v4.html b/old-apps/projects/src/app/pages/nav/html/side-navigation-v4.html
new file mode 100644
index 0000000..1131b4d
--- /dev/null
+++ b/old-apps/projects/src/app/pages/nav/html/side-navigation-v4.html
@@ -0,0 +1,211 @@
+<div class="padding-component hide@md no-js:is-hidden">
+ <button class="btn btn--primary" aria-controls="sidenav-v4">Show sidebar</button>
+</div>
+
+<div class="flex@md">
+ <aside id="sidenav-v4" class="sidebar sidebar--static@md js-sidebar" data-static-class="position-relative z-index-2 bg width-100% max-width-xxxxs shadow-sm">
+ <div class="sidebar__panel">
+ <!-- 👇 header visible only on mobile -->
+ <header class="sidebar__header bg padding-y-sm padding-left-md padding-right-sm border-bottom z-index-2">
+ <h1 class="text-md text-truncate" id="sidebar-title">Menu</h1>
+
+ <button class="reset sidebar__close-btn js-sidebar__close-btn js-tab-focus">
+ <svg class="icon icon--xs" viewBox="0 0 16 16"><title>Close panel</title><g stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10"><line x1="13.5" y1="2.5" x2="2.5" y2="13.5"></line><line x1="2.5" y1="2.5" x2="13.5" y2="13.5"></line></g></svg>
+ </button>
+ </header>
+
+ <div class="position-relative z-index-1">
+ <nav class="sidenav-v4 padding-xs js-sidenav-v4">
+ <ul>
+ <li class="sidenav-v4__item">
+ <a class="sidenav-v4__link js-sidenav-v4__link" href="#0">
+ <svg class="sidenav-v4__icon icon" viewBox="0 0 20 20">
+ <g fill="currentColor">
+ <path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 12v7H4v-7"></path>
+ <path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1 10l9-9 9 9"></path>
+ <path d="M10 14a2 2 0 0 1 2 2v2H8v-2a2 2 0 0 1 2-2z"></path>
+ </g>
+ </svg>
+
+ <span>Overview</span>
+
+ <svg class="sidenav-v4__arrow-icon icon margin-left-auto" viewBox="0 0 20 20">
+ <g class="icon__group" fill="none" stroke="currentColor" stroke-width="2px" stroke-linecap="round" stroke-linejoin="round">
+ <line x1="3" y1="3" x2="17" y2="17" />
+ <line x1="17" y1="3" x2="3" y2="17" />
+ </g>
+ </svg>
+ </a>
+
+ <ul class="sidenav-v4__sub-list">
+ <li>
+ <a class="sidenav-v4__sub-link" href="#0">All Data</a>
+ </li>
+
+ <li>
+ <a class="sidenav-v4__sub-link" href="#0">Category 1</a>
+ </li>
+
+ <li>
+ <a class="sidenav-v4__sub-link" href="#0">Category 2</a>
+ </li>
+ </ul>
+ </li>
+
+ <li class="sidenav-v4__item sidenav-v4__item--current">
+ <a class="sidenav-v4__link js-sidenav-v4__link" href="#0">
+ <svg class="sidenav-v4__icon icon" viewBox="0 0 20 20">
+ <g fill="currentColor">
+ <path d="M10 20a2 2 0 0 1-2-2h4a2 2 0 0 1-2 2z"></path>
+ <path d="M19 15a3 3 0 0 1-3-3V7a6 6 0 0 0-6-6 6 6 0 0 0-6 6v5a3 3 0 0 1-3 3h18z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"></path>
+ </g>
+ </svg>
+
+ <span>Notifications</span>
+
+ <span class="sidenav-v4__notification-marker">8 <i class="sr-only">notifications</i></span>
+
+ <svg class="sidenav-v4__arrow-icon icon margin-left-auto" viewBox="0 0 20 20">
+ <g class="icon__group" fill="none" stroke="currentColor" stroke-width="2px" stroke-linecap="round" stroke-linejoin="round">
+ <line x1="3" y1="3" x2="17" y2="17" />
+ <line x1="17" y1="3" x2="3" y2="17" />
+ </g>
+ </svg>
+ </a>
+
+ <ul class="sidenav-v4__sub-list">
+ <li>
+ <a class="sidenav-v4__sub-link" href="#0">All Notifications</a>
+ </li>
+
+ <li>
+ <a class="sidenav-v4__sub-link" href="#0" aria-current="page">Friends</a>
+ </li>
+
+ <li>
+ <a class="sidenav-v4__sub-link" href="#0">Other</a>
+ </li>
+ </ul>
+ </li>
+
+ <li class="sidenav-v4__item">
+ <a class="sidenav-v4__link js-sidenav-v4__link" href="#0">
+ <svg class="sidenav-v4__icon icon" viewBox="0 0 20 20">
+ <g fill="currentColor">
+ <path d="M17 2H3a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h4l3 4 3-4h4a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"></path>
+ </g>
+ </svg>
+
+ <span>Comments</span>
+
+ <svg class="sidenav-v4__arrow-icon icon margin-left-auto" viewBox="0 0 20 20">
+ <g class="icon__group" fill="none" stroke="currentColor" stroke-width="2px" stroke-linecap="round" stroke-linejoin="round">
+ <line x1="3" y1="3" x2="17" y2="17" />
+ <line x1="17" y1="3" x2="3" y2="17" />
+ </g>
+ </svg>
+ </a>
+
+ <ul class="sidenav-v4__sub-list">
+ <li>
+ <a class="sidenav-v4__sub-link" href="#0">All Comments</a>
+ </li>
+
+ <li>
+ <a class="sidenav-v4__sub-link" href="#0">+ New Comment</a>
+ </li>
+
+ <li>
+ <a class="sidenav-v4__sub-link" href="#0">Spam</a>
+ </li>
+ </ul>
+ </li>
+
+ <li class="sidenav-v4__separator" role="presentation"><span></span></li>
+
+ <li class="sidenav-v4__item">
+ <a class="sidenav-v4__link js-sidenav-v4__link" href="#0">
+ <svg class="sidenav-v4__icon icon" viewBox="0 0 20 20">
+ <g fill="currentColor">
+ <rect x="2" y="2" width="16" height="16" rx="2" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"></rect>
+ <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 14l6-6 2 6H6z"></path><circle cx="6.5" cy="6.5" r="1.5"></circle>
+ </g>
+ </svg>
+
+ <span>Assets</span>
+
+ <svg class="sidenav-v4__arrow-icon icon margin-left-auto" viewBox="0 0 20 20">
+ <g class="icon__group" fill="none" stroke="currentColor" stroke-width="2px" stroke-linecap="round" stroke-linejoin="round">
+ <line x1="3" y1="3" x2="17" y2="17" />
+ <line x1="17" y1="3" x2="3" y2="17" />
+ </g>
+ </svg>
+ </a>
+
+ <ul class="sidenav-v4__sub-list">
+ <li>
+ <a class="sidenav-v4__sub-link" href="#0">All Assets</a>
+ </li>
+
+ <li>
+ <a class="sidenav-v4__sub-link" href="#0">Upload</a>
+ </li>
+ </ul>
+ </li>
+
+ <li class="sidenav-v4__item">
+ <a class="sidenav-v4__link js-sidenav-v4__link" href="#0">
+ <svg class="sidenav-v4__icon icon" viewBox="0 0 20 20">
+ <g fill="currentColor">
+ <circle cx="10" cy="4" r="3" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"></circle>
+ <path d="M10 11a8 8 0 0 0-7.562 5.383A2 2 0 0 0 4.347 19h11.306a2 2 0 0 0 1.909-2.617A8 8 0 0 0 10 11z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"></path>
+ </g>
+ </svg>
+
+ <span>Users</span>
+
+ <svg class="sidenav-v4__arrow-icon icon margin-left-auto" viewBox="0 0 20 20">
+ <g class="icon__group" fill="none" stroke="currentColor" stroke-width="2px" stroke-linecap="round" stroke-linejoin="round">
+ <line x1="3" y1="3" x2="17" y2="17" />
+ <line x1="17" y1="3" x2="3" y2="17" />
+ </g>
+ </svg>
+ </a>
+
+ <ul class="sidenav-v4__sub-list">
+ <li>
+ <a class="sidenav-v4__sub-link" href="#0">All Users</a>
+ </li>
+
+ <li>
+ <a class="sidenav-v4__sub-link" href="#0">+ New User</a>
+ </li>
+ </ul>
+ </li>
+
+ <li class="sidenav-v4__item">
+ <a class="sidenav-v4__link js-sidenav-v4__link" href="#0">
+ <svg class="sidenav-v4__icon icon" viewBox="0 0 20 20">
+ <g fill="currentColor">
+ <path d="M11 16l-1.55 1.55a4.95 4.95 0 0 1-7 0 4.95 4.95 0 0 1 0-7l2.192-2.192a4.95 4.95 0 0 1 7 0A4.907 4.907 0 0 1 12.731 10" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"></path>
+ <path d="M9 4l1.55-1.55a4.95 4.95 0 0 1 7 0 4.95 4.95 0 0 1 0 7l-2.192 2.192a4.95 4.95 0 0 1-7 0A4.907 4.907 0 0 1 7.269 10" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"></path>
+ </g>
+ </svg>
+
+ <span>Link</span>
+ </a>
+ </li>
+ </ul>
+ </nav>
+ </div>
+ </div>
+ </aside>
+
+ <main class="position-relative z-index-1 flex-grow height-100vh sidebar-loaded:show">
+ <!-- start main content -->
+ <div class="text-component padding-md">
+ <p>Main content.</p>
+ </div>
+ <!-- end main content -->
+ </main>
+</div> \ No newline at end of file
diff --git a/old-apps/projects/src/app/pages/nav/index.ts b/old-apps/projects/src/app/pages/nav/index.ts
new file mode 100644
index 0000000..ca91c20
--- /dev/null
+++ b/old-apps/projects/src/app/pages/nav/index.ts
@@ -0,0 +1,6 @@
+import NavWrapper from "./nav-wrapper.svelte";
+import NavItem from "./nav-item.svelte";
+export {
+ NavWrapper,
+ NavItem
+} \ No newline at end of file
diff --git a/old-apps/projects/src/app/pages/nav/js/_1_diagonal-movement.js b/old-apps/projects/src/app/pages/nav/js/_1_diagonal-movement.js
new file mode 100644
index 0000000..ed4a47d
--- /dev/null
+++ b/old-apps/projects/src/app/pages/nav/js/_1_diagonal-movement.js
@@ -0,0 +1,296 @@
+// File#: _1_diagonal-movement
+// Usage: codyhouse.co/license
+/*
+ Modified version of the jQuery-menu-aim plugin
+ https://github.com/kamens/jQuery-menu-aim
+ - Replaced jQuery with Vanilla JS
+ - Minor changes
+*/
+(function() {
+ var menuAim = function(opts) {
+ init(opts);
+ };
+
+ window.menuAim = menuAim;
+
+ function init(opts) {
+ var activeRow = null,
+ mouseLocs = [],
+ lastDelayLoc = null,
+ timeoutId = null,
+ options = Util.extend({
+ menu: '',
+ rows: false, //if false, get direct children - otherwise pass nodes list
+ submenuSelector: "*",
+ submenuDirection: "right",
+ tolerance: 75, // bigger = more forgivey when entering submenu
+ enter: function(){},
+ exit: function(){},
+ activate: function(){},
+ deactivate: function(){},
+ exitMenu: function(){}
+ }, opts),
+ menu = options.menu;
+
+ var MOUSE_LOCS_TRACKED = 3, // number of past mouse locations to track
+ DELAY = 300; // ms delay when user appears to be entering submenu
+
+ /**
+ * Keep track of the last few locations of the mouse.
+ */
+ var mouseMoveFallback = function(event) {
+ (!window.requestAnimationFrame) ? mousemoveDocument(event) : window.requestAnimationFrame(function(){mousemoveDocument(event);});
+ };
+
+ var mousemoveDocument = function(e) {
+ mouseLocs.push({x: e.pageX, y: e.pageY});
+
+ if (mouseLocs.length > MOUSE_LOCS_TRACKED) {
+ mouseLocs.shift();
+ }
+ };
+
+ /**
+ * Cancel possible row activations when leaving the menu entirely
+ */
+ var mouseleaveMenu = function() {
+ if (timeoutId) {
+ clearTimeout(timeoutId);
+ }
+
+ // If exitMenu is supplied and returns true, deactivate the
+ // currently active row on menu exit.
+ if (options.exitMenu(this)) {
+ if (activeRow) {
+ options.deactivate(activeRow);
+ }
+
+ activeRow = null;
+ }
+ };
+
+ /**
+ * Trigger a possible row activation whenever entering a new row.
+ */
+ var mouseenterRow = function() {
+ if (timeoutId) {
+ // Cancel any previous activation delays
+ clearTimeout(timeoutId);
+ }
+
+ options.enter(this);
+ possiblyActivate(this);
+ },
+ mouseleaveRow = function() {
+ options.exit(this);
+ };
+
+ /*
+ * Immediately activate a row if the user clicks on it.
+ */
+ var clickRow = function() {
+ activate(this);
+ };
+
+ /**
+ * Activate a menu row.
+ */
+ var activate = function(row) {
+ if (row == activeRow) {
+ return;
+ }
+
+ if (activeRow) {
+ options.deactivate(activeRow);
+ }
+
+ options.activate(row);
+ activeRow = row;
+ };
+
+ /**
+ * Possibly activate a menu row. If mouse movement indicates that we
+ * shouldn't activate yet because user may be trying to enter
+ * a submenu's content, then delay and check again later.
+ */
+ var possiblyActivate = function(row) {
+ var delay = activationDelay();
+
+ if (delay) {
+ timeoutId = setTimeout(function() {
+ possiblyActivate(row);
+ }, delay);
+ } else {
+ activate(row);
+ }
+ };
+
+ /**
+ * Return the amount of time that should be used as a delay before the
+ * currently hovered row is activated.
+ *
+ * Returns 0 if the activation should happen immediately. Otherwise,
+ * returns the number of milliseconds that should be delayed before
+ * checking again to see if the row should be activated.
+ */
+ var activationDelay = function() {
+ if (!activeRow || !Util.is(activeRow, options.submenuSelector)) {
+ // If there is no other submenu row already active, then
+ // go ahead and activate immediately.
+ return 0;
+ }
+
+ function getOffset(element) {
+ var rect = element.getBoundingClientRect();
+ return { top: rect.top + window.pageYOffset, left: rect.left + window.pageXOffset };
+ };
+
+ var offset = getOffset(menu),
+ upperLeft = {
+ x: offset.left,
+ y: offset.top - options.tolerance
+ },
+ upperRight = {
+ x: offset.left + menu.offsetWidth,
+ y: upperLeft.y
+ },
+ lowerLeft = {
+ x: offset.left,
+ y: offset.top + menu.offsetHeight + options.tolerance
+ },
+ lowerRight = {
+ x: offset.left + menu.offsetWidth,
+ y: lowerLeft.y
+ },
+ loc = mouseLocs[mouseLocs.length - 1],
+ prevLoc = mouseLocs[0];
+
+ if (!loc) {
+ return 0;
+ }
+
+ if (!prevLoc) {
+ prevLoc = loc;
+ }
+
+ if (prevLoc.x < offset.left || prevLoc.x > lowerRight.x || prevLoc.y < offset.top || prevLoc.y > lowerRight.y) {
+ // If the previous mouse location was outside of the entire
+ // menu's bounds, immediately activate.
+ return 0;
+ }
+
+ if (lastDelayLoc && loc.x == lastDelayLoc.x && loc.y == lastDelayLoc.y) {
+ // If the mouse hasn't moved since the last time we checked
+ // for activation status, immediately activate.
+ return 0;
+ }
+
+ // Detect if the user is moving towards the currently activated
+ // submenu.
+ //
+ // If the mouse is heading relatively clearly towards
+ // the submenu's content, we should wait and give the user more
+ // time before activating a new row. If the mouse is heading
+ // elsewhere, we can immediately activate a new row.
+ //
+ // We detect this by calculating the slope formed between the
+ // current mouse location and the upper/lower right points of
+ // the menu. We do the same for the previous mouse location.
+ // If the current mouse location's slopes are
+ // increasing/decreasing appropriately compared to the
+ // previous's, we know the user is moving toward the submenu.
+ //
+ // Note that since the y-axis increases as the cursor moves
+ // down the screen, we are looking for the slope between the
+ // cursor and the upper right corner to decrease over time, not
+ // increase (somewhat counterintuitively).
+ function slope(a, b) {
+ return (b.y - a.y) / (b.x - a.x);
+ };
+
+ var decreasingCorner = upperRight,
+ increasingCorner = lowerRight;
+
+ // Our expectations for decreasing or increasing slope values
+ // depends on which direction the submenu opens relative to the
+ // main menu. By default, if the menu opens on the right, we
+ // expect the slope between the cursor and the upper right
+ // corner to decrease over time, as explained above. If the
+ // submenu opens in a different direction, we change our slope
+ // expectations.
+ if (options.submenuDirection == "left") {
+ decreasingCorner = lowerLeft;
+ increasingCorner = upperLeft;
+ } else if (options.submenuDirection == "below") {
+ decreasingCorner = lowerRight;
+ increasingCorner = lowerLeft;
+ } else if (options.submenuDirection == "above") {
+ decreasingCorner = upperLeft;
+ increasingCorner = upperRight;
+ }
+
+ var decreasingSlope = slope(loc, decreasingCorner),
+ increasingSlope = slope(loc, increasingCorner),
+ prevDecreasingSlope = slope(prevLoc, decreasingCorner),
+ prevIncreasingSlope = slope(prevLoc, increasingCorner);
+
+ if (decreasingSlope < prevDecreasingSlope && increasingSlope > prevIncreasingSlope) {
+ // Mouse is moving from previous location towards the
+ // currently activated submenu. Delay before activating a
+ // new menu row, because user may be moving into submenu.
+ lastDelayLoc = loc;
+ return DELAY;
+ }
+
+ lastDelayLoc = null;
+ return 0;
+ };
+
+ var reset = function(triggerDeactivate) {
+ if (timeoutId) {
+ clearTimeout(timeoutId);
+ }
+
+ if (activeRow && triggerDeactivate) {
+ options.deactivate(activeRow);
+ }
+
+ activeRow = null;
+ };
+
+ var destroyInstance = function() {
+ menu.removeEventListener('mouseleave', mouseleaveMenu);
+ document.removeEventListener('mousemove', mouseMoveFallback);
+ if(rows.length > 0) {
+ for(var i = 0; i < rows.length; i++) {
+ rows[i].removeEventListener('mouseenter', mouseenterRow);
+ rows[i].removeEventListener('mouseleave', mouseleaveRow);
+ rows[i].removeEventListener('click', clickRow);
+ }
+ }
+
+ };
+
+ /**
+ * Hook up initial menu events
+ */
+ menu.addEventListener('mouseleave', mouseleaveMenu);
+ var rows = (options.rows) ? options.rows : menu.children;
+ if(rows.length > 0) {
+ for(var i = 0; i < rows.length; i++) {(function(i){
+ rows[i].addEventListener('mouseenter', mouseenterRow);
+ rows[i].addEventListener('mouseleave', mouseleaveRow);
+ rows[i].addEventListener('click', clickRow);
+ })(i);}
+ }
+
+ document.addEventListener('mousemove', mouseMoveFallback);
+
+ /* Reset/destroy menu */
+ menu.addEventListener('reset', function(event){
+ reset(event.detail);
+ });
+ menu.addEventListener('destroy', destroyInstance);
+ };
+}());
+
diff --git a/old-apps/projects/src/app/pages/nav/js/_1_responsive-sidebar.js b/old-apps/projects/src/app/pages/nav/js/_1_responsive-sidebar.js
new file mode 100644
index 0000000..f9599d8
--- /dev/null
+++ b/old-apps/projects/src/app/pages/nav/js/_1_responsive-sidebar.js
@@ -0,0 +1,215 @@
+// File#: _1_responsive-sidebar
+// Usage: codyhouse.co/license
+(function() {
+ var Sidebar = function(element) {
+ this.element = element;
+ this.triggers = document.querySelectorAll('[aria-controls="'+this.element.getAttribute('id')+'"]');
+ this.firstFocusable = null;
+ this.lastFocusable = null;
+ this.selectedTrigger = null;
+ this.showClass = "sidebar--is-visible";
+ this.staticClass = "sidebar--static";
+ this.customStaticClass = "";
+ this.readyClass = "sidebar--loaded";
+ this.contentReadyClass = "sidebar-loaded:show";
+ this.layout = false; // this will be static or mobile
+ this.preventScrollEl = getPreventScrollEl(this);
+ getCustomStaticClass(this); // custom classes for static version
+ initSidebar(this);
+ };
+
+ function getPreventScrollEl(element) {
+ var scrollEl = false;
+ var querySelector = element.element.getAttribute('data-sidebar-prevent-scroll');
+ if(querySelector) scrollEl = document.querySelector(querySelector);
+ return scrollEl;
+ };
+
+ function getCustomStaticClass(element) {
+ var customClasses = element.element.getAttribute('data-static-class');
+ if(customClasses) element.customStaticClass = ' '+customClasses;
+ };
+
+ function initSidebar(sidebar) {
+ initSidebarResize(sidebar); // handle changes in layout -> mobile to static and viceversa
+
+ if ( sidebar.triggers ) { // open sidebar when clicking on trigger buttons - mobile layout only
+ for(var i = 0; i < sidebar.triggers.length; i++) {
+ sidebar.triggers[i].addEventListener('click', function(event) {
+ event.preventDefault();
+ toggleSidebar(sidebar, event.target);
+ });
+ }
+ }
+
+ // use the 'openSidebar' event to trigger the sidebar
+ sidebar.element.addEventListener('openSidebar', function(event) {
+ toggleSidebar(sidebar, event.detail);
+ });
+ };
+
+ function toggleSidebar(sidebar, target) {
+ if(Util.hasClass(sidebar.element, sidebar.showClass)) {
+ sidebar.selectedTrigger = target;
+ closeSidebar(sidebar);
+ return;
+ }
+ sidebar.selectedTrigger = target;
+ showSidebar(sidebar);
+ initSidebarEvents(sidebar);
+ };
+
+ function showSidebar(sidebar) { // mobile layout only
+ Util.addClass(sidebar.element, sidebar.showClass);
+ getFocusableElements(sidebar);
+ Util.moveFocus(sidebar.element);
+ // change the overflow of the preventScrollEl
+ if(sidebar.preventScrollEl) sidebar.preventScrollEl.style.overflow = 'hidden';
+ };
+
+ function closeSidebar(sidebar) { // mobile layout only
+ Util.removeClass(sidebar.element, sidebar.showClass);
+ sidebar.firstFocusable = null;
+ sidebar.lastFocusable = null;
+ if(sidebar.selectedTrigger) sidebar.selectedTrigger.focus();
+ sidebar.element.removeAttribute('tabindex');
+ //remove listeners
+ cancelSidebarEvents(sidebar);
+ // change the overflow of the preventScrollEl
+ if(sidebar.preventScrollEl) sidebar.preventScrollEl.style.overflow = '';
+ };
+
+ function initSidebarEvents(sidebar) { // mobile layout only
+ //add event listeners
+ sidebar.element.addEventListener('keydown', handleEvent.bind(sidebar));
+ sidebar.element.addEventListener('click', handleEvent.bind(sidebar));
+ };
+
+ function cancelSidebarEvents(sidebar) { // mobile layout only
+ //remove event listeners
+ sidebar.element.removeEventListener('keydown', handleEvent.bind(sidebar));
+ sidebar.element.removeEventListener('click', handleEvent.bind(sidebar));
+ };
+
+ function handleEvent(event) { // mobile layout only
+ switch(event.type) {
+ case 'click': {
+ initClick(this, event);
+ }
+ case 'keydown': {
+ initKeyDown(this, event);
+ }
+ }
+ };
+
+ function initKeyDown(sidebar, event) { // mobile layout only
+ if( event.keyCode && event.keyCode == 27 || event.key && event.key == 'Escape' ) {
+ //close sidebar window on esc
+ closeSidebar(sidebar);
+ } else if( event.keyCode && event.keyCode == 9 || event.key && event.key == 'Tab' ) {
+ //trap focus inside sidebar
+ trapFocus(sidebar, event);
+ }
+ };
+
+ function initClick(sidebar, event) { // mobile layout only
+ //close sidebar when clicking on close button or sidebar bg layer
+ if( !event.target.closest('.js-sidebar__close-btn') && !Util.hasClass(event.target, 'js-sidebar') ) return;
+ event.preventDefault();
+ closeSidebar(sidebar);
+ };
+
+ function trapFocus(sidebar, event) { // mobile layout only
+ if( sidebar.firstFocusable == document.activeElement && event.shiftKey) {
+ //on Shift+Tab -> focus last focusable element when focus moves out of sidebar
+ event.preventDefault();
+ sidebar.lastFocusable.focus();
+ }
+ if( sidebar.lastFocusable == document.activeElement && !event.shiftKey) {
+ //on Tab -> focus first focusable element when focus moves out of sidebar
+ event.preventDefault();
+ sidebar.firstFocusable.focus();
+ }
+ };
+
+ function initSidebarResize(sidebar) {
+ // custom event emitted when window is resized - detect only if the sidebar--static@{breakpoint} class was added
+ var beforeContent = getComputedStyle(sidebar.element, ':before').getPropertyValue('content');
+ if(beforeContent && beforeContent !='' && beforeContent !='none') {
+ checkSidebarLayout(sidebar);
+
+ sidebar.element.addEventListener('update-sidebar', function(event){
+ checkSidebarLayout(sidebar);
+ });
+ }
+ // check if there a main element to show
+ var mainContent = document.getElementsByClassName(sidebar.contentReadyClass);
+ if(mainContent.length > 0) Util.removeClass(mainContent[0], sidebar.contentReadyClass);
+ Util.addClass(sidebar.element, sidebar.readyClass);
+ };
+
+ function checkSidebarLayout(sidebar) {
+ var layout = getComputedStyle(sidebar.element, ':before').getPropertyValue('content').replace(/\'|"/g, '');
+ if(layout == sidebar.layout) return;
+ sidebar.layout = layout;
+ if(layout != 'static') Util.addClass(sidebar.element, 'is-hidden');
+ Util.toggleClass(sidebar.element, sidebar.staticClass + sidebar.customStaticClass, layout == 'static');
+ if(layout != 'static') setTimeout(function(){Util.removeClass(sidebar.element, 'is-hidden')});
+ // reset element role
+ (layout == 'static') ? sidebar.element.removeAttribute('role', 'alertdialog') : sidebar.element.setAttribute('role', 'alertdialog');
+ // reset mobile behaviour
+ if(layout == 'static' && Util.hasClass(sidebar.element, sidebar.showClass)) closeSidebar(sidebar);
+ };
+
+ function getFocusableElements(sidebar) {
+ //get all focusable elements inside the drawer
+ var allFocusable = sidebar.element.querySelectorAll('[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), iframe, object, embed, [tabindex]:not([tabindex="-1"]), [contenteditable], audio[controls], video[controls], summary');
+ getFirstVisible(sidebar, allFocusable);
+ getLastVisible(sidebar, allFocusable);
+ };
+
+ function getFirstVisible(sidebar, elements) {
+ //get first visible focusable element inside the sidebar
+ for(var i = 0; i < elements.length; i++) {
+ if( elements[i].offsetWidth || elements[i].offsetHeight || elements[i].getClientRects().length ) {
+ sidebar.firstFocusable = elements[i];
+ return true;
+ }
+ }
+ };
+
+ function getLastVisible(sidebar, elements) {
+ //get last visible focusable element inside the sidebar
+ for(var i = elements.length - 1; i >= 0; i--) {
+ if( elements[i].offsetWidth || elements[i].offsetHeight || elements[i].getClientRects().length ) {
+ sidebar.lastFocusable = elements[i];
+ return true;
+ }
+ }
+ };
+
+ window.Sidebar = Sidebar;
+
+ //initialize the Sidebar objects
+ var sidebar = document.getElementsByClassName('js-sidebar');
+ if( sidebar.length > 0 ) {
+ for( var i = 0; i < sidebar.length; i++) {
+ (function(i){new Sidebar(sidebar[i]);})(i);
+ }
+ // switch from mobile to static layout
+ var customEvent = new CustomEvent('update-sidebar');
+ window.addEventListener('resize', function(event){
+ (!window.requestAnimationFrame) ? setTimeout(function(){resetLayout();}, 250) : window.requestAnimationFrame(resetLayout);
+ });
+
+ (window.requestAnimationFrame) // init sidebar layout
+ ? window.requestAnimationFrame(resetLayout)
+ : resetLayout();
+
+ function resetLayout() {
+ for( var i = 0; i < sidebar.length; i++) {
+ (function(i){sidebar[i].dispatchEvent(customEvent)})(i);
+ };
+ };
+ }
+}()); \ No newline at end of file
diff --git a/old-apps/projects/src/app/pages/nav/js/_2_side-navigation-v4.js b/old-apps/projects/src/app/pages/nav/js/_2_side-navigation-v4.js
new file mode 100644
index 0000000..63ef9c4
--- /dev/null
+++ b/old-apps/projects/src/app/pages/nav/js/_2_side-navigation-v4.js
@@ -0,0 +1,73 @@
+// File#: _2_side-navigation-v4
+// Usage: codyhouse.co/license
+(function() {
+ function initSideNav(nav) {
+ // create btns - visible on mobile only
+ createBtns(nav);
+ // toggle sublists on mobile when clicking on buttons
+ toggleSubLists(nav);
+ // init diagonal movement
+ initDiagonalMove(nav);
+ };
+
+ function createBtns(nav) {
+ // on mobile -> create a <button> element for each link with a submenu
+ var expandableLinks = nav.getElementsByClassName('js-sidenav-v4__link');
+ for(var i = 0; i < expandableLinks.length; i++) {
+ createSingleBtn(expandableLinks[i]);
+ }
+ };
+
+ function createSingleBtn(link) {
+ if(!hasSubList(link)) return;
+ // create btn and insert it into the DOM
+ var btnClasses = link.getAttribute('class').replace('js-sidenav-v4__link', 'js-sidenav-v4__btn');
+ btnClasses = btnClasses +' sidenav-v4__link--btn';
+ var btnHtml = '<button class="reset '+btnClasses+'">'+link.innerHTML+'</button>';
+ link.insertAdjacentHTML('afterend', btnHtml);
+ // add class to link element
+ Util.addClass(link, 'sidenav-v4__link--href');
+ // check if we need to add the collpsed class to the <li> element
+ var listItem = link.parentElement;
+ if(!Util.hasClass(listItem, 'sidenav-v4__item--current')) Util.addClass(listItem, 'sidenav-v4__item--collapsed');
+ };
+
+ function hasSubList(link) {
+ // check if link has submenu
+ var sublist = link.nextElementSibling;
+ if(!sublist) return false;
+ return Util.hasClass(sublist, 'sidenav-v4__sub-list');
+ };
+
+ function toggleSubLists(nav) {
+ // open/close sublist on mobile
+ nav.addEventListener('click', function(event){
+ var btn = event.target.closest('.js-sidenav-v4__btn');
+ if(!btn) return;
+ Util.toggleClass(btn.parentElement, 'sidenav-v4__item--collapsed', !Util.hasClass(btn.parentElement, 'sidenav-v4__item--collapsed'));
+ });
+ };
+
+ function initDiagonalMove(nav) {
+ // improve dropdown navigation
+ new menuAim({
+ menu: nav.querySelector('ul'),
+ activate: function(row) {
+ Util.addClass(row, 'sidenav-v4__item--hover');
+ },
+ deactivate: function(row) {
+ Util.removeClass(row, 'sidenav-v4__item--hover');
+ },
+ exitMenu: function() {
+ return true;
+ },
+ });
+ };
+
+ var sideNavs = document.getElementsByClassName('js-sidenav-v4');
+ if( sideNavs.length > 0 ) {
+ for( var i = 0; i < sideNavs.length; i++) {
+ (function(i){initSideNav(sideNavs[i]);})(i);
+ }
+ }
+}()); \ No newline at end of file
diff --git a/old-apps/projects/src/app/pages/nav/nav-item.svelte b/old-apps/projects/src/app/pages/nav/nav-item.svelte
new file mode 100644
index 0000000..335cbbb
--- /dev/null
+++ b/old-apps/projects/src/app/pages/nav/nav-item.svelte
@@ -0,0 +1,18 @@
+<script lang="ts">
+ import {link} from "svelte-spa-router";
+ import Icon from "$shared/components/icon.svelte";
+
+ export let external = "";
+ export let to = "";
+ export let text;
+ export let icon;
+</script>
+
+<li class="sidenav-v4__item">
+ <a class="sidenav-v4__link" href={to ?? external} use:link={external === ""}>
+ {#if icon}
+ <Icon class="sidenav-v4__icon icon" name="{icon}" />
+ {/if}
+ <span>{text}</span>
+ </a>
+</li>
diff --git a/old-apps/projects/src/app/pages/nav/nav-wrapper.svelte b/old-apps/projects/src/app/pages/nav/nav-wrapper.svelte
new file mode 100644
index 0000000..8321544
--- /dev/null
+++ b/old-apps/projects/src/app/pages/nav/nav-wrapper.svelte
@@ -0,0 +1,20 @@
+<script lang="ts">
+ import {random_string} from "$shared/lib/helpers";
+
+ export let id = "nav__" + random_string(4);
+ const staticClasses = "position-relative z-index-2 bg width-100% max-width-xxxxs shadow-sm"
+</script>
+<div class="flex@md">
+ <aside id="{id}" class="sidebar sidebar--static@md {staticClasses}">
+ <div class="sidebar__panel">
+ <div class="position-relative z-index-1">
+ <nav class="sidenav-v4 padding-xs">
+ <slot name="navigation-items"></slot>
+ </nav>
+ </div>
+ </div>
+ </aside>
+ <main class="container max-width-xl position-relative z-index-1 flex-grow min-height-100vh position-sticky@md top-0@md height-100vh@md overflow-auto@m">
+ <slot name="main-content"></slot>
+ </main>
+</div> \ No newline at end of file
diff --git a/old-apps/projects/src/app/pages/nav/scss/_1_responsive-sidebar.scss b/old-apps/projects/src/app/pages/nav/scss/_1_responsive-sidebar.scss
new file mode 100644
index 0000000..e4304f1
--- /dev/null
+++ b/old-apps/projects/src/app/pages/nav/scss/_1_responsive-sidebar.scss
@@ -0,0 +1,147 @@
+@use '../base' as *;
+
+/* --------------------------------
+
+File#: _1_responsive-sidebar
+Title: Responsive Sidebar
+Descr: Responsive sidebar container
+Usage: codyhouse.co/license
+
+-------------------------------- */
+
+/* mobile version only (--default) 👇 */
+.sidebar:not(.sidebar--static) {
+ position: fixed;
+ top: 0;
+ left: 0;
+ z-index: var(--z-index-fixed-element, 10);
+ width: 100%;
+ height: 100%;
+ visibility: hidden;
+ transition: visibility 0s 0.3s;
+
+ &::after { /* overlay layer */
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background-color: alpha(var(--color-black), 0);
+ transition: background-color .3s;
+ z-index: 1;
+ }
+
+ .sidebar__panel { /* content */
+ position: absolute;
+ top: 0;
+ left: 0;
+ z-index: 2;
+ width: 100%;
+ max-width: 380px;
+ height: 100%;
+ overflow: auto;
+ -webkit-overflow-scrolling: touch;
+ background-color: var(--color-bg);
+ transform: translateX(-100%);
+ transition: box-shadow 0.3s,transform 0.3s;
+ }
+
+ &.sidebar--right-on-mobile {
+ .sidebar__panel {
+ left: auto;
+ right: 0;
+ transform: translateX(100%);
+ }
+ }
+
+ &.sidebar--is-visible {
+ visibility: visible;
+ transition: none;
+
+ &::after {
+ background-color: alpha(var(--color-black), 0.85);
+ }
+
+ .sidebar__panel {
+ transform: translateX(0);
+ box-shadow: var(--shadow-md);
+ }
+ }
+}
+/* end mobile version */
+
+.sidebar__header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ position: sticky;
+ top: 0;
+}
+
+.sidebar__close-btn {
+ --size: 32px;
+ width: var(--size);
+ height: var(--size);
+ display: flex;
+ border-radius: 50%;
+ background-color: var(--color-bg-light);
+ box-shadow: var(--inner-glow), var(--shadow-sm);
+ transition: .2s;
+ flex-shrink: 0;
+
+ .icon {
+ display: block;
+ margin: auto;
+ }
+
+ &:hover {
+ background-color: var(--color-bg-lighter);
+ box-shadow: var(--inner-glow), var(--shadow-md);
+ }
+}
+
+/* desktop version only (--static) 👇 */
+.sidebar--static {
+ flex-shrink: 0;
+ flex-grow: 1;
+
+ .sidebar__header {
+ display: none;
+ }
+}
+
+.sidebar--sticky-on-desktop {
+ position: sticky;
+ top: var(--space-sm);
+ max-height: calc(100vh - var(--space-sm));
+ overflow: auto;
+ -webkit-overflow-scrolling: touch;
+}
+/* end desktop version */
+
+.sidebar, .sidebar-loaded\:show {
+ opacity: 0; /* hide sidebar - or other elements using the .sidebar-loaded:show class - while it is initialized in JS */
+}
+
+.sidebar--loaded {
+ opacity: 1;
+}
+
+/* detect when the sidebar needs to switch from the mobile layout to a static one - used in JS */
+[class*="sidebar--static"]::before {
+ display: none;
+}
+
+.sidebar--static::before {
+ content: 'static';
+}
+
+@each $breakpoint, $value in $breakpoints {
+ .sidebar--static\@#{$breakpoint}::before {
+ content: 'mobile';
+ @include breakpoint(#{$breakpoint}) {
+ content: 'static';
+ }
+ }
+} \ No newline at end of file
diff --git a/old-apps/projects/src/app/pages/nav/scss/_2_side-navigation-v4.scss b/old-apps/projects/src/app/pages/nav/scss/_2_side-navigation-v4.scss
new file mode 100644
index 0000000..2b421df
--- /dev/null
+++ b/old-apps/projects/src/app/pages/nav/scss/_2_side-navigation-v4.scss
@@ -0,0 +1,237 @@
+@use '../base' as *;
+@use '_1_responsive-sidebar.scss' as *;
+
+/* --------------------------------
+
+File#: _2_side-navigation-v4
+Title: Side Navigation v4
+Descr: Main, side navigation
+Usage: codyhouse.co/license
+
+-------------------------------- */
+
+.sidenav-v4 {
+ --sidenav-v4-icon-size: 20px;
+ --sidenav-v4-icon-margin-right: var(--space-xxs);
+}
+
+.sidenav-v4__item {
+ position: relative;
+}
+
+.sidenav-v4__link,
+.sidenav-v4__sub-link,
+.sidenav-v4__separator {
+ padding: var(--space-sm);
+}
+
+.sidenav-v4__link, .sidenav-v4__sub-link {
+ display: flex;
+ align-items: center;
+
+ width: 100%;
+ border-radius: var(--radius-md);
+
+ text-decoration: none;
+ color: inherit;
+ line-height: 1;
+ font-size: var(--text-md);
+
+ transition: .2s;
+
+ &:hover {
+ color: var(--color-primary);
+ background-color: alpha(var(--color-contrast-higher), 0.075);
+ }
+
+ &[aria-current="page"] {
+ color: var(--color-primary);
+ }
+}
+
+.sidenav-v4__sub-link {
+ position: relative;
+ color: var(--color-contrast-medium);
+
+ /* dot indicator */
+ &::before {
+ content: '';
+ display: block;
+ --size: 6px;
+ width: var(--size);
+ height: var(--size);
+ background: currentColor;
+ border-radius: 50%;
+ margin-left: calc(var(--sidenav-v4-icon-size)/2 - var(--size)/2);
+ margin-right: calc(var(--sidenav-v4-icon-size)/2 - var(--size)/2 + var(--sidenav-v4-icon-margin-right));
+
+ opacity: 0; /* visible only if current */
+ }
+
+ &[aria-current="page"] {
+ &::before { /* show dot indicator */
+ opacity: 1;
+ }
+ }
+}
+
+.sidenav-v4__notification-marker {
+ margin-left: auto;
+ background-color: var(--color-accent);
+ border-radius: var(--radius-md);
+
+ height: 16px;
+ line-height: 16px;
+ padding: 0 4px;
+ color: var(--color-white);
+ font-size: 12px;
+
+ /* hide - visible only on desktop */
+ display: none;
+}
+
+/* label icon */
+.sidenav-v4__icon {
+ --size: var(--sidenav-v4-icon-size);
+ margin-right: var(--sidenav-v4-icon-margin-right);
+}
+
+/* arrow icon - visible on mobile if item is expandable */
+.sidenav-v4__arrow-icon {
+ --size: 20px;
+
+ .icon__group {
+ will-change: transform;
+ transform-origin: 50% 50%;
+ transform: rotate(-90deg);
+ transition: transform .3s var(--ease-out);
+
+ > * {
+ transform-origin: 50% 50%;
+ stroke-dasharray: 20;
+ stroke-dashoffset: 0;
+ transform: translateY(0px);
+ transition: transform .3s, stroke-dashoffset .3s;
+ transition-timing-function: var(--ease-out);
+ }
+
+ .sidenav-v4__item--collapsed & {
+ transform: rotate(0deg);
+
+ > * {
+ transform: translateY(4px);
+ }
+
+ > *:first-child {
+ stroke-dashoffset: 10.15;
+ }
+
+ > *:last-child {
+ stroke-dashoffset: 10.15;
+ }
+ }
+ }
+
+ /* hide icon for links - show only for buttons created in JS */
+ .sidenav-v4__link--href & {
+ display: none;
+ }
+}
+
+/* current item */
+.sidenav-v4__item--current {
+ .sidenav-v4__sub-list {
+ display: block; /* show sublist */
+ }
+}
+
+/* separator */
+.sidenav-v4__separator {
+ span {
+ display: block;
+ width: var(--sidenav-v4-icon-size);
+ height: 1px;
+ background-color: var(--color-contrast-lower);
+ }
+}
+
+/* mobile only */
+@include breakpoint(md, "not all") {
+ .sidenav-v4__item--collapsed {
+ .sidenav-v4__sub-list {
+ display: none;
+ }
+ }
+
+ .sidenav-v4__link--href {
+ display: none; /* hide link -> show button */
+ }
+}
+
+/* desktop */
+@include breakpoint(md) {
+ .sidenav-v4__sub-list {
+ display: none;
+ }
+
+ .sidenav-v4__link,
+ .sidenav-v4__sub-link,
+ .sidenav-v4__separator {
+ padding: var(--space-xs);
+ }
+
+ .sidenav-v4__link,
+ .sidenav-v4__sub-link {
+ font-size: var(--text-sm);
+ }
+
+ .sidenav-v4__link--btn {
+ display: none; /* hide button -> show link */
+ }
+
+ /* tooltip */
+ .sidenav-v4__item:not(.sidenav-v4__item--current) {
+ .sidenav-v4__sub-list {
+ width: 220px;
+ position: absolute;
+ z-index: var(--z-index-overlay);
+ left: 100%;
+ top: 0;
+
+ background-color: var(--color-bg-light);
+ box-shadow: var(--inner-glow), var(--shadow-md);
+ border-radius: var(--radius-md);
+
+ overflow: hidden;
+ }
+
+ .sidenav-v4__sub-link {
+ border-radius: 0;
+ color: var(--color-contrast-high);
+
+ &::before {
+ display: none; /* remove dot indicator */
+ }
+
+ &:hover {
+ color: var(--color-primary);
+ }
+ }
+
+ &.sidenav-v4__item--hover, &:focus-within {
+ .sidenav-v4__sub-list {
+ display: block;
+ }
+ }
+
+ &:hover .sidenav-v4__link { /* highlight main link if tooltip is visible */
+ color: var(--color-primary);
+ background-color: alpha(var(--color-contrast-higher), 0.075);
+ }
+ }
+
+ /* notification marker */
+ .sidenav-v4__notification-marker {
+ display: block;
+ }
+} \ No newline at end of file
diff --git a/old-apps/projects/src/app/pages/nav/side-navigation-v4.zip b/old-apps/projects/src/app/pages/nav/side-navigation-v4.zip
new file mode 100644
index 0000000..d034eaf
--- /dev/null
+++ b/old-apps/projects/src/app/pages/nav/side-navigation-v4.zip
Binary files differ
diff --git a/old-apps/projects/src/app/pages/not-found.svelte b/old-apps/projects/src/app/pages/not-found.svelte
new file mode 100644
index 0000000..8822e0e
--- /dev/null
+++ b/old-apps/projects/src/app/pages/not-found.svelte
@@ -0,0 +1,25 @@
+<script>
+ import LL from "$app/lib/i18n/i18n-svelte";
+ import {link} from "svelte-spa-router";
+</script>
+
+<style>
+ header {
+ font-size: 12rem;
+ }
+
+ main {
+ position: absolute;
+ left: 50%;
+ top: 50%;
+ transform: translate(-50%, -50%);
+ text-align: center;
+ }
+</style>
+
+<main>
+ <header>404</header>
+ <p>{$LL.messages.pageNotFound()}</p>
+ <a use:link
+ href="/">{$LL.messages.goToFrontpage()}</a>
+</main>
diff --git a/old-apps/projects/src/app/pages/settings.svelte b/old-apps/projects/src/app/pages/settings.svelte
new file mode 100644
index 0000000..ca9fd47
--- /dev/null
+++ b/old-apps/projects/src/app/pages/settings.svelte
@@ -0,0 +1,12 @@
+<script>
+ import Layout from "./_layout.svelte";
+ import CategoriesTile from "$app/pages/views/settings-categories-tile.svelte";
+ import LabelsTile from "$app/pages/views/settings-labels-tile.svelte";
+</script>
+
+<Layout>
+ <section class="grid gap-md">
+ <CategoriesTile/>
+ <LabelsTile/>
+ </section>
+</Layout>
diff --git a/old-apps/projects/src/app/pages/ui-workbench.svelte b/old-apps/projects/src/app/pages/ui-workbench.svelte
new file mode 100644
index 0000000..ff2b058
--- /dev/null
+++ b/old-apps/projects/src/app/pages/ui-workbench.svelte
@@ -0,0 +1,7 @@
+<script>
+ import {NavWrapper} from "./nav/index";
+</script>
+
+<NavWrapper>
+
+</NavWrapper> \ No newline at end of file
diff --git a/old-apps/projects/src/app/pages/views/category-form/index.svelte b/old-apps/projects/src/app/pages/views/category-form/index.svelte
new file mode 100644
index 0000000..21024c3
--- /dev/null
+++ b/old-apps/projects/src/app/pages/views/category-form/index.svelte
@@ -0,0 +1,144 @@
+<script lang="ts">
+ import Alert from "$shared/components/alert.svelte";
+ import Dropdown from "$shared/components/dropdown.svelte";
+ import labels, {create_label_async} from "$app/lib/stores/labels";
+ import {generate_random_hex_color} from "$shared/lib/colors";
+ import LL from "$app/lib/i18n/i18n-svelte";
+
+ let LabelsDropdown;
+
+ const dough = {
+ error: "",
+ fields: {
+ name: {
+ value: "",
+ error: "",
+ validate() {
+ return false;
+ }
+ },
+ color: {
+ value: "",
+ error: "",
+ validate() {
+ return true;
+ }
+ },
+ labels: {
+ loading: false,
+ value: [],
+ error: "",
+ validate() {
+ return true;
+ },
+ async create({name}) {
+ dough.fields.labels.loading = true;
+ const response = await create_label_async({
+ name: name,
+ color: generate_random_hex_color(),
+ });
+ dough.fields.labels.loading = false;
+ if (response.ok) {
+ // Small pause to allow loading state to update everywhere.
+ setTimeout(() => LabelsDropdown.select_entry(response.data.id), 50);
+ }
+ }
+ },
+ archived: {
+ value: false,
+ error: "",
+ validate() {
+ return true;
+ }
+ }
+ },
+ bake() {
+ // labels.filter((c) => Object.hasOwn(c, "selected") && c.selected === true);
+ return {
+ labels: dough.fields.labels.value,
+ name: dough.fields.name.value,
+ color: dough.fields.color.value,
+ };
+ },
+ submit(event) {
+ const bread = dough.bake();
+ console.log(bread);
+ console.log("Submitted");
+ }
+ };
+
+ const functions = {
+ set(values) {
+ functions.set_archived(values.archived);
+ functions.set_labels(values.labels);
+ functions.set_color(values.color);
+ functions.set_name(values.name);
+ },
+ is_valid() {
+ let isValid = true;
+ if (!dough.fields.labels.validate()) isValid = false;
+ if (!dough.fields.color.validate()) isValid = false;
+ if (!dough.fields.name.validate()) isValid = false;
+ if (!dough.fields.archived.validate()) isValid = false;
+ return isValid;
+ },
+ set_archived(value) {
+ dough.fields.archived.value = value;
+ },
+ set_labels(value) {
+ dough.fields.labels.value = value;
+ },
+ set_color(value) {
+ dough.fields.color.value = value;
+ },
+ set_name(value) {
+ dough.fields.name.value = value;
+ },
+ };
+</script>
+
+<form on:submit|preventDefault={dough.submit}>
+ <div class="margin-y-sm">
+ <Alert visible={dough.error !== ""}
+ message={dough.error}
+ type="error"/>
+ </div>
+ <div class="grid gap-x-xs margin-bottom-sm">
+ <div class="col-10">
+ <label for="name"
+ class="form-label margin-bottom-xxs">{$LL.views.categoryForm.name()}</label>
+ <input type="text"
+ class="form-control width-100%"
+ id="name"
+ bind:value={dough.fields.name.value}/>
+ {#if dough.fields.name.error}
+ <small class="color-error">{dough.fields.name.error}</small>
+ {/if}
+ </div>
+ <div class="col-2">
+ <label for="color"
+ class="form-label margin-bottom-xxs">{$LL.views.categoryForm.color()}</label>
+ <input type="color"
+ class="form-control width-100%"
+ id="color"
+ style="height: 41px"
+ bind:value={dough.fields.color.value}/>
+ {#if dough.fields.color.error}
+ <small class="color-error">{dough.fields.color.error}</small>
+ {/if}
+ </div>
+ </div>
+ <div class="margin-bottom-sm">
+ <label for="labels"
+ class="form-label margin-bottom-xxs">{$LL.views.categoryForm.defaultLabels()}</label>
+ <Dropdown id="labels"
+ createable={true}
+ placeholder="{$LL.views.categoryForm.labelsPlaceholder()}"
+ entries={$labels}
+ multiple={true}
+ on_create_async={(name) => dough.fields.labels.create({name})}/>
+ {#if dough.fields.labels.error}
+ <small class="color-error">{dough.fields.labels.error}</small>
+ {/if}
+ </div>
+</form>
diff --git a/old-apps/projects/src/app/pages/views/data-table-paginator.svelte b/old-apps/projects/src/app/pages/views/data-table-paginator.svelte
new file mode 100644
index 0000000..b2649eb
--- /dev/null
+++ b/old-apps/projects/src/app/pages/views/data-table-paginator.svelte
@@ -0,0 +1,101 @@
+<script>
+ import LL from "$app/lib/i18n/i18n-svelte";
+ import {createEventDispatcher, onMount} from "svelte";
+ import {restrict_input_to_numbers} from "$shared/lib/helpers";
+
+ const dispatch = createEventDispatcher();
+ export let page = 1;
+ export let pageCount = 1;
+ let prevCount = page;
+ let canIncrement = false;
+ let canDecrement = false;
+ $: canIncrement = page < pageCount;
+ $: canDecrement = page > 1;
+
+ onMount(() => {
+ restrict_input_to_numbers(document.querySelector("#curr-page"));
+ });
+
+ function increment() {
+ if (canIncrement) {
+ page++;
+ }
+ }
+
+ function decrement() {
+ if (canDecrement) {
+ page--;
+ }
+ }
+
+ $: if (page) {
+ handle_change();
+ }
+
+ function handle_change() {
+ if (page === prevCount) {
+ return;
+ }
+ prevCount = page;
+ if (page > pageCount) {
+ page = pageCount;
+ }
+ dispatch("value_change", {
+ newValue: page,
+ });
+ }
+</script>
+
+<nav class="pagination"
+ aria-label="Pagination">
+ <ul class="pagination__list flex flex-wrap gap-xxxs justify-center justify-end@md">
+ <li>
+ <button on:click={decrement}
+ class="reset pagination__item {canDecrement ? '' : 'c-disabled'}">
+ <svg class="icon icon--xs flip-x"
+ viewBox="0 0 16 16">
+ <title>{$LL.views.dataTablePaginator.goToPrevPage()}</title>
+ <polyline
+ points="6 2 12 8 6 14"
+ fill="none"
+ stroke="currentColor"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ stroke-width="2"
+ />
+ </svg>
+ </button>
+ </li>
+
+ <li>
+ <span class="pagination__jumper flex items-center">
+ <input aria-label="Page number"
+ class="form-control"
+ id="curr-page"
+ type="text"
+ on:change={handle_change}
+ value={page}
+ />
+ <em>{$LL.views.dataTablePaginator.of()} {pageCount}</em>
+ </span>
+ </li>
+
+ <li>
+ <button on:click={increment}
+ class="reset pagination__item {canIncrement ? '' : 'c-disabled'}">
+ <svg class="icon icon--xs"
+ viewBox="0 0 16 16">
+ <title>{$LL.views.dataTablePaginator.goToNextPage()}</title>
+ <polyline
+ points="6 2 12 8 6 14"
+ fill="none"
+ stroke="currentColor"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ stroke-width="2"
+ />
+ </svg>
+ </button>
+ </li>
+ </ul>
+</nav>
diff --git a/old-apps/projects/src/app/pages/views/entry-form/index.svelte b/old-apps/projects/src/app/pages/views/entry-form/index.svelte
new file mode 100644
index 0000000..e43d2a9
--- /dev/null
+++ b/old-apps/projects/src/app/pages/views/entry-form/index.svelte
@@ -0,0 +1,199 @@
+<script lang="ts">
+ import LL from "$app/lib/i18n/i18n-svelte";
+ import {TimeEntryDto} from "$shared/lib/models/TimeEntryDto";
+ import {Temporal} from "@js-temporal/polyfill";
+ import {createEventDispatcher, onMount, onDestroy} from "svelte";
+ import DateTimePart from "./sections/date-time.svelte";
+ import LabelsPart from "./sections/labels.svelte";
+ import CategoryPart from "./sections/category.svelte";
+ import Button from "$shared/components/button.svelte";
+ import {Textarea} from "$shared/components/form";
+ import Alert from "$shared/components/alert.svelte";
+ import {is_guid} from "$shared/lib/helpers";
+ import {create_entry_async, edit_entry_async} from "$app/lib/stores/entries";
+
+ const dispatch = createEventDispatcher();
+
+ let formError = "";
+ let formIsLoading = false;
+ let isSubmitting = false;
+ let description = "";
+ let descriptionError = "";
+ let dateTimePart;
+ let labelsPart;
+ let categoryPart;
+ let entryId;
+
+ onMount(() => {
+ formIsLoading = true;
+
+ Promise.all([categoryPart.load_categories(), labelsPart.load_labels()]).then(() => {
+ formIsLoading = false;
+ });
+
+ window.addEventListener("keydown", handle_window_keydown);
+ });
+
+ onDestroy(() => {
+ window.removeEventListener("keydown", handle_window_keydown);
+ });
+
+ function handle_window_keydown(event) {
+ if (event.ctrlKey && event.code === "Enter") {
+ submit_form();
+ }
+ }
+
+ function validate_form() {
+ return dateTimePart.is_valid() && categoryPart.is_valid() && description_is_valid();
+ }
+
+ function description_is_valid() {
+ if (!description) {
+ descriptionError = $LL.views.entryForm.errDescriptionReq();
+ } else {
+ descriptionError = "";
+ }
+
+ return description;
+ }
+
+ function get_payload() {
+ const response = {} as TimeEntryDto;
+ const values = get_values();
+ if (!is_guid(values.id)) {
+ delete values.id;
+ } else {
+ response.id = values.id;
+ }
+
+ const currentTimeZone = Temporal.Now.zonedDateTimeISO().offset;
+ response.start = values.date + "T" + values.fromTimeValue + currentTimeZone.toString();
+ response.stop = values.date + "T" + values.toTimeValue + currentTimeZone.toString();
+
+ response.category = {
+ id: values.category.id,
+ };
+
+ const selectedLabels = values.labels;
+ if (selectedLabels?.length > 0 ?? false) {
+ response.labels = selectedLabels;
+ }
+
+ const descriptionContent = description?.trim();
+ if (descriptionContent?.length > 0 ?? false) {
+ response.description = descriptionContent;
+ }
+
+ return response;
+ }
+
+ async function submit_form() {
+ formError = "";
+ if (validate_form()) {
+ const payload = get_payload() as TimeEntryDto;
+ isSubmitting = true;
+ if (is_guid(payload.id)) {
+ const response = await edit_entry_async(payload);
+ if (response.ok) {
+ functions.reset();
+ dispatch("updated", response.data);
+ } else {
+ formError = $LL.views.entryForm.entryUpdateError();
+ isSubmitting = false;
+ }
+ } else {
+ const response = await create_entry_async(payload);
+ if (response.ok) {
+ functions.reset();
+ dispatch("created");
+ } else {
+ formError = $LL.views.entryForm.entryCreateError();
+ isSubmitting = false;
+ }
+ }
+ }
+ }
+
+ function get_values() {
+ return {
+ id: entryId,
+ toTimeValue: dateTimePart.get_to_time_value(),
+ fromTimeValue: dateTimePart.get_from_time_value(),
+ date: dateTimePart.get_date(),
+ category: categoryPart.get_selected(),
+ labels: labelsPart.get_selected(),
+ description: description,
+ };
+ }
+
+ export const functions = {
+ set_values(values) {
+ entryId = values.id;
+ dateTimePart.set_values(values);
+ labelsPart.select_labels(values?.labels.map((c) => c.id) ?? []);
+ categoryPart.select_category(values?.category?.id);
+ description = values.description;
+ },
+ set_time(value: {to: Temporal.PlainTime, from: Temporal.PlainTime}) {
+ dateTimePart.set_times(value);
+ },
+ set_description(value: string) {
+ if (description) description = description + "\n\n" + value;
+ else description = value;
+ },
+ reset() {
+ isSubmitting = false;
+ formIsLoading = false;
+ entryId = "";
+ labelsPart.reset();
+ categoryPart.reset();
+ dateTimePart.reset(true);
+ description = "";
+ formError = "";
+ },
+ };
+</script>
+
+<form on:submit|preventDefault={submit_form}
+ on:reset={() => functions.reset()}>
+ <div class="margin-y-sm">
+ <Alert visible={formError !== ""}
+ message={formError}
+ type="error"/>
+ </div>
+
+ <div class="margin-bottom-sm">
+ <DateTimePart bind:functions={dateTimePart}/>
+ </div>
+
+ <div class="margin-bottom-sm">
+ <CategoryPart bind:functions={categoryPart}/>
+ </div>
+
+ <div class="margin-bottom-sm">
+ <LabelsPart bind:functions={labelsPart}/>
+ </div>
+
+ <div class="margin-bottom-sm">
+ <Textarea class="width-100%"
+ id="description"
+ label="{$LL.views.entryForm.description()}"
+ errorText="{descriptionError}"
+ bind:value={description}></Textarea>
+ </div>
+
+ <div class="flex flex-row justify-end gap-x-xs">
+ {#if entryId}
+ <Button text="{$LL.views.entryForm.reset()}"
+ on:click={() => functions.reset()}
+ variant="subtle"
+ />
+ {/if}
+ <Button loading={isSubmitting}
+ type="submit"
+ variant="primary"
+ text={entryId ? $LL.views.entryForm.save() : $LL.views.entryForm.create()}
+ />
+ </div>
+</form>
diff --git a/old-apps/projects/src/app/pages/views/entry-form/sections/category.svelte b/old-apps/projects/src/app/pages/views/entry-form/sections/category.svelte
new file mode 100644
index 0000000..f7af382
--- /dev/null
+++ b/old-apps/projects/src/app/pages/views/entry-form/sections/category.svelte
@@ -0,0 +1,76 @@
+<script>
+ import {generate_random_hex_color} from "$shared/lib/colors";
+ import Dropdown from "$shared/components/dropdown.svelte";
+ import {is_guid, move_focus} from "$shared/lib/helpers";
+ import categories, {reload_categories, create_category_async} from "$app/lib/stores/categories";
+ import LL from "$app/lib/i18n/i18n-svelte"
+
+ let categoriesError = "";
+ let loading = false;
+
+ let DropdownExports;
+
+ function reset() {
+ DropdownExports.reset();
+ categoriesError = "";
+ console.log($LL.views.entryForm.category._logReset());
+ }
+
+ async function on_create({name}) {
+ loading = true;
+ const response = await create_category_async({
+ name: name,
+ color: generate_random_hex_color(),
+ });
+ loading = false;
+ if (response.ok) {
+ // Small pause to allow loading state to update everywhere.
+ setTimeout(() => select_category(response.data.id), 50);
+ }
+ }
+
+ function get_selected() {
+ return $categories.find((c) => c.selected === true);
+ }
+
+ function select_category(id) {
+ DropdownExports.select(id);
+ }
+
+ function is_valid() {
+ let isValid = true;
+ const category = get_selected();
+ if (!is_guid(category?.id)) {
+ categoriesError = $LL.views.entryForm.category.errisRequired();
+ isValid = false;
+ move_focus(document.getElementById("category-dropdown"));
+ } else {
+ categoriesError = "";
+ }
+ return isValid;
+ }
+
+ export const functions = {
+ get_selected,
+ reset,
+ is_valid,
+ select_category,
+ load_categories: reload_categories,
+ };
+</script>
+
+<Dropdown
+ entries={$categories}
+ label="{$LL.views.entryForm.category.category()}"
+ maxlength="50"
+ createable={true}
+ placeholder="{$LL.views.entryForm.category.placeholder()}"
+ id="category-dropdown"
+ loading={loading}
+ name="category-dropdown"
+ on_create_async={on_create}
+ noResultsText="{$LL.views.entryForm.category.noResults()}"
+ errorText="{categoriesError}"
+ bind:this={DropdownExports}
+/>
+
diff --git a/old-apps/projects/src/app/pages/views/entry-form/sections/date-time.svelte b/old-apps/projects/src/app/pages/views/entry-form/sections/date-time.svelte
new file mode 100644
index 0000000..b91f1a4
--- /dev/null
+++ b/old-apps/projects/src/app/pages/views/entry-form/sections/date-time.svelte
@@ -0,0 +1,167 @@
+<script lang="ts">
+ import LL from "$app/lib/i18n/i18n-svelte";
+ import { Temporal } from "@js-temporal/polyfill";
+
+ // TIME
+ let fromTimeValue = "";
+ let fromTimeError = "";
+ let toTimeValue = "";
+ let toTimeError = "";
+
+ function handle_from_time_changed(e) {
+ fromTimeValue = e.target.value;
+ if (fromTimeValue) {
+ fromTimeError = "";
+ }
+ }
+
+ function handle_to_time_changed(e) {
+ toTimeValue = e.target.value;
+ if (toTimeValue) {
+ toTimeError = "";
+ }
+ }
+
+ // DATE
+ let date = Temporal.Now.plainDateTimeISO().toString().substring(0, 10);
+ let dateError = "";
+
+ function is_valid() {
+ let isValid = true;
+ let focusIsSet = false;
+ if (!date) {
+ dateError = $LL.views.entryForm.dateTime.errDateIsRequired();
+ isValid = false;
+ if (!focusIsSet) {
+ document.getElementById("date")?.focus();
+ focusIsSet = true;
+ }
+ } else {
+ dateError = "";
+ }
+
+ if (!fromTimeValue) {
+ fromTimeError = $LL.views.entryForm.dateTime.errFromIsRequired();
+ isValid = false;
+ if (!focusIsSet) {
+ document.getElementById("from")?.focus();
+ focusIsSet = true;
+ }
+ } else if (toTimeValue && fromTimeValue > toTimeValue) {
+ fromTimeError = $LL.views.entryForm.dateTime.errFromAfterTo();
+ isValid = false;
+ if (!focusIsSet) {
+ document.getElementById("from")?.focus();
+ focusIsSet = true;
+ }
+ } else if (fromTimeValue === toTimeValue) {
+ fromTimeError = $LL.views.entryForm.dateTime.errFromEqTo();
+
+ isValid = false;
+ if (!focusIsSet) {
+ document.getElementById("from")?.focus();
+ focusIsSet = true;
+ }
+ } else {
+ fromTimeError = "";
+ }
+
+ if (!toTimeValue) {
+ toTimeError = $LL.views.entryForm.dateTime.errToIsRequired();
+ isValid = false;
+ if (!focusIsSet) {
+ document.getElementById("to")?.focus();
+ focusIsSet = true;
+ }
+ } else if (fromTimeValue && toTimeValue < fromTimeValue) {
+ toTimeError = $LL.views.entryForm.dateTime.errToBeforeFrom();
+ isValid = false;
+ if (!focusIsSet) {
+ document.getElementById("to")?.focus();
+ focusIsSet = true;
+ }
+ } else {
+ toTimeError = "";
+ }
+
+ return isValid;
+ }
+
+ export const functions = {
+ get_from_time_value() {
+ return fromTimeValue;
+ },
+ get_to_time_value() {
+ return toTimeValue;
+ },
+ get_date() {
+ return date;
+ },
+ is_valid,
+ reset(focusDate = false) {
+ fromTimeValue = "";
+ toTimeValue = "";
+ if (focusDate) {
+ document.getElementById("date")?.focus();
+ }
+ console.log($LL.views.entryForm.dateTime._logReset());
+ },
+ set_times(value) {
+ fromTimeValue = value.from.toPlainTime().toString().substring(0, 5);
+ toTimeValue = value.to.toPlainTime().toString().substring(0, 5);
+ },
+ set_date(new_date: Temporal.PlainDate) {
+ date = new_date.toString();
+ },
+ set_values(values) {
+ const currentTimeZone = Temporal.Now.timeZone().id;
+ const startDate = Temporal.Instant.from(values.start);
+ const stopDate = Temporal.Instant.from(values.stop);
+ fromTimeValue = startDate.toZonedDateTimeISO(currentTimeZone).toPlainTime().toString().substring(0, 5);
+ toTimeValue = stopDate.toZonedDateTimeISO(currentTimeZone).toPlainTime().toString().substring(0, 5);
+ date = startDate.toZonedDateTimeISO(currentTimeZone).toPlainDate().toString();
+ }
+ };
+</script>
+
+<div class="grid gap-xs">
+ <div class="col-4">
+ <label for="date"
+ class="form-label margin-bottom-xxs">{$LL.views.entryForm.dateTime.date()}</label>
+ <input type="date"
+ id="date"
+ class="form-control width-100%"
+ bind:value={date}>
+ {#if dateError}
+ <small class="color-error">{dateError}</small>
+ {/if}
+ </div>
+ <div class="col-4">
+ <label for="from"
+ class="form-label margin-bottom-xxs">{$LL.views.entryForm.dateTime.from()}</label>
+ <input id="from"
+ class="form-control width-100%"
+ pattern="[0-9][0-9]:[0-9][0-9]"
+ type="time"
+ bind:value={fromTimeValue}
+ on:input={handle_from_time_changed}
+ />
+ {#if fromTimeError}
+ <small class="color-error">{fromTimeError}</small>
+ {/if}
+ </div>
+ <div class="col-4">
+ <label for="to"
+ class="form-label margin-bottom-xxs">{$LL.views.entryForm.dateTime.to()}</label>
+ <input id="to"
+ class="form-control width-100%"
+ pattern="[0-9][0-9]:[0-9][0-9]"
+ type="time"
+ bind:value={toTimeValue}
+ on:input={handle_to_time_changed}
+ />
+ {#if toTimeError}
+ <small class="color-error">{toTimeError}</small>
+ {/if}
+ </div>
+</div>
diff --git a/old-apps/projects/src/app/pages/views/entry-form/sections/labels.svelte b/old-apps/projects/src/app/pages/views/entry-form/sections/labels.svelte
new file mode 100644
index 0000000..a6f324b
--- /dev/null
+++ b/old-apps/projects/src/app/pages/views/entry-form/sections/labels.svelte
@@ -0,0 +1,66 @@
+<script>
+ import LL from "$app/lib/i18n/i18n-svelte";
+ import {generate_random_hex_color} from "$shared/lib/colors";
+ import labels, {reload_labels, create_label_async} from "$app/lib/stores/labels";
+ import Dropdown from "$shared/components/dropdown.svelte";
+
+ let labelsError = "";
+ let loading = false;
+ let DropdownExports;
+
+ function reset() {
+ DropdownExports.reset();
+ console.log($LL.views.entryForm.labels._logReset());
+ }
+
+ function get_selected() {
+ return $labels.filter((c) => Object.hasOwn(c, "selected") && c.selected === true);
+ }
+
+ function select_label(id) {
+ DropdownExports.select(id);
+ }
+
+ function select_labels(ids) {
+ for (const id of ids) {
+ DropdownExports.select(id);
+ }
+ }
+
+ async function on_create({name}) {
+ loading = true;
+ const response = await create_label_async({
+ name: name,
+ color: generate_random_hex_color(),
+ });
+ loading = false;
+ if (response.ok) {
+ // Small pause to allow loading state to update everywhere.
+ setTimeout(() => select_label(response.data.id), 50);
+ }
+ }
+
+ export const functions = {
+ get_selected,
+ reset,
+ load_labels: reload_labels,
+ select_labels,
+ select_label,
+ };
+</script>
+
+<Dropdown
+ entries={$labels}
+ label="{$LL.views.entryForm.labels.labels()}"
+ maxlength="50"
+ createable={true}
+ placeholder="{$LL.views.entryForm.labels.placeholder()}"
+ multiple="{true}"
+ id="labels-search"
+ name="labels-search"
+ on_create_async={on_create}
+ noResultsText="{$LL.views.entryForm.labels.placeholder()}"
+ errorText="{labelsError}"
+ bind:this={DropdownExports}
+ {loading}
+/>
diff --git a/old-apps/projects/src/app/pages/views/profile-modal.svelte b/old-apps/projects/src/app/pages/views/profile-modal.svelte
new file mode 100644
index 0000000..7560175
--- /dev/null
+++ b/old-apps/projects/src/app/pages/views/profile-modal.svelte
@@ -0,0 +1,156 @@
+<script>
+ import {update_profile} from "$shared/lib/api/user";
+ import Modal from "$shared/components/modal.svelte";
+ import Alert from "$shared/components/alert.svelte";
+ import Button from "$shared/components/button.svelte";
+ import {is_email} from "$shared/lib/helpers";
+ import {api_base} from "$shared/lib/configuration";
+ import {get_session_data} from "$shared/lib/session";
+
+ const archiveLink = api_base("_/api/account/archive");
+
+ let modal;
+ let understands = false;
+
+ let formIsLoading = false;
+ let formError;
+
+ let username = get_session_data()?.profile.username;
+ let usernameFieldMessage;
+ let usernameFieldMessageClass = "color-error";
+
+ let password;
+ let passwordFieldMessage;
+ let passwordFieldMessageClass = "color-error";
+
+ async function submit_form(e) {
+ e.preventDefault();
+ if (!username && !password) {
+ console.error("Not submitting becuase both values is empty");
+ return;
+ }
+
+ usernameFieldMessage = "";
+ passwordFieldMessage = "";
+
+ if (username && !is_email(username)) {
+ usernameFieldMessage = "Username has to be a valid email";
+ return;
+ }
+
+ if (password && password?.length < 6) {
+ passwordFieldMessage = "The new password must contain at least 6 characters";
+ return;
+ }
+
+ formIsLoading = true;
+
+ const response = await update_profile({
+ username,
+ password,
+ });
+
+ formIsLoading = false;
+
+ if (response.ok) {
+ if (password) {
+ passwordFieldMessage = "Successfully updated";
+ passwordFieldMessageClass = "color-success";
+ password = "";
+ }
+ if (username) {
+ usernameFieldMessage = "Successfully updated";
+ usernameFieldMessageClass = "color-success";
+ password = "";
+ }
+ } else {
+ formError = response.data.title ?? "An unknown error occured";
+ }
+ }
+
+ async function handle_delete_account_button_click() {
+ alert("Not implemented");
+ return;
+ if (understands && confirm("Are you absolutely sure that you want to delete your account?")) {
+ }
+ }
+
+ export const functions = {
+ open() {
+ modal.open();
+ },
+ close() {
+ // modal.close();
+ },
+ };
+</script>
+
+<Modal title="Profile"
+ bind:functions={modal}>
+ <section class="margin-bottom-md">
+ <p class="text-md margin-bottom-sm">Update your information</p>
+ <form on:submit={submit_form}
+ autocomplete="new-password">
+ {#if formError}
+ <small class="color-danger">{formError}</small>
+ {/if}
+ <div class="margin-bottom-sm">
+ <label for="email"
+ class="form-label margin-bottom-xxs">New username</label>
+ <input type="email"
+ class="form-control width-100%"
+ id="email"
+ placeholder={username}
+ bind:value={username}/>
+ {#if usernameFieldMessage}
+ <small class={usernameFieldMessageClass}>{usernameFieldMessage}</small>
+ {/if}
+ </div>
+ <div class="margin-bottom-sm">
+ <label for="password"
+ class="form-label margin-bottom-xxs">New password</label>
+ <input type="password"
+ class="form-control width-100%"
+ id="password"
+ bind:value={password}/>
+ {#if passwordFieldMessage}
+ <small class={passwordFieldMessageClass}>{passwordFieldMessage}</small>
+ {/if}
+ </div>
+ <div class="flex justify-end">
+ <Button text="Save"
+ on:click={submit_form}
+ variant="primary"
+ loading={formIsLoading}/>
+ </div>
+ </form>
+ </section>
+ <section class="margin-bottom-md">
+ <p class="text-md margin-bottom-sm">Download your data</p>
+ <a class="btn btn--subtle"
+ href={archiveLink}
+ download>Click here to download your data</a>
+ </section>
+ <section>
+ <p class="text-md margin-bottom-sm">Delete account</p>
+ <div class="margin-bottom-sm">
+ <Alert
+ message="Deleting your account and data means that all of your data (entries, categories, etc.) will be unrecoverable forever.<br>You should probably download your data before continuing."
+ type="info"
+ />
+ </div>
+ <div class="form-check margin-bottom-sm">
+ <input type="checkbox"
+ class="checkbox"
+ id="the-consequences"
+ bind:checked={understands}/>
+ <label for="the-consequences">I understand the consequences of deleting my account and data.</label>
+ </div>
+ <div class="flex justify-end">
+ <Button text="Delete everything"
+ variant="accent"
+ disabled={!understands}
+ on:click={handle_delete_account_button_click}/>
+ </div>
+ </section>
+</Modal>
diff --git a/old-apps/projects/src/app/pages/views/settings-categories-tile.svelte b/old-apps/projects/src/app/pages/views/settings-categories-tile.svelte
new file mode 100644
index 0000000..8d2480f
--- /dev/null
+++ b/old-apps/projects/src/app/pages/views/settings-categories-tile.svelte
@@ -0,0 +1,126 @@
+<script>
+ import {IconNames} from "$shared/lib/configuration";
+ import {onMount} from "svelte";
+ import {
+ delete_time_category,
+ get_time_categories,
+ } from "$shared/lib/api/time-entry";
+ import Button from "$shared/components/button.svelte";
+ import Tile from "$shared/components/tile.svelte";
+ import {Table, THead, TBody, TCell, TRow} from "$shared/components/table";
+ import LL from "$app/lib/i18n/i18n-svelte";
+
+ let is_loading = true;
+ let categories = [];
+
+ $: active_categories = categories.filter(c => !c.archived);
+ $: archived_categories = categories.filter(c => c.archived);
+
+ async function load_categories() {
+ is_loading = true;
+ const response = await get_time_categories();
+ if (response.status === 200) {
+ categories = response.data;
+ } else if (response.status === 204) {
+ categories = [];
+ console.log("Empty response when getting time categories");
+ } else {
+ categories = [];
+ console.error("Error when getting time categories");
+ }
+ is_loading = false;
+ }
+
+ async function handle_edit_category_click(event) {
+ }
+
+ async function handle_delete_category_click(event) {
+ const row = event.target.closest("tr");
+ if (
+ row &&
+ row.dataset.id &&
+ confirm($LL.views.settingsCategoriesTile.deleteAllConfirm())
+ ) {
+ const response = await delete_time_category(row.dataset.id);
+ if (response.ok) {
+ // svelte errors if we remove the row.
+ row.classList.add("d-none");
+ }
+ }
+ }
+
+ onMount(() => {
+ load_categories();
+ });
+</script>
+
+<Tile class="col-6@md col-12 {is_loading ? 'c-disabled loading' : ''}">
+ <h2 class="margin-bottom-xxs">{$LL.views.settingsCategoriesTile.categories()}</h2>
+ {#if active_categories.length > 0 && archived_categories.length > 0}
+ <nav class="s-tabs text-sm">
+ <ul class="s-tabs__list">
+ <li><a class="s-tabs__link s-tabs__link--current"
+ href="#0">{$LL.views.settingsCategoriesTile.active()} ({active_categories.length})</a></li>
+ <li><a class="s-tabs__link"
+ href="#0">{$LL.views.settingsCategoriesTile.archived()} ({archived_categories.length})</a></li>
+ </ul>
+ </nav>
+ {/if}
+ <div class="max-width-100% overflow-auto">
+ <Table class="text-sm width-100%">
+ <THead class="text-left">
+ <TCell type="th"
+ thScope="col">
+ {$LL.views.settingsCategoriesTile.name()}
+ </TCell>
+ <TCell type="th"
+ thScope="col">
+ {$LL.views.settingsCategoriesTile.color()}
+ </TCell>
+ <TCell type="th"
+ thScope="col"
+ style="width:50px"></TCell>
+ </THead>
+ <TBody class="text-left">
+ {#if categories.length > 0}
+ {#each categories as category}
+ <TRow class="text-nowrap"
+ data-id={category.id}>
+ <TCell>
+ {category.name}
+ </TCell>
+ <TCell>
+ <span style="border-left: 3px solid {category.color}; background-color:{category.color}25;">
+ {category.color}
+ </span>
+ </TCell>
+ <TCell>
+ <Button icon="{IconNames.pencilSquare}"
+ variant="reset"
+ icon_width="1.2rem"
+ class="hide"
+ icon_height="1.2rem"
+ on:click={handle_edit_category_click}
+ title="{$LL.views.settingsCategoriesTile.editEntry()}"/>
+ <Button icon="{IconNames.trash}"
+ variant="reset"
+ icon_width="1.2rem"
+ icon_height="1.2rem"
+ on:click={handle_delete_category_click}
+ title="{$LL.views.settingsCategoriesTile.deleteEntry()}"/>
+
+ </TCell>
+ </TRow>
+ {/each}
+ {:else}
+ <TRow>
+ <TCell type="th"
+ thScope="3">
+ {$LL.views.settingsCategoriesTile.noCategories()}
+ </TCell>
+ </TRow>
+ {/if}
+ </TBody>
+ </Table>
+ </div>
+</Tile>
diff --git a/old-apps/projects/src/app/pages/views/settings-labels-tile.svelte b/old-apps/projects/src/app/pages/views/settings-labels-tile.svelte
new file mode 100644
index 0000000..3d5a567
--- /dev/null
+++ b/old-apps/projects/src/app/pages/views/settings-labels-tile.svelte
@@ -0,0 +1,111 @@
+<script>
+ import {IconNames} from "$shared/lib/configuration";
+ import {onMount} from "svelte";
+ import labels, {reload_labels, delete_label_async} from "$app/lib/stores/labels";
+ import Button from "$shared/components/button.svelte";
+ import Tile from "$shared/components/tile.svelte";
+ import {Table, THead, TBody, TCell, TRow} from "$shared/components/table";
+ import LL from "$app/lib/i18n/i18n-svelte";
+
+ let isLoadingLabels = true;
+
+ $: active_labels = $labels.filter(c => !c.archived);
+ $: archived_labels = $labels.filter(c => c.archived);
+
+ async function load_labels() {
+ isLoadingLabels = true;
+ await reload_labels();
+ isLoadingLabels = false;
+ }
+
+ async function handle_edit_label_click(event) {
+ }
+
+ async function handle_delete_label_click(event) {
+ const row = event.target.closest("tr");
+ if (
+ row &&
+ row.dataset.id &&
+ confirm($LL.views.settingsLabelsTile.deleteAllConfirm())
+ ) {
+ await delete_label_async({id: row.dataset.id});
+ row.classList.add("d-none");
+ }
+ }
+
+ onMount(() => {
+ load_labels();
+ });
+</script>
+
+<Tile class="col-6@md col-12 {isLoadingLabels ? 'c-disabled loading' : ''}">
+ <h2 class="margin-bottom-xxs">{$LL.views.settingsLabelsTile.labels()}</h2>
+ {#if active_labels.length > 0 && archived_labels.length > 0}
+ <nav class="s-tabs text-sm">
+ <ul class="s-tabs__list">
+ <li><a class="s-tabs__link s-tabs__link--current"
+ href="#0">{$LL.views.settingsLabelsTile.active()} ({active_labels.length})</a></li>
+ <li><a class="s-tabs__link"
+ href="#0">{$LL.views.settingsLabelsTile.archived()} ({archived_labels.length})</a></li>
+ </ul>
+ </nav>
+ {/if}
+ <div class="max-width-100% overflow-auto">
+ <Table class="text-sm width-100%">
+ <THead class="text-left">
+ <TCell type="th"
+ thScope="row">
+ {$LL.views.settingsLabelsTile.name()}
+ </TCell>
+ <TCell type="th"
+ thScope="row">
+ {$LL.views.settingsLabelsTile.color()}
+ </TCell>
+ <TCell type="th"
+ thScope="row"
+ style="width: 50px;">
+ </TCell>
+ </THead>
+ <TBody class="text-left">
+ {#if $labels.length > 0}
+ {#each $labels as label}
+ <TRow class="text-nowrap"
+ dataId={label.id}>
+ <TCell>
+ {label.name}
+ </TCell>
+ <TCell>
+ <span style="border-left: 3px solid {label.color}; background-color:{label.color}25;">
+ {label.color}
+ </span>
+ </TCell>
+ <TCell>
+ <Button icon="{IconNames.pencilSquare}"
+ variant="reset"
+ icon_width="1.2rem"
+ class="hide"
+ icon_height="1.2rem"
+ on:click={handle_edit_label_click}
+ title="{$LL.views.settingsLabelsTile.editEntry()}"/>
+ <Button icon="{IconNames.trash}"
+ variant="reset"
+ icon_width="1.2rem"
+ icon_height="1.2rem"
+ on:click={handle_delete_label_click}
+ title="{$LL.views.settingsLabelsTile.deleteEntry()}"/>
+ </TCell>
+ </TRow>
+ {/each}
+ {:else}
+ <TRow>
+ <TCell type="th"
+ thScope="row"
+ colspan="3">
+ {$LL.views.settingsLabelsTile.noLabels()}
+ </TCell>
+ </TRow>
+ {/if}
+ </TBody>
+ </Table>
+ </div>
+</Tile>
diff --git a/old-apps/projects/src/index.html b/old-apps/projects/src/index.html
new file mode 100644
index 0000000..80eab63
--- /dev/null
+++ b/old-apps/projects/src/index.html
@@ -0,0 +1,55 @@
+<!doctype html>
+<html lang="en">
+
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport"
+ content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
+ <link rel="apple-touch-icon"
+ sizes="180x180"
+ href="./_assets/pwa/apple-touch-icon.png">
+ <link rel="icon"
+ type="image/png"
+ sizes="32x32"
+ href="./_assets/pwa/favicon-32x32.png">
+ <link rel="icon"
+ type="image/png"
+ sizes="16x16"
+ href="./_assets/pwa/favicon-16x16.png">
+ <link rel="manifest"
+ href="./_assets/pwa/manifest.json">
+ <link rel="mask-icon"
+ href="./_assets/pwa/safari-pinned-tab.svg"
+ color="#5bbad5">
+ <meta name="msapplication-TileColor"
+ content="#da532c">
+ <link rel="icon"
+ href="./_assets/pwa/favicon.svg">
+ <link rel="stylesheet" href="./_assets/preload.css">
+ <script src="./_assets/preload.js"></script>
+ <title>Projects - Greatoffice</title>
+</head>
+
+<body>
+
+<noscript>
+ This page is built with javascript. Allow it and try again.
+</noscript>
+
+<div class="fill-loader fill-loader--v4"
+ id="loader"
+ role="alert">
+ <p class="fill-loader__label">Loading Projects...</p>
+ <div aria-hidden="true">
+ <div class="fill-loader__base"></div>
+ <div class="fill-loader__fill"></div>
+ </div>
+</div>
+
+<div id="root"></div>
+
+<script type="module"
+ src="./app/index.ts"></script>
+</body>
+
+</html>
diff --git a/old-apps/projects/src/package.json b/old-apps/projects/src/package.json
new file mode 100644
index 0000000..839f3ba
--- /dev/null
+++ b/old-apps/projects/src/package.json
@@ -0,0 +1,28 @@
+{
+ "name": "greatoffice-projects",
+ "version": "0.0.1",
+ "private": "true",
+ "scripts": {
+ "dev": "npm-run-all --parallel vite typesafe-i18n",
+ "vite": "vite",
+ "typesafe-i18n": "typesafe-i18n",
+ "build": "vite build"
+ },
+ "devDependencies": {
+ "@sveltejs/vite-plugin-svelte": "1.0.1",
+ "@sveltestack/svelte-query": "^1.6.0",
+ "broadcast-channel": "^4.14.0",
+ "npm-run-all": "^4.1.5",
+ "sass": "^1.54.0",
+ "svelte": "^3.49.0",
+ "svelte-preprocess": "^4.10.7",
+ "svelte-spa-router": "^3.2.0",
+ "typescript": "4.7.4",
+ "vite": "^3.0.4"
+ },
+ "dependencies": {
+ "@js-temporal/polyfill": "^0.4.2",
+ "fuzzysort": "^2.0.1",
+ "typesafe-i18n": "^5.11.0"
+ }
+}
diff --git a/old-apps/projects/src/pnpm-lock.yaml b/old-apps/projects/src/pnpm-lock.yaml
new file mode 100644
index 0000000..bcba63e
--- /dev/null
+++ b/old-apps/projects/src/pnpm-lock.yaml
@@ -0,0 +1,1374 @@
+lockfileVersion: 5.4
+
+specifiers:
+ '@js-temporal/polyfill': ^0.4.2
+ '@sveltejs/vite-plugin-svelte': 1.0.1
+ '@sveltestack/svelte-query': ^1.6.0
+ broadcast-channel: ^4.14.0
+ fuzzysort: ^2.0.1
+ npm-run-all: ^4.1.5
+ sass: ^1.54.0
+ svelte: ^3.49.0
+ svelte-preprocess: ^4.10.7
+ svelte-spa-router: ^3.2.0
+ typesafe-i18n: ^5.11.0
+ typescript: 4.7.4
+ vite: ^3.0.4
+
+dependencies:
+ '@js-temporal/polyfill': 0.4.2
+ fuzzysort: 2.0.1
+ typesafe-i18n: 5.11.0_typescript@4.7.4
+
+devDependencies:
+ '@sveltejs/vite-plugin-svelte': 1.0.1_svelte@3.49.0+vite@3.0.4
+ '@sveltestack/svelte-query': 1.6.0_broadcast-channel@4.14.0
+ broadcast-channel: 4.14.0
+ npm-run-all: 4.1.5
+ sass: 1.54.0
+ svelte: 3.49.0
+ svelte-preprocess: 4.10.7_qqyngjnvpp2z5rj6eppfx7s47e
+ svelte-spa-router: 3.2.0
+ typescript: 4.7.4
+ vite: 3.0.4_sass@1.54.0
+
+packages:
+
+ /@babel/runtime/7.18.9:
+ resolution: {integrity: sha512-lkqXDcvlFT5rvEjiu6+QYO+1GXrEHRo2LOtS7E4GtX5ESIZOgepqsZBVIj6Pv+a6zqsya9VCgiK1KAK4BvJDAw==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ regenerator-runtime: 0.13.9
+ dev: true
+
+ /@js-temporal/polyfill/0.4.2:
+ resolution: {integrity: sha512-c85vRxyqnJaXKyf4tvYij8jwiVIZhNLYDI9C4LLuOwVEHf4HUqGg07BBn70Le71W193QT/vmKg3jPUyQxJRHKQ==}
+ engines: {node: '>=12'}
+ dependencies:
+ jsbi: 4.3.0
+ tslib: 2.4.0
+ dev: false
+
+ /@rollup/pluginutils/4.2.1:
+ resolution: {integrity: sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==}
+ engines: {node: '>= 8.0.0'}
+ dependencies:
+ estree-walker: 2.0.2
+ picomatch: 2.3.1
+ dev: true
+
+ /@sveltejs/vite-plugin-svelte/1.0.1_svelte@3.49.0+vite@3.0.4:
+ resolution: {integrity: sha512-PorCgUounn0VXcpeJu+hOweZODKmGuLHsLomwqSj+p26IwjjGffmYQfVHtiTWq+NqaUuuHWWG7vPge6UFw4Aeg==}
+ engines: {node: ^14.18.0 || >= 16}
+ peerDependencies:
+ diff-match-patch: ^1.0.5
+ svelte: ^3.44.0
+ vite: ^3.0.0
+ peerDependenciesMeta:
+ diff-match-patch:
+ optional: true
+ dependencies:
+ '@rollup/pluginutils': 4.2.1
+ debug: 4.3.4
+ deepmerge: 4.2.2
+ kleur: 4.1.5
+ magic-string: 0.26.2
+ svelte: 3.49.0
+ svelte-hmr: 0.14.12_svelte@3.49.0
+ vite: 3.0.4_sass@1.54.0
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /@sveltestack/svelte-query/1.6.0_broadcast-channel@4.14.0:
+ resolution: {integrity: sha512-C0wWuh6av1zu3Pzwrg6EQmX3BhDZQ4gMAdYu6Tfv4bjbEZTB00uEDz52z92IZdONh+iUKuyo0xRZ2e16k2Xifg==}
+ peerDependencies:
+ broadcast-channel: ^4.5.0
+ peerDependenciesMeta:
+ broadcast-channel:
+ optional: true
+ dependencies:
+ broadcast-channel: 4.14.0
+ dev: true
+
+ /@types/node/18.6.3:
+ resolution: {integrity: sha512-6qKpDtoaYLM+5+AFChLhHermMQxc3TOEFIDzrZLPRGHPrLEwqFkkT5Kx3ju05g6X7uDPazz3jHbKPX0KzCjntg==}
+ dev: true
+
+ /@types/pug/2.0.6:
+ resolution: {integrity: sha512-SnHmG9wN1UVmagJOnyo/qkk0Z7gejYxOYYmaAwr5u2yFYfsupN3sg10kyzN8Hep/2zbHxCnsumxOoRIRMBwKCg==}
+ dev: true
+
+ /@types/sass/1.43.1:
+ resolution: {integrity: sha512-BPdoIt1lfJ6B7rw35ncdwBZrAssjcwzI5LByIrYs+tpXlj/CAkuVdRsgZDdP4lq5EjyWzwxZCqAoFyHKFwp32g==}
+ dependencies:
+ '@types/node': 18.6.3
+ dev: true
+
+ /ansi-styles/3.2.1:
+ resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==}
+ engines: {node: '>=4'}
+ dependencies:
+ color-convert: 1.9.3
+ dev: true
+
+ /anymatch/3.1.2:
+ resolution: {integrity: sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==}
+ engines: {node: '>= 8'}
+ dependencies:
+ normalize-path: 3.0.0
+ picomatch: 2.3.1
+ dev: true
+
+ /balanced-match/1.0.2:
+ resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
+ dev: true
+
+ /binary-extensions/2.2.0:
+ resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==}
+ engines: {node: '>=8'}
+ dev: true
+
+ /brace-expansion/1.1.11:
+ resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==}
+ dependencies:
+ balanced-match: 1.0.2
+ concat-map: 0.0.1
+ dev: true
+
+ /braces/3.0.2:
+ resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==}
+ engines: {node: '>=8'}
+ dependencies:
+ fill-range: 7.0.1
+ dev: true
+
+ /broadcast-channel/4.14.0:
+ resolution: {integrity: sha512-uNzxOgBQ+boWCRDESLNg3zZWQ3iz/X7j/uD8pAfr4/S7wQerXVvJI/SBKd9J6ckaPt2jil0gq+7l+3b+kuxJYw==}
+ dependencies:
+ '@babel/runtime': 7.18.9
+ detect-node: 2.1.0
+ microtime: 3.1.0
+ oblivious-set: 1.1.1
+ p-queue: 6.6.2
+ rimraf: 3.0.2
+ unload: 2.3.1
+ dev: true
+
+ /buffer-crc32/0.2.13:
+ resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==}
+ dev: true
+
+ /call-bind/1.0.2:
+ resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==}
+ dependencies:
+ function-bind: 1.1.1
+ get-intrinsic: 1.1.2
+ dev: true
+
+ /chalk/2.4.2:
+ resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==}
+ engines: {node: '>=4'}
+ dependencies:
+ ansi-styles: 3.2.1
+ escape-string-regexp: 1.0.5
+ supports-color: 5.5.0
+ dev: true
+
+ /chokidar/3.5.3:
+ resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==}
+ engines: {node: '>= 8.10.0'}
+ dependencies:
+ anymatch: 3.1.2
+ braces: 3.0.2
+ glob-parent: 5.1.2
+ is-binary-path: 2.1.0
+ is-glob: 4.0.3
+ normalize-path: 3.0.0
+ readdirp: 3.6.0
+ optionalDependencies:
+ fsevents: 2.3.2
+ dev: true
+
+ /color-convert/1.9.3:
+ resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
+ dependencies:
+ color-name: 1.1.3
+ dev: true
+
+ /color-name/1.1.3:
+ resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==}
+ dev: true
+
+ /concat-map/0.0.1:
+ resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
+ dev: true
+
+ /cross-spawn/6.0.5:
+ resolution: {integrity: sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==}
+ engines: {node: '>=4.8'}
+ dependencies:
+ nice-try: 1.0.5
+ path-key: 2.0.1
+ semver: 5.7.1
+ shebang-command: 1.2.0
+ which: 1.3.1
+ dev: true
+
+ /debug/4.3.4:
+ resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
+ engines: {node: '>=6.0'}
+ peerDependencies:
+ supports-color: '*'
+ peerDependenciesMeta:
+ supports-color:
+ optional: true
+ dependencies:
+ ms: 2.1.2
+ dev: true
+
+ /deepmerge/4.2.2:
+ resolution: {integrity: sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==}
+ engines: {node: '>=0.10.0'}
+ dev: true
+
+ /define-properties/1.1.4:
+ resolution: {integrity: sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==}
+ engines: {node: '>= 0.4'}
+ dependencies:
+ has-property-descriptors: 1.0.0
+ object-keys: 1.1.1
+ dev: true
+
+ /detect-indent/6.1.0:
+ resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==}
+ engines: {node: '>=8'}
+ dev: true
+
+ /detect-node/2.1.0:
+ resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==}
+ dev: true
+
+ /error-ex/1.3.2:
+ resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==}
+ dependencies:
+ is-arrayish: 0.2.1
+ dev: true
+
+ /es-abstract/1.20.1:
+ resolution: {integrity: sha512-WEm2oBhfoI2sImeM4OF2zE2V3BYdSF+KnSi9Sidz51fQHd7+JuF8Xgcj9/0o+OWeIeIS/MiuNnlruQrJf16GQA==}
+ engines: {node: '>= 0.4'}
+ dependencies:
+ call-bind: 1.0.2
+ es-to-primitive: 1.2.1
+ function-bind: 1.1.1
+ function.prototype.name: 1.1.5
+ get-intrinsic: 1.1.2
+ get-symbol-description: 1.0.0
+ has: 1.0.3
+ has-property-descriptors: 1.0.0
+ has-symbols: 1.0.3
+ internal-slot: 1.0.3
+ is-callable: 1.2.4
+ is-negative-zero: 2.0.2
+ is-regex: 1.1.4
+ is-shared-array-buffer: 1.0.2
+ is-string: 1.0.7
+ is-weakref: 1.0.2
+ object-inspect: 1.12.2
+ object-keys: 1.1.1
+ object.assign: 4.1.2
+ regexp.prototype.flags: 1.4.3
+ string.prototype.trimend: 1.0.5
+ string.prototype.trimstart: 1.0.5
+ unbox-primitive: 1.0.2
+ dev: true
+
+ /es-to-primitive/1.2.1:
+ resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==}
+ engines: {node: '>= 0.4'}
+ dependencies:
+ is-callable: 1.2.4
+ is-date-object: 1.0.5
+ is-symbol: 1.0.4
+ dev: true
+
+ /es6-promise/3.3.1:
+ resolution: {integrity: sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==}
+ dev: true
+
+ /esbuild-android-64/0.14.51:
+ resolution: {integrity: sha512-6FOuKTHnC86dtrKDmdSj2CkcKF8PnqkaIXqvgydqfJmqBazCPdw+relrMlhGjkvVdiiGV70rpdnyFmA65ekBCQ==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [android]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /esbuild-android-arm64/0.14.51:
+ resolution: {integrity: sha512-vBtp//5VVkZWmYYvHsqBRCMMi1MzKuMIn5XDScmnykMTu9+TD9v0NMEDqQxvtFToeYmojdo5UCV2vzMQWJcJ4A==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [android]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /esbuild-darwin-64/0.14.51:
+ resolution: {integrity: sha512-YFmXPIOvuagDcwCejMRtCDjgPfnDu+bNeh5FU2Ryi68ADDVlWEpbtpAbrtf/lvFTWPexbgyKgzppNgsmLPr8PA==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [darwin]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /esbuild-darwin-arm64/0.14.51:
+ resolution: {integrity: sha512-juYD0QnSKwAMfzwKdIF6YbueXzS6N7y4GXPDeDkApz/1RzlT42mvX9jgNmyOlWKN7YzQAYbcUEJmZJYQGdf2ow==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [darwin]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /esbuild-freebsd-64/0.14.51:
+ resolution: {integrity: sha512-cLEI/aXjb6vo5O2Y8rvVSQ7smgLldwYY5xMxqh/dQGfWO+R1NJOFsiax3IS4Ng300SVp7Gz3czxT6d6qf2cw0g==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [freebsd]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /esbuild-freebsd-arm64/0.14.51:
+ resolution: {integrity: sha512-TcWVw/rCL2F+jUgRkgLa3qltd5gzKjIMGhkVybkjk6PJadYInPtgtUBp1/hG+mxyigaT7ib+od1Xb84b+L+1Mg==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [freebsd]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /esbuild-linux-32/0.14.51:
+ resolution: {integrity: sha512-RFqpyC5ChyWrjx8Xj2K0EC1aN0A37H6OJfmUXIASEqJoHcntuV3j2Efr9RNmUhMfNE6yEj2VpYuDteZLGDMr0w==}
+ engines: {node: '>=12'}
+ cpu: [ia32]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /esbuild-linux-64/0.14.51:
+ resolution: {integrity: sha512-dxjhrqo5i7Rq6DXwz5v+MEHVs9VNFItJmHBe1CxROWNf4miOGoQhqSG8StStbDkQ1Mtobg6ng+4fwByOhoQoeA==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /esbuild-linux-arm/0.14.51:
+ resolution: {integrity: sha512-LsJynDxYF6Neg7ZC7748yweCDD+N8ByCv22/7IAZglIEniEkqdF4HCaa49JNDLw1UQGlYuhOB8ZT/MmcSWzcWg==}
+ engines: {node: '>=12'}
+ cpu: [arm]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /esbuild-linux-arm64/0.14.51:
+ resolution: {integrity: sha512-D9rFxGutoqQX3xJPxqd6o+kvYKeIbM0ifW2y0bgKk5HPgQQOo2k9/2Vpto3ybGYaFPCE5qTGtqQta9PoP6ZEzw==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /esbuild-linux-mips64le/0.14.51:
+ resolution: {integrity: sha512-vS54wQjy4IinLSlb5EIlLoln8buh1yDgliP4CuEHumrPk4PvvP4kTRIG4SzMXm6t19N0rIfT4bNdAxzJLg2k6A==}
+ engines: {node: '>=12'}
+ cpu: [mips64el]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /esbuild-linux-ppc64le/0.14.51:
+ resolution: {integrity: sha512-xcdd62Y3VfGoyphNP/aIV9LP+RzFw5M5Z7ja+zdpQHHvokJM7d0rlDRMN+iSSwvUymQkqZO+G/xjb4/75du8BQ==}
+ engines: {node: '>=12'}
+ cpu: [ppc64]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /esbuild-linux-riscv64/0.14.51:
+ resolution: {integrity: sha512-syXHGak9wkAnFz0gMmRBoy44JV0rp4kVCEA36P5MCeZcxFq8+fllBC2t6sKI23w3qd8Vwo9pTADCgjTSf3L3rA==}
+ engines: {node: '>=12'}
+ cpu: [riscv64]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /esbuild-linux-s390x/0.14.51:
+ resolution: {integrity: sha512-kFAJY3dv+Wq8o28K/C7xkZk/X34rgTwhknSsElIqoEo8armCOjMJ6NsMxm48KaWY2h2RUYGtQmr+RGuUPKBhyw==}
+ engines: {node: '>=12'}
+ cpu: [s390x]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /esbuild-netbsd-64/0.14.51:
+ resolution: {integrity: sha512-ZZBI7qrR1FevdPBVHz/1GSk1x5GDL/iy42Zy8+neEm/HA7ma+hH/bwPEjeHXKWUDvM36CZpSL/fn1/y9/Hb+1A==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [netbsd]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /esbuild-openbsd-64/0.14.51:
+ resolution: {integrity: sha512-7R1/p39M+LSVQVgDVlcY1KKm6kFKjERSX1lipMG51NPcspJD1tmiZSmmBXoY5jhHIu6JL1QkFDTx94gMYK6vfA==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [openbsd]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /esbuild-sunos-64/0.14.51:
+ resolution: {integrity: sha512-HoHaCswHxLEYN8eBTtyO0bFEWvA3Kdb++hSQ/lLG7TyKF69TeSG0RNoBRAs45x/oCeWaTDntEZlYwAfQlhEtJA==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [sunos]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /esbuild-windows-32/0.14.51:
+ resolution: {integrity: sha512-4rtwSAM35A07CBt1/X8RWieDj3ZUHQqUOaEo5ZBs69rt5WAFjP4aqCIobdqOy4FdhYw1yF8Z0xFBTyc9lgPtEg==}
+ engines: {node: '>=12'}
+ cpu: [ia32]
+ os: [win32]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /esbuild-windows-64/0.14.51:
+ resolution: {integrity: sha512-HoN/5HGRXJpWODprGCgKbdMvrC3A2gqvzewu2eECRw2sYxOUoh2TV1tS+G7bHNapPGI79woQJGV6pFH7GH7qnA==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [win32]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /esbuild-windows-arm64/0.14.51:
+ resolution: {integrity: sha512-JQDqPjuOH7o+BsKMSddMfmVJXrnYZxXDHsoLHc0xgmAZkOOCflRmC43q31pk79F9xuyWY45jDBPolb5ZgGOf9g==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [win32]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /esbuild/0.14.51:
+ resolution: {integrity: sha512-+CvnDitD7Q5sT7F+FM65sWkF8wJRf+j9fPcprxYV4j+ohmzVj2W7caUqH2s5kCaCJAfcAICjSlKhDCcvDpU7nw==}
+ engines: {node: '>=12'}
+ hasBin: true
+ requiresBuild: true
+ optionalDependencies:
+ esbuild-android-64: 0.14.51
+ esbuild-android-arm64: 0.14.51
+ esbuild-darwin-64: 0.14.51
+ esbuild-darwin-arm64: 0.14.51
+ esbuild-freebsd-64: 0.14.51
+ esbuild-freebsd-arm64: 0.14.51
+ esbuild-linux-32: 0.14.51
+ esbuild-linux-64: 0.14.51
+ esbuild-linux-arm: 0.14.51
+ esbuild-linux-arm64: 0.14.51
+ esbuild-linux-mips64le: 0.14.51
+ esbuild-linux-ppc64le: 0.14.51
+ esbuild-linux-riscv64: 0.14.51
+ esbuild-linux-s390x: 0.14.51
+ esbuild-netbsd-64: 0.14.51
+ esbuild-openbsd-64: 0.14.51
+ esbuild-sunos-64: 0.14.51
+ esbuild-windows-32: 0.14.51
+ esbuild-windows-64: 0.14.51
+ esbuild-windows-arm64: 0.14.51
+ dev: true
+
+ /escape-string-regexp/1.0.5:
+ resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==}
+ engines: {node: '>=0.8.0'}
+ dev: true
+
+ /estree-walker/2.0.2:
+ resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
+ dev: true
+
+ /eventemitter3/4.0.7:
+ resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
+ dev: true
+
+ /fill-range/7.0.1:
+ resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==}
+ engines: {node: '>=8'}
+ dependencies:
+ to-regex-range: 5.0.1
+ dev: true
+
+ /fs.realpath/1.0.0:
+ resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
+ dev: true
+
+ /fsevents/2.3.2:
+ resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+ os: [darwin]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /function-bind/1.1.1:
+ resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==}
+ dev: true
+
+ /function.prototype.name/1.1.5:
+ resolution: {integrity: sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==}
+ engines: {node: '>= 0.4'}
+ dependencies:
+ call-bind: 1.0.2
+ define-properties: 1.1.4
+ es-abstract: 1.20.1
+ functions-have-names: 1.2.3
+ dev: true
+
+ /functions-have-names/1.2.3:
+ resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==}
+ dev: true
+
+ /fuzzysort/2.0.1:
+ resolution: {integrity: sha512-SlgbPAq0eQ6JQ1h3l4MNeGH/t9DHKH8GGM0RD/6RhmJrNnSoWt3oIVaiQm9g9BPB+wAhRMeMqlUTbhbd7+Ufcg==}
+ dev: false
+
+ /get-intrinsic/1.1.2:
+ resolution: {integrity: sha512-Jfm3OyCxHh9DJyc28qGk+JmfkpO41A4XkneDSujN9MDXrm4oDKdHvndhZ2dN94+ERNfkYJWDclW6k2L/ZGHjXA==}
+ dependencies:
+ function-bind: 1.1.1
+ has: 1.0.3
+ has-symbols: 1.0.3
+ dev: true
+
+ /get-symbol-description/1.0.0:
+ resolution: {integrity: sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==}
+ engines: {node: '>= 0.4'}
+ dependencies:
+ call-bind: 1.0.2
+ get-intrinsic: 1.1.2
+ dev: true
+
+ /glob-parent/5.1.2:
+ resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
+ engines: {node: '>= 6'}
+ dependencies:
+ is-glob: 4.0.3
+ dev: true
+
+ /glob/7.2.3:
+ resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
+ dependencies:
+ fs.realpath: 1.0.0
+ inflight: 1.0.6
+ inherits: 2.0.4
+ minimatch: 3.1.2
+ once: 1.4.0
+ path-is-absolute: 1.0.1
+ dev: true
+
+ /graceful-fs/4.2.10:
+ resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==}
+ dev: true
+
+ /has-bigints/1.0.2:
+ resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==}
+ dev: true
+
+ /has-flag/3.0.0:
+ resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==}
+ engines: {node: '>=4'}
+ dev: true
+
+ /has-property-descriptors/1.0.0:
+ resolution: {integrity: sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==}
+ dependencies:
+ get-intrinsic: 1.1.2
+ dev: true
+
+ /has-symbols/1.0.3:
+ resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==}
+ engines: {node: '>= 0.4'}
+ dev: true
+
+ /has-tostringtag/1.0.0:
+ resolution: {integrity: sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==}
+ engines: {node: '>= 0.4'}
+ dependencies:
+ has-symbols: 1.0.3
+ dev: true
+
+ /has/1.0.3:
+ resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==}
+ engines: {node: '>= 0.4.0'}
+ dependencies:
+ function-bind: 1.1.1
+ dev: true
+
+ /hosted-git-info/2.8.9:
+ resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==}
+ dev: true
+
+ /immutable/4.1.0:
+ resolution: {integrity: sha512-oNkuqVTA8jqG1Q6c+UglTOD1xhC1BtjKI7XkCXRkZHrN5m18/XsnUp8Q89GkQO/z+0WjonSvl0FLhDYftp46nQ==}
+ dev: true
+
+ /inflight/1.0.6:
+ resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
+ dependencies:
+ once: 1.4.0
+ wrappy: 1.0.2
+ dev: true
+
+ /inherits/2.0.4:
+ resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
+ dev: true
+
+ /internal-slot/1.0.3:
+ resolution: {integrity: sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==}
+ engines: {node: '>= 0.4'}
+ dependencies:
+ get-intrinsic: 1.1.2
+ has: 1.0.3
+ side-channel: 1.0.4
+ dev: true
+
+ /is-arrayish/0.2.1:
+ resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==}
+ dev: true
+
+ /is-bigint/1.0.4:
+ resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==}
+ dependencies:
+ has-bigints: 1.0.2
+ dev: true
+
+ /is-binary-path/2.1.0:
+ resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
+ engines: {node: '>=8'}
+ dependencies:
+ binary-extensions: 2.2.0
+ dev: true
+
+ /is-boolean-object/1.1.2:
+ resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==}
+ engines: {node: '>= 0.4'}
+ dependencies:
+ call-bind: 1.0.2
+ has-tostringtag: 1.0.0
+ dev: true
+
+ /is-callable/1.2.4:
+ resolution: {integrity: sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==}
+ engines: {node: '>= 0.4'}
+ dev: true
+
+ /is-core-module/2.9.0:
+ resolution: {integrity: sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==}
+ dependencies:
+ has: 1.0.3
+ dev: true
+
+ /is-date-object/1.0.5:
+ resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==}
+ engines: {node: '>= 0.4'}
+ dependencies:
+ has-tostringtag: 1.0.0
+ dev: true
+
+ /is-extglob/2.1.1:
+ resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
+ engines: {node: '>=0.10.0'}
+ dev: true
+
+ /is-glob/4.0.3:
+ resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
+ engines: {node: '>=0.10.0'}
+ dependencies:
+ is-extglob: 2.1.1
+ dev: true
+
+ /is-negative-zero/2.0.2:
+ resolution: {integrity: sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==}
+ engines: {node: '>= 0.4'}
+ dev: true
+
+ /is-number-object/1.0.7:
+ resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==}
+ engines: {node: '>= 0.4'}
+ dependencies:
+ has-tostringtag: 1.0.0
+ dev: true
+
+ /is-number/7.0.0:
+ resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
+ engines: {node: '>=0.12.0'}
+ dev: true
+
+ /is-regex/1.1.4:
+ resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==}
+ engines: {node: '>= 0.4'}
+ dependencies:
+ call-bind: 1.0.2
+ has-tostringtag: 1.0.0
+ dev: true
+
+ /is-shared-array-buffer/1.0.2:
+ resolution: {integrity: sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==}
+ dependencies:
+ call-bind: 1.0.2
+ dev: true
+
+ /is-string/1.0.7:
+ resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==}
+ engines: {node: '>= 0.4'}
+ dependencies:
+ has-tostringtag: 1.0.0
+ dev: true
+
+ /is-symbol/1.0.4:
+ resolution: {integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==}
+ engines: {node: '>= 0.4'}
+ dependencies:
+ has-symbols: 1.0.3
+ dev: true
+
+ /is-weakref/1.0.2:
+ resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==}
+ dependencies:
+ call-bind: 1.0.2
+ dev: true
+
+ /isexe/2.0.0:
+ resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
+ dev: true
+
+ /jsbi/4.3.0:
+ resolution: {integrity: sha512-SnZNcinB4RIcnEyZqFPdGPVgrg2AcnykiBy0sHVJQKHYeaLUvi3Exj+iaPpLnFVkDPZIV4U0yvgC9/R4uEAZ9g==}
+ dev: false
+
+ /json-parse-better-errors/1.0.2:
+ resolution: {integrity: sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==}
+ dev: true
+
+ /kleur/4.1.5:
+ resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==}
+ engines: {node: '>=6'}
+ dev: true
+
+ /load-json-file/4.0.0:
+ resolution: {integrity: sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==}
+ engines: {node: '>=4'}
+ dependencies:
+ graceful-fs: 4.2.10
+ parse-json: 4.0.0
+ pify: 3.0.0
+ strip-bom: 3.0.0
+ dev: true
+
+ /magic-string/0.25.9:
+ resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==}
+ dependencies:
+ sourcemap-codec: 1.4.8
+ dev: true
+
+ /magic-string/0.26.2:
+ resolution: {integrity: sha512-NzzlXpclt5zAbmo6h6jNc8zl2gNRGHvmsZW4IvZhTC4W7k4OlLP+S5YLussa/r3ixNT66KOQfNORlXHSOy/X4A==}
+ engines: {node: '>=12'}
+ dependencies:
+ sourcemap-codec: 1.4.8
+ dev: true
+
+ /memorystream/0.3.1:
+ resolution: {integrity: sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==}
+ engines: {node: '>= 0.10.0'}
+ dev: true
+
+ /microtime/3.1.0:
+ resolution: {integrity: sha512-GcjhfC2y/DF2znac8IRwri7+YUIy34QRHz/iZK3bHrh74qrNNOpAJQwiOMnIG+v1J0K4eiqd+RiGzN3F1eofTQ==}
+ engines: {node: '>= 14.13.0'}
+ requiresBuild: true
+ dependencies:
+ node-addon-api: 5.0.0
+ node-gyp-build: 4.5.0
+ dev: true
+
+ /min-indent/1.0.1:
+ resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==}
+ engines: {node: '>=4'}
+ dev: true
+
+ /minimatch/3.1.2:
+ resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
+ dependencies:
+ brace-expansion: 1.1.11
+ dev: true
+
+ /minimist/1.2.6:
+ resolution: {integrity: sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==}
+ dev: true
+
+ /mkdirp/0.5.6:
+ resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==}
+ hasBin: true
+ dependencies:
+ minimist: 1.2.6
+ dev: true
+
+ /ms/2.1.2:
+ resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
+ dev: true
+
+ /nanoid/3.3.4:
+ resolution: {integrity: sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==}
+ engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
+ hasBin: true
+ dev: true
+
+ /nice-try/1.0.5:
+ resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==}
+ dev: true
+
+ /node-addon-api/5.0.0:
+ resolution: {integrity: sha512-CvkDw2OEnme7ybCykJpVcKH+uAOLV2qLqiyla128dN9TkEWfrYmxG6C2boDe5KcNQqZF3orkqzGgOMvZ/JNekA==}
+ dev: true
+
+ /node-gyp-build/4.5.0:
+ resolution: {integrity: sha512-2iGbaQBV+ITgCz76ZEjmhUKAKVf7xfY1sRl4UiKQspfZMH2h06SyhNsnSVy50cwkFQDGLyif6m/6uFXHkOZ6rg==}
+ hasBin: true
+ dev: true
+
+ /normalize-package-data/2.5.0:
+ resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==}
+ dependencies:
+ hosted-git-info: 2.8.9
+ resolve: 1.22.1
+ semver: 5.7.1
+ validate-npm-package-license: 3.0.4
+ dev: true
+
+ /normalize-path/3.0.0:
+ resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
+ engines: {node: '>=0.10.0'}
+ dev: true
+
+ /npm-run-all/4.1.5:
+ resolution: {integrity: sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==}
+ engines: {node: '>= 4'}
+ hasBin: true
+ dependencies:
+ ansi-styles: 3.2.1
+ chalk: 2.4.2
+ cross-spawn: 6.0.5
+ memorystream: 0.3.1
+ minimatch: 3.1.2
+ pidtree: 0.3.1
+ read-pkg: 3.0.0
+ shell-quote: 1.7.3
+ string.prototype.padend: 3.1.3
+ dev: true
+
+ /object-inspect/1.12.2:
+ resolution: {integrity: sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==}
+ dev: true
+
+ /object-keys/1.1.1:
+ resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==}
+ engines: {node: '>= 0.4'}
+ dev: true
+
+ /object.assign/4.1.2:
+ resolution: {integrity: sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==}
+ engines: {node: '>= 0.4'}
+ dependencies:
+ call-bind: 1.0.2
+ define-properties: 1.1.4
+ has-symbols: 1.0.3
+ object-keys: 1.1.1
+ dev: true
+
+ /oblivious-set/1.1.1:
+ resolution: {integrity: sha512-Oh+8fK09mgGmAshFdH6hSVco6KZmd1tTwNFWj35OvzdmJTMZtAkbn05zar2iG3v6sDs1JLEtOiBGNb6BHwkb2w==}
+ dev: true
+
+ /once/1.4.0:
+ resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
+ dependencies:
+ wrappy: 1.0.2
+ dev: true
+
+ /p-finally/1.0.0:
+ resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==}
+ engines: {node: '>=4'}
+ dev: true
+
+ /p-queue/6.6.2:
+ resolution: {integrity: sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==}
+ engines: {node: '>=8'}
+ dependencies:
+ eventemitter3: 4.0.7
+ p-timeout: 3.2.0
+ dev: true
+
+ /p-timeout/3.2.0:
+ resolution: {integrity: sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==}
+ engines: {node: '>=8'}
+ dependencies:
+ p-finally: 1.0.0
+ dev: true
+
+ /parse-json/4.0.0:
+ resolution: {integrity: sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==}
+ engines: {node: '>=4'}
+ dependencies:
+ error-ex: 1.3.2
+ json-parse-better-errors: 1.0.2
+ dev: true
+
+ /path-is-absolute/1.0.1:
+ resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==}
+ engines: {node: '>=0.10.0'}
+ dev: true
+
+ /path-key/2.0.1:
+ resolution: {integrity: sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==}
+ engines: {node: '>=4'}
+ dev: true
+
+ /path-parse/1.0.7:
+ resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
+ dev: true
+
+ /path-type/3.0.0:
+ resolution: {integrity: sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==}
+ engines: {node: '>=4'}
+ dependencies:
+ pify: 3.0.0
+ dev: true
+
+ /picocolors/1.0.0:
+ resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==}
+ dev: true
+
+ /picomatch/2.3.1:
+ resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
+ engines: {node: '>=8.6'}
+ dev: true
+
+ /pidtree/0.3.1:
+ resolution: {integrity: sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA==}
+ engines: {node: '>=0.10'}
+ hasBin: true
+ dev: true
+
+ /pify/3.0.0:
+ resolution: {integrity: sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==}
+ engines: {node: '>=4'}
+ dev: true
+
+ /postcss/8.4.14:
+ resolution: {integrity: sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==}
+ engines: {node: ^10 || ^12 || >=14}
+ dependencies:
+ nanoid: 3.3.4
+ picocolors: 1.0.0
+ source-map-js: 1.0.2
+ dev: true
+
+ /read-pkg/3.0.0:
+ resolution: {integrity: sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==}
+ engines: {node: '>=4'}
+ dependencies:
+ load-json-file: 4.0.0
+ normalize-package-data: 2.5.0
+ path-type: 3.0.0
+ dev: true
+
+ /readdirp/3.6.0:
+ resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
+ engines: {node: '>=8.10.0'}
+ dependencies:
+ picomatch: 2.3.1
+ dev: true
+
+ /regenerator-runtime/0.13.9:
+ resolution: {integrity: sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==}
+ dev: true
+
+ /regexp.prototype.flags/1.4.3:
+ resolution: {integrity: sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==}
+ engines: {node: '>= 0.4'}
+ dependencies:
+ call-bind: 1.0.2
+ define-properties: 1.1.4
+ functions-have-names: 1.2.3
+ dev: true
+
+ /regexparam/2.0.0:
+ resolution: {integrity: sha512-gJKwd2MVPWHAIFLsaYDZfyKzHNS4o7E/v8YmNf44vmeV2e4YfVoDToTOKTvE7ab68cRJ++kLuEXJBaEeJVt5ow==}
+ engines: {node: '>=8'}
+ dev: true
+
+ /resolve/1.22.1:
+ resolution: {integrity: sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==}
+ hasBin: true
+ dependencies:
+ is-core-module: 2.9.0
+ path-parse: 1.0.7
+ supports-preserve-symlinks-flag: 1.0.0
+ dev: true
+
+ /rimraf/2.7.1:
+ resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==}
+ hasBin: true
+ dependencies:
+ glob: 7.2.3
+ dev: true
+
+ /rimraf/3.0.2:
+ resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==}
+ hasBin: true
+ dependencies:
+ glob: 7.2.3
+ dev: true
+
+ /rollup/2.77.2:
+ resolution: {integrity: sha512-m/4YzYgLcpMQbxX3NmAqDvwLATZzxt8bIegO78FZLl+lAgKJBd1DRAOeEiZcKOIOPjxE6ewHWHNgGEalFXuz1g==}
+ engines: {node: '>=10.0.0'}
+ hasBin: true
+ optionalDependencies:
+ fsevents: 2.3.2
+ dev: true
+
+ /sander/0.5.1:
+ resolution: {integrity: sha512-3lVqBir7WuKDHGrKRDn/1Ye3kwpXaDOMsiRP1wd6wpZW56gJhsbp5RqQpA6JG/P+pkXizygnr1dKR8vzWaVsfA==}
+ dependencies:
+ es6-promise: 3.3.1
+ graceful-fs: 4.2.10
+ mkdirp: 0.5.6
+ rimraf: 2.7.1
+ dev: true
+
+ /sass/1.54.0:
+ resolution: {integrity: sha512-C4zp79GCXZfK0yoHZg+GxF818/aclhp9F48XBu/+bm9vXEVAYov9iU3FBVRMq3Hx3OA4jfKL+p2K9180mEh0xQ==}
+ engines: {node: '>=12.0.0'}
+ hasBin: true
+ dependencies:
+ chokidar: 3.5.3
+ immutable: 4.1.0
+ source-map-js: 1.0.2
+ dev: true
+
+ /semver/5.7.1:
+ resolution: {integrity: sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==}
+ hasBin: true
+ dev: true
+
+ /shebang-command/1.2.0:
+ resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==}
+ engines: {node: '>=0.10.0'}
+ dependencies:
+ shebang-regex: 1.0.0
+ dev: true
+
+ /shebang-regex/1.0.0:
+ resolution: {integrity: sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==}
+ engines: {node: '>=0.10.0'}
+ dev: true
+
+ /shell-quote/1.7.3:
+ resolution: {integrity: sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==}
+ dev: true
+
+ /side-channel/1.0.4:
+ resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==}
+ dependencies:
+ call-bind: 1.0.2
+ get-intrinsic: 1.1.2
+ object-inspect: 1.12.2
+ dev: true
+
+ /sorcery/0.10.0:
+ resolution: {integrity: sha512-R5ocFmKZQFfSTstfOtHjJuAwbpGyf9qjQa1egyhvXSbM7emjrtLXtGdZsDJDABC85YBfVvrOiGWKSYXPKdvP1g==}
+ hasBin: true
+ dependencies:
+ buffer-crc32: 0.2.13
+ minimist: 1.2.6
+ sander: 0.5.1
+ sourcemap-codec: 1.4.8
+ dev: true
+
+ /source-map-js/1.0.2:
+ resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==}
+ engines: {node: '>=0.10.0'}
+ dev: true
+
+ /sourcemap-codec/1.4.8:
+ resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==}
+ dev: true
+
+ /spdx-correct/3.1.1:
+ resolution: {integrity: sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==}
+ dependencies:
+ spdx-expression-parse: 3.0.1
+ spdx-license-ids: 3.0.11
+ dev: true
+
+ /spdx-exceptions/2.3.0:
+ resolution: {integrity: sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==}
+ dev: true
+
+ /spdx-expression-parse/3.0.1:
+ resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==}
+ dependencies:
+ spdx-exceptions: 2.3.0
+ spdx-license-ids: 3.0.11
+ dev: true
+
+ /spdx-license-ids/3.0.11:
+ resolution: {integrity: sha512-Ctl2BrFiM0X3MANYgj3CkygxhRmr9mi6xhejbdO960nF6EDJApTYpn0BQnDKlnNBULKiCN1n3w9EBkHK8ZWg+g==}
+ dev: true
+
+ /string.prototype.padend/3.1.3:
+ resolution: {integrity: sha512-jNIIeokznm8SD/TZISQsZKYu7RJyheFNt84DUPrh482GC8RVp2MKqm2O5oBRdGxbDQoXrhhWtPIWQOiy20svUg==}
+ engines: {node: '>= 0.4'}
+ dependencies:
+ call-bind: 1.0.2
+ define-properties: 1.1.4
+ es-abstract: 1.20.1
+ dev: true
+
+ /string.prototype.trimend/1.0.5:
+ resolution: {integrity: sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog==}
+ dependencies:
+ call-bind: 1.0.2
+ define-properties: 1.1.4
+ es-abstract: 1.20.1
+ dev: true
+
+ /string.prototype.trimstart/1.0.5:
+ resolution: {integrity: sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg==}
+ dependencies:
+ call-bind: 1.0.2
+ define-properties: 1.1.4
+ es-abstract: 1.20.1
+ dev: true
+
+ /strip-bom/3.0.0:
+ resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==}
+ engines: {node: '>=4'}
+ dev: true
+
+ /strip-indent/3.0.0:
+ resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==}
+ engines: {node: '>=8'}
+ dependencies:
+ min-indent: 1.0.1
+ dev: true
+
+ /supports-color/5.5.0:
+ resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==}
+ engines: {node: '>=4'}
+ dependencies:
+ has-flag: 3.0.0
+ dev: true
+
+ /supports-preserve-symlinks-flag/1.0.0:
+ resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
+ engines: {node: '>= 0.4'}
+ dev: true
+
+ /svelte-hmr/0.14.12_svelte@3.49.0:
+ resolution: {integrity: sha512-4QSW/VvXuqVcFZ+RhxiR8/newmwOCTlbYIezvkeN6302YFRE8cXy0naamHcjz8Y9Ce3ITTZtrHrIL0AGfyo61w==}
+ engines: {node: ^12.20 || ^14.13.1 || >= 16}
+ peerDependencies:
+ svelte: '>=3.19.0'
+ dependencies:
+ svelte: 3.49.0
+ dev: true
+
+ /svelte-preprocess/4.10.7_qqyngjnvpp2z5rj6eppfx7s47e:
+ resolution: {integrity: sha512-sNPBnqYD6FnmdBrUmBCaqS00RyCsCpj2BG58A1JBswNF7b0OKviwxqVrOL/CKyJrLSClrSeqQv5BXNg2RUbPOw==}
+ engines: {node: '>= 9.11.2'}
+ requiresBuild: true
+ peerDependencies:
+ '@babel/core': ^7.10.2
+ coffeescript: ^2.5.1
+ less: ^3.11.3 || ^4.0.0
+ node-sass: '*'
+ postcss: ^7 || ^8
+ postcss-load-config: ^2.1.0 || ^3.0.0 || ^4.0.0
+ pug: ^3.0.0
+ sass: ^1.26.8
+ stylus: ^0.55.0
+ sugarss: ^2.0.0
+ svelte: ^3.23.0
+ typescript: ^3.9.5 || ^4.0.0
+ peerDependenciesMeta:
+ '@babel/core':
+ optional: true
+ coffeescript:
+ optional: true
+ less:
+ optional: true
+ node-sass:
+ optional: true
+ postcss:
+ optional: true
+ postcss-load-config:
+ optional: true
+ pug:
+ optional: true
+ sass:
+ optional: true
+ stylus:
+ optional: true
+ sugarss:
+ optional: true
+ typescript:
+ optional: true
+ dependencies:
+ '@types/pug': 2.0.6
+ '@types/sass': 1.43.1
+ detect-indent: 6.1.0
+ magic-string: 0.25.9
+ sass: 1.54.0
+ sorcery: 0.10.0
+ strip-indent: 3.0.0
+ svelte: 3.49.0
+ typescript: 4.7.4
+ dev: true
+
+ /svelte-spa-router/3.2.0:
+ resolution: {integrity: sha512-igemo5Vs82TGBBw+DjWt6qKameXYzNs6aDXcTxou5XbEvOjiRcAM6MLkdVRCatn6u8r42dE99bt/br7T4qe/AQ==}
+ dependencies:
+ regexparam: 2.0.0
+ dev: true
+
+ /svelte/3.49.0:
+ resolution: {integrity: sha512-+lmjic1pApJWDfPCpUUTc1m8azDqYCG1JN9YEngrx/hUyIcFJo6VZhj0A1Ai0wqoHcEIuQy+e9tk+4uDgdtsFA==}
+ engines: {node: '>= 8'}
+ dev: true
+
+ /to-regex-range/5.0.1:
+ resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
+ engines: {node: '>=8.0'}
+ dependencies:
+ is-number: 7.0.0
+ dev: true
+
+ /tslib/2.4.0:
+ resolution: {integrity: sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==}
+ dev: false
+
+ /typesafe-i18n/5.11.0_typescript@4.7.4:
+ resolution: {integrity: sha512-OVX/6/F834XldHTMdmo3TcMPePcvLXwYrkDgqWYxmuVCTyCrk0aIdUOIWM0RPZEQ2D106+/LcWFCkJiBCuK2pA==}
+ hasBin: true
+ peerDependencies:
+ typescript: '>=3.5.1'
+ dependencies:
+ typescript: 4.7.4
+ dev: false
+
+ /typescript/4.7.4:
+ resolution: {integrity: sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==}
+ engines: {node: '>=4.2.0'}
+ hasBin: true
+
+ /unbox-primitive/1.0.2:
+ resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==}
+ dependencies:
+ call-bind: 1.0.2
+ has-bigints: 1.0.2
+ has-symbols: 1.0.3
+ which-boxed-primitive: 1.0.2
+ dev: true
+
+ /unload/2.3.1:
+ resolution: {integrity: sha512-MUZEiDqvAN9AIDRbbBnVYVvfcR6DrjCqeU2YQMmliFZl9uaBUjTkhuDQkBiyAy8ad5bx1TXVbqZ3gg7namsWjA==}
+ dependencies:
+ '@babel/runtime': 7.18.9
+ detect-node: 2.1.0
+ dev: true
+
+ /validate-npm-package-license/3.0.4:
+ resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==}
+ dependencies:
+ spdx-correct: 3.1.1
+ spdx-expression-parse: 3.0.1
+ dev: true
+
+ /vite/3.0.4_sass@1.54.0:
+ resolution: {integrity: sha512-NU304nqnBeOx2MkQnskBQxVsa0pRAH5FphokTGmyy8M3oxbvw7qAXts2GORxs+h/2vKsD+osMhZ7An6yK6F1dA==}
+ engines: {node: ^14.18.0 || >=16.0.0}
+ hasBin: true
+ peerDependencies:
+ less: '*'
+ sass: '*'
+ stylus: '*'
+ terser: ^5.4.0
+ peerDependenciesMeta:
+ less:
+ optional: true
+ sass:
+ optional: true
+ stylus:
+ optional: true
+ terser:
+ optional: true
+ dependencies:
+ esbuild: 0.14.51
+ postcss: 8.4.14
+ resolve: 1.22.1
+ rollup: 2.77.2
+ sass: 1.54.0
+ optionalDependencies:
+ fsevents: 2.3.2
+ dev: true
+
+ /which-boxed-primitive/1.0.2:
+ resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==}
+ dependencies:
+ is-bigint: 1.0.4
+ is-boolean-object: 1.1.2
+ is-number-object: 1.0.7
+ is-string: 1.0.7
+ is-symbol: 1.0.4
+ dev: true
+
+ /which/1.3.1:
+ resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==}
+ hasBin: true
+ dependencies:
+ isexe: 2.0.0
+ dev: true
+
+ /wrappy/1.0.2:
+ resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
+ dev: true
diff --git a/old-apps/projects/src/tsconfig.json b/old-apps/projects/src/tsconfig.json
new file mode 100644
index 0000000..c60fce6
--- /dev/null
+++ b/old-apps/projects/src/tsconfig.json
@@ -0,0 +1,27 @@
+{
+ "include": [
+ "./**/*.d.ts",
+ "./**/*.ts",
+ "./**/*.js",
+ "./**/*.svelte"
+ ],
+ "exclude": [
+ "./node_modules"
+ ],
+ "compilerOptions": {
+ "target": "esnext",
+ "useDefineForClassFields": true,
+ "module": "esnext",
+ "moduleResolution": "node",
+ "allowJs": true,
+ "checkJs": false,
+ "paths": {
+ "$app/*": [
+ "./app/*"
+ ],
+ "$shared/*": [
+ "../../web-shared/src/*"
+ ]
+ }
+ }
+}
diff --git a/old-apps/projects/src/vite.config.ts b/old-apps/projects/src/vite.config.ts
new file mode 100644
index 0000000..1686884
--- /dev/null
+++ b/old-apps/projects/src/vite.config.ts
@@ -0,0 +1,32 @@
+import {defineConfig} from "vite";
+import {svelte} from "@sveltejs/vite-plugin-svelte";
+import sveltePreprocess from "svelte-preprocess";
+// @ts-ignore
+import path from "path";
+
+// https://vitejs.dev/config/
+export default defineConfig({
+ resolve: {
+ alias: {
+ "$shared": path.resolve(__dirname, "../../web-shared/src"),
+ "$app": path.resolve(__dirname, "./app")
+ }
+ },
+ build: {
+ outDir: "build",
+ emptyOutDir: true,
+ rollupOptions: {
+ input: {
+ main: path.resolve(__dirname, "index.html"),
+ }
+ }
+ },
+ server: {
+ port: 3000
+ },
+ plugins: [
+ svelte({
+ preprocess: sveltePreprocess()
+ })
+ ],
+});