Compare commits

..
29 Commits
Author SHA1 Message Date
drake 60ad76d48a raise new item and increase height
ci / docker_image (push) Successful in 3m24s
ci / deploy (push) Successful in 1m2s
2026-07-01 13:02:21 -05:00
drake 38fe82731e t to i
ci / docker_image (push) Successful in 2m15s
ci / deploy (push) Successful in 30s
2026-06-30 13:12:56 -05:00
drake 5cce7928ec ordering
ci / docker_image (push) Successful in 2m17s
ci / deploy (push) Successful in 26s
2026-06-30 13:05:01 -05:00
drake 6f375cd990 propagation
ci / docker_image (push) Successful in 2m38s
ci / deploy (push) Successful in 29s
2026-06-30 12:57:04 -05:00
drake 681b15e793 autofocus
ci / docker_image (push) Successful in 2m21s
ci / deploy (push) Successful in 31s
2026-06-30 12:43:04 -05:00
drake 53a75f6573 ux updates
ci / docker_image (push) Successful in 3m0s
ci / deploy (push) Successful in 1m5s
2026-06-30 12:19:06 -05:00
drake eb40444e46 db fixed
ci / docker_image (push) Successful in 2m22s
ci / deploy (push) Successful in 29s
2026-06-29 17:09:32 -05:00
drake 1b179f9d16 downscale pre-ai
ci / docker_image (push) Successful in 2m28s
ci / deploy (push) Successful in 31s
2026-06-29 16:21:40 -05:00
drake 7e6e31aa83 updates before nlc
ci / docker_image (push) Successful in 2m55s
ci / deploy (push) Successful in 29s
2026-06-29 14:43:12 -05:00
drake ee19bd9dea round 2
ci / docker_image (push) Successful in 2m9s
ci / deploy (push) Successful in 25s
2026-04-13 03:45:02 +00:00
drake aa501b4c16 apostrophe
ci / deploy (push) Blocked by required conditions
ci / docker_image (push) Has been cancelled
2026-04-13 03:44:38 +00:00
drake feb80e41ea opacity
ci / docker_image (push) Successful in 2m9s
ci / deploy (push) Successful in 24s
2026-04-12 16:28:39 -05:00
drake d9607afd2c spinner sizing
ci / docker_image (push) Successful in 2m12s
ci / deploy (push) Successful in 28s
2026-04-12 15:43:20 -05:00
drake adaf9534b5 visuals
ci / docker_image (push) Successful in 2m9s
ci / deploy (push) Successful in 25s
2026-04-12 01:24:33 -05:00
drake 8ce4ff1210 unprint
ci / docker_image (push) Successful in 2m10s
ci / deploy (push) Successful in 24s
2026-04-11 23:54:15 -05:00
drake 8c26522ec2 test
ci / docker_image (push) Successful in 2m9s
ci / deploy (push) Successful in 25s
2026-04-11 23:49:59 -05:00
drake 264b44f555 fix stale stuff?
ci / docker_image (push) Successful in 2m7s
ci / deploy (push) Successful in 25s
2026-04-11 23:40:23 -05:00
drake bce9a19776 fix account save
ci / docker_image (push) Successful in 2m17s
ci / deploy (push) Successful in 50s
2026-04-11 09:28:45 -05:00
drake e6e4b068c9 Update .gitea/workflows/ci.yaml
ci / docker_image (push) Successful in 2m18s
ci / deploy (push) Successful in 57s
2026-04-04 22:50:19 +00:00
drake 9c221fa828 sonner
ci / docker_image (push) Failing after 2m25s
ci / deploy (push) Has been skipped
2026-04-04 17:37:54 -05:00
drake 3925f4e67a settings and fixes
ci / deploy (push) Blocked by required conditions
ci / docker_image (push) Has been cancelled
2026-04-04 17:37:17 -05:00
drake a0efc6c628 updates 2026-03-30 15:44:11 -05:00
drake 09cdce350d fix inquiry message emails
ci / docker_image (push) Successful in 3m12s
ci / deploy (push) Successful in 27s
2026-02-07 01:27:47 -06:00
drake 81f0113ce2 cleanup inquiries
ci / docker_image (push) Successful in 3m10s
ci / deploy (push) Successful in 26s
2026-02-07 01:11:04 -06:00
drake 1dcbca0445 login cleanup
ci / deploy (push) Blocked by required conditions
ci / docker_image (push) Has been cancelled
2026-02-07 01:08:35 -06:00
drake 2a1436515b Readme fix
ci / docker_image (push) Successful in 3m14s
ci / deploy (push) Successful in 30s
2026-02-07 01:03:54 -06:00
drake 3955f95830 cleanup
ci / docker_image (push) Has been cancelled
ci / deploy (push) Blocked by required conditions
2026-02-07 01:03:12 -06:00
drake 60b5705107 readme 2026-02-07 01:01:37 -06:00
drake cd128d7914 account page 2026-02-07 00:45:35 -06:00
37 changed files with 1396 additions and 606 deletions
+17
View File
@@ -0,0 +1,17 @@
POSTGRES_HOST=192.168.0.200
POSTGRES_PORT=5432
POSTGRES_DB=fbla
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgrespw
JWT_SECRET=CHANGE_ME
BASE_URL=https://fbla26.marinodev.com
EMAIL_HOST=marinodev.com
EMAIL_PORT=465
EMAIL_USER=westuffind@marinodev.com
EMAIL_PASS=CHANGE_ME
FBLA26_PORT=8000
BODY_SIZE_LIMIT=10MB
LLAMA_PORT=8001
LLAMA_HOST=192.168.0.200
LLAMA_MODEL=Qwen3VL-2B-Instruct-Q4_K_M.gguf
LLAMA_MMPROJ=mmproj-Qwen3VL-2B-Instruct-Q8_0.gguf
-3
View File
@@ -12,9 +12,6 @@ jobs:
- name: Check out repository code - name: Check out repository code
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Install Docker
run: curl -fsSL https://get.docker.com | sh
- name: Login to Docker Registry - name: Login to Docker Registry
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
+73 -18
View File
@@ -1,38 +1,93 @@
# sv # Westuffind - FBLA 2026
Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli). ## Overview
## Creating a project This is a lost and found application built using [SvelteKit](https://kit.svelte.dev/) for the 2026 FBLA Website Coding &
Development event. It allows users to browse items, post found items, and manage them. The
application is designed for fast performance and a seamless user experience.
If you're seeing this, you've probably already done this step. Congrats! ## Features
- User authentication (login/signup/logout)
- Email-only token-based methods for non-admins
- Browse/search items
- Post found items
- Inquire about items
- Claim items
- Email notifications
- Themes
## Installation
To set up the project locally, follow these steps:
### Prerequisites
- [Node.js](https://nodejs.org/) (LTS recommended)
- [npm](https://www.npmjs.com/) or [pnpm](https://pnpm.io/)
### Clone the repository
```sh ```sh
# create a new project in the current directory git clone https://git.marinodev.com/MarinoDev/FBLA25
npx sv create cd FBLA25
# create a new project in my-app
npx sv create my-app
``` ```
## Developing Create a `.env` file in the root directory and configure environment variables. `.env.example` is provided as a
template.
Download a LLaMA compatible LLM (and mmproj) to `llm-models`. I
recommend [Qwen3-VL-2B-Instruct](https://huggingface.co/Qwen/Qwen3-VL-2B-Instruct-GGUF).
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: ### Docker
A `Dockerfile` and `docker-compose.yml` file are provided for running the application in a Docker container.
### Manual
Using Docker is strongly recommended, as it bundles the database and the AI.
#### Install dependencies
```sh
npm install
```
#### Start the development server
```sh ```sh
npm run dev npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
``` ```
## Building Go to `http://localhost:5173/` (or the port shown in the terminal).
To create a production version of your app: ## Deployment
To deploy the application, build it using:
```sh ```sh
npm run build npm run build
node build
``` ```
You can preview the production build with `npm run preview`. ## Resources Used
### Technologies
- [SvelteKit](https://kit.svelte.dev/)
- [Tailwind CSS](https://tailwindcss.com/)
- [Shadcn (Svelte version)](https://www.shadcn-svelte.com)
### Libraries
- [dotenv](https://www.npmjs.com/package/dotenv)
- [bcrypt](https://www.npmjs.com/package/bcrypt)
- [desm](https://www.npmjs.com/package/desm)
- [nodemailer](https://www.npmjs.com/package/nodemailer)
- [jsonwebtoken](https://www.npmjs.com/package/jsonwebtoken)
- [postgres.js](https://www.npmjs.com/package/postgres)
- [lucide](https://www.npmjs.com/package/@lucide/svelte)
- [sharp](https://www.npmjs.com/package/sharp)
- [valibot](https://www.npmjs.com/package/valibot)
> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.
+2 -2
View File
@@ -34,9 +34,9 @@ services:
- ./llm-models:/models:ro - ./llm-models:/models:ro
command: command:
- -m - -m
- /models/Qwen3VL-2B-Instruct-Q4_K_M.gguf - /models/${LLAMA_MODEL}
- --mmproj - --mmproj
- /models/mmproj-Qwen3VL-2B-Instruct-Q8_0.gguf - /models/${LLAMA_MMPROJ}
- --host - --host
- 0.0.0.0 - 0.0.0.0
- --port - --port
+290 -153
View File
@@ -8,6 +8,7 @@
"name": "fbla26", "name": "fbla26",
"version": "0.0.1", "version": "0.0.1",
"dependencies": { "dependencies": {
"@rive-app/canvas": "^2.37.1",
"bcrypt": "^6.0.0", "bcrypt": "^6.0.0",
"desm": "^1.3.1", "desm": "^1.3.1",
"dotenv": "^17.2.3", "dotenv": "^17.2.3",
@@ -24,7 +25,7 @@
"@eslint/compat": "^1.4.0", "@eslint/compat": "^1.4.0",
"@eslint/js": "^9.36.0", "@eslint/js": "^9.36.0",
"@internationalized/date": "^3.10.1", "@internationalized/date": "^3.10.1",
"@lucide/svelte": "^0.561.0", "@lucide/svelte": "^1.7.0",
"@sveltejs/adapter-node": "^5.3.2", "@sveltejs/adapter-node": "^5.3.2",
"@sveltejs/kit": "2.49.5", "@sveltejs/kit": "2.49.5",
"@sveltejs/vite-plugin-svelte": "^6.2.0", "@sveltejs/vite-plugin-svelte": "^6.2.0",
@@ -47,6 +48,7 @@
"prettier-plugin-tailwindcss": "^0.6.14", "prettier-plugin-tailwindcss": "^0.6.14",
"svelte": "^5.39.5", "svelte": "^5.39.5",
"svelte-check": "^4.3.2", "svelte-check": "^4.3.2",
"svelte-sonner": "^1.1.0",
"tailwind-merge": "^3.4.0", "tailwind-merge": "^3.4.0",
"tailwind-variants": "^3.2.2", "tailwind-variants": "^3.2.2",
"tailwindcss": "^4.1.13", "tailwindcss": "^4.1.13",
@@ -1337,9 +1339,9 @@
} }
}, },
"node_modules/@lucide/svelte": { "node_modules/@lucide/svelte": {
"version": "0.561.0", "version": "1.7.0",
"resolved": "https://registry.npmjs.org/@lucide/svelte/-/svelte-0.561.0.tgz", "resolved": "https://registry.npmjs.org/@lucide/svelte/-/svelte-1.7.0.tgz",
"integrity": "sha512-vofKV2UFVrKE6I4ewKJ3dfCXSV6iP6nWVmiM83MLjsU91EeJcEg7LoWUABLp/aOTxj1HQNbJD1f3g3L0JQgH9A==", "integrity": "sha512-YytBKOUBGox7yWcykZnYxOkn5WpR5G1qYXLYXV/j1B79SOTTEKzB+s5yF5Rq9l9OkweDStNH2b4yTqfvhEhV8g==",
"dev": true, "dev": true,
"license": "ISC", "license": "ISC",
"peerDependencies": { "peerDependencies": {
@@ -1391,6 +1393,12 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@rive-app/canvas": {
"version": "2.37.1",
"resolved": "https://registry.npmjs.org/@rive-app/canvas/-/canvas-2.37.1.tgz",
"integrity": "sha512-Uw14b9vXD5g56ttUktlV+mXX1EOpAwqOoMS+DAVSs6EDoeaUYM4bRSKB1glDiOotqjshLSfNihua73mLPp10cQ==",
"license": "MIT"
},
"node_modules/@rollup/plugin-commonjs": { "node_modules/@rollup/plugin-commonjs": {
"version": "28.0.6", "version": "28.0.6",
"resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.6.tgz", "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.6.tgz",
@@ -1488,9 +1496,9 @@
} }
}, },
"node_modules/@rollup/rollup-android-arm-eabi": { "node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.52.4", "version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.4.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz",
"integrity": "sha512-BTm2qKNnWIQ5auf4deoetINJm2JzvihvGb9R6K/ETwKLql/Bb3Eg2H1FBp1gUb4YGbydMA3jcmQTR73q7J+GAA==", "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@@ -1502,9 +1510,9 @@
] ]
}, },
"node_modules/@rollup/rollup-android-arm64": { "node_modules/@rollup/rollup-android-arm64": {
"version": "4.52.4", "version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.4.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz",
"integrity": "sha512-P9LDQiC5vpgGFgz7GSM6dKPCiqR3XYN1WwJKA4/BUVDjHpYsf3iBEmVz62uyq20NGYbiGPR5cNHI7T1HqxNs2w==", "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -1516,9 +1524,9 @@
] ]
}, },
"node_modules/@rollup/rollup-darwin-arm64": { "node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.52.4", "version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.4.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz",
"integrity": "sha512-QRWSW+bVccAvZF6cbNZBJwAehmvG9NwfWHwMy4GbWi/BQIA/laTIktebT2ipVjNncqE6GLPxOok5hsECgAxGZg==", "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -1530,9 +1538,9 @@
] ]
}, },
"node_modules/@rollup/rollup-darwin-x64": { "node_modules/@rollup/rollup-darwin-x64": {
"version": "4.52.4", "version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.4.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz",
"integrity": "sha512-hZgP05pResAkRJxL1b+7yxCnXPGsXU0fG9Yfd6dUaoGk+FhdPKCJ5L1Sumyxn8kvw8Qi5PvQ8ulenUbRjzeCTw==", "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -1544,9 +1552,9 @@
] ]
}, },
"node_modules/@rollup/rollup-freebsd-arm64": { "node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.52.4", "version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.4.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz",
"integrity": "sha512-xmc30VshuBNUd58Xk4TKAEcRZHaXlV+tCxIXELiE9sQuK3kG8ZFgSPi57UBJt8/ogfhAF5Oz4ZSUBN77weM+mQ==", "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -1558,9 +1566,9 @@
] ]
}, },
"node_modules/@rollup/rollup-freebsd-x64": { "node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.52.4", "version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.4.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz",
"integrity": "sha512-WdSLpZFjOEqNZGmHflxyifolwAiZmDQzuOzIq9L27ButpCVpD7KzTRtEG1I0wMPFyiyUdOO+4t8GvrnBLQSwpw==", "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -1572,13 +1580,16 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm-gnueabihf": { "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.52.4", "version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.4.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz",
"integrity": "sha512-xRiOu9Of1FZ4SxVbB0iEDXc4ddIcjCv2aj03dmW8UrZIW7aIQ9jVJdLBIhxBI+MaTnGAKyvMwPwQnoOEvP7FgQ==", "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1586,13 +1597,16 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm-musleabihf": { "node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.52.4", "version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.4.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz",
"integrity": "sha512-FbhM2p9TJAmEIEhIgzR4soUcsW49e9veAQCziwbR+XWB2zqJ12b4i/+hel9yLiD8pLncDH4fKIPIbt5238341Q==", "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1600,13 +1614,16 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm64-gnu": { "node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.52.4", "version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.4.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz",
"integrity": "sha512-4n4gVwhPHR9q/g8lKCyz0yuaD0MvDf7dV4f9tHt0C73Mp8h38UCtSCSE6R9iBlTbXlmA8CjpsZoujhszefqueg==", "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1614,13 +1631,16 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm64-musl": { "node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.52.4", "version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.4.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz",
"integrity": "sha512-u0n17nGA0nvi/11gcZKsjkLj1QIpAuPFQbR48Subo7SmZJnGxDpspyw2kbpuoQnyK+9pwf3pAoEXerJs/8Mi9g==", "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1628,13 +1648,33 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-loong64-gnu": { "node_modules/@rollup/rollup-linux-loong64-gnu": {
"version": "4.52.4", "version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.4.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz",
"integrity": "sha512-0G2c2lpYtbTuXo8KEJkDkClE/+/2AFPdPAbmaHoE870foRFs4pBrDehilMcrSScrN/fB/1HTaWO4bqw+ewBzMQ==", "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==",
"cpu": [ "cpu": [
"loong64" "loong64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-loong64-musl": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz",
"integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==",
"cpu": [
"loong64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1642,13 +1682,33 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-ppc64-gnu": { "node_modules/@rollup/rollup-linux-ppc64-gnu": {
"version": "4.52.4", "version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.4.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz",
"integrity": "sha512-teSACug1GyZHmPDv14VNbvZFX779UqWTsd7KtTM9JIZRDI5NUwYSIS30kzI8m06gOPB//jtpqlhmraQ68b5X2g==", "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-ppc64-musl": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz",
"integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==",
"cpu": [
"ppc64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1656,13 +1716,16 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-riscv64-gnu": { "node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.52.4", "version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.4.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz",
"integrity": "sha512-/MOEW3aHjjs1p4Pw1Xk4+3egRevx8Ji9N6HUIA1Ifh8Q+cg9dremvFCUbOX2Zebz80BwJIgCBUemjqhU5XI5Eg==", "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==",
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1670,13 +1733,16 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-riscv64-musl": { "node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.52.4", "version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.4.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz",
"integrity": "sha512-1HHmsRyh845QDpEWzOFtMCph5Ts+9+yllCrREuBR/vg2RogAQGGBRC8lDPrPOMnrdOJ+mt1WLMOC2Kao/UwcvA==", "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==",
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1684,13 +1750,16 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-s390x-gnu": { "node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.52.4", "version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.4.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz",
"integrity": "sha512-seoeZp4L/6D1MUyjWkOMRU6/iLmCU2EjbMTyAG4oIOs1/I82Y5lTeaxW0KBfkUdHAWN7j25bpkt0rjnOgAcQcA==", "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==",
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1698,13 +1767,16 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-x64-gnu": { "node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.52.4", "version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.4.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz",
"integrity": "sha512-Wi6AXf0k0L7E2gteNsNHUs7UMwCIhsCTs6+tqQ5GPwVRWMaflqGec4Sd8n6+FNFDw9vGcReqk2KzBDhCa1DLYg==", "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1712,9 +1784,26 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-x64-musl": { "node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.52.4", "version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.4.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz",
"integrity": "sha512-dtBZYjDmCQ9hW+WgEkaffvRRCKm767wWhxsFW3Lw86VXz/uJRuD438/XvbZT//B96Vs8oTA8Q4A0AfHbrxP9zw==", "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==",
"cpu": [
"x64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-openbsd-x64": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz",
"integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -1722,13 +1811,13 @@
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"linux" "openbsd"
] ]
}, },
"node_modules/@rollup/rollup-openharmony-arm64": { "node_modules/@rollup/rollup-openharmony-arm64": {
"version": "4.52.4", "version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.4.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz",
"integrity": "sha512-1ox+GqgRWqaB1RnyZXL8PD6E5f7YyRUJYnCqKpNzxzP0TkaUh112NDrR9Tt+C8rJ4x5G9Mk8PQR3o7Ku2RKqKA==", "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -1740,9 +1829,9 @@
] ]
}, },
"node_modules/@rollup/rollup-win32-arm64-msvc": { "node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.52.4", "version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.4.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz",
"integrity": "sha512-8GKr640PdFNXwzIE0IrkMWUNUomILLkfeHjXBi/nUvFlpZP+FA8BKGKpacjW6OUUHaNI6sUURxR2U2g78FOHWQ==", "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -1754,9 +1843,9 @@
] ]
}, },
"node_modules/@rollup/rollup-win32-ia32-msvc": { "node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.52.4", "version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.4.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz",
"integrity": "sha512-AIy/jdJ7WtJ/F6EcfOb2GjR9UweO0n43jNObQMb6oGxkYTfLcnN7vYYpG+CN3lLxrQkzWnMOoNSHTW54pgbVxw==", "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@@ -1768,9 +1857,9 @@
] ]
}, },
"node_modules/@rollup/rollup-win32-x64-gnu": { "node_modules/@rollup/rollup-win32-x64-gnu": {
"version": "4.52.4", "version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.4.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz",
"integrity": "sha512-UF9KfsH9yEam0UjTwAgdK0anlQ7c8/pWPU2yVjyWcF1I1thABt6WXE47cI71pGiZ8wGvxohBoLnxM04L/wj8mQ==", "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -1782,9 +1871,9 @@
] ]
}, },
"node_modules/@rollup/rollup-win32-x64-msvc": { "node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.52.4", "version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.4.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz",
"integrity": "sha512-bf9PtUa0u8IXDVxzRToFQKsNCRz9qLYfR/MpECxl4mRoWYjAeFjgxj1XdZr2M/GNVpT05p+LgQOHopYDlUu6/w==", "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -2342,6 +2431,12 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/trusted-types": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"license": "MIT"
},
"node_modules/@typescript-eslint/eslint-plugin": { "node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.46.0", "version": "8.46.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.0.tgz",
@@ -2493,7 +2588,7 @@
"version": "8.46.0", "version": "8.46.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.0.tgz",
"integrity": "sha512-bHGGJyVjSE4dJJIO5yyEWt/cHyNwga/zXGJbJJ8TiO01aVREK6gCTu3L+5wrkb1FbDkQ+TKjMNe9R/QQQP9+rA==", "integrity": "sha512-bHGGJyVjSE4dJJIO5yyEWt/cHyNwga/zXGJbJJ8TiO01aVREK6gCTu3L+5wrkb1FbDkQ+TKjMNe9R/QQQP9+rA==",
"dev": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -2533,9 +2628,9 @@
} }
}, },
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
"version": "2.0.2", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -2543,13 +2638,13 @@
} }
}, },
"node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
"version": "9.0.5", "version": "9.0.9",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
"dev": true, "dev": true,
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"brace-expansion": "^2.0.1" "brace-expansion": "^2.0.2"
}, },
"engines": { "engines": {
"node": ">=16 || 14 >=14.17" "node": ">=16 || 14 >=14.17"
@@ -2797,9 +2892,9 @@
} }
}, },
"node_modules/ajv": { "node_modules/ajv": {
"version": "6.12.6", "version": "6.14.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -2946,9 +3041,9 @@
} }
}, },
"node_modules/brace-expansion": { "node_modules/brace-expansion": {
"version": "1.1.12", "version": "1.1.14",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -3294,10 +3389,9 @@
} }
}, },
"node_modules/devalue": { "node_modules/devalue": {
"version": "5.6.2", "version": "5.7.1",
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.2.tgz", "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.7.1.tgz",
"integrity": "sha512-nPRkjWzzDQlsejL1WVifk5rvcFi/y1onBRxjaFMjZeR9mFpqu2gmAZ9xUB9/IEanEP/vBtGeGganC/GO1fmufg==", "integrity": "sha512-MUbZ586EgQqdRnC4yDrlod3BEdyvE4TapGYHMW2CiaW+KkkFmWEFqBUaLltEZCGi0iFXCEjRF0OjF0DV2QHjOA==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/dom-accessibility-api": { "node_modules/dom-accessibility-api": {
@@ -3648,12 +3742,20 @@
} }
}, },
"node_modules/esrap": { "node_modules/esrap": {
"version": "2.1.0", "version": "2.2.5",
"resolved": "https://registry.npmjs.org/esrap/-/esrap-2.1.0.tgz", "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.5.tgz",
"integrity": "sha512-yzmPNpl7TBbMRC5Lj2JlJZNPml0tzqoqP5B1JXycNUwtqma9AKCO0M2wHrdgsHcy1WRW7S9rJknAMtByg3usgA==", "integrity": "sha512-/yLB1538mag+dn0wsePTe8C0rDIjUOaJpMs2McodSzmM2msWcZsBSdRtg6HOBt0A/r82BN+Md3pgwSc/uWt2Ig==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@jridgewell/sourcemap-codec": "^1.4.15" "@jridgewell/sourcemap-codec": "^1.4.15"
},
"peerDependencies": {
"@typescript-eslint/types": "^8.2.0"
},
"peerDependenciesMeta": {
"@typescript-eslint/types": {
"optional": true
}
} }
}, },
"node_modules/esrecurse": { "node_modules/esrecurse": {
@@ -3925,9 +4027,9 @@
} }
}, },
"node_modules/flatted": { "node_modules/flatted": {
"version": "3.3.3", "version": "3.4.2",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
"integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
@@ -4287,9 +4389,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/js-yaml": { "node_modules/js-yaml": {
"version": "4.1.0", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -4805,9 +4907,9 @@
} }
}, },
"node_modules/micromatch/node_modules/picomatch": { "node_modules/micromatch/node_modules/picomatch": {
"version": "2.3.1", "version": "2.3.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -4853,9 +4955,9 @@
} }
}, },
"node_modules/minimatch": { "node_modules/minimatch": {
"version": "3.1.2", "version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"dev": true, "dev": true,
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
@@ -5174,9 +5276,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/path-to-regexp": { "node_modules/path-to-regexp": {
"version": "8.3.0", "version": "8.4.2",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz",
"integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"type": "opencollective", "type": "opencollective",
@@ -5208,9 +5310,9 @@
"license": "ISC" "license": "ISC"
}, },
"node_modules/picomatch": { "node_modules/picomatch": {
"version": "4.0.3", "version": "4.0.4",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -5312,9 +5414,9 @@
} }
}, },
"node_modules/postcss-load-config/node_modules/yaml": { "node_modules/postcss-load-config/node_modules/yaml": {
"version": "1.10.2", "version": "1.10.3",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz",
"integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==",
"devOptional": true, "devOptional": true,
"license": "ISC", "license": "ISC",
"engines": { "engines": {
@@ -5578,9 +5680,9 @@
} }
}, },
"node_modules/qs": { "node_modules/qs": {
"version": "6.14.1", "version": "6.15.1",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz",
"integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==",
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
"dependencies": { "dependencies": {
"side-channel": "^1.1.0" "side-channel": "^1.1.0"
@@ -5701,9 +5803,9 @@
} }
}, },
"node_modules/rollup": { "node_modules/rollup": {
"version": "4.52.4", "version": "4.60.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.4.tgz", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz",
"integrity": "sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ==", "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -5717,28 +5819,31 @@
"npm": ">=8.0.0" "npm": ">=8.0.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.52.4", "@rollup/rollup-android-arm-eabi": "4.60.1",
"@rollup/rollup-android-arm64": "4.52.4", "@rollup/rollup-android-arm64": "4.60.1",
"@rollup/rollup-darwin-arm64": "4.52.4", "@rollup/rollup-darwin-arm64": "4.60.1",
"@rollup/rollup-darwin-x64": "4.52.4", "@rollup/rollup-darwin-x64": "4.60.1",
"@rollup/rollup-freebsd-arm64": "4.52.4", "@rollup/rollup-freebsd-arm64": "4.60.1",
"@rollup/rollup-freebsd-x64": "4.52.4", "@rollup/rollup-freebsd-x64": "4.60.1",
"@rollup/rollup-linux-arm-gnueabihf": "4.52.4", "@rollup/rollup-linux-arm-gnueabihf": "4.60.1",
"@rollup/rollup-linux-arm-musleabihf": "4.52.4", "@rollup/rollup-linux-arm-musleabihf": "4.60.1",
"@rollup/rollup-linux-arm64-gnu": "4.52.4", "@rollup/rollup-linux-arm64-gnu": "4.60.1",
"@rollup/rollup-linux-arm64-musl": "4.52.4", "@rollup/rollup-linux-arm64-musl": "4.60.1",
"@rollup/rollup-linux-loong64-gnu": "4.52.4", "@rollup/rollup-linux-loong64-gnu": "4.60.1",
"@rollup/rollup-linux-ppc64-gnu": "4.52.4", "@rollup/rollup-linux-loong64-musl": "4.60.1",
"@rollup/rollup-linux-riscv64-gnu": "4.52.4", "@rollup/rollup-linux-ppc64-gnu": "4.60.1",
"@rollup/rollup-linux-riscv64-musl": "4.52.4", "@rollup/rollup-linux-ppc64-musl": "4.60.1",
"@rollup/rollup-linux-s390x-gnu": "4.52.4", "@rollup/rollup-linux-riscv64-gnu": "4.60.1",
"@rollup/rollup-linux-x64-gnu": "4.52.4", "@rollup/rollup-linux-riscv64-musl": "4.60.1",
"@rollup/rollup-linux-x64-musl": "4.52.4", "@rollup/rollup-linux-s390x-gnu": "4.60.1",
"@rollup/rollup-openharmony-arm64": "4.52.4", "@rollup/rollup-linux-x64-gnu": "4.60.1",
"@rollup/rollup-win32-arm64-msvc": "4.52.4", "@rollup/rollup-linux-x64-musl": "4.60.1",
"@rollup/rollup-win32-ia32-msvc": "4.52.4", "@rollup/rollup-openbsd-x64": "4.60.1",
"@rollup/rollup-win32-x64-gnu": "4.52.4", "@rollup/rollup-openharmony-arm64": "4.60.1",
"@rollup/rollup-win32-x64-msvc": "4.52.4", "@rollup/rollup-win32-arm64-msvc": "4.60.1",
"@rollup/rollup-win32-ia32-msvc": "4.60.1",
"@rollup/rollup-win32-x64-gnu": "4.60.1",
"@rollup/rollup-win32-x64-msvc": "4.60.1",
"fsevents": "~2.3.2" "fsevents": "~2.3.2"
} }
}, },
@@ -6179,21 +6284,23 @@
} }
}, },
"node_modules/svelte": { "node_modules/svelte": {
"version": "5.39.11", "version": "5.55.3",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.39.11.tgz", "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.55.3.tgz",
"integrity": "sha512-8MxWVm2+3YwrFbPaxOlT1bbMi6OTenrAgks6soZfiaS8Fptk4EVyRIFhJc3RpO264EeSNwgjWAdki0ufg4zkGw==", "integrity": "sha512-dS1N+i3bA1v+c4UDb750MlN5vCO82G6vxh8HeTsPsTdJ1BLsN1zxSyDlIdBBqUjqZ/BxEwM8UrFf98aaoVnZFQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@jridgewell/remapping": "^2.3.4", "@jridgewell/remapping": "^2.3.4",
"@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/sourcemap-codec": "^1.5.0",
"@sveltejs/acorn-typescript": "^1.0.5", "@sveltejs/acorn-typescript": "^1.0.5",
"@types/estree": "^1.0.5", "@types/estree": "^1.0.5",
"@types/trusted-types": "^2.0.7",
"acorn": "^8.12.1", "acorn": "^8.12.1",
"aria-query": "^5.3.1", "aria-query": "5.3.1",
"axobject-query": "^4.1.0", "axobject-query": "^4.1.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"devalue": "^5.6.4",
"esm-env": "^1.2.1", "esm-env": "^1.2.1",
"esrap": "^2.1.0", "esrap": "^2.2.4",
"is-reference": "^3.0.3", "is-reference": "^3.0.3",
"locate-character": "^3.0.0", "locate-character": "^3.0.0",
"magic-string": "^0.30.11", "magic-string": "^0.30.11",
@@ -6311,6 +6418,36 @@
} }
} }
}, },
"node_modules/svelte-sonner": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/svelte-sonner/-/svelte-sonner-1.1.0.tgz",
"integrity": "sha512-3lYM6ZIqWe+p9vwwWHGWP/ZdvHiUtzURsud2quIxivrX4rvpXh6i+geBGn0m3JS6KwW6W8VgbOl3xQMcDuh6gg==",
"dev": true,
"license": "MIT",
"dependencies": {
"runed": "^0.28.0"
},
"peerDependencies": {
"svelte": "^5.0.0"
}
},
"node_modules/svelte-sonner/node_modules/runed": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/runed/-/runed-0.28.0.tgz",
"integrity": "sha512-k2xx7RuO9hWcdd9f+8JoBeqWtYrm5CALfgpkg2YDB80ds/QE4w0qqu34A7fqiAwiBBSBQOid7TLxwxVC27ymWQ==",
"dev": true,
"funding": [
"https://github.com/sponsors/huntabyte",
"https://github.com/sponsors/tglide"
],
"license": "MIT",
"dependencies": {
"esm-env": "^1.0.0"
},
"peerDependencies": {
"svelte": "^5.7.0"
}
},
"node_modules/svelte-toolbelt": { "node_modules/svelte-toolbelt": {
"version": "0.10.6", "version": "0.10.6",
"resolved": "https://registry.npmjs.org/svelte-toolbelt/-/svelte-toolbelt-0.10.6.tgz", "resolved": "https://registry.npmjs.org/svelte-toolbelt/-/svelte-toolbelt-0.10.6.tgz",
@@ -6333,9 +6470,9 @@
} }
}, },
"node_modules/svelte/node_modules/aria-query": { "node_modules/svelte/node_modules/aria-query": {
"version": "5.3.2", "version": "5.3.1",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz",
"integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", "integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==",
"license": "Apache-2.0", "license": "Apache-2.0",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@@ -6410,11 +6547,11 @@
} }
}, },
"node_modules/tar": { "node_modules/tar": {
"version": "7.5.1", "version": "7.5.13",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.1.tgz", "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.13.tgz",
"integrity": "sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g==", "integrity": "sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==",
"dev": true, "dev": true,
"license": "ISC", "license": "BlueOak-1.0.0",
"dependencies": { "dependencies": {
"@isaacs/fs-minipass": "^4.0.0", "@isaacs/fs-minipass": "^4.0.0",
"chownr": "^3.0.0", "chownr": "^3.0.0",
+4 -2
View File
@@ -19,7 +19,7 @@
"@eslint/compat": "^1.4.0", "@eslint/compat": "^1.4.0",
"@eslint/js": "^9.36.0", "@eslint/js": "^9.36.0",
"@internationalized/date": "^3.10.1", "@internationalized/date": "^3.10.1",
"@lucide/svelte": "^0.561.0", "@lucide/svelte": "^1.7.0",
"@sveltejs/adapter-node": "^5.3.2", "@sveltejs/adapter-node": "^5.3.2",
"@sveltejs/kit": "2.49.5", "@sveltejs/kit": "2.49.5",
"@sveltejs/vite-plugin-svelte": "^6.2.0", "@sveltejs/vite-plugin-svelte": "^6.2.0",
@@ -42,6 +42,7 @@
"prettier-plugin-tailwindcss": "^0.6.14", "prettier-plugin-tailwindcss": "^0.6.14",
"svelte": "^5.39.5", "svelte": "^5.39.5",
"svelte-check": "^4.3.2", "svelte-check": "^4.3.2",
"svelte-sonner": "^1.1.0",
"tailwind-merge": "^3.4.0", "tailwind-merge": "^3.4.0",
"tailwind-variants": "^3.2.2", "tailwind-variants": "^3.2.2",
"tailwindcss": "^4.1.13", "tailwindcss": "^4.1.13",
@@ -63,6 +64,7 @@
"postgres": "^3.4.7", "postgres": "^3.4.7",
"sharp": "^0.34.5", "sharp": "^0.34.5",
"svelte-preprocess": "^6.0.3", "svelte-preprocess": "^6.0.3",
"valibot": "^1.2.0" "valibot": "^1.2.0",
"@rive-app/canvas": "^2.37.1"
} }
} }
+77 -9
View File
@@ -8,20 +8,22 @@
--radius: 0.65rem; --radius: 0.65rem;
--background: oklch(1 0 0); --background: oklch(1 0 0);
--foreground: oklch(0.141 0.005 285.823); --foreground: oklch(0.141 0.005 285.823);
--card: oklch(1 0 0); --card: oklch(0.95 0 0);
--card-foreground: oklch(0.141 0.005 285.823); --card-foreground: oklch(0.141 0.005 285.823);
--popover: oklch(1 0 0); --popover: oklch(1 0 0);
--popover-foreground: oklch(0.141 0.005 285.823); --popover-foreground: oklch(0.141 0.005 285.823);
--primary: oklch(0.5852 0.25 292.717); --primary: oklch(0.5852 0.25 292.717);
--primary-foreground: oklch(0.969 0.016 293.756); --primary-foreground: oklch(0.969 0.016 293.756);
--dull-primary: oklch(0.8225 0.0927 293.01);
--dull-primary-foreground: oklch(0.0 0.016 293.756);
--secondary: oklch(0.967 0.001 286.375); --secondary: oklch(0.967 0.001 286.375);
--secondary-foreground: oklch(0.21 0.006 285.885); --secondary-foreground: oklch(0.21 0.006 285.885);
--muted: oklch(0.967 0.001 286.375); --muted: oklch(0.967 0.001 286.375);
--muted-foreground: oklch(0.552 0.016 285.938); --muted-foreground: oklch(0.502 0.016 285.938);
--accent: oklch(0.967 0.001 286.375); --accent: oklch(0.967 0.001 286.375);
--accent-foreground: oklch(0.21 0.006 285.885); --accent-foreground: oklch(0.21 0.006 285.885);
--destructive: oklch(0.577 0.245 27.325); --destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.92 0.004 286.32); --border: oklch(0.9 0.004 286.32);
--input: oklch(0.92 0.004 286.32); --input: oklch(0.92 0.004 286.32);
--ring: oklch(0.606 0.25 292.717); --ring: oklch(0.606 0.25 292.717);
--chart-1: oklch(0.646 0.222 41.116); --chart-1: oklch(0.646 0.222 41.116);
@@ -37,10 +39,10 @@
--sidebar-accent-foreground: oklch(0.21 0.006 285.885); --sidebar-accent-foreground: oklch(0.21 0.006 285.885);
--sidebar-border: oklch(0.92 0.004 286.32); --sidebar-border: oklch(0.92 0.004 286.32);
--sidebar-ring: oklch(0.606 0.25 292.717); --sidebar-ring: oklch(0.606 0.25 292.717);
--warning: oklch(0.84 0.16 84); --warning: oklch(0.6 0.16 84);
--error: oklch(0.577 0.245 27.325); --error: oklch(0.577 0.245 27.325);
--positive: oklch(0.5 0.2067 147.18); --positive: oklch(0.5 0.2067 147.18);
--action: oklch(0.5852 0.2263 260.47); --action: oklch(0.4852 0.2263 260.47);
} }
.dark { .dark {
@@ -50,12 +52,15 @@
--card-foreground: oklch(0.985 0 0); --card-foreground: oklch(0.985 0 0);
--popover: oklch(0.21 0.006 285.885); --popover: oklch(0.21 0.006 285.885);
--popover-foreground: oklch(0.985 0 0); --popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.541 0.281 293.009); --primary: oklch(0.5 0.331 293.009);
--primary-foreground: oklch(0.969 0.016 293.756); --primary-foreground: oklch(0.0 0.016 293.756);
--dull-primary: oklch(0.3 0.281 293.009);
--dull-primary-foreground: oklch(0.969 0.016 293.756);
/*--primary-foreground: oklch(0.969 0.016 293.756);*/
--secondary: oklch(0.274 0.006 286.033); --secondary: oklch(0.274 0.006 286.033);
--secondary-foreground: oklch(0.985 0 0); --secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.274 0.006 286.033); --muted: oklch(0.274 0.006 286.033);
--muted-foreground: oklch(0.705 0.015 286.067); --muted-foreground: oklch(0.655 0.015 286.067);
--accent: oklch(0.274 0.006 286.033); --accent: oklch(0.274 0.006 286.033);
--accent-foreground: oklch(0.985 0 0); --accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216); --destructive: oklch(0.704 0.191 22.216);
@@ -78,7 +83,7 @@
--warning: oklch(0.84 0.16 84); --warning: oklch(0.84 0.16 84);
--error: oklch(0.704 0.191 22.216); --error: oklch(0.704 0.191 22.216);
--positive: oklch(0.7522 0.2067 147.18); --positive: oklch(0.7522 0.2067 147.18);
--action: oklch(0.6098 0.1872 260.47); --action: oklch(0.7098 0.1872 260.47);
} }
@theme inline { @theme inline {
@@ -94,6 +99,8 @@
--color-popover-foreground: var(--popover-foreground); --color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary); --color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground); --color-primary-foreground: var(--primary-foreground);
--color-dull-primary: var(--dull-primary);
--color-dull-primary-foreground: var(--dull-primary-foreground);
--color-secondary: var(--secondary); --color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground); --color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted); --color-muted: var(--muted);
@@ -133,3 +140,64 @@
} }
} }
@custom-variant data-open {
&:where([data-state="open"]), &:where([data-open]:not([data-open="false"])) {
@slot;
}
}
@custom-variant data-closed {
&:where([data-state="closed"]), &:where([data-closed]:not([data-closed="false"])) {
@slot;
}
}
@custom-variant data-checked {
&:where([data-state="checked"]), &:where([data-checked]:not([data-checked="false"])) {
@slot;
}
}
@custom-variant data-unchecked {
&:where([data-state="unchecked"]), &:where([data-unchecked]:not([data-unchecked="false"])) {
@slot;
}
}
@custom-variant data-selected {
&:where([data-selected]) {
@slot;
}
}
@custom-variant data-disabled {
&:where([data-disabled="true"]), &:where([data-disabled]:not([data-disabled="false"])) {
@slot;
}
}
@custom-variant data-active {
&:where([data-state="active"]), &:where([data-active]:not([data-active="false"])) {
@slot;
}
}
@custom-variant data-horizontal {
&:where([data-orientation="horizontal"]) {
@slot;
}
}
@custom-variant data-vertical {
&:where([data-orientation="vertical"]) {
@slot;
}
}
@utility no-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
}
+9 -4
View File
@@ -1,4 +1,4 @@
import type { User, UserPayload } from '$lib/types/user'; import { DefaultUserSettings, type User, type UserPayload } from '$lib/types/user';
import bcrypt from 'bcrypt'; import bcrypt from 'bcrypt';
import sql from '$lib/db/db.server'; import sql from '$lib/db/db.server';
import { type Cookies, error } from '@sveltejs/kit'; import { type Cookies, error } from '@sveltejs/kit';
@@ -9,7 +9,10 @@ export function setJWTCookie(cookies: Cookies, user: User) {
const payload = { const payload = {
id: user.id, id: user.id,
email: user.email, email: user.email,
name: user.name name: user.name,
settings: user.settings || DefaultUserSettings,
createdAt: user.createdAt,
lastSignIn: user.lastSignIn
}; };
if (process.env.JWT_SECRET === undefined) { if (process.env.JWT_SECRET === undefined) {
@@ -61,12 +64,14 @@ export async function login(email: string, password: string): Promise<User> {
SELECT * FROM users SELECT * FROM users
WHERE email = ${email}; WHERE email = ${email};
`; `;
if (typeof user.settings == 'string') {
user.settings = JSON.parse(user.settings);
}
if (user) { if (user) {
if (await bcrypt.compare(password, user.passwordHash!)) { if (await bcrypt.compare(password, user.passwordHash!)) {
delete user.passwordHash; delete user.passwordHash;
// eslint-disable-next-line @typescript-eslint/no-unused-expressions await sql`
sql`
UPDATE users UPDATE users
SET last_sign_in = NOW() SET last_sign_in = NOW()
WHERE id = ${user.id}; WHERE id = ${user.id};
@@ -15,7 +15,7 @@
</script> </script>
<Dialog.Root bind:open> <Dialog.Root bind:open>
<Dialog.Content> <Dialog.Content onOpenAutoFocus={(e) => e.preventDefault()}>
<Dialog.Header> <Dialog.Header>
<Dialog.Title>Claim Item</Dialog.Title> <Dialog.Title>Claim Item</Dialog.Title>
<Dialog.Description> <Dialog.Description>
@@ -23,7 +23,7 @@
</Dialog.Description> </Dialog.Description>
</Dialog.Header> </Dialog.Header>
<form method="post" action="?/claim"> <form method="post" action="?/claim&id={item?.id}">
<Field.Group> <Field.Group>
<div class="flex gap-4"> <div class="flex gap-4">
@@ -44,10 +44,7 @@
<div class="inline-block"> <div class="inline-block">
<div class="flex-1">{item?.description}</div> <div class="flex-1">{item?.description}</div>
{#if item?.foundLocation} {#if item?.foundLocation}
<div class="mt-2"> <span class="mt-2"><strong class="mr-1">Found at:</strong> {item.foundLocation}</span>
<LocationIcon class="float-left mr-1" size={24} />
<div>{item?.foundLocation}</div>
</div>
{/if} {/if}
</div> </div>
</div> </div>
@@ -117,10 +117,13 @@
<Dialog.Footer class="mt-4"> <Dialog.Footer class="mt-4">
<div class="flex justify-between w-full"> <div class="flex justify-between w-full">
<Button variant="destructive" <Dialog.Close
onclick={() => {approveDenyItem({id: item?.id || 0, approved: false}); invalidateAll();}}> type="button"
class={buttonVariants({ variant: "destructive" })}
onclick={async () => {await approveDenyItem({ id: item?.id || 0, approved: false }); await invalidateAll(); }}
>
Delete Delete
</Button> </Dialog.Close>
<div> <div>
@@ -30,7 +30,6 @@
} }
function handleFiles(files: FileList | null) { function handleFiles(files: FileList | null) {
console.log('handleFiles');
if (!files || files.length === 0) return; if (!files || files.length === 0) return;
const selected = files[0]; const selected = files[0];
@@ -16,7 +16,7 @@
</script> </script>
<Dialog.Root bind:open> <Dialog.Root bind:open>
<Dialog.Content> <Dialog.Content onOpenAutoFocus={(e) => e.preventDefault()}>
<Dialog.Header> <Dialog.Header>
<Dialog.Title>Item Inquiry</Dialog.Title> <Dialog.Title>Item Inquiry</Dialog.Title>
<Dialog.Description> <Dialog.Description>
@@ -45,10 +45,7 @@
<div class="inline-block"> <div class="inline-block">
<div class="flex-1">{item?.description}</div> <div class="flex-1">{item?.description}</div>
{#if item?.foundLocation} {#if item?.foundLocation}
<div class="mt-2"> <span class="mt-2 flex"><strong class="mr-1">Found at:</strong> {item.foundLocation}</span>
<LocationIcon class="float-left mr-1" size={24} />
<div>{item?.foundLocation}</div>
</div>
{/if} {/if}
</div> </div>
</div> </div>
+63 -36
View File
@@ -7,50 +7,54 @@
import XIcon from '@lucide/svelte/icons/x'; import XIcon from '@lucide/svelte/icons/x';
import PencilIcon from '@lucide/svelte/icons/pencil'; import PencilIcon from '@lucide/svelte/icons/pencil';
import NotebookPenIcon from '@lucide/svelte/icons/notebook-pen'; import NotebookPenIcon from '@lucide/svelte/icons/notebook-pen';
import TrashIcon from '@lucide/svelte/icons/trash';
import StarIcon from '@lucide/svelte/icons/star'; import StarIcon from '@lucide/svelte/icons/star';
import ArchiveRestoreIcon from '@lucide/svelte/icons/archive-restore';
import { Button } from '$lib/components/ui/button'; import { Button } from '$lib/components/ui/button';
import * as Tooltip from '$lib/components/ui/tooltip'; import * as Tooltip from '$lib/components/ui/tooltip';
import { dateFormatOptions } from '$lib/shared'; import { dateFormatOptions } from '$lib/shared';
import { approveDenyItem } from '$lib/db/items.remote'; import { approveDenyItem, restoreClaimedItem } from '$lib/db/items.remote';
import { invalidateAll } from '$app/navigation'; import { invalidateAll } from '$app/navigation';
import NoImagePlaceholder from './no-image-placeholder.svelte'; import NoImagePlaceholder from './no-image-placeholder.svelte';
import type { User } from '$lib/types/user';
export let item: Item = <Item>{}; export let item: Item = <Item>{};
export let admin = false; export let user: User | null = null;
export let editCallback: (item: Item) => void; export let editCallback: (item: Item) => void;
export let inquireCallback: (item: Item) => void; export let inquireCallback: (item: Item) => void;
export let claimCallback: (item: Item) => void; export let claimCallback: (item: Item) => void;
let timeSincePosted: number | string = (new Date().getTime() - item.foundDate.getTime()) / 1000 / 60 / 60 / 24; // days // Normalize the foundDate in case it was serialized (string) in prod builds
if (timeSincePosted < 1) { // (SSR/JSON serialization often turns Date objects into strings).
timeSincePosted = '<1'; const foundDateObj: Date = item?.foundDate instanceof Date
} else { ? item.foundDate
timeSincePosted = Math.round(timeSincePosted); : new Date(item?.foundDate);
}
// Compute days since posted as a number (always a number, fall back to 0 on invalid date)
let timeSincePosted: number = (() => {
const t = foundDateObj?.getTime?.();
if (!t || Number.isNaN(t)) return 0;
return (Date.now() - t) / 1000 / 60 / 60 / 24; // days (float)
})();
// If you want to round/display differently you can do that in the template.
</script> </script>
<div <div
class="h-full bg-card text-card-foreground flex flex-col gap-2 rounded-xl border shadow-sm max-w-sm overflow-hidden min-w-2xs"> class="text-left h-full bg-card text-card-foreground flex flex-col gap-2 rounded-xl border shadow-sm max-w-sm overflow-hidden min-w-2xs" onclick={() => {claimCallback(item)}}>
{#if item.image} {#if item.image}
<img src="https://fbla26.marinodev.com/uploads/{item.id}.jpg" class="object-cover min-h-48 max-h-48" <img src="https://fbla26.marinodev.com/uploads/{item.id}.jpg" class="object-cover min-h-56 max-h-56"
alt="Lost item"> alt="Lost item">
{:else} {:else}
<div class="min-h-48 w-full bg-accent flex flex-col justify-center"> <div class="min-h-48 w-full bg-accent flex flex-col justify-center">
<div class="justify-center flex "> <div class="justify-center flex ">
<NoImagePlaceholder className="" /> <NoImagePlaceholder className="" />
</div> </div>
<p class="text-center mt-4">No image available</p> <p class="text-center mt-4">No image available</p>
</div> </div>
{/if} {/if}
<div class="flex-col flex h-full px-2 pb-2"> <div class="flex-col flex h-full px-2 pb-2">
<!-- <div class="font-bold inline-block">{item.title}</div>-->
<!-- <div class="inline-block">-->
<div> <div>
{#if item.transferred} {#if item.transferred}
<Badge variant="secondary" class="inline-block">In Lost & Found</Badge> <Badge variant="secondary" class="inline-block">In Lost & Found</Badge>
{:else} {:else}
@@ -60,60 +64,83 @@
<Tooltip.Root> <Tooltip.Root>
<Tooltip.Trigger <Tooltip.Trigger
> >
<Badge variant="outline" class="inline-block">{timeSincePosted} <Badge variant="outline"
day{(timeSincePosted === 1 || timeSincePosted === '<1') ? '' : 's'} ago class="inline-block {user?.settings?.staleItemDays !== undefined && timeSincePosted >= user.settings.staleItemDays ? 'text-warning' : ''}">{timeSincePosted < 1 ? "<1" : Math.round(timeSincePosted)}
day{(timeSincePosted <= 1) ? '' : 's'} ago
</Badge> </Badge>
</Tooltip.Trigger </Tooltip.Trigger
> >
<Tooltip.Content> <Tooltip.Content>
<p>{item.foundDate.toLocaleDateString('en-US', dateFormatOptions)}</p> <p>{foundDateObj.toLocaleDateString('en-US', dateFormatOptions)}</p>
</Tooltip.Content> </Tooltip.Content>
</Tooltip.Root> </Tooltip.Root>
</Tooltip.Provider> </Tooltip.Provider>
</div> </div>
<div class="flex-1">{item.description}</div> <div class="flex-1">{item.description}</div>
{#if item.foundLocation} {#if item.foundLocation}
<div class="mt-2"> <div>
<LocationIcon class="float-left mr-1" size={24} />
<div>{item.foundLocation}</div> <Tooltip.Provider>
<Tooltip.Root>
<Tooltip.Trigger>
<span class="mt-2 flex"><strong class="mr-1">Found at:</strong> {item.foundLocation}</span>
</Tooltip.Trigger
>
<Tooltip.Content>
<p>Item was found here</p>
</Tooltip.Content>
</Tooltip.Root>
</Tooltip.Provider>
</div> </div>
{/if} {/if}
{#if user !== null}
{#if admin}
<div class="mt-2 justify-between flex"> <div class="mt-2 justify-between flex">
{#if item.approvedDate === null} {#if item.approvedDate === null}
<Button variant="ghost" class="text-positive" <Button variant="ghost" class="text-positive"
onclick={async () => {await approveDenyItem({id: item.id, approved: true}); onclick={async (e) => {e.stopPropagation(); await approveDenyItem({id: item.id, approved: true});
invalidateAll()}}> await invalidateAll()}}>
<CheckIcon /> <CheckIcon />
Approve Approve
</Button> </Button>
<Button variant="ghost" class="text-destructive" <Button variant="ghost" class="text-destructive"
onclick={async () => {await approveDenyItem({id: item.id, approved: false}); onclick={async (e) => {e.stopPropagation(); await approveDenyItem({id: item.id, approved: false});
invalidateAll()}}> await invalidateAll()}}>
<XIcon /> <XIcon />
Deny Deny
</Button> </Button>
{/if} {/if}
{#if item.claimedDate === null}
<Button variant="ghost" class="text-action" <Button variant="ghost" class="text-action"
onclick={() => {editCallback(item)}}> onclick={(e) => {e.stopPropagation(); editCallback(item)}}>
<PencilIcon /> <PencilIcon />
Manage {!item.approvedDate ? 'Edit' : 'Manage'}
</Button> </Button>
{:else}
<Button variant="ghost" class="text-destructive"
onclick={async (e) => {e.stopPropagation(); await approveDenyItem({id: item.id, approved: false});
await invalidateAll()}}>
<TrashIcon />
Delete
</Button>
<Button variant="ghost" class="text-action"
onclick={async (e) => {e.stopPropagation(); await restoreClaimedItem(item.id);
await invalidateAll()}}>
<ArchiveRestoreIcon />
Restore
</Button>
{/if}
</div> </div>
{:else} {:else}
<div class="mt-2 justify-between flex"> <div class="mt-2 justify-between flex">
<Button variant="ghost" class="text-action" <Button variant="ghost" class="text-action z-10"
onclick={() => {inquireCallback(item)}}> onclick={(e) => {e.stopPropagation(); inquireCallback(item)}}>
<NotebookPenIcon /> <NotebookPenIcon />
Inquire Inquire
</Button> </Button>
<Button variant="ghost" class="text-primary" <Button variant="ghost" class="text-primary z-10"
onclick={() => {claimCallback(item)}}> onclick={(e) => {e.stopPropagation(); claimCallback(item)}}>
<StarIcon /> <StarIcon />
Claim Claim
</Button> </Button>
@@ -0,0 +1,42 @@
<script>
import { onMount, onDestroy } from 'svelte';
export let src = '/mdev_triangle_loading.riv';
export let stateMachine = null;
export let size = 120;
export let classes = '';
let canvas;
let riveInstance;
onMount(async () => {
if (typeof window === 'undefined') return;
const mod = await import('@rive-app/canvas');
// Handle BOTH CJS and ESM shapes safely
const Rive = mod?.Rive || mod?.default?.Rive || mod?.default;
if (!Rive) {
console.error('Rive failed to load:', mod);
return;
}
riveInstance = new Rive({
src,
canvas,
autoplay: true,
// stateMachines: stateMachine || undefined,
onLoad: () => {
riveInstance.resizeDrawingSurfaceToCanvas();
}
});
});
onDestroy(() => {
riveInstance?.cleanup?.();
});
</script>
<canvas bind:this={canvas} class={classes} />
@@ -6,7 +6,7 @@
import { Label } from '$lib/components/ui/label'; import { Label } from '$lib/components/ui/label';
import * as Field from '$lib/components/ui/field'; import * as Field from '$lib/components/ui/field';
import { fileToBase64 } from '$lib/shared'; import { fileToBase64 } from '$lib/shared';
import LoadingSpinner from './loading-spinner.svelte';
import ImageUpload from '$lib/components/custom/image-upload/image-upload.svelte'; import ImageUpload from '$lib/components/custom/image-upload/image-upload.svelte';
import { genDescription } from '$lib/db/items.remote'; import { genDescription } from '$lib/db/items.remote';
import { EMAIL_REGEX_STRING } from '$lib/consts'; import { EMAIL_REGEX_STRING } from '$lib/consts';
@@ -17,12 +17,47 @@
let description: string | undefined = $state(); let description: string | undefined = $state();
let isGenerating = $state(false); let isGenerating = $state(false);
async function resizeImage(
file: File,
maxWidth = 300,
maxHeight = 300,
quality = 0.8
): Promise<File> {
const bitmap = await createImageBitmap(file);
let { width, height } = bitmap;
const scale = Math.min(maxWidth / width, maxHeight / height, 1);
width = Math.round(width * scale);
height = Math.round(height * scale);
const canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext("2d")!;
ctx.drawImage(bitmap, 0, 0, width, height);
const blob = await new Promise<Blob>((resolve) =>
canvas.toBlob((blob) => resolve(blob!), "image/jpeg", quality)
);
return new File([blob], file.name, {
type: "image/jpeg",
lastModified: Date.now()
});
}
async function onSelect(file: File) { async function onSelect(file: File) {
isGenerating = true; isGenerating = true;
const base64 = await fileToBase64(file); const resized = await resizeImage(file, 300, 300, 0.7);
const base64 = await fileToBase64(resized);
description = await genDescription(base64); description = await genDescription(base64);
isGenerating = false; isGenerating = false;
} }
@@ -30,7 +65,7 @@
</script> </script>
<Dialog.Root bind:open> <Dialog.Root bind:open>
<Dialog.Content> <Dialog.Content onOpenAutoFocus={(e) => e.preventDefault()}>
<Dialog.Header> <Dialog.Header>
<Dialog.Title>Submit Found Item</Dialog.Title> <Dialog.Title>Submit Found Item</Dialog.Title>
<Dialog.Description> <Dialog.Description>
@@ -69,7 +104,11 @@
/> />
</Field.Field> </Field.Field>
<RadioGroup.Root name="location" bind:value={itemLocation}>
<RadioGroup.Root name="location" bind:value={itemLocation} required>
<Field.Label for="location">
Where is it? <span class="text-error">*</span>
</Field.Label>
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<RadioGroup.Item value="finderPossession" id="finderPossession" /> <RadioGroup.Item value="finderPossession" id="finderPossession" />
<Label for="finderPossession"> <Label for="finderPossession">
@@ -115,8 +154,11 @@
</Dialog.Root> </Dialog.Root>
{#if isGenerating} <div
<div class="fixed inset-0 bg-black/75 z-999999 w-screen h-screen justify-center items-center flex"> class="fixed inset-0 bg-black/85 z-[999999] w-screen h-screen flex justify-center items-center transition-opacity duration-150 flex-col md:flex-row"
<p class="text-6xl text-primary">Loading...</p> class:opacity-0={!isGenerating}
</div> class:pointer-events-none={!isGenerating}
{/if} >
<LoadingSpinner classes="w-28 md:w-48 mb-2 md:-mb-4" />
<p class="text-4xl md:text-6xl text-white">Loading...</p>
</div>
+27 -25
View File
@@ -1,38 +1,40 @@
<script lang="ts" module> <script lang="ts" module>
import { cn, type WithElementRef } from "$lib/utils.js"; import { cn, type WithElementRef } from '$lib/utils.js';
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from "svelte/elements"; import type { HTMLAnchorAttributes, HTMLButtonAttributes } from 'svelte/elements';
import { type VariantProps, tv } from "tailwind-variants"; import { type VariantProps, tv } from 'tailwind-variants';
export const buttonVariants = tv({ export const buttonVariants = tv({
base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", base: 'focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4',
variants: { variants: {
variant: { variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90 shadow-xs", default: 'bg-dull-primary text-dull-primary-foreground hover:bg-primary/90 shadow-xs',
// dull_default: 'bg-dull-primary text-dull-primary-foreground hover:bg-dull-primary/90 shadow-xs',
destructive: destructive:
"bg-destructive hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60 text-white shadow-xs", 'bg-destructive hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60 text-white shadow-xs',
outline: outline:
"bg-background hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 border shadow-xs", 'bg-background hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 border shadow-xs',
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80 shadow-xs", secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80 shadow-xs',
ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", ghost: 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
link: "text-primary underline-offset-4 hover:underline", link: 'text-primary underline-offset-4 hover:underline'
}, },
size: { size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3", default: 'h-9 px-4 py-2 has-[>svg]:px-3',
sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5", sm: 'h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5',
lg: "h-10 rounded-md px-6 has-[>svg]:px-4", lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
icon: "size-9", xl: 'h-14 rounded-md px-8 has-[>svg]:px-6',
"icon-sm": "size-8", icon: 'size-9',
"icon-lg": "size-10", 'icon-sm': 'size-8',
}, 'icon-lg': 'size-10'
}
}, },
defaultVariants: { defaultVariants: {
variant: "default", variant: 'default',
size: "default", size: 'default'
}, }
}); });
export type ButtonVariant = VariantProps<typeof buttonVariants>["variant"]; export type ButtonVariant = VariantProps<typeof buttonVariants>['variant'];
export type ButtonSize = VariantProps<typeof buttonVariants>["size"]; export type ButtonSize = VariantProps<typeof buttonVariants>['size'];
export type ButtonProps = WithElementRef<HTMLButtonAttributes> & export type ButtonProps = WithElementRef<HTMLButtonAttributes> &
WithElementRef<HTMLAnchorAttributes> & { WithElementRef<HTMLAnchorAttributes> & {
@@ -44,11 +46,11 @@
<script lang="ts"> <script lang="ts">
let { let {
class: className, class: className,
variant = "default", variant = 'default',
size = "default", size = 'default',
ref = $bindable(null), ref = $bindable(null),
href = undefined, href = undefined,
type = "button", type = 'button',
disabled, disabled,
children, children,
...restProps ...restProps
+1
View File
@@ -0,0 +1 @@
export { default as Toaster } from "./sonner.svelte";
@@ -0,0 +1,34 @@
<script lang="ts">
import { Toaster as Sonner, type ToasterProps as SonnerProps } from "svelte-sonner";
import { mode } from "mode-watcher";
import Loader2Icon from '@lucide/svelte/icons/loader-2';
import CircleCheckIcon from '@lucide/svelte/icons/circle-check';
import OctagonXIcon from '@lucide/svelte/icons/octagon-x';
import InfoIcon from '@lucide/svelte/icons/info';
import TriangleAlertIcon from '@lucide/svelte/icons/triangle-alert';
let { ...restProps }: SonnerProps = $props();
</script>
<Sonner
theme={mode.current}
class="toaster group"
style="--normal-bg: var(--color-popover); --normal-text: var(--color-popover-foreground); --normal-border: var(--color-border);"
{...restProps}
>
{#snippet loadingIcon()}
<Loader2Icon class="size-4 animate-spin" />
{/snippet}
{#snippet successIcon()}
<CircleCheckIcon class="size-4" />
{/snippet}
{#snippet errorIcon()}
<OctagonXIcon class="size-4" />
{/snippet}
{#snippet infoIcon()}
<InfoIcon class="size-4" />
{/snippet}
{#snippet warningIcon()}
<TriangleAlertIcon class="size-4" />
{/snippet}
</Sonner>
+7
View File
@@ -0,0 +1,7 @@
import Root from "./switch.svelte";
export {
Root,
//
Root as Switch,
};
@@ -0,0 +1,29 @@
<script lang="ts">
import { Switch as SwitchPrimitive } from "bits-ui";
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
checked = $bindable(false),
...restProps
}: WithoutChildrenOrChild<SwitchPrimitive.RootProps> = $props();
</script>
<SwitchPrimitive.Root
bind:ref
bind:checked
data-slot="switch"
class={cn(
"data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 peer inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...restProps}
>
<SwitchPrimitive.Thumb
data-slot="switch-thumb"
class={cn(
"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0"
)}
/>
</SwitchPrimitive.Root>
+2 -1
View File
@@ -25,4 +25,5 @@ const EMAIL_REGEX =
/^(?!\.)(?!.*\.\.)([a-z0-9_'+\-\.]*)[a-z0-9_'+\-]@([a-z0-9][a-z0-9\-]*\.)+[a-z]{2,}$/i; /^(?!\.)(?!.*\.\.)([a-z0-9_'+\-\.]*)[a-z0-9_'+\-]@([a-z0-9][a-z0-9\-]*\.)+[a-z]{2,}$/i;
// Replace single quote with HTML entity or remove it from the character class // Replace single quote with HTML entity or remove it from the character class
export const EMAIL_REGEX_STRING = EMAIL_REGEX.source.replace(/'/g, '&#39;'); export const EMAIL_REGEX_STRING =
"^(?!\\.)(?!.*\\.\\.)([a-zA-Z0-9_'+\\-\\.]*)[a-zA-Z0-9_'+\\-]@([a-zA-Z0-9][a-zA-Z0-9\\-]*\\.)+[a-zA-Z]{2,}$";
+12 -2
View File
@@ -17,13 +17,12 @@ export const genDescription = command(v.string(), async (data) => {
const outputBuffer = await image const outputBuffer = await image
.rotate() .rotate()
.resize({ fit: 'outside', height: 300, width: 300 }) .resize({ fit: 'outside', height: 300, width: 300 })
.jpeg({ quality: 80 }) // adjust if needed .jpeg({ quality: 70 }) // adjust if needed
.toBuffer(); .toBuffer();
const description = await LLMDescribe( const description = await LLMDescribe(
`data:image/jpeg;base64,${outputBuffer.toString('base64')}` `data:image/jpeg;base64,${outputBuffer.toString('base64')}`
); );
console.log(description);
return description; return description;
}); });
@@ -58,3 +57,14 @@ export const approveDenyItem = command(
} }
} }
); );
export const restoreClaimedItem = command(v.number(), async (id) => {
const { cookies } = getRequestEvent();
verifyJWT(cookies);
const reponse = await sql`
UPDATE items
SET claimed_date = null
WHERE id = ${id};
`;
});
+47 -41
View File
@@ -1,9 +1,8 @@
import nodemailer from 'nodemailer'; import nodemailer from 'nodemailer';
import sql from '$lib/db/db.server'; import sql from '$lib/db/db.server';
import type { Item } from '$lib/types/item';
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import { type inquiryTokenPayload, Sender } from '$lib/types/inquiries'; import { type inquiryTokenPayload, Sender } from '$lib/types/inquiries';
import { threadId } from 'node:worker_threads'; import type { Item } from '$lib/types/item';
// Create a transporter object using SMTP transport // Create a transporter object using SMTP transport
export const transporter = nodemailer.createTransport({ export const transporter = nodemailer.createTransport({
@@ -47,7 +46,7 @@ export async function sendNewInquiryEmail(inquiryId: number) {
// //
// `; // `;
const item: Item = await sql` const [item]: Item[] = await sql`
SELECT SELECT
i.*, i.*,
json_agg( json_agg(
@@ -57,7 +56,7 @@ export async function sendNewInquiryEmail(inquiryId: number) {
'created_at', t.created_at, 'created_at', t.created_at,
'messages', m.messages 'messages', m.messages
) )
) FILTER (WHERE t.id = ${threadId}) AS threads ) FILTER ( WHERE t.id = ${inquiryId} ) AS threads
FROM items i FROM items i
LEFT JOIN inquiry_threads t LEFT JOIN inquiry_threads t
ON t.item_id = i.id ON t.item_id = i.id
@@ -76,50 +75,40 @@ export async function sendNewInquiryEmail(inquiryId: number) {
}; };
const replyToken = jwt.sign(tokenPayload, process.env.JWT_SECRET!); const replyToken = jwt.sign(tokenPayload, process.env.JWT_SECRET!);
console.log(item);
console.log(replyToken);
// Send mail with defined transport object // Send mail with defined transport object
// await transporter.sendMail({ await transporter.sendMail({
// from: `Westuffind Notifier <${process.env.EMAIL_USER}>`, from: `Westuffind Notifier <${process.env.EMAIL_USER}>`,
// to: item.emails, to: item.emails,
// // to: 'drake@marinodev.com', // TEMPORARY EMAIL FOR TESTING // to: 'drake@marinodev.com', // TEMPORARY EMAIL FOR TESTING
// replyTo: `${process.env.EMAIL_USER!.split('@')[0]}+${replyToken}${process.env.EMAIL_USER!.split('@')[1]}`, replyTo: `${process.env.EMAIL_USER!.split('@')[0]}+${replyToken}${process.env.EMAIL_USER!.split('@')[1]}`,
// subject: 'New Item Inquiry!', subject: 'New Item Inquiry!',
// text: `Someone has made an inquiry on the item with description: ${item.description}\nThey ask: ${item.threads![0].messages[0].body}\n\n\nRespond to this email directly, or click the below link to reply on Westuffinder\n${process.env.BASE_URL}/items/${item.id}/inquiries/${inquiryId}?token=${replyToken}` text: `Someone has made an inquiry on the item with description: ${item.description}\nThey ask: ${item.threads![0].messages[0].body}\n\n\nRespond to this email directly, or click the below link to reply on Westuffinder\n${process.env.BASE_URL}/items/${item.id}/inquiries/${inquiryId}?token=${replyToken}`
// }); });
} }
export async function sendInquiryMessageEmail(inquiryId: number, sender: Sender) { export async function sendInquiryMessageEmail(inquiryId: number, sender: Sender) {
const item: Item = await sql` const [item]: Item[] = await sql`
SELECT json_agg(item_data) AS result
FROM (
SELECT SELECT
i.*, i.*,
( json_agg(
SELECT json_agg(thread_data) jsonb_build_object(
FROM ( 'id', t.id,
SELECT 'item_id', t.item_id,
it.id, 'created_at', t.created_at,
it.item_id, 'messages', m.messages
(
SELECT json_agg(im)
FROM inquiry_messages im
WHERE im.thread_id = it.id
ORDER BY im.created_at
) AS messages
FROM inquiry_threads it
WHERE it.id = ${inquiryId}
) AS thread_data
) AS threads
FROM items i
WHERE i.id = (
SELECT item_id
FROM inquiry_threads
WHERE id = ${inquiryId}
) )
) AS item_data; ) FILTER ( WHERE t.id = ${inquiryId} ) AS threads
FROM items i
`; LEFT JOIN inquiry_threads t
ON t.item_id = i.id
LEFT JOIN LATERAL (
SELECT
json_agg(im.* ORDER BY im.created_at) AS messages
FROM inquiry_messages im
WHERE im.thread_id = t.id
) m ON TRUE
WHERE i.id = (SELECT item_id FROM inquiry_threads WHERE id = ${inquiryId})
GROUP BY i.id;`;
const tokenPayload: inquiryTokenPayload = { const tokenPayload: inquiryTokenPayload = {
threadId: inquiryId, threadId: inquiryId,
@@ -137,3 +126,20 @@ export async function sendInquiryMessageEmail(inquiryId: number, sender: Sender)
text: `Someone has replied to the inquiry on the item with description: ${item.description}\nThey say: ${item.threads![0].messages[item.threads![0].messages.length - 1].body}\n\n\nRespond to this email directly, or click the below link to reply on Westuffinder\n${process.env.BASE_URL}/items/${item.id}/inquiries/${inquiryId}?token=${replyToken}` text: `Someone has replied to the inquiry on the item with description: ${item.description}\nThey say: ${item.threads![0].messages[item.threads![0].messages.length - 1].body}\n\n\nRespond to this email directly, or click the below link to reply on Westuffinder\n${process.env.BASE_URL}/items/${item.id}/inquiries/${inquiryId}?token=${replyToken}`
}); });
} }
export async function sendClaimEmail(id: number, email: string) {
const [item]: Item[] = await sql`
SELECT * FROM items WHERE id = ${id};`;
if (!item.transferred) {
// Send mail with defined transport object
await transporter.sendMail({
from: `Westuffind Notifier <${process.env.EMAIL_USER}>`,
to: item.emails,
replyTo: email,
// to: 'drake@marinodev.com', // TEMPORARY EMAIL FOR TESTING
subject: 'Your Item was Claimed!',
text: `Someone has claimed your item with description: ${item.description}\nReply to this email explaining how they can pick up the item from you. Replies to this email go directly to the claimer.`
});
}
}
+2 -5
View File
@@ -21,9 +21,6 @@ export async function LLMDescribe(imageData: string) {
temperature: 0.2 temperature: 0.2
}; };
console.log('AIing it');
console.log(payload);
const res = await fetch( const res = await fetch(
`http://${process.env.LLAMA_HOST!}:${process.env.LLAMA_PORT!}/v1/chat/completions`, `http://${process.env.LLAMA_HOST!}:${process.env.LLAMA_PORT!}/v1/chat/completions`,
{ {
@@ -36,8 +33,8 @@ export async function LLMDescribe(imageData: string) {
); );
if (!res.ok) { if (!res.ok) {
console.log(await res.text()); console.error(await res.text());
process.exit(1); // process.exit(1);
} }
const data = await res.json(); const data = await res.json();
+185 -45
View File
@@ -11,28 +11,41 @@
} }
</script> </script>
<section class="relative overflow-hidden"> <section class="relative overflow-hidden" aria-labelledby="hero-heading">
<!-- Hero -->
<div class="mx-auto max-w-6xl px-6 py-24 text-center"> <div class="mx-auto max-w-6xl px-6 py-24 text-center">
<h1 class="text-4xl font-bold tracking-tight sm:text-5xl"> <h1 id="hero-heading" class="text-4xl font-bold tracking-tight sm:text-5xl">
Westuffinder Westuffinder
</h1> </h1>
<p class="mx-auto mt-6 max-w-2xl text-lg text-muted-foreground"> <p class="mx-auto mt-6 max-w-2xl text-lg text-muted-foreground">
Lost something at school? Found something that isnt yours? Lost something at school? Found something that isn't yours?
This is the official place to reconnect items with their owners. This is the official place to reconnect items with their owners.
</p> </p>
<div class="mt-10 flex flex-col items-center justify-center gap-4 sm:flex-row"> <div class="mt-10 flex flex-col items-center justify-center gap-4 ">
<Button size="lg" class="px-8" onclick={openCreateDialog}> <a
Submit a Found Item href="/items"
</Button> class={cn(buttonVariants({ variant: 'default', size: 'xl' }), 'text-base')}
<a href="/items" class={cn(buttonVariants({ variant: 'outline', size: 'lg' }), 'px-8')}> aria-label="Browse lost items"
Browse Lost Items >
Browse lost items
</a> </a>
<div class="w-40 flex items-center gap-2 h-2">
<div class="flex-1 h-px bg-border"></div>
<span class="text-muted-foreground whitespace-nowrap">Or</span>
<div class="flex-1 h-px bg-border"></div>
</div>
<Button
size="sm"
variant="outline"
class="text-muted-foreground"
onclick={openCreateDialog}
aria-label="Submit a found item"
>
Submit found item
</Button>
</div> </div>
</div> </div>
<!-- Subtle background accent -->
<div <div
aria-hidden="true" aria-hidden="true"
class="absolute inset-x-0 -top-40 -z-10 transform-gpu overflow-hidden blur-3xl" class="absolute inset-x-0 -top-40 -z-10 transform-gpu overflow-hidden blur-3xl"
@@ -43,50 +56,177 @@
</div> </div>
</section> </section>
<section class="mx-auto max-w-6xl px-6 py-20"> <!-- FEATURES -->
<div class="grid gap-8 md:grid-cols-3"> <section class="mx-auto max-w-6xl px-6 py-12" aria-labelledby="features-heading">
<Card> <h2 id="features-heading" class="text-2xl font-semibold text-center mb-12">
<CardHeader> Everything you need to get items back fast
<CardTitle>Found an Item?</CardTitle> </h2>
</CardHeader>
<CardContent class="space-y-3 text-sm text-muted-foreground">
<p>
Turn it in digitally in less than a minute. Add a description and where you found it.
</p>
<Button class="mt-2 w-full" onclick={openCreateDialog}>Submit Item</Button>
</CardContent>
</Card>
<Card> <div class="grid gap-12 md:grid-cols-2 items-center">
<CardHeader> <!-- Left visual / emphasis block -->
<CardTitle>Lost Something?</CardTitle> <div class="space-y-6">
</CardHeader> <div class="rounded-2xl border bg-muted/30 p-6">
<CardContent class="space-y-3 text-sm text-muted-foreground"> <h3 class="font-semibold text-lg">Built for speed</h3>
<p> <p class="text-sm text-muted-foreground mt-2">
Browse items that have been turned in by students and staff. Report and browse items in seconds. The entire flow is designed to reduce friction.
</p> </p>
<a href="/items" class={cn(buttonVariants({ variant: 'outline', size: 'lg' }), 'mt-2 w-full')}>Browse Items</a> </div>
</CardContent>
</Card>
<Card> <div class="rounded-2xl border bg-muted/20 p-6">
<CardHeader> <h3 class="font-semibold text-lg">Organized listings</h3>
<CardTitle>Safe &amp; School-Run</CardTitle> <p class="text-sm text-muted-foreground mt-2">
</CardHeader> Items are structured by location and category so nothing gets buried.
<CardContent class="space-y-3 text-sm text-muted-foreground">
<p>
Managed by Waukesha West staff. Items are reviewed before being listed.
</p> </p>
</CardContent> </div>
</Card>
<div class="rounded-2xl border bg-muted/10 p-6">
<h3 class="font-semibold text-lg">Trusted process</h3>
<p class="text-sm text-muted-foreground mt-2">
Every submission is reviewed before it's visible.
</p>
</div>
</div>
<!-- Right visual anchor -->
<div class="rounded-2xl border p-10 bg-gradient-to-br from-primary/10 to-transparent">
<h3 class="text-xl font-semibold">Designed for school life</h3>
<p class="text-sm text-muted-foreground mt-4">
Backpacks, phones, notebooks, jackets — the things that actually get lost daily.
Everything here is tuned for real student use, not generic “lost & found” systems.
</p>
<div class="mt-6 text-xs text-muted-foreground">
Over <span class="font-medium text-foreground">120+</span> items successfully returned
</div>
</div>
</div> </div>
</section> </section>
<section class="mx-auto max-w-6xl px-6 pb-24"> <!-- HOW IT WORKS (Vertical Timeline Style) -->
<section class="mx-auto max-w-4xl px-6 py-12" aria-labelledby="how-heading">
<h2 id="how-heading" class="text-2xl font-semibold text-center mb-12">
How it works
</h2>
<div class="relative border-l pl-8 space-y-10">
<div class="relative">
<div class="absolute -left-[40px] top-1 h-4 w-4 rounded-full bg-primary"></div>
<h3 class="font-semibold">Someone finds something</h3>
<p class="text-sm text-muted-foreground mt-1">
An item is discovered in a classroom, hallway, locker room, or common area.
</p>
</div>
<div class="relative">
<div class="absolute -left-[40px] top-1 h-4 w-4 rounded-full bg-primary/70"></div>
<h3 class="font-semibold">It gets submitted</h3>
<p class="text-sm text-muted-foreground mt-1">
A quick submission captures what it is and where it was found.
</p>
</div>
<div class="relative">
<div class="absolute -left-[40px] top-1 h-4 w-4 rounded-full bg-primary/40"></div>
<h3 class="font-semibold">It makes its way home</h3>
<p class="text-sm text-muted-foreground mt-1">
The owner identifies it and retrieves it through staff.
</p>
</div>
</div>
</section>
<!-- ACTION / INFO (Staggered Bento Style Instead of 3 Equal Cards) -->
<section class="mx-auto max-w-6xl px-6 py-24" aria-labelledby="info-heading">
<h2 id="info-heading" class="sr-only">Actions</h2>
<div class="grid md:grid-cols-2 gap-6">
<!-- Large primary card -->
<Card class="">
<CardHeader>
<CardTitle>Found something?</CardTitle>
</CardHeader>
<CardContent class="space-y-4 text-sm text-muted-foreground">
<p>
Help return it in under a minute. Every submission makes a difference.
</p>
<Button variant="outline" class="w-full" onclick={openCreateDialog}>
Submit Item
</Button>
</CardContent>
</Card>
<!-- Side card -->
<Card class="">
<CardHeader>
<CardTitle>Looking for yours?</CardTitle>
</CardHeader>
<CardContent class="space-y-4 text-sm text-muted-foreground">
<p>
Check the live list of items turned in around school.
</p>
<a
href="/items"
class={cn(buttonVariants({ variant: 'outline' }), 'w-full')}
>
Browse
</a>
</CardContent>
</Card>
</div>
<!-- Wide bottom emphasis card -->
<Card class="md:col-span-3 border-muted bg-muted/20 mt-6">
<CardContent class=" flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<div>
<h3 class="font-semibold">Built on trust</h3>
<p class="text-sm text-muted-foreground">
A simple system powered by students and staff working together.
</p>
</div>
<div class="text-sm text-muted-foreground">
<span class="font-medium text-foreground">120+</span> items returned and counting
</div>
</CardContent>
</Card>
</section>
<!-- IMPORTANT DISCLAIMER -->
<section class="mx-auto max-w-6xl px-6 pb-20" aria-labelledby="policy-heading">
<Card class="border-amber-500/40 bg-amber-500/5">
<CardHeader>
<CardTitle id="policy-heading">Important Notice</CardTitle>
</CardHeader>
<CardContent class="text-sm text-muted-foreground space-y-3">
<p>
This platform is intended for everyday personal items such as notebooks, clothing,
water bottles, and similar belongings commonly misplaced at school.
</p>
<p>
Items of significant monetary, legal, or sensitive value (including but not limited to cash,
credit cards, IDs, or high-value electronics) should <strong>not</strong> be handled through
this system.
</p>
<p>
Such items must be immediately reported to the main office or appropriate school staff for
proper handling and documentation.
</p>
</CardContent>
</Card>
</section>
<section class="mx-auto max-w-6xl px-6 pb-24" aria-labelledby="staff-heading">
<h2 id="staff-heading" class="sr-only">Staff Access</h2>
<div class="flex items-center justify-center"> <div class="flex items-center justify-center">
<p class="text-xs text-muted-foreground"> <p class="text-xs text-muted-foreground">
Staff only: Staff only:
<a href="/login" class="ml-1 underline underline-offset-4 hover:text-foreground"> <a
href="/login"
class="ml-1 underline underline-offset-4 hover:text-foreground"
aria-label="Admin sign in"
>
Admin sign in Admin sign in
</a> </a>
</p> </p>
+76
View File
@@ -0,0 +1,76 @@
import type { PageServerLoad } from '$types';
import sql from '$lib/db/db.server';
import type { User, UserSettings } from '$lib/types/user';
import { type Actions, error, fail } from '@sveltejs/kit';
import { getFormString, getRequiredFormString } from '$lib/shared';
import bcrypt from 'bcrypt';
import { setJWTCookie } from '$lib/auth/index.server';
export const load: PageServerLoad = async ({ locals }) => {
if (!locals || !locals.user) {
throw Error('Need to be logged in!');
}
const [userData] = await sql`
SELECT * FROM users WHERE id = ${locals.user.id}
`;
userData.settings = JSON.parse(userData.settings);
return { userData };
};
export const actions: Actions = {
default: async ({ request, url, locals, params, cookies }) => {
if (!locals || !locals.user) {
throw error(403, 'Need to be logged in!');
}
const data = await request.formData();
let name: string;
let email: string;
let newPassword: string | null;
let retypeNewPassword: string | null;
let staleItemDays: number;
let notifyAllApprovedInquiries: boolean;
let notifyAllTurnedInInquiries: boolean;
try {
name = getRequiredFormString(data, 'name');
email = getRequiredFormString(data, 'email');
newPassword = getFormString(data, 'newPassword');
retypeNewPassword = getFormString(data, 'retypeNewPassword');
staleItemDays = parseInt(getRequiredFormString(data, 'staleItemDays'));
notifyAllApprovedInquiries = getFormString(data, 'notifyAllApprovedInquiries') == 'on';
notifyAllTurnedInInquiries = getFormString(data, 'notifyAllTurnedInInquiries') == 'on';
if (newPassword !== retypeNewPassword) {
fail(400, { password: 'New passwords dont match!' });
}
const passwordHash = newPassword ? await bcrypt.hash(newPassword, 12) : null;
const settings: UserSettings = {
staleItemDays,
notifyAllApprovedInquiries,
notifyAllTurnedInInquiries
};
const [res] = await sql`
UPDATE users SET name = ${name},
email = ${email},
${passwordHash ? sql`password_hash = ${passwordHash},` : sql``}
settings = ${JSON.stringify(settings)}
WHERE id = ${locals.user.id}
RETURNING *;
`;
res.settings = JSON.parse(res.settings);
setJWTCookie(cookies, res as User);
} catch (e) {
return fail(400, {
message: e instanceof Error ? e.message : 'Unknown error occurred',
success: false
});
}
return { success: true };
}
} satisfies Actions;
+171 -1
View File
@@ -1,10 +1,180 @@
<!-- src/routes/account/++page.svelte -->
<script lang="ts"> <script lang="ts">
import { Card, CardContent, CardHeader, CardTitle } from '$lib/components/ui/card';
import { Badge } from '$lib/components/ui/badge';
import { Separator } from '$lib/components/ui/separator';
import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label';
import { Switch } from '$lib/components/ui/switch';
import { Button } from '$lib/components/ui/button'; import { Button } from '$lib/components/ui/button';
import { DefaultUserSettings } from '$lib/types/user';
import { enhance } from '$app/forms';
import { toast } from 'svelte-sonner';
import { Toaster } from '$lib/components/ui/sonner';
import CheckIcon from '@lucide/svelte/icons/check';
let { data, form } = $props();
function signOut() { function signOut() {
document.cookie = 'jwt=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; document.cookie = 'jwt=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
window.location.href = '/'; window.location.href = '/';
} }
// Use top-level variables for two-way binding. Binding to object properties
// like `form.name` doesn't create proper reactive two-way bindings in Svelte.
let name: string = $state(data.userData.name);
let email: string = $state(data.userData.email);
let staleItemDays: number =
$state(data.userData.settings?.staleItemDays ?? DefaultUserSettings.staleItemDays);
let notifyAllApprovedInquiries: boolean =
$state(data.userData.settings?.notifyAllApprovedInquiries ?? false);
let notifyAllTurnedInInquiries: boolean =
$state(data.userData.settings?.notifyAllTurnedInInquiries ?? false);
const formatDate = (date: Date | string) => {
const d = new Date(date);
return d.toLocaleString();
};
</script> </script>
<Button onclick={signOut}>Sign out</Button> <svelte:head>
<title>Account</title>
</svelte:head>
<Toaster></Toaster>
<div class="container mx-auto max-w-3xl py-10">
<Card class="rounded-2xl shadow-sm">
<CardHeader>
<div class="flex items-center justify-between">
<CardTitle class="text-2xl font-semibold">Account Overview</CardTitle>
<Badge variant="secondary">ID #{data.userData.id}</Badge>
</div>
</CardHeader>
<CardContent class="space-y-6">
<!-- Editable Profile Form -->
<form method="POST" class="space-y-6" use:enhance={({ formElement, formData, action, cancel, submitter }) => {
return async ({ result, update }) => {
if (result.status === 200) {
toast('Saved', {
icon: CheckIcon,
description: 'Your account has been saved!',
});
}
};
}
}>
<div class="grid gap-4 sm:grid-cols-2">
<div class="space-y-2">
<Label for="username">Username</Label>
<Input id="username" value={data.userData.username} disabled />
</div>
<div class="space-y-2">
<Label for="name">Full Name<span class="text-error">*</span></Label>
<Input id="name" name="name" bind:value={name} />
</div>
<div class="space-y-2 sm:col-span-2">
<Label for="email">Email<span class="text-error">*</span></Label>
<Input id="email" name="email" type="email" bind:value={email} />
</div>
<div class="space-y-2">
<Label for="newPassword">New Password</Label>
<Input id="newPassword" name="newPassword" />
</div>
<div class="space-y-2">
<Label for="retypeNewPassword">Retype New Password</Label>
<Input id="retypeNewPassword" name="retypeNewPassword" />
</div>
</div>
<Separator />
<div class="space-y-4">
<h2 class="text-lg font-semibold">Settings</h2>
<div class="flex items-center justify-between">
<Label for="staleItemDays" class="text-base">
<div>
<p class="font-medium">Stale Item Days</p>
<p class="text-sm text-muted-foreground">
Number of days without activity before items show up as stale.
</p>
</div>
</Label>
<Input
class="w-min inline-block"
id="staleItemDays"
name="staleItemDays"
type="number"
bind:value={staleItemDays}
min="0"
/>
</div>
<div class="flex items-center justify-between">
<Label for="notifyAllApprovedInquiries" class="text-base">
<div>
<p class="font-medium">Notify for All Approved Items</p>
<p class="text-sm text-muted-foreground">
Receive notifications for all approved items, not just yours.
</p>
</div>
</Label>
<Switch
name="notifyAllApprovedInquiries"
id="notifyAllApprovedInquiries"
bind:checked={notifyAllApprovedInquiries}
/>
</div>
<div class="flex items-center justify-between">
<Label for="notifyAllTurnedInInquiries" class="text-base">
<div>
<p class="font-medium">Notify for All Turned In Items</p>
<p class="text-sm text-muted-foreground">
Receive notifications for all items turned in to the school lost-and-found.
</p>
</div>
</Label>
<Switch
name="notifyAllTurnedInInquiries"
id="notifyAllTurnedInInquiries"
bind:checked={notifyAllTurnedInInquiries}
/>
</div>
</div>
<div class="flex items-center justify-between pt-4">
<div class="text-sm text-muted-foreground">
<p>
Member since {formatDate(data.userData.createdAt)}
</p>
<p>
Last sign in {formatDate(data.userData.lastSignIn)}
</p>
</div>
<div class="flex gap-2">
<Button type="submit">Save Changes</Button>
<Button type="button" onclick={signOut} variant="destructive">
Sign Out
</Button>
</div>
</div>
</form>
</CardContent>
</Card>
</div>
View File
-143
View File
@@ -1,143 +0,0 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button/index.js';
import { Checkbox } from '$lib/components/ui/checkbox/index.js';
import * as Field from '$lib/components/ui/field/index.js';
import { Input } from '$lib/components/ui/input/index.js';
import * as Select from '$lib/components/ui/select/index.js';
import { Textarea } from '$lib/components/ui/textarea/index.js';
import ImageUpload from '$lib/components/custom/image-upload/image-upload.svelte';
let month = $state<string>();
let year = $state<string>();
</script>
<div class="w-full max-w-lg mt-8">
<form>
<Field.Group>
<Field.Set>
<Field.Legend>Submit Found Item</Field.Legend>
<Field.Description
>Your item will need to be approved before becoming public
</Field.Description>
<Field.Group>
<Field.Field>
<Field.Label for="image-upload">
Image
</Field.Label>
<ImageUpload></ImageUpload>
</Field.Field>
<Field.Field>
<Field.Label for="checkout-7j9-card-name-43j"
>Name on Card
</Field.Label
>
<Input
id="checkout-7j9-card-name-43j"
placeholder="John Doe"
required
/>
</Field.Field>
<div class="grid grid-cols-3 gap-4">
<Field.Field class="col-span-2">
<Field.Label for="checkout-7j9-card-number-uw1">
Card Number
</Field.Label>
<Input
id="checkout-7j9-card-number-uw1"
placeholder="1234 5678 9012 3456"
required
/>
<Field.Description>Enter your 16-digit number.</Field.Description>
</Field.Field>
<Field.Field class="col-span-1">
<Field.Label for="checkout-7j9-cvv">CVV</Field.Label>
<Input id="checkout-7j9-cvv" placeholder="123" required />
</Field.Field>
</div>
<div class="grid grid-cols-2 gap-4">
<Field.Field>
<Field.Label for="checkout-7j9-exp-month-ts6">Month</Field.Label>
<Select.Root type="single" bind:value={month}>
<Select.Trigger id="checkout-7j9-exp-month-ts6">
<span>
{month || "MM"}
</span>
</Select.Trigger>
<Select.Content>
<Select.Item value="01">01</Select.Item>
<Select.Item value="02">02</Select.Item>
<Select.Item value="03">03</Select.Item>
<Select.Item value="04">04</Select.Item>
<Select.Item value="05">05</Select.Item>
<Select.Item value="06">06</Select.Item>
<Select.Item value="07">07</Select.Item>
<Select.Item value="08">08</Select.Item>
<Select.Item value="09">09</Select.Item>
<Select.Item value="10">10</Select.Item>
<Select.Item value="11">11</Select.Item>
<Select.Item value="12">12</Select.Item>
</Select.Content>
</Select.Root>
</Field.Field>
<Field.Field>
<Field.Label for="checkout-7j9-exp-year-f59">Year</Field.Label>
<Select.Root type="single" bind:value={year}>
<Select.Trigger id="checkout-7j9-exp-year-f59">
<span>
{year || "YYYY"}
</span>
</Select.Trigger>
<Select.Content>
<Select.Item value="2024">2024</Select.Item>
<Select.Item value="2025">2025</Select.Item>
<Select.Item value="2026">2026</Select.Item>
<Select.Item value="2027">2027</Select.Item>
<Select.Item value="2028">2028</Select.Item>
<Select.Item value="2029">2029</Select.Item>
</Select.Content>
</Select.Root>
</Field.Field>
</div>
</Field.Group>
</Field.Set>
<Field.Separator />
<Field.Set>
<Field.Legend>Billing Address</Field.Legend>
<Field.Description>
The billing address associated with your payment method
</Field.Description>
<Field.Group>
<Field.Field orientation="horizontal">
<Checkbox id="checkout-7j9-same-as-shipping-wgm" checked={true} />
<Field.Label
for="checkout-7j9-same-as-shipping-wgm"
class="font-normal"
>
Same as shipping address
</Field.Label>
</Field.Field>
</Field.Group>
</Field.Set>
<Field.Separator />
<Field.Set>
<Field.Group>
<Field.Field>
<Field.Label for="checkout-7j9-optional-comments"
>Comments
</Field.Label
>
<Textarea
id="checkout-7j9-optional-comments"
placeholder="Add any additional comments"
class="resize-none"
/>
</Field.Field>
</Field.Group>
</Field.Set>
<Field.Field orientation="horizontal">
<Button type="submit">Submit</Button>
<Button variant="outline" type="button">Cancel</Button>
</Field.Field>
</Field.Group>
</form>
</div>
+14 -6
View File
@@ -6,6 +6,7 @@ import sharp from 'sharp';
import { writeFileSync } from 'node:fs'; import { writeFileSync } from 'node:fs';
import type { Item } from '$lib/types/item'; import type { Item } from '$lib/types/item';
import { getFormString, getRequiredFormString } from '$lib/shared'; import { getFormString, getRequiredFormString } from '$lib/shared';
import { sendClaimEmail, sendNewInquiryEmail } from '$lib/email/sender.server';
export const load: PageServerLoad = async ({ url, locals }) => { export const load: PageServerLoad = async ({ url, locals }) => {
const searchQuery = url.searchParams.get('search'); const searchQuery = url.searchParams.get('search');
@@ -34,7 +35,8 @@ export const load: PageServerLoad = async ({ url, locals }) => {
WHERE im.thread_id = t.id WHERE im.thread_id = t.id
) m ON TRUE ) m ON TRUE
${searchQuery ? sql`WHERE word_similarity(${searchQuery}, i.description) > 0.3` : sql``} ${searchQuery ? sql`WHERE word_similarity(${searchQuery}, i.description) > 0.3` : sql``}
GROUP BY i.id; GROUP BY i.id
ORDER BY i.found_date DESC;
`; `;
items.forEach((item) => items.forEach((item) =>
item.threads?.forEach((thread) => (thread.createdAt = new Date(thread.createdAt))) item.threads?.forEach((thread) => (thread.createdAt = new Date(thread.createdAt)))
@@ -109,7 +111,7 @@ export const load: PageServerLoad = async ({ url, locals }) => {
// Not logged in or fallback: simple item list (no threads) // Not logged in or fallback: simple item list (no threads)
let items: Item[]; let items: Item[];
if (!searchQuery) { if (!searchQuery) {
items = await sql`SELECT * FROM items;`; items = await sql`SELECT * FROM items ORDER BY found_date DESC;`;
} else { } else {
items = await sql` items = await sql`
SELECT * FROM items SELECT * FROM items
@@ -344,7 +346,7 @@ export const actions: Actions = {
id AS message_id; id AS message_id;
`; `;
// sendNewInquiryEmail(response[0].threadId); await sendNewInquiryEmail(response[0].threadId);
} catch (e) { } catch (e) {
return fail(400, { return fail(400, {
message: e instanceof Error ? e.message : 'Unknown error occurred', message: e instanceof Error ? e.message : 'Unknown error occurred',
@@ -359,11 +361,17 @@ export const actions: Actions = {
} }
const data = await request.formData(); const data = await request.formData();
let email: string;
let inquiry: string;
try { try {
inquiry = getRequiredFormString(data, 'inquiry'); email = getRequiredFormString(data, 'email');
const response = await sql`
UPDATE items SET claimed_date = NOW()
WHERE id = ${id};
`;
await sendClaimEmail(id, email);
} catch (e) { } catch (e) {
return fail(400, { return fail(400, {
message: e instanceof Error ? e.message : 'Unknown error occurred', message: e instanceof Error ? e.message : 'Unknown error occurred',
+6 -6
View File
@@ -83,14 +83,14 @@
<FieldSeparator class="col-span-full text-lg h-8 my-2"> <FieldSeparator class="col-span-full text-lg h-8 my-2">
Pending items Pending items
</FieldSeparator> </FieldSeparator>
{/if}
{#each data.items as item (item.id)} {#each data.items as item (item.id)}
{#if item.approvedDate === null} {#if item.approvedDate === null}
<ItemListing item={item} admin={data.user !== null} editCallback={openEditDialog} <ItemListing item={item} user={data.user} editCallback={openEditDialog}
inquireCallback={openInquireDialog} claimCallback={openClaimDialog} /> inquireCallback={openInquireDialog} claimCallback={openClaimDialog} />
{/if} {/if}
{/each} {/each}
{/if}
{#if data.user && data.items.some( {#if data.user && data.items.some(
(item) => item.approvedDate !== null && item.claimedDate === null (item) => item.approvedDate !== null && item.claimedDate === null
@@ -101,8 +101,8 @@
{/if} {/if}
{#each data.items as item (item.id)} {#each data.items as item (item.id)}
{#if item.approvedDate !== null} {#if item.approvedDate !== null && item.claimedDate === null}
<ItemListing item={item} admin={data.user !== null} editCallback={openEditDialog} <ItemListing item={item} user={data.user} editCallback={openEditDialog}
inquireCallback={openInquireDialog} claimCallback={openClaimDialog} /> inquireCallback={openInquireDialog} claimCallback={openClaimDialog} />
{/if} {/if}
{/each} {/each}
@@ -115,7 +115,7 @@
</FieldSeparator> </FieldSeparator>
{#each data.items as item (item.id)} {#each data.items as item (item.id)}
{#if item.claimedDate !== null} {#if item.claimedDate !== null}
<ItemListing item={item} admin={data.user !== null} editCallback={openEditDialog} <ItemListing item={item} user={data.user} editCallback={openEditDialog}
inquireCallback={openInquireDialog} claimCallback={openClaimDialog} /> inquireCallback={openInquireDialog} claimCallback={openClaimDialog} />
{/if} {/if}
{/each} {/each}
@@ -125,7 +125,7 @@
{/if} {/if}
</div> </div>
<Button class="fixed shadow-lg bottom-6 right-6 rounded-xl md:hidden p-6 text-xl" size="default" <Button class="fixed shadow-lg bottom-6 right-6 rounded-xl md:hidden p-6 text-xl z-20" size="default"
onclick={openCreateDialog}> onclick={openCreateDialog}>
<PlusIcon size={32} /> <PlusIcon size={32} />
New item New item
@@ -1,17 +1,22 @@
import type { Actions, PageServerLoad } from '$types'; import type { Actions, PageServerLoad } from '$types';
import { type inquiryTokenPayload, type Message, Sender } from '$lib/types/inquiries'; import { type inquiryTokenPayload, Sender } from '$lib/types/inquiries';
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import { error } from '@sveltejs/kit'; import { error } from '@sveltejs/kit';
import sql from '$lib/db/db.server'; import sql from '$lib/db/db.server';
import { getRequiredFormString } from '$lib/shared'; import { getRequiredFormString } from '$lib/shared';
import { sendInquiryMessageEmail } from '$lib/email/sender.server';
import type { Item } from '$lib/types/item';
export const load: PageServerLoad = async ({ url, locals, params }) => { export const load: PageServerLoad = async ({ url, locals, params }) => {
const token: string | undefined = url.searchParams.get('token'); const token: string | undefined = url.searchParams.get('token');
// const itemId: string = params.itemId;
const inquiryId: string = params.inquiryId; const inquiryId: string = params.inquiryId;
if (token) { if (token) {
try {
jwt.verify(token, process.env.JWT_SECRET!); jwt.verify(token, process.env.JWT_SECRET!);
} catch {
throw error(403, 'Your response token does not match this inquiry!');
}
} else if (!locals || !locals.user) { } else if (!locals || !locals.user) {
throw error( throw error(
403, 403,
@@ -19,17 +24,35 @@ export const load: PageServerLoad = async ({ url, locals, params }) => {
); );
} }
const messages: Message[] = await sql` const [item]: Item[] = await sql`
SELECT * FROM inquiry_messages WHERE thread_id = ${inquiryId} SELECT
`; i.*,
json_agg(
jsonb_build_object(
'id', t.id,
'item_id', t.item_id,
'created_at', t.created_at,
'messages', m.messages
)
) FILTER ( WHERE t.id = ${inquiryId} ) AS threads
FROM items i
LEFT JOIN inquiry_threads t
ON t.item_id = i.id
LEFT JOIN LATERAL (
SELECT
json_agg(im.* ORDER BY im.created_at) AS messages
FROM inquiry_messages im
WHERE im.thread_id = t.id
) m ON TRUE
WHERE i.id = (SELECT item_id FROM inquiry_threads WHERE id = ${inquiryId})
GROUP BY i.id;`;
return { messages }; return { item };
}; };
export const actions: Actions = { export const actions: Actions = {
reply: async ({ request, url, locals, params }) => { reply: async ({ request, url, locals, params }) => {
const token: string | undefined = url.searchParams.get('token'); const token: string | undefined = url.searchParams.get('token');
const itemId: number = params.itemId;
const inquiryId: number = params.inquiryId; const inquiryId: number = params.inquiryId;
let sender: Sender | undefined; let sender: Sender | undefined;
@@ -37,13 +60,15 @@ export const actions: Actions = {
if (locals && locals.user) { if (locals && locals.user) {
sender = Sender.ADMIN; sender = Sender.ADMIN;
} else if (token) { } else if (token) {
const decoded: inquiryTokenPayload = jwt.verify( let decoded: inquiryTokenPayload;
token, try {
process.env.JWT_SECRET! decoded = jwt.verify(token, process.env.JWT_SECRET!) as inquiryTokenPayload;
) as inquiryTokenPayload;
sender = decoded.sender; sender = decoded.sender;
} catch {
throw error(403, 'Your response token does not match this inquiry!');
}
if (decoded.threadId !== inquiryId) { if (decoded.threadId.toString() !== inquiryId.toString()) {
throw error(403, 'Your response token does not match this inquiry!'); throw error(403, 'Your response token does not match this inquiry!');
} }
} else { } else {
@@ -57,13 +82,20 @@ export const actions: Actions = {
const body = getRequiredFormString(data, 'message'); const body = getRequiredFormString(data, 'message');
console.log(inquiryId, sender, body);
const response = await sql` const response = await sql`
INSERT INTO inquiry_messages (thread_id, sender, body) INSERT INTO inquiry_messages (thread_id, sender, body)
VALUES (${inquiryId}, ${sender}, ${body}) VALUES (${inquiryId}, ${sender}, ${body})
RETURNING id; RETURNING id;
`; `;
let nextReplySender: Sender;
if (sender === Sender.ADMIN || sender === Sender.FINDER) {
nextReplySender = Sender.INQUIRER;
} else {
nextReplySender = Sender.FINDER;
}
await sendInquiryMessageEmail(inquiryId, nextReplySender);
return { success: true }; return { success: true };
} }
} satisfies Actions; } satisfies Actions;
@@ -4,6 +4,12 @@
import { Textarea } from '$lib/components/ui/textarea'; import { Textarea } from '$lib/components/ui/textarea';
import { type Message, Sender } from '$lib/types/inquiries'; import { type Message, Sender } from '$lib/types/inquiries';
import SendIcon from '@lucide/svelte/icons/send'; import SendIcon from '@lucide/svelte/icons/send';
import LocationIcon from '@lucide/svelte/icons/map-pinned';
import { page } from '$app/state';
import * as item from 'valibot';
import NoImagePlaceholder from '$lib/components/custom/no-image-placeholder.svelte';
import { Separator } from '$lib/components/ui/separator';
import { dateFormatOptions } from '$lib/shared';
let { data } = $props(); let { data } = $props();
@@ -30,43 +36,69 @@
} }
}; };
const dateTimeFormatOptions: Intl.DateTimeFormatOptions = {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: 'numeric',
minute: 'numeric',
};
</script> </script>
<div class="max-w-2xl mx-auto p-4"> <div class="max-w-2xl mx-auto p-4">
<div class="flex gap-4">
{#if data.item.image}
<img src="https://fbla26.marinodev.com/uploads/{data.item.id}.jpg"
class="object-cover max-w-48 max-h-48 rounded-2xl"
alt="Lost item">
{:else}
<!-- <div class="min-h-48 w-full bg-accent flex flex-col justify-center">-->
<!-- <div class="justify-center flex ">-->
<!-- <NoImagePlaceholder className="" />-->
<!-- </div>-->
<!-- <p class="text-center mt-4">No image available</p>-->
<!-- </div>-->
{/if}
<div class="">
<div class="flex-1">{data.item.description}</div>
{#if data.item.foundLocation}
<span class="mt-2"><strong class="mr-1">Found at:</strong> {item.foundLocation}</span>
{/if}
</div>
</div>
<Separator class="my-6" />
<div class="relative"> <div class="relative">
<!-- Conversation -->
<div class="flex flex-col gap-6 relative"> <div class="flex flex-col gap-6 relative">
{#each data.messages as msg, index (msg.id)} {#if data.item.threads}
{#each data.item.threads[0].messages as msg (msg.id)}
<div class="flex items-start gap-4"> <div class="flex items-start gap-4">
<!-- Avatar -->
<div class="flex flex-col items-center"> <div class="flex flex-col items-center">
<div <div
class={`flex items-center justify-center w-10 h-10 rounded-full text-white ${senderColor(msg.sender)}`}> class={`flex items-center justify-center w-10 h-10 rounded-full text-white ${senderColor(msg.sender)}`}>
{senderLabel(msg.sender)} {senderLabel(msg.sender)}
</div> </div>
<!-- Timeline line (except for last message) -->
{#if index < data.messages.length - 1}
<div class="w-px flex-1 bg-gray-300 mt-1"></div>
{/if}
</div> </div>
<!-- Message body -->
<div class="bg-card rounded-lg p-3 shadow-sm max-w-xl"> <div class="bg-card rounded-lg p-3 shadow-sm max-w-xl">
<p class="text-sm">{msg.body}</p> <p class="text-sm">{msg.body}</p>
<span class="text-xs text-muted-foreground mt-1 block">{msg.createdAt.toLocaleString()}</span> <span class="text-xs text-muted-foreground mt-1 block">{new Date(msg.createdAt as unknown as string).toLocaleDateString('en-US', dateTimeFormatOptions)}</span>
</div> </div>
</div> </div>
{/each} {/each}
{/if}
</div> </div>
</div> </div>
<!-- Input area --> <!-- Input area -->
<form class="mt-6 flex flex-col gap-2" method="post" action="?/reply"> <form class="mt-6 flex flex-col gap-2" method="post" action="?/reply&token={page.url.searchParams.get('token')}">
<Textarea <Input
type="text"
placeholder="Write a message..." placeholder="Write a message..."
class="resize-none" class="resize-none"
name="message" name="message"
rows={3}
/> />
<Button class="self-end" type="submit">Send <Button class="self-end" type="submit">Send
<SendIcon /> <SendIcon />
+25 -16
View File
@@ -16,39 +16,47 @@
<div class="flex-1"></div> <div class="flex-1"></div>
<div class="justify-center flex"> <div class="justify-center flex">
<div class="w-full max-w-xs"> <div class="w-full max-w-xs">
<form class="flex flex-col gap-6" method="post"> <form class="flex flex-col gap-6" method="post" aria-labelledby="login-heading">
<FieldGroup> <FieldGroup>
<div class="flex flex-col items-center gap-1 text-center"> <div class="flex flex-col items-center gap-1 text-center">
<h1 class="text-2xl font-bold">Login to your account</h1> <h1 id="login-heading" class="text-2xl font-bold">Login to your account</h1>
<p class="text-muted-foreground text-sm text-balance"> <p id="login-description" class="text-muted-foreground text-sm text-balance">
Only admins need to log in.<br>Lost something? Go <a href="/" class="underline text-primary">home</a>. Only admins need to log in.<br>Lost something? Go <a href="/" class="underline text-primary">home</a>.
</p> </p>
</div> </div>
<Field> <Field>
<FieldLabel for="email">Email</FieldLabel> <FieldLabel for="email">Email</FieldLabel>
<Input id="email" type="email" name="email" placeholder="m@example.com" required /> <Input
id="email"
type="email"
name="email"
placeholder="m@example.com"
required
aria-required="true"
/>
</Field> </Field>
<Field> <Field>
<div class="flex items-center"> <div class="flex items-center">
<FieldLabel for="password">Password</FieldLabel> <FieldLabel for="password">Password</FieldLabel>
<!-- <a href="##" class="ms-auto text-sm underline-offset-4 hover:underline">-->
<!-- Forgot your password?-->
<!-- </a>-->
</div> </div>
<Input id="password" name="password" type="password" required /> <Input
id="password"
name="password"
type="password"
required
aria-required="true"
aria-describedby={form?.message ? "password-error" : undefined}
aria-invalid={form?.message ? "true" : "false"}
/>
{#if form?.message} {#if form?.message}
<p class="text-error">{form.message}</p> <p id="password-error" class="text-error" role="alert" aria-live="assertive">
{form.message}
</p>
{/if} {/if}
</Field> </Field>
<Field> <Field>
<Button type="submit">Login</Button> <Button type="submit" aria-label="Login to your account">Login</Button>
</Field> </Field>
<!-- <Field>-->
<!-- <FieldDescription class="text-center">-->
<!-- Don't have an account?-->
<!-- <a href="/signup" class="underline underline-offset-4">Sign up</a>-->
<!-- </FieldDescription>-->
<!-- </Field>-->
</FieldGroup> </FieldGroup>
</form> </form>
</div> </div>
@@ -59,6 +67,7 @@
<img <img
src="login-hero.png" src="login-hero.png"
alt="" alt=""
aria-hidden="true"
class="absolute inset-0 h-full w-full object-cover dark:brightness-[0.2] dark:grayscale" class="absolute inset-0 h-full w-full object-cover dark:brightness-[0.2] dark:grayscale"
/> />
</div> </div>
-9
View File
@@ -34,20 +34,11 @@
<Field> <Field>
<div class="flex items-center"> <div class="flex items-center">
<FieldLabel for="password">Password</FieldLabel> <FieldLabel for="password">Password</FieldLabel>
<!-- <a href="##" class="ms-auto text-sm underline-offset-4 hover:underline">-->
<!-- Forgot your password?-->
<!-- </a>-->
</div> </div>
<Input id="password" type="password" required /> <Input id="password" type="password" required />
</Field> </Field>
<Field> <Field>
<Button type="submit">Login</Button> <Button type="submit">Login</Button>
</Field> </Field>
<!-- <Field>-->
<!-- <FieldDescription class="text-center">-->
<!-- Don't have an account?-->
<!-- <a href="/signup" class="underline underline-offset-4">Sign up</a>-->
<!-- </FieldDescription>-->
<!-- </Field>-->
</FieldGroup> </FieldGroup>
</form> </form>
+1 -1
View File
@@ -3,7 +3,7 @@ import { describe, expect, it } from 'vitest';
import { render } from 'vitest-browser-svelte'; import { render } from 'vitest-browser-svelte';
import Page from './items/+page.svelte'; import Page from './items/+page.svelte';
describe('/+page.svelte', () => { describe('/++page.svelte', () => {
it('should render h1', async () => { it('should render h1', async () => {
render(Page); render(Page);
Binary file not shown.