Compare commits
29
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c463fcedb6 | ||
|
|
d08f227e65 | ||
|
|
e3b98d9f35 | ||
|
|
5bb5a05a25 | ||
|
|
c79580b4ea | ||
|
|
8728d0abd7 | ||
|
|
1f4f4ddaa1 | ||
|
|
4fcc249ff4 | ||
|
|
c2bed98b06 | ||
|
|
ae1864c134 | ||
|
|
3f30fbe89c | ||
|
|
951db0f28a | ||
|
|
ad59d39f55 | ||
|
|
1283689d84 | ||
|
|
7d1370d7f2 | ||
|
|
ab0b56780f | ||
|
|
1a39ac37db | ||
|
|
e5dc784301 | ||
|
|
b2de22b0ef | ||
|
|
ad20eda182 | ||
|
|
da65aa4109 | ||
|
|
1f0798752c | ||
|
|
5f28b9b406 | ||
|
|
9e8fc7c07f | ||
|
|
ca51f8aca2 | ||
|
|
c086b1371c | ||
|
|
398350795d | ||
|
|
5be9ec1edf | ||
|
|
f77967a092 |
Executable
+19
@@ -0,0 +1,19 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
if [[ ! -d "/Users/ms/Development/personal/imphnen-frontend-service" ]]; then
|
||||||
|
echo "Cannot find source directory; Did you move it?"
|
||||||
|
echo "(Looking for "/Users/ms/Development/personal/imphnen-frontend-service")"
|
||||||
|
echo 'Cannot force reload with this script - use "direnv reload" manually and then try again'
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# rebuild the cache forcefully
|
||||||
|
_nix_direnv_force_reload=1 direnv exec "/Users/ms/Development/personal/imphnen-frontend-service" true
|
||||||
|
|
||||||
|
# Update the mtime for .envrc.
|
||||||
|
# This will cause direnv to reload again - but without re-building.
|
||||||
|
touch "/Users/ms/Development/personal/imphnen-frontend-service/.envrc"
|
||||||
|
|
||||||
|
# Also update the timestamp of whatever profile_rc we have.
|
||||||
|
# This makes sure that we know we are up to date.
|
||||||
|
touch -r "/Users/ms/Development/personal/imphnen-frontend-service/.envrc" "/Users/ms/Development/personal/imphnen-frontend-service/.direnv"/*.rc
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
|
|
||||||
VITE_SUPABASE_URL=https://your-project-id.supabase.co
|
|
||||||
VITE_SUPABASE_ANON_KEY=your-anon-key-here
|
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
name: Nix Build & Cache
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ['develop']
|
||||||
|
pull_request:
|
||||||
|
branches: ['develop']
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Nix
|
||||||
|
uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
|
||||||
|
- name: Setup Cachix
|
||||||
|
uses: cachix/cachix-action@v15
|
||||||
|
with:
|
||||||
|
name: msdqn
|
||||||
|
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
|
||||||
|
|
||||||
|
- name: Build all packages
|
||||||
|
run: |
|
||||||
|
nix build .#landing -o result-landing
|
||||||
|
nix build .#backoffice -o result-backoffice
|
||||||
|
nix build .#gacha -o result-gacha
|
||||||
|
nix build .#dimentorin -o result-dimentorin
|
||||||
|
nix build .#hackathon -o result-hackathon
|
||||||
|
nix build .#infra -o result-infra
|
||||||
|
|
||||||
|
- name: Show build outputs
|
||||||
|
run: |
|
||||||
|
echo "Landing: $(readlink result-landing)"
|
||||||
|
echo "Backoffice: $(readlink result-backoffice)"
|
||||||
|
echo "Gacha: $(readlink result-gacha)"
|
||||||
|
echo "Dimentorin: $(readlink result-dimentorin)"
|
||||||
|
echo "Hackathon: $(readlink result-hackathon)"
|
||||||
|
echo "Infra: $(readlink result-infra)"
|
||||||
@@ -10,6 +10,7 @@ This repository is a **monorepo** for all frontend services of IMPHNEN. The mono
|
|||||||
2. **Backoffice** - Application for <a href="https://gacha.imphnen.dev/" target="_blank">Internal Management Website</a>.
|
2. **Backoffice** - Application for <a href="https://gacha.imphnen.dev/" target="_blank">Internal Management Website</a>.
|
||||||
3. **Dimentorin** - Application for <a href="https://dimentorin.imphnen.dev/" target="_blank">Mentoring Service</a>.
|
3. **Dimentorin** - Application for <a href="https://dimentorin.imphnen.dev/" target="_blank">Mentoring Service</a>.
|
||||||
4. **Landing Page** - Application for <a href="https://imphnen.dev/" target="_blank">Landing Page</a>.
|
4. **Landing Page** - Application for <a href="https://imphnen.dev/" target="_blank">Landing Page</a>.
|
||||||
|
5. **QR Campaign** - Application for QR Campaign Management.
|
||||||
|
|
||||||
## How to install
|
## How to install
|
||||||
|
|
||||||
@@ -54,6 +55,10 @@ Use the following commands to run in development mode:
|
|||||||
```sh
|
```sh
|
||||||
npm run landing:dev
|
npm run landing:dev
|
||||||
```
|
```
|
||||||
|
- **QR Campaign**:
|
||||||
|
```sh
|
||||||
|
npm run qrcampaign:dev
|
||||||
|
```
|
||||||
|
|
||||||
### Build
|
### Build
|
||||||
|
|
||||||
@@ -75,6 +80,10 @@ Use the following commands to build the applications:
|
|||||||
```sh
|
```sh
|
||||||
npm run landing:build
|
npm run landing:build
|
||||||
```
|
```
|
||||||
|
- **QR Campaign**:
|
||||||
|
```sh
|
||||||
|
npm run qrcampaign:build
|
||||||
|
```
|
||||||
|
|
||||||
### Production
|
### Production
|
||||||
|
|
||||||
@@ -96,6 +105,10 @@ Use the following commands to run the applications in production mode:
|
|||||||
```sh
|
```sh
|
||||||
npm run landing:prod
|
npm run landing:prod
|
||||||
```
|
```
|
||||||
|
- **QR Campaign**:
|
||||||
|
```sh
|
||||||
|
npm run qrcampaign:prod
|
||||||
|
```
|
||||||
|
|
||||||
### Storybook
|
### Storybook
|
||||||
|
|
||||||
|
|||||||
@@ -1,236 +0,0 @@
|
|||||||
{
|
|
||||||
"message": "Success",
|
|
||||||
"data": [
|
|
||||||
{
|
|
||||||
"id": "afbead4b-2ffa-435e-ae7b-91f26e111fc6",
|
|
||||||
"team_id": "ab436f6e-027f-4831-873b-474345273491",
|
|
||||||
"team": {
|
|
||||||
"id": "ab436f6e-027f-4831-873b-474345273491",
|
|
||||||
"name": "nama timnya apa ya @All",
|
|
||||||
"description": "pikri carry",
|
|
||||||
"city": "Kota Depok",
|
|
||||||
"visibility": "public",
|
|
||||||
"logo": null,
|
|
||||||
"banner": "https://voymsjeqmvwrryetfldy.supabase.co/storage/v1/object/public/hackathon-uploads/teams/9a6dc902-c91b-4276-89cf-31e821531158-1764517227821.png",
|
|
||||||
"leader_id": "9a6dc902-c91b-4276-89cf-31e821531158",
|
|
||||||
"created_at": "2025-11-30T15:40:29.865564Z",
|
|
||||||
"updated_at": "2025-11-30T15:48:52.520850Z"
|
|
||||||
},
|
|
||||||
"rank": 1,
|
|
||||||
"prize": "Rp6.000.000",
|
|
||||||
"announced_at": "2025-12-15T16:41:01.931757Z",
|
|
||||||
"created_at": "2025-12-15T16:41:01.931757Z",
|
|
||||||
"updated_at": "2025-12-15T16:41:01.931757Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "8e591dc6-3cff-4019-bdfc-5e42ea081208",
|
|
||||||
"team_id": "06cbf0f4-cdad-40d0-b0d0-1fa8a3d50968",
|
|
||||||
"team": {
|
|
||||||
"id": "06cbf0f4-cdad-40d0-b0d0-1fa8a3d50968",
|
|
||||||
"name": "TheLastOfUs",
|
|
||||||
"description": "We truly believe in Meaning First — designing innovation, powered by technology.",
|
|
||||||
"city": "Bandung",
|
|
||||||
"visibility": "private",
|
|
||||||
"logo": "https://voymsjeqmvwrryetfldy.supabase.co/storage/v1/object/public/hackathon-uploads/teams/a32c0e81-1fbd-4bec-8e91-4cd6d10f553b-1764074851150.png",
|
|
||||||
"banner": "https://voymsjeqmvwrryetfldy.supabase.co/storage/v1/object/public/hackathon-uploads/teams/a32c0e81-1fbd-4bec-8e91-4cd6d10f553b-1764074852181.png",
|
|
||||||
"leader_id": "a32c0e81-1fbd-4bec-8e91-4cd6d10f553b",
|
|
||||||
"created_at": "2025-11-25T12:47:33.040330Z",
|
|
||||||
"updated_at": "2025-11-25T12:47:33.040330Z"
|
|
||||||
},
|
|
||||||
"rank": 2,
|
|
||||||
"prize": "Rp4.000.000",
|
|
||||||
"announced_at": "2025-12-15T16:41:32.534295Z",
|
|
||||||
"created_at": "2025-12-15T16:41:32.534295Z",
|
|
||||||
"updated_at": "2025-12-15T16:41:32.534295Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "5ca29971-5726-40dd-872c-853a6bfa03d9",
|
|
||||||
"team_id": "9d9ed9db-3523-47ae-a387-66c36dbec983",
|
|
||||||
"team": {
|
|
||||||
"id": "9d9ed9db-3523-47ae-a387-66c36dbec983",
|
|
||||||
"name": "Pecinta Capybara",
|
|
||||||
"description": "Ingin ikut meramaikan suasana.",
|
|
||||||
"city": "Tangerang",
|
|
||||||
"visibility": "public",
|
|
||||||
"logo": "https://voymsjeqmvwrryetfldy.supabase.co/storage/v1/object/public/hackathon-uploads/teams/648f6d3d-a92e-49cc-805e-2f600de87074-1764205746897.jpg",
|
|
||||||
"banner": "https://voymsjeqmvwrryetfldy.supabase.co/storage/v1/object/public/hackathon-uploads/teams/648f6d3d-a92e-49cc-805e-2f600de87074-1764205747479.png",
|
|
||||||
"leader_id": "648f6d3d-a92e-49cc-805e-2f600de87074",
|
|
||||||
"created_at": "2025-11-27T01:09:32.787150Z",
|
|
||||||
"updated_at": "2025-11-29T15:30:43.334609Z"
|
|
||||||
},
|
|
||||||
"rank": 3,
|
|
||||||
"prize": "Rp2.500.000",
|
|
||||||
"announced_at": "2025-12-15T16:41:59.091294Z",
|
|
||||||
"created_at": "2025-12-15T16:41:59.091294Z",
|
|
||||||
"updated_at": "2025-12-15T16:41:59.091294Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "e9f82de6-f5d0-43c1-a0bd-4687f2e4ed5e",
|
|
||||||
"team_id": "95f782f4-433f-489d-a06d-eed89ca83beb",
|
|
||||||
"team": {
|
|
||||||
"id": "95f782f4-433f-489d-a06d-eed89ca83beb",
|
|
||||||
"name": "Savora",
|
|
||||||
"description": "a complete food ordering application for Indonesian SMEs (UMKM)",
|
|
||||||
"city": "Bandung",
|
|
||||||
"visibility": "private",
|
|
||||||
"logo": "https://voymsjeqmvwrryetfldy.supabase.co/storage/v1/object/public/hackathon-uploads/teams/748de15d-374a-4756-89e2-75754e90542f-1764071952995.png",
|
|
||||||
"banner": "https://voymsjeqmvwrryetfldy.supabase.co/storage/v1/object/public/hackathon-uploads/teams/748de15d-374a-4756-89e2-75754e90542f-1764071953753.png",
|
|
||||||
"leader_id": "748de15d-374a-4756-89e2-75754e90542f",
|
|
||||||
"created_at": "2025-11-25T11:57:52.098043Z",
|
|
||||||
"updated_at": "2025-11-25T11:59:14.508877Z"
|
|
||||||
},
|
|
||||||
"rank": 4,
|
|
||||||
"prize": "Rp500.000",
|
|
||||||
"announced_at": "2025-12-15T16:42:39.361469Z",
|
|
||||||
"created_at": "2025-12-15T16:42:39.361469Z",
|
|
||||||
"updated_at": "2025-12-15T16:42:39.361469Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "ed93e080-0913-4c70-b69d-fce9bc0cdda7",
|
|
||||||
"team_id": "f9747574-8c01-4c3e-b20a-7ac1a70b947a",
|
|
||||||
"team": {
|
|
||||||
"id": "f9747574-8c01-4c3e-b20a-7ac1a70b947a",
|
|
||||||
"name": "AMBANET Corporation 🙀😸🤓",
|
|
||||||
"description": "Bersiaplah ambanet bakal mengguncang seluruh team hackathon ygy",
|
|
||||||
"city": "Kota Padang",
|
|
||||||
"visibility": "public",
|
|
||||||
"logo": "https://voymsjeqmvwrryetfldy.supabase.co/storage/v1/object/public/hackathon-uploads/teams/a5ea628a-24d9-4754-b461-3eda53da53bf-1764351234491.png",
|
|
||||||
"banner": "https://voymsjeqmvwrryetfldy.supabase.co/storage/v1/object/public/hackathon-uploads/teams/a5ea628a-24d9-4754-b461-3eda53da53bf-1764219109981.gif",
|
|
||||||
"leader_id": "a5ea628a-24d9-4754-b461-3eda53da53bf",
|
|
||||||
"created_at": "2025-11-27T04:48:53.080955Z",
|
|
||||||
"updated_at": "2025-11-29T13:31:51.276246Z"
|
|
||||||
},
|
|
||||||
"rank": 5,
|
|
||||||
"prize": "Rp500.000",
|
|
||||||
"announced_at": "2025-12-15T16:42:57.065225Z",
|
|
||||||
"created_at": "2025-12-15T16:42:57.065225Z",
|
|
||||||
"updated_at": "2025-12-15T16:42:57.065225Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "7ed55b12-d585-419d-bae2-1a362a57798e",
|
|
||||||
"team_id": "4f3e2e85-3f3f-4880-9104-b3f66bb1c9d9",
|
|
||||||
"team": {
|
|
||||||
"id": "4f3e2e85-3f3f-4880-9104-b3f66bb1c9d9",
|
|
||||||
"name": "Pemuja Deadline Anti Refund",
|
|
||||||
"description": "Diusahakan banyak deadline tapi kg refund ~ \"Angga Kentod\"",
|
|
||||||
"city": "Kota Bandung",
|
|
||||||
"visibility": "public",
|
|
||||||
"logo": "https://voymsjeqmvwrryetfldy.supabase.co/storage/v1/object/public/hackathon-uploads/teams/6cf045e2-d853-4480-91b8-244fb0b76b7f-1764332887921.jpg",
|
|
||||||
"banner": null,
|
|
||||||
"leader_id": "6cf045e2-d853-4480-91b8-244fb0b76b7f",
|
|
||||||
"created_at": "2025-11-27T05:02:35.128625Z",
|
|
||||||
"updated_at": "2025-11-28T12:28:09.697222Z"
|
|
||||||
},
|
|
||||||
"rank": 6,
|
|
||||||
"prize": "Rp200.000",
|
|
||||||
"announced_at": "2025-12-15T16:43:09.471633Z",
|
|
||||||
"created_at": "2025-12-15T16:43:09.471633Z",
|
|
||||||
"updated_at": "2025-12-15T16:43:09.471633Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "0b3a0b5c-2038-4d09-85a7-ae4709702951",
|
|
||||||
"team_id": "9d8cd404-8e69-4efd-a125-fc0863d7afc7",
|
|
||||||
"team": {
|
|
||||||
"id": "9d8cd404-8e69-4efd-a125-fc0863d7afc7",
|
|
||||||
"name": "Aku nak meletup",
|
|
||||||
"description": "masih pemula bg",
|
|
||||||
"city": "Boyolali",
|
|
||||||
"visibility": "public",
|
|
||||||
"logo": "https://voymsjeqmvwrryetfldy.supabase.co/storage/v1/object/public/hackathon-uploads/teams/c79d4b2b-e63f-4187-a8cd-520fde333ab3-1764211224866.jpg",
|
|
||||||
"banner": null,
|
|
||||||
"leader_id": "c79d4b2b-e63f-4187-a8cd-520fde333ab3",
|
|
||||||
"created_at": "2025-11-27T02:40:25.376372Z",
|
|
||||||
"updated_at": "2025-11-27T02:40:25.376372Z"
|
|
||||||
},
|
|
||||||
"rank": 7,
|
|
||||||
"prize": "Rp200.000",
|
|
||||||
"announced_at": "2025-12-15T16:43:22.467439Z",
|
|
||||||
"created_at": "2025-12-15T16:43:22.467439Z",
|
|
||||||
"updated_at": "2025-12-15T16:43:22.467439Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "bb0706b9-b723-4a49-b0b0-3be9af70225b",
|
|
||||||
"team_id": "dd6c62dd-2632-4156-9da5-8ff9a3560f06",
|
|
||||||
"team": {
|
|
||||||
"id": "dd6c62dd-2632-4156-9da5-8ff9a3560f06",
|
|
||||||
"name": "Iseng Hackathon",
|
|
||||||
"description": "HME ITB Oke Champ",
|
|
||||||
"city": "Kota Bandung",
|
|
||||||
"visibility": "public",
|
|
||||||
"logo": null,
|
|
||||||
"banner": null,
|
|
||||||
"leader_id": "d1e4e3bf-098f-453e-bf00-612685d311d9",
|
|
||||||
"created_at": "2025-11-30T03:58:52.429673Z",
|
|
||||||
"updated_at": "2025-11-30T03:58:52.429673Z"
|
|
||||||
},
|
|
||||||
"rank": 8,
|
|
||||||
"prize": "Rp200.000",
|
|
||||||
"announced_at": "2025-12-15T16:43:34.191207Z",
|
|
||||||
"created_at": "2025-12-15T16:43:34.191207Z",
|
|
||||||
"updated_at": "2025-12-15T16:43:34.191207Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "4a3e807c-2cf2-4557-a6af-99fcc726cebb",
|
|
||||||
"team_id": "52316c25-5697-4436-bfc2-02baef8ca956",
|
|
||||||
"team": {
|
|
||||||
"id": "52316c25-5697-4436-bfc2-02baef8ca956",
|
|
||||||
"name": "Gorengan 3 Ronde",
|
|
||||||
"description": "Jam 10 izin turun",
|
|
||||||
"city": "Kota Medan",
|
|
||||||
"visibility": "public",
|
|
||||||
"logo": "https://voymsjeqmvwrryetfldy.supabase.co/storage/v1/object/public/hackathon-uploads/teams/e7bec056-7f63-4642-a763-afa39c3c340e-1764509780084.png",
|
|
||||||
"banner": "https://voymsjeqmvwrryetfldy.supabase.co/storage/v1/object/public/hackathon-uploads/teams/e7bec056-7f63-4642-a763-afa39c3c340e-1764509810717.png",
|
|
||||||
"leader_id": "e7bec056-7f63-4642-a763-afa39c3c340e",
|
|
||||||
"created_at": "2025-11-30T13:36:52.344019Z",
|
|
||||||
"updated_at": "2025-11-30T13:36:52.344019Z"
|
|
||||||
},
|
|
||||||
"rank": 9,
|
|
||||||
"prize": "Rp200.000",
|
|
||||||
"announced_at": "2025-12-15T16:43:51.988908Z",
|
|
||||||
"created_at": "2025-12-15T16:43:51.988908Z",
|
|
||||||
"updated_at": "2025-12-15T16:43:51.988908Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "e18827dd-839d-4e54-9126-c1faebce2c2c",
|
|
||||||
"team_id": "d2b2fb99-e934-405c-836e-711352e0f8a0",
|
|
||||||
"team": {
|
|
||||||
"id": "d2b2fb99-e934-405c-836e-711352e0f8a0",
|
|
||||||
"name": "Spark Developer",
|
|
||||||
"description": "Yang penting yakin untuk menang.",
|
|
||||||
"city": "Kota Batam",
|
|
||||||
"visibility": "public",
|
|
||||||
"logo": "https://voymsjeqmvwrryetfldy.supabase.co/storage/v1/object/public/hackathon-uploads/teams/087921fb-2d17-423b-9628-73de2176360f-1764486044138.png",
|
|
||||||
"banner": "https://voymsjeqmvwrryetfldy.supabase.co/storage/v1/object/public/hackathon-uploads/teams/087921fb-2d17-423b-9628-73de2176360f-1764486059581.jpg",
|
|
||||||
"leader_id": "087921fb-2d17-423b-9628-73de2176360f",
|
|
||||||
"created_at": "2025-11-29T04:49:54.266394Z",
|
|
||||||
"updated_at": "2025-11-30T07:01:02.316185Z"
|
|
||||||
},
|
|
||||||
"rank": 10,
|
|
||||||
"prize": "Rp200.000",
|
|
||||||
"announced_at": "2025-12-15T16:44:02.680036Z",
|
|
||||||
"created_at": "2025-12-15T16:44:02.680036Z",
|
|
||||||
"updated_at": "2025-12-15T16:44:02.680036Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "a0ed8e58-f8e3-491c-8c03-20e82090d6f8",
|
|
||||||
"team_id": "d30d7d03-dce7-4220-9e7f-a537a45c54e0",
|
|
||||||
"team": {
|
|
||||||
"id": "d30d7d03-dce7-4220-9e7f-a537a45c54e0",
|
|
||||||
"name": "Eye of Horus",
|
|
||||||
"description": "Eye of horus",
|
|
||||||
"city": "Garut",
|
|
||||||
"visibility": "public",
|
|
||||||
"logo": "https://voymsjeqmvwrryetfldy.supabase.co/storage/v1/object/public/hackathon-uploads/teams/96dc1c2b-a1ef-441a-93d5-da2ccad37d81-1764145807852.png",
|
|
||||||
"banner": "https://voymsjeqmvwrryetfldy.supabase.co/storage/v1/object/public/hackathon-uploads/teams/96dc1c2b-a1ef-441a-93d5-da2ccad37d81-1764145810329.jpeg",
|
|
||||||
"leader_id": "96dc1c2b-a1ef-441a-93d5-da2ccad37d81",
|
|
||||||
"created_at": "2025-11-26T08:30:10.385714Z",
|
|
||||||
"updated_at": "2025-11-26T09:35:12.386986Z"
|
|
||||||
},
|
|
||||||
"rank": 11,
|
|
||||||
"prize": "hadiah",
|
|
||||||
"announced_at": "2025-12-16T04:19:09.502015Z",
|
|
||||||
"created_at": "2025-12-16T04:19:09.502015Z",
|
|
||||||
"updated_at": "2025-12-16T04:19:09.502015Z"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
useAuthStore,
|
useAuthStore,
|
||||||
useMyTeams,
|
useMyTeams,
|
||||||
useTeamById,
|
useTeamById,
|
||||||
|
useTeamSubmission,
|
||||||
useWinners,
|
useWinners,
|
||||||
} from '@imphnen-frontend-service/service';
|
} from '@imphnen-frontend-service/service';
|
||||||
import QRCode from 'qrcode';
|
import QRCode from 'qrcode';
|
||||||
@@ -37,8 +38,20 @@ const formatOrdinalRank = (rank: number): string => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const CERT_WIDTH = 1000;
|
// Keep the template size aligned with the SVG native size (842x595) using an integer multiplier.
|
||||||
const CERT_HEIGHT = (CERT_WIDTH * 595) / 842; // matches blank_winner_cert.svg aspect ratio
|
// This reduces sub-pixel scaling artifacts (blur) on thin lines when rasterizing with html2canvas.
|
||||||
|
const CERT_WIDTH = 842 * 2;
|
||||||
|
const CERT_HEIGHT = 595 * 2;
|
||||||
|
|
||||||
|
// Balance between output sharpness and file size.
|
||||||
|
// Output resolution will be (CERT_WIDTH * EXPORT_SCALE) x (CERT_HEIGHT * EXPORT_SCALE).
|
||||||
|
const EXPORT_SCALE = 2;
|
||||||
|
|
||||||
|
// The original layout was tuned around a ~1000px-wide template.
|
||||||
|
// We keep the same visual proportions by scaling fixed pixel values.
|
||||||
|
const LAYOUT_BASE_WIDTH = 1000;
|
||||||
|
const LAYOUT_SCALE = CERT_WIDTH / LAYOUT_BASE_WIDTH;
|
||||||
|
const s = (px: number) => Math.round(px * LAYOUT_SCALE);
|
||||||
|
|
||||||
const CertificateWinnerPage: FC = (): ReactElement => {
|
const CertificateWinnerPage: FC = (): ReactElement => {
|
||||||
const { certId } = useParams<{ certId: string }>();
|
const { certId } = useParams<{ certId: string }>();
|
||||||
@@ -87,7 +100,12 @@ const CertificateWinnerPage: FC = (): ReactElement => {
|
|||||||
!!decodedTeamId
|
!!decodedTeamId
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { data: submissionData, isLoading: isLoadingSubmission } =
|
||||||
|
useTeamSubmission(decodedTeamId, !!decodedTeamId);
|
||||||
|
|
||||||
const team = teamData?.data;
|
const team = teamData?.data;
|
||||||
|
const submission = submissionData?.data;
|
||||||
|
const submissionName = submission?.project_name || '(Submission unavailable)';
|
||||||
|
|
||||||
const memberNames = useMemo(() => {
|
const memberNames = useMemo(() => {
|
||||||
const members = team?.members || [];
|
const members = team?.members || [];
|
||||||
@@ -112,7 +130,7 @@ const CertificateWinnerPage: FC = (): ReactElement => {
|
|||||||
const certificateUrl = `${window.location.origin}/certificate/winner/${encodedCertId}`;
|
const certificateUrl = `${window.location.origin}/certificate/winner/${encodedCertId}`;
|
||||||
|
|
||||||
QRCode.toDataURL(certificateUrl, {
|
QRCode.toDataURL(certificateUrl, {
|
||||||
width: 200,
|
width: s(200),
|
||||||
margin: 1,
|
margin: 1,
|
||||||
color: {
|
color: {
|
||||||
dark: '#000000',
|
dark: '#000000',
|
||||||
@@ -130,13 +148,15 @@ const CertificateWinnerPage: FC = (): ReactElement => {
|
|||||||
if (!team?.name) return;
|
if (!team?.name) return;
|
||||||
if (!qrCodeUrl) return;
|
if (!qrCodeUrl) return;
|
||||||
if (!winnerEntry?.rank) return;
|
if (!winnerEntry?.rank) return;
|
||||||
|
if (isLoadingSubmission) return;
|
||||||
|
|
||||||
setIsGenerating(true);
|
setIsGenerating(true);
|
||||||
try {
|
try {
|
||||||
|
setShowTemplate(true);
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1500));
|
await new Promise((resolve) => setTimeout(resolve, 1500));
|
||||||
|
|
||||||
const canvas = await html2canvas(certificateRef.current, {
|
const canvas = await html2canvas(certificateRef.current, {
|
||||||
scale: 4,
|
scale: EXPORT_SCALE,
|
||||||
useCORS: true,
|
useCORS: true,
|
||||||
backgroundColor: '#ffffff',
|
backgroundColor: '#ffffff',
|
||||||
logging: false,
|
logging: false,
|
||||||
@@ -158,7 +178,14 @@ const CertificateWinnerPage: FC = (): ReactElement => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
generateCertificate();
|
generateCertificate();
|
||||||
}, [team?.name, memberNames, qrCodeUrl, winnerEntry?.rank]);
|
}, [
|
||||||
|
team?.name,
|
||||||
|
memberNames,
|
||||||
|
qrCodeUrl,
|
||||||
|
winnerEntry?.rank,
|
||||||
|
isLoadingSubmission,
|
||||||
|
submissionName,
|
||||||
|
]);
|
||||||
|
|
||||||
const handleDownloadCertificate = () => {
|
const handleDownloadCertificate = () => {
|
||||||
if (!certificateImage) return;
|
if (!certificateImage) return;
|
||||||
@@ -323,7 +350,8 @@ const CertificateWinnerPage: FC = (): ReactElement => {
|
|||||||
style={{
|
style={{
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
backgroundImage: 'url(/images/blank_winner_cert.svg)',
|
backgroundImage: 'url(/images/blank_winner_cert.svg)',
|
||||||
backgroundSize: 'cover',
|
backgroundSize: '100% 100%',
|
||||||
|
backgroundRepeat: 'no-repeat',
|
||||||
backgroundPosition: 'center',
|
backgroundPosition: 'center',
|
||||||
width: `${CERT_WIDTH}px`,
|
width: `${CERT_WIDTH}px`,
|
||||||
height: `${CERT_HEIGHT}px`,
|
height: `${CERT_HEIGHT}px`,
|
||||||
@@ -339,7 +367,7 @@ const CertificateWinnerPage: FC = (): ReactElement => {
|
|||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: '37%',
|
top: '35%',
|
||||||
left: '3.5%',
|
left: '3.5%',
|
||||||
width: '55%',
|
width: '55%',
|
||||||
}}
|
}}
|
||||||
@@ -350,7 +378,7 @@ const CertificateWinnerPage: FC = (): ReactElement => {
|
|||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
color: '#59bef5',
|
color: '#59bef5',
|
||||||
textAlign: 'left',
|
textAlign: 'left',
|
||||||
fontSize: '28px',
|
fontSize: `${s(28)}px`,
|
||||||
lineHeight: '1.2',
|
lineHeight: '1.2',
|
||||||
wordBreak: 'break-word',
|
wordBreak: 'break-word',
|
||||||
margin: 0,
|
margin: 0,
|
||||||
@@ -364,7 +392,7 @@ const CertificateWinnerPage: FC = (): ReactElement => {
|
|||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: '42.5%',
|
top: '40.5%',
|
||||||
left: '3.5%',
|
left: '3.5%',
|
||||||
width: '55%',
|
width: '55%',
|
||||||
}}
|
}}
|
||||||
@@ -374,7 +402,7 @@ const CertificateWinnerPage: FC = (): ReactElement => {
|
|||||||
style={{
|
style={{
|
||||||
margin: 0,
|
margin: 0,
|
||||||
fontFamily: 'Poppins, sans-serif',
|
fontFamily: 'Poppins, sans-serif',
|
||||||
fontSize: '18px',
|
fontSize: `${s(18)}px`,
|
||||||
lineHeight: '1.35',
|
lineHeight: '1.35',
|
||||||
color: '#59bef5',
|
color: '#59bef5',
|
||||||
}}
|
}}
|
||||||
@@ -392,7 +420,7 @@ const CertificateWinnerPage: FC = (): ReactElement => {
|
|||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: '62%',
|
top: '60%',
|
||||||
left: '3.5%',
|
left: '3.5%',
|
||||||
width: '60%',
|
width: '60%',
|
||||||
}}
|
}}
|
||||||
@@ -401,7 +429,7 @@ const CertificateWinnerPage: FC = (): ReactElement => {
|
|||||||
style={{
|
style={{
|
||||||
margin: 0,
|
margin: 0,
|
||||||
fontFamily: 'Poppins, sans-serif',
|
fontFamily: 'Poppins, sans-serif',
|
||||||
fontSize: '18px',
|
fontSize: `${s(18)}px`,
|
||||||
lineHeight: '1.35',
|
lineHeight: '1.35',
|
||||||
color: '#6B6B6B',
|
color: '#6B6B6B',
|
||||||
}}
|
}}
|
||||||
@@ -410,6 +438,10 @@ const CertificateWinnerPage: FC = (): ReactElement => {
|
|||||||
<br />
|
<br />
|
||||||
<b>JUARA {winnerEntry.rank}</b> pada Hackathon IMPHNEN x
|
<b>JUARA {winnerEntry.rank}</b> pada Hackathon IMPHNEN x
|
||||||
Kolosal.ai
|
Kolosal.ai
|
||||||
|
<br />
|
||||||
|
<span>
|
||||||
|
dengan nama project: <b>{submissionName}</b>
|
||||||
|
</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -420,7 +452,7 @@ const CertificateWinnerPage: FC = (): ReactElement => {
|
|||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: '8%',
|
top: '8%',
|
||||||
right: '8.5%',
|
right: '8.5%',
|
||||||
width: '190px',
|
width: `${s(190)}px`,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
}}
|
}}
|
||||||
@@ -430,12 +462,12 @@ const CertificateWinnerPage: FC = (): ReactElement => {
|
|||||||
display: 'inline-flex',
|
display: 'inline-flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
padding: '6px 16px',
|
padding: `${s(6)}px ${s(16)}px`,
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
fontFamily: 'Poppins, sans-serif',
|
fontFamily: 'Poppins, sans-serif',
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
color: '#78350F',
|
color: '#78350F',
|
||||||
fontSize: '24px',
|
fontSize: `${s(24)}px`,
|
||||||
lineHeight: '1',
|
lineHeight: '1',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -450,8 +482,8 @@ const CertificateWinnerPage: FC = (): ReactElement => {
|
|||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: '33%',
|
top: '33%',
|
||||||
right: '9.3%',
|
right: '9.3%',
|
||||||
width: '190px',
|
width: `${s(190)}px`,
|
||||||
height: '190px',
|
height: `${s(190)}px`,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
@@ -461,7 +493,11 @@ const CertificateWinnerPage: FC = (): ReactElement => {
|
|||||||
<img
|
<img
|
||||||
src={qrCodeUrl}
|
src={qrCodeUrl}
|
||||||
alt="Certificate QR Code"
|
alt="Certificate QR Code"
|
||||||
style={{ width: '190px', height: '190px', display: 'block' }}
|
style={{
|
||||||
|
width: `${s(190)}px`,
|
||||||
|
height: `${s(190)}px`,
|
||||||
|
display: 'block',
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import baseConfig from '../../eslint.config.mjs';
|
||||||
|
|
||||||
|
export default [...baseConfig];
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>IMPHNEN Infrastructure</title>
|
||||||
|
<base href="/" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<link rel="icon" type="image/x-icon" href="/logos/simple.svg" />
|
||||||
|
<link rel="stylesheet" href="/src/index.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
'@tailwindcss/postcss': {
|
||||||
|
base: join(import.meta.dirname, '../../'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"name": "infra",
|
||||||
|
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||||
|
"sourceRoot": "apps/infra/src",
|
||||||
|
"projectType": "application",
|
||||||
|
"tags": [],
|
||||||
|
"targets": {}
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 351 KiB |
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 511 KiB |
@@ -0,0 +1,235 @@
|
|||||||
|
const apps = [
|
||||||
|
{ name: 'Landing', domain: 'imphnen.dev', type: 'Next.js', color: 'blue' },
|
||||||
|
{ name: 'Gacha', domain: 'gacha.imphnen.dev', type: 'Vite', color: 'green' },
|
||||||
|
{ name: 'Backoffice', domain: 'backoffice.imphnen.dev', type: 'Vite', color: 'green' },
|
||||||
|
{ name: 'Dimentorin', domain: 'dimentorin.imphnen.dev', type: 'Vite', color: 'green' },
|
||||||
|
{ name: 'Hackathon', domain: 'hackathon.imphnen.dev', type: 'Vite', color: 'green' },
|
||||||
|
{ name: 'Infra', domain: 'infra.imphnen.dev', type: 'Vite', color: 'green' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const techStack = [
|
||||||
|
{ category: 'OS', items: ['NixOS 24.11'] },
|
||||||
|
{ category: 'Web Server', items: ['Nginx'] },
|
||||||
|
{ category: 'SSL', items: ['Let\'s Encrypt (ACME)'] },
|
||||||
|
{ category: 'Build', items: ['Nix Flakes', 'Nx Monorepo'] },
|
||||||
|
{ category: 'Frontend', items: ['React', 'Next.js', 'Vite', 'TailwindCSS'] },
|
||||||
|
{ category: 'Secrets', items: ['sops-nix'] },
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-[#0a0a0a]">
|
||||||
|
{/* Header */}
|
||||||
|
<header className="border-b border-zinc-800 bg-[#0a0a0a]/80 backdrop-blur-sm sticky top-0 z-50">
|
||||||
|
<div className="container mx-auto px-6 py-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-8 h-8 bg-blue-500 rounded-lg flex items-center justify-center font-bold">
|
||||||
|
I
|
||||||
|
</div>
|
||||||
|
<span className="text-xl font-semibold">IMPHNEN Infrastructure</span>
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
href="https://github.com/IMPHNEN"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-zinc-400 hover:text-white transition-colors"
|
||||||
|
>
|
||||||
|
GitHub
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main className="container mx-auto px-6 py-12">
|
||||||
|
{/* Hero */}
|
||||||
|
<section className="text-center mb-16">
|
||||||
|
<h1 className="text-4xl md:text-5xl font-bold mb-4">
|
||||||
|
Infrastructure Overview
|
||||||
|
</h1>
|
||||||
|
<p className="text-zinc-400 text-lg max-w-2xl mx-auto">
|
||||||
|
Declarative NixOS infrastructure powering IMPHNEN's frontend applications
|
||||||
|
on Hetzner Cloud.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Architecture Diagram */}
|
||||||
|
<section className="mb-16">
|
||||||
|
<h2 className="text-2xl font-semibold mb-6">Architecture</h2>
|
||||||
|
<div className="card bg-[#141414] border-zinc-800 p-8">
|
||||||
|
<div className="flex flex-col items-center gap-6">
|
||||||
|
{/* Internet */}
|
||||||
|
<div className="flex items-center gap-2 text-zinc-400">
|
||||||
|
<span className="text-2xl">🌐</span>
|
||||||
|
<span>Internet</span>
|
||||||
|
</div>
|
||||||
|
<div className="w-px h-8 bg-zinc-700" />
|
||||||
|
|
||||||
|
{/* Cloudflare */}
|
||||||
|
<div className="card bg-orange-500/10 border-orange-500/30 px-6 py-3">
|
||||||
|
<span className="badge badge-orange">Cloudflare</span>
|
||||||
|
<p className="text-sm text-zinc-400 mt-1">DNS + CDN + DDoS Protection</p>
|
||||||
|
</div>
|
||||||
|
<div className="w-px h-8 bg-zinc-700" />
|
||||||
|
|
||||||
|
{/* Hetzner VPS */}
|
||||||
|
<div className="card bg-[#1a1a1a] border-zinc-700 w-full max-w-3xl">
|
||||||
|
<div className="text-center mb-4">
|
||||||
|
<span className="badge badge-purple">Hetzner Cloud VPS</span>
|
||||||
|
<p className="text-sm text-zinc-400 mt-1">NixOS | 167.235.70.37</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Nginx */}
|
||||||
|
<div className="card bg-green-500/10 border-green-500/30 mb-4">
|
||||||
|
<div className="text-center">
|
||||||
|
<span className="badge badge-green">Nginx</span>
|
||||||
|
<p className="text-sm text-zinc-400 mt-1">Reverse Proxy + Static Files + SSL Termination</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Apps Grid */}
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
|
||||||
|
{apps.map((app) => (
|
||||||
|
<div
|
||||||
|
key={app.name}
|
||||||
|
className="card bg-[#0a0a0a] border-zinc-800 text-center p-3"
|
||||||
|
>
|
||||||
|
<p className="font-medium text-sm">{app.name}</p>
|
||||||
|
<p className="text-xs text-zinc-500">{app.domain}</p>
|
||||||
|
<span className={`badge badge-${app.color} mt-2 text-xs`}>
|
||||||
|
{app.type}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Deployed Apps */}
|
||||||
|
<section className="mb-16">
|
||||||
|
<h2 className="text-2xl font-semibold mb-6">Deployed Applications</h2>
|
||||||
|
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
{apps.map((app) => (
|
||||||
|
<a
|
||||||
|
key={app.name}
|
||||||
|
href={`https://${app.domain}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="card bg-[#141414] border-zinc-800 hover:border-zinc-600 transition-colors group"
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div>
|
||||||
|
<h3 className="font-semibold group-hover:text-blue-400 transition-colors">
|
||||||
|
{app.name}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-zinc-500">{app.domain}</p>
|
||||||
|
</div>
|
||||||
|
<span className={`badge badge-${app.color}`}>{app.type}</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Tech Stack */}
|
||||||
|
<section className="mb-16">
|
||||||
|
<h2 className="text-2xl font-semibold mb-6">Technology Stack</h2>
|
||||||
|
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
{techStack.map((tech) => (
|
||||||
|
<div key={tech.category} className="card bg-[#141414] border-zinc-800">
|
||||||
|
<h3 className="text-sm text-zinc-500 uppercase tracking-wider mb-2">
|
||||||
|
{tech.category}
|
||||||
|
</h3>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{tech.items.map((item) => (
|
||||||
|
<span key={item} className="badge badge-blue">
|
||||||
|
{item}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Deployment Flow */}
|
||||||
|
<section className="mb-16">
|
||||||
|
<h2 className="text-2xl font-semibold mb-6">Deployment Flow</h2>
|
||||||
|
<div className="card bg-[#141414] border-zinc-800">
|
||||||
|
<div className="flex flex-col md:flex-row items-center justify-between gap-4">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="w-12 h-12 bg-zinc-800 rounded-full flex items-center justify-center mx-auto mb-2">
|
||||||
|
<span>1</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm font-medium">Push to GitHub</p>
|
||||||
|
<p className="text-xs text-zinc-500">develop branch</p>
|
||||||
|
</div>
|
||||||
|
<div className="hidden md:block text-zinc-600">→</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="w-12 h-12 bg-zinc-800 rounded-full flex items-center justify-center mx-auto mb-2">
|
||||||
|
<span>2</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm font-medium">Update Flake</p>
|
||||||
|
<p className="text-xs text-zinc-500">nix flake update</p>
|
||||||
|
</div>
|
||||||
|
<div className="hidden md:block text-zinc-600">→</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="w-12 h-12 bg-zinc-800 rounded-full flex items-center justify-center mx-auto mb-2">
|
||||||
|
<span>3</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm font-medium">Remote Build</p>
|
||||||
|
<p className="text-xs text-zinc-500">nixos-rebuild</p>
|
||||||
|
</div>
|
||||||
|
<div className="hidden md:block text-zinc-600">→</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="w-12 h-12 bg-blue-500 rounded-full flex items-center justify-center mx-auto mb-2">
|
||||||
|
<span>✓</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm font-medium">Live</p>
|
||||||
|
<p className="text-xs text-zinc-500">Zero downtime</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Repositories */}
|
||||||
|
<section>
|
||||||
|
<h2 className="text-2xl font-semibold mb-6">Repositories</h2>
|
||||||
|
<div className="grid md:grid-cols-2 gap-4">
|
||||||
|
<a
|
||||||
|
href="https://github.com/IMPHNEN/imphnen-frontend-service"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="card bg-[#141414] border-zinc-800 hover:border-zinc-600 transition-colors"
|
||||||
|
>
|
||||||
|
<h3 className="font-semibold mb-1">imphnen-frontend-service</h3>
|
||||||
|
<p className="text-sm text-zinc-400">
|
||||||
|
Nx monorepo containing all frontend applications
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="https://github.com/IMPHNEN/imphnen-infrastructure"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="card bg-[#141414] border-zinc-800 hover:border-zinc-600 transition-colors"
|
||||||
|
>
|
||||||
|
<h3 className="font-semibold mb-1">imphnen-infrastructure</h3>
|
||||||
|
<p className="text-sm text-zinc-400">
|
||||||
|
NixOS flake configuration for server deployment
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<footer className="border-t border-zinc-800 mt-16">
|
||||||
|
<div className="container mx-auto px-6 py-8 text-center text-zinc-500 text-sm">
|
||||||
|
<p>IMPHNEN Infrastructure © {new Date().getFullYear()}</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
@import 'tailwindcss';
|
||||||
|
|
||||||
|
@theme {
|
||||||
|
--color-primary: #3b82f6;
|
||||||
|
--color-primary-hover: #2563eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background-color: #0a0a0a;
|
||||||
|
color: #ffffff;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer components {
|
||||||
|
.card {
|
||||||
|
background: #141414;
|
||||||
|
border: 1px solid #27272a;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
border-radius: 9999px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-blue {
|
||||||
|
background: rgba(59, 130, 246, 0.2);
|
||||||
|
color: #60a5fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-green {
|
||||||
|
background: rgba(34, 197, 94, 0.2);
|
||||||
|
color: #4ade80;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-purple {
|
||||||
|
background: rgba(168, 85, 247, 0.2);
|
||||||
|
color: #c084fc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-orange {
|
||||||
|
background: rgba(249, 115, 22, 0.2);
|
||||||
|
color: #fb923c;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { createRoot } from 'react-dom/client';
|
||||||
|
import { StrictMode } from 'react';
|
||||||
|
import App from './app/App';
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
|
const rootElement = document.getElementById('root');
|
||||||
|
|
||||||
|
if (!rootElement) throw new Error('Failed to find the root element');
|
||||||
|
|
||||||
|
createRoot(rootElement).render(
|
||||||
|
<StrictMode>
|
||||||
|
<App />
|
||||||
|
</StrictMode>
|
||||||
|
);
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "../../dist/out-tsc",
|
||||||
|
"types": ["node", "vite/client"]
|
||||||
|
},
|
||||||
|
"exclude": ["src/**/*.spec.ts", "src/**/*.test.ts", "src/**/*.spec.tsx", "src/**/*.test.tsx"],
|
||||||
|
"include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"allowJs": false,
|
||||||
|
"esModuleInterop": false,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"strict": true,
|
||||||
|
"types": ["vite/client"]
|
||||||
|
},
|
||||||
|
"files": [],
|
||||||
|
"include": [],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.app.json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"extends": "../../tsconfig.base.json"
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
/// <reference types='vitest' />
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
|
||||||
|
import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin';
|
||||||
|
|
||||||
|
export default defineConfig(() => ({
|
||||||
|
root: __dirname,
|
||||||
|
cacheDir: '../../node_modules/.vite/apps/infra',
|
||||||
|
server: {
|
||||||
|
port: 3006,
|
||||||
|
host: 'localhost',
|
||||||
|
},
|
||||||
|
preview: {
|
||||||
|
port: 3007,
|
||||||
|
host: 'localhost',
|
||||||
|
},
|
||||||
|
plugins: [react(), nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])],
|
||||||
|
build: {
|
||||||
|
outDir: '../../dist/apps/infra',
|
||||||
|
emptyOutDir: true,
|
||||||
|
reportCompressedSize: true,
|
||||||
|
commonjsOptions: {
|
||||||
|
transformMixedEsModules: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -54,9 +54,19 @@ export function HeroSection() {
|
|||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ duration: 0.5 }}
|
transition={{ duration: 0.5 }}
|
||||||
>
|
>
|
||||||
<span className="inline-flex items-center rounded-full border px-3 py-1 text-sm w-fit">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
{communityLabel}
|
<span className="inline-flex items-center rounded-full border px-3 py-1 text-sm">
|
||||||
</span>
|
{communityLabel}
|
||||||
|
</span>
|
||||||
|
<a
|
||||||
|
href="https://ancikri.com"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="inline-flex items-center rounded-full border px-3 py-1 text-sm hover:bg-muted transition-colors"
|
||||||
|
>
|
||||||
|
Powered By Ancikri
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<h1 className="text-4xl md:text-5xl lg:text-6xl xl:text-7xl font-bold tracking-tighter bg-clip-text text-transparent bg-gradient-to-r from-foreground via-foreground to-foreground/70">
|
<h1 className="text-4xl md:text-5xl lg:text-6xl xl:text-7xl font-bold tracking-tighter bg-clip-text text-transparent bg-gradient-to-r from-foreground via-foreground to-foreground/70">
|
||||||
|
|||||||
@@ -104,11 +104,21 @@ export default function Footer() {
|
|||||||
<ul className="space-y-2"></ul>
|
<ul className="space-y-2"></ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-8 border-t pt-8 text-center">
|
<div className="mt-8 border-t pt-8 text-center space-y-2">
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
© {new Date().getFullYear()} IMPHNEN - Ingin Menjadi Programmer
|
© {new Date().getFullYear()} IMPHNEN - Ingin Menjadi Programmer
|
||||||
Handal, Namun Enggan Ngoding. All rights reserved.
|
Handal, Namun Enggan Ngoding. All rights reserved.
|
||||||
</p>
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Powered by{' '}
|
||||||
|
<Link
|
||||||
|
href="https://ancikri.com"
|
||||||
|
target="_blank"
|
||||||
|
className="font-medium text-foreground hover:underline"
|
||||||
|
>
|
||||||
|
Ancikri
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|||||||
@@ -1,13 +1,23 @@
|
|||||||
import { Bai_Jamjuree, Poppins } from 'next/font/google';
|
import localFont from 'next/font/local';
|
||||||
|
|
||||||
export const baiJamjureeFont = Bai_Jamjuree({
|
export const baiJamjureeFont = localFont({
|
||||||
subsets: ['latin'],
|
src: [
|
||||||
weight: ['300', '400', '500', '600', '700'],
|
{ path: '../../public/fonts/BaiJamjuree-Light.ttf', weight: '300', style: 'normal' },
|
||||||
|
{ path: '../../public/fonts/BaiJamjuree-Regular.ttf', weight: '400', style: 'normal' },
|
||||||
|
{ path: '../../public/fonts/BaiJamjuree-Medium.ttf', weight: '500', style: 'normal' },
|
||||||
|
{ path: '../../public/fonts/BaiJamjuree-SemiBold.ttf', weight: '600', style: 'normal' },
|
||||||
|
{ path: '../../public/fonts/BaiJamjuree-Bold.ttf', weight: '700', style: 'normal' },
|
||||||
|
],
|
||||||
display: 'swap',
|
display: 'swap',
|
||||||
});
|
});
|
||||||
|
|
||||||
export const poppinsFont = Poppins({
|
export const poppinsFont = localFont({
|
||||||
subsets: ['latin'],
|
src: [
|
||||||
weight: ['300', '400', '500', '600', '700'],
|
{ path: '../../public/fonts/Poppins-Light.ttf', weight: '300', style: 'normal' },
|
||||||
|
{ path: '../../public/fonts/Poppins-Regular.ttf', weight: '400', style: 'normal' },
|
||||||
|
{ path: '../../public/fonts/Poppins-Medium.ttf', weight: '500', style: 'normal' },
|
||||||
|
{ path: '../../public/fonts/Poppins-SemiBold.ttf', weight: '600', style: 'normal' },
|
||||||
|
{ path: '../../public/fonts/Poppins-Bold.ttf', weight: '700', style: 'normal' },
|
||||||
|
],
|
||||||
display: 'swap',
|
display: 'swap',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
FROM node:22-alpine AS deps
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package.json package-lock.json ./
|
||||||
|
RUN npm ci --prefer-offline --no-audit --no-fund
|
||||||
|
|
||||||
|
FROM node:22-alpine AS builder
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
|
COPY . .
|
||||||
|
ENV NX_DAEMON=false
|
||||||
|
RUN npm run qrcampaign:build
|
||||||
|
|
||||||
|
FROM nginx:alpine AS runner
|
||||||
|
RUN rm -rf /usr/share/nginx/html/*
|
||||||
|
COPY --from=builder /app/dist/apps/qrcampaign /usr/share/nginx/html
|
||||||
|
EXPOSE 80
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import nx from '@nx/eslint-plugin';
|
||||||
|
import baseConfig from '../../eslint.config.mjs';
|
||||||
|
|
||||||
|
export default [
|
||||||
|
...baseConfig,
|
||||||
|
...nx.configs['flat/react'],
|
||||||
|
{
|
||||||
|
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
|
||||||
|
// Override or add rules here
|
||||||
|
rules: {},
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>QR Code Generator - IMPHNEN</title>
|
||||||
|
<base href="/" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="description" content="QR Code Generator - IMPHNEN" />
|
||||||
|
<meta name="keywords" content="qr, campaign, imphnen" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/images/imphnen-logo.svg" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
'@tailwindcss/postcss': {
|
||||||
|
base: join(import.meta.dirname, '../../'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"name": "qrcampaign",
|
||||||
|
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||||
|
"sourceRoot": "apps/qrcampaign/src",
|
||||||
|
"projectType": "application",
|
||||||
|
"tags": [],
|
||||||
|
"// targets": "to see all targets run: nx show project qrcampaign --web",
|
||||||
|
"targets": {}
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 351 KiB |
@@ -0,0 +1,23 @@
|
|||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
export default function NotFoundPage() {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex items-center justify-center bg-gray-50">
|
||||||
|
<div className="text-center">
|
||||||
|
<h1 className="text-9xl font-bold text-gray-200 mb-4">404</h1>
|
||||||
|
<h2 className="text-3xl font-semibold text-gray-900 mb-4">
|
||||||
|
Page Not Found
|
||||||
|
</h2>
|
||||||
|
<p className="text-gray-600 mb-8">
|
||||||
|
The page you are looking for doesn't exist or has been moved.
|
||||||
|
</p>
|
||||||
|
<Link
|
||||||
|
to="/"
|
||||||
|
className="inline-block px-6 py-3 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors"
|
||||||
|
>
|
||||||
|
Go Back Home
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,262 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { ColumnDef } from '@tanstack/react-table';
|
||||||
|
import { DataTable } from '@imphnen-frontend-service/ui/organisms';
|
||||||
|
import {
|
||||||
|
campaignService,
|
||||||
|
Campaign,
|
||||||
|
CreateCampaignRequest,
|
||||||
|
} from '../../features/admin/api/campaign.service';
|
||||||
|
import {
|
||||||
|
DeleteOutlined,
|
||||||
|
CheckCircleOutlined,
|
||||||
|
PlusOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
|
||||||
|
export default function CampaignsPage() {
|
||||||
|
const [campaigns, setCampaigns] = useState<Campaign[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||||
|
const [createLoading, setCreateLoading] = useState(false);
|
||||||
|
const [formData, setFormData] = useState<CreateCampaignRequest>({
|
||||||
|
name: '',
|
||||||
|
url: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetchCampaigns = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
const data = await campaignService.getCampaigns();
|
||||||
|
setCampaigns(data);
|
||||||
|
} catch (err) {
|
||||||
|
setError('Failed to load campaigns');
|
||||||
|
console.error('Error fetching campaigns:', err);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchCampaigns();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleCreateCampaign = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
try {
|
||||||
|
setCreateLoading(true);
|
||||||
|
await campaignService.createCampaign(formData);
|
||||||
|
setShowCreateModal(false);
|
||||||
|
setFormData({ name: '', url: '' });
|
||||||
|
await fetchCampaigns();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error creating campaign:', err);
|
||||||
|
alert('Failed to create campaign');
|
||||||
|
} finally {
|
||||||
|
setCreateLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleActivateCampaign = async (campaignId: string) => {
|
||||||
|
try {
|
||||||
|
await campaignService.activateCampaign(campaignId);
|
||||||
|
await fetchCampaigns();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error activating campaign:', err);
|
||||||
|
alert('Failed to activate campaign');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteCampaign = async (
|
||||||
|
campaignId: string,
|
||||||
|
campaignName: string
|
||||||
|
) => {
|
||||||
|
if (!confirm(`Are you sure you want to delete "${campaignName}"?`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await campaignService.deleteCampaign(campaignId);
|
||||||
|
await fetchCampaigns();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error deleting campaign:', err);
|
||||||
|
alert('Failed to delete campaign');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns: ColumnDef<Campaign>[] = [
|
||||||
|
{
|
||||||
|
accessorKey: 'name',
|
||||||
|
header: 'Name',
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div className="font-medium text-gray-900">{row.original.name}</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'url',
|
||||||
|
header: 'URL',
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<a
|
||||||
|
href={row.original.url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-primary-600 hover:underline"
|
||||||
|
>
|
||||||
|
{row.original.url}
|
||||||
|
</a>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'is_active',
|
||||||
|
header: 'Status',
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<span
|
||||||
|
className={`px-2 py-1 rounded-2xl text-xs font-medium ${
|
||||||
|
row.original.is_active
|
||||||
|
? 'bg-success-100 text-success-800'
|
||||||
|
: 'bg-gray-100 text-gray-800'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{row.original.is_active ? 'Active' : 'Inactive'}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'created_at',
|
||||||
|
header: 'Created At',
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<span className="text-gray-600">
|
||||||
|
{new Date(row.original.created_at).toLocaleDateString()}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'actions',
|
||||||
|
header: 'Actions',
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{!row.original.is_active && (
|
||||||
|
<button
|
||||||
|
onClick={() => handleActivateCampaign(row.original.id)}
|
||||||
|
className="p-2 text-success-600 hover:bg-success-50 rounded-md transition-colors cursor-pointer"
|
||||||
|
title="Activate Campaign"
|
||||||
|
>
|
||||||
|
<CheckCircleOutlined />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
handleDeleteCampaign(row.original.id, row.original.name)
|
||||||
|
}
|
||||||
|
className="p-2 text-danger-600 hover:bg-danger-50 rounded-md transition-colors cursor-pointer"
|
||||||
|
title="Delete Campaign"
|
||||||
|
>
|
||||||
|
<DeleteOutlined />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<div className="text-gray-600">Loading campaigns...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="bg-red-50 border border-red-200 rounded-lg p-4 text-red-800">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="flex justify-between items-center mb-6">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-bold text-gray-900">
|
||||||
|
Campaign Management
|
||||||
|
</h1>
|
||||||
|
<p className="text-gray-600 mt-1">Manage your QR campaigns</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowCreateModal(true)}
|
||||||
|
className="flex items-center gap-2 bg-primary-500 text-white px-4 py-2 rounded-lg hover:bg-primary-600 transition-colors cursor-pointer"
|
||||||
|
>
|
||||||
|
<PlusOutlined />
|
||||||
|
Create Campaign
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DataTable data={campaigns} columns={columns} pageSize={10} />
|
||||||
|
|
||||||
|
{/* Create Campaign Modal */}
|
||||||
|
{showCreateModal && (
|
||||||
|
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
||||||
|
<div className="bg-white rounded-lg p-6 w-full max-w-md">
|
||||||
|
<h2 className="text-xl font-bold mb-4">Create New Campaign</h2>
|
||||||
|
<form onSubmit={handleCreateCampaign}>
|
||||||
|
<div className="mb-4">
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Campaign Name
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={formData.name}
|
||||||
|
onChange={(e) =>
|
||||||
|
setFormData({ ...formData, name: e.target.value })
|
||||||
|
}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
placeholder="IMPHNEN Promo Campaign"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mb-6">
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Campaign URL
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
value={formData.url}
|
||||||
|
onChange={(e) =>
|
||||||
|
setFormData({ ...formData, url: e.target.value })
|
||||||
|
}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
placeholder="https://imphnen.dev/promo"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-3 justify-end">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setShowCreateModal(false);
|
||||||
|
setFormData({ name: '', url: '' });
|
||||||
|
}}
|
||||||
|
className="px-4 py-2 text-gray-700 bg-gray-100 rounded-md hover:bg-gray-200 transition-colors cursor-pointer"
|
||||||
|
disabled={createLoading}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="px-4 py-2 bg-primary-500 text-white rounded-md hover:bg-primary-600 transition-colors disabled:opacity-50 cursor-pointer"
|
||||||
|
disabled={createLoading}
|
||||||
|
>
|
||||||
|
{createLoading ? 'Creating...' : 'Create'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { Outlet } from 'react-router-dom';
|
||||||
|
import { RequireAdmin } from '../features/admin/components/RequireAdmin';
|
||||||
|
|
||||||
|
export default function AdminLayout() {
|
||||||
|
return (
|
||||||
|
<RequireAdmin>
|
||||||
|
<Outlet />
|
||||||
|
</RequireAdmin>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export default function AdminPage() {
|
||||||
|
return <div className="p-4">Select a menu item from the sidebar.</div>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { ColumnDef } from '@tanstack/react-table';
|
||||||
|
import { DataTable } from '@imphnen-frontend-service/ui/organisms';
|
||||||
|
import { userService, User } from '../../features/admin/api/user.service';
|
||||||
|
import { DeleteOutlined, EditOutlined } from '@ant-design/icons';
|
||||||
|
|
||||||
|
export default function UsersPage() {
|
||||||
|
const [users, setUsers] = useState<User[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [editingUserId, setEditingUserId] = useState<string | null>(null);
|
||||||
|
const [selectedRole, setSelectedRole] = useState<string>('');
|
||||||
|
|
||||||
|
const fetchUsers = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
const data = await userService.getUsers();
|
||||||
|
setUsers(data);
|
||||||
|
} catch (err) {
|
||||||
|
setError('Failed to load users');
|
||||||
|
console.error('Error fetching users:', err);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchUsers();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleUpdateRole = async (userId: string, currentRole: string) => {
|
||||||
|
if (editingUserId === userId) {
|
||||||
|
// Save the role
|
||||||
|
try {
|
||||||
|
await userService.updateUserRole(userId, selectedRole);
|
||||||
|
setEditingUserId(null);
|
||||||
|
setSelectedRole('');
|
||||||
|
await fetchUsers();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error updating user role:', err);
|
||||||
|
alert('Failed to update user role');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Start editing
|
||||||
|
setEditingUserId(userId);
|
||||||
|
setSelectedRole(currentRole);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteUser = async (userId: string, userName: string) => {
|
||||||
|
if (!confirm(`Are you sure you want to delete user "${userName}"?`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await userService.deleteUser(userId);
|
||||||
|
await fetchUsers();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error deleting user:', err);
|
||||||
|
alert('Failed to delete user');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns: ColumnDef<User>[] = [
|
||||||
|
{
|
||||||
|
accessorKey: 'name',
|
||||||
|
header: 'Name',
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div className="font-medium text-gray-900">{row.original.name}</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'email',
|
||||||
|
header: 'Email',
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div className="text-gray-600">{row.original.email}</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'role',
|
||||||
|
header: 'Role',
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const isEditing = editingUserId === row.original.id;
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{isEditing ? (
|
||||||
|
<select
|
||||||
|
value={selectedRole}
|
||||||
|
onChange={(e) => setSelectedRole(e.target.value)}
|
||||||
|
className="px-2 py-1 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
>
|
||||||
|
<option value="user">User</option>
|
||||||
|
<option value="admin">Admin</option>
|
||||||
|
</select>
|
||||||
|
) : (
|
||||||
|
<span
|
||||||
|
className={`px-2 py-1 rounded-2xl text-xs font-medium ${
|
||||||
|
row.original.role === 'admin'
|
||||||
|
? 'bg-danger-100 text-danger-800'
|
||||||
|
: 'bg-primary-100 text-primary-800'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{row.original.role.charAt(0).toUpperCase() +
|
||||||
|
row.original.role.slice(1)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'created_at',
|
||||||
|
header: 'Created At',
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<span className="text-gray-600">
|
||||||
|
{new Date(row.original.created_at).toLocaleDateString()}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'actions',
|
||||||
|
header: 'Actions',
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const isEditing = editingUserId === row.original.id;
|
||||||
|
return (
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
handleUpdateRole(row.original.id, row.original.role)
|
||||||
|
}
|
||||||
|
className={`p-2 rounded-md transition-colors cursor-pointer ${
|
||||||
|
isEditing
|
||||||
|
? 'text-success-600 hover:bg-success-50'
|
||||||
|
: 'text-primary-600 hover:bg-primary-50'
|
||||||
|
}`}
|
||||||
|
title={isEditing ? 'Save Role' : 'Change Role'}
|
||||||
|
>
|
||||||
|
<EditOutlined />
|
||||||
|
</button>
|
||||||
|
{isEditing && (
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setEditingUserId(null);
|
||||||
|
setSelectedRole('');
|
||||||
|
}}
|
||||||
|
className="p-2 text-gray-600 hover:bg-gray-50 rounded-md transition-colors cursor-pointer"
|
||||||
|
title="Cancel"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{!isEditing && (
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
handleDeleteUser(row.original.id, row.original.name)
|
||||||
|
}
|
||||||
|
className="p-2 text-danger-600 hover:bg-danger-50 rounded-md transition-colors cursor-pointer"
|
||||||
|
title="Delete User"
|
||||||
|
>
|
||||||
|
<DeleteOutlined />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<div className="text-gray-600">Loading users...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="bg-red-50 border border-red-200 rounded-lg p-4 text-red-800">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="mb-6">
|
||||||
|
<h1 className="text-2xl font-bold text-gray-900">User Management</h1>
|
||||||
|
<p className="text-gray-600 mt-1">Manage users and their roles</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DataTable data={users} columns={columns} pageSize={10} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
import { FC, ReactElement, useEffect, useState, useRef } from 'react';
|
||||||
|
import { useNavigate } from 'react-router';
|
||||||
|
import { useGitHubCallback } from '@imphnen-frontend-service/service';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
const CallbackPage: FC = (): ReactElement => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { mutateAsync: exchangeGitHubCode } = useGitHubCallback();
|
||||||
|
const [isProcessing, setIsProcessing] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const hasRunRef = useRef(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleCallback = async () => {
|
||||||
|
if (hasRunRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
hasRunRef.current = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check URL hash for Supabase email confirmation callback
|
||||||
|
const hashParams = new URLSearchParams(
|
||||||
|
globalThis.location.hash.substring(1)
|
||||||
|
);
|
||||||
|
const urlParams = new URLSearchParams(globalThis.location.search);
|
||||||
|
|
||||||
|
const type = hashParams.get('type') || urlParams.get('type');
|
||||||
|
const accessToken =
|
||||||
|
hashParams.get('access_token') || urlParams.get('access_token');
|
||||||
|
|
||||||
|
// Debug: log what we received
|
||||||
|
console.log('[Callback] Params:', {
|
||||||
|
type,
|
||||||
|
accessToken: !!accessToken,
|
||||||
|
hash: globalThis.location.hash,
|
||||||
|
search: globalThis.location.search,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle Supabase email callbacks (has access_token in hash or query)
|
||||||
|
// This includes: signup confirmation, email confirmation, password recovery
|
||||||
|
if (accessToken) {
|
||||||
|
setIsProcessing(false);
|
||||||
|
|
||||||
|
// Password recovery - type is 'recovery' or we have access_token from reset email
|
||||||
|
if (type === 'recovery' || type === 'magiclink') {
|
||||||
|
toast.success('Email verified! Please set your new password.');
|
||||||
|
navigate('/auth/reset-password?access_token=' + accessToken);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Signup/Email confirmation
|
||||||
|
if (type === 'signup' || type === 'email_confirmation') {
|
||||||
|
toast.success(
|
||||||
|
'Email verified successfully! Please log in to continue.'
|
||||||
|
);
|
||||||
|
navigate('/auth/login');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have access_token but unknown type, assume it's password recovery
|
||||||
|
// (Supabase sometimes sends without explicit type)
|
||||||
|
toast.success('Email verified! Please set your new password.');
|
||||||
|
navigate('/auth/reset-password?access_token=' + accessToken);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the code from URL query params (GitHub OAuth)
|
||||||
|
const code = urlParams.get('code');
|
||||||
|
|
||||||
|
if (!code) {
|
||||||
|
throw new Error('No authorization code received');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exchange the code for tokens using backend API (GitHub OAuth)
|
||||||
|
const result = await exchangeGitHubCode({ code });
|
||||||
|
|
||||||
|
toast.success('Login successful!');
|
||||||
|
setIsProcessing(false);
|
||||||
|
|
||||||
|
// Check if user has completed onboarding (has location)
|
||||||
|
if (result.user.location) {
|
||||||
|
globalThis.location.replace('/dashboard');
|
||||||
|
} else {
|
||||||
|
globalThis.location.replace('/onboarding/user');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Callback] Error:', err);
|
||||||
|
setError((err as Error).message);
|
||||||
|
setIsProcessing(false);
|
||||||
|
toast.error('An error occurred during login');
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
navigate('/auth/login');
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handleCallback();
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
// Check if error is related to private email
|
||||||
|
const isPrivateEmailError =
|
||||||
|
error.toLowerCase().includes('failed to create user') ||
|
||||||
|
error.toLowerCase().includes('email') ||
|
||||||
|
error.toLowerCase().includes('user record');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center items-center min-h-screen bg-gray-50 px-4">
|
||||||
|
<div className="bg-white w-full max-w-2xl p-8 rounded-2xl shadow-lg border border-red-200">
|
||||||
|
<div className="text-center mb-6">
|
||||||
|
<div className="text-red-500 text-5xl mb-4">⚠️</div>
|
||||||
|
<h2 className="text-2xl font-bold text-gray-900 mb-2">
|
||||||
|
GitHub Login Failed
|
||||||
|
</h2>
|
||||||
|
<p className="text-red-600 mb-4 whitespace-pre-line">{error}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isPrivateEmailError && (
|
||||||
|
<div className="bg-amber-50 border border-amber-200 rounded-lg p-4 mb-6">
|
||||||
|
<h3 className="font-semibold text-amber-800 mb-2">
|
||||||
|
Is your GitHub email set to private?
|
||||||
|
</h3>
|
||||||
|
<p className="text-amber-700 text-sm mb-3">
|
||||||
|
GitHub login requires a public email address. Please follow
|
||||||
|
these steps:
|
||||||
|
</p>
|
||||||
|
<ol className="text-amber-700 text-sm list-decimal list-inside space-y-1 mb-3">
|
||||||
|
<li>
|
||||||
|
Go to{' '}
|
||||||
|
<a
|
||||||
|
href="https://github.com/settings/emails"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="underline hover:text-amber-900"
|
||||||
|
>
|
||||||
|
GitHub Email Settings
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>Uncheck "Keep my email addresses private"</li>
|
||||||
|
<li>
|
||||||
|
Or go to{' '}
|
||||||
|
<a
|
||||||
|
href="https://github.com/settings/profile"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="underline hover:text-amber-900"
|
||||||
|
>
|
||||||
|
Profile Settings
|
||||||
|
</a>{' '}
|
||||||
|
and set a public email
|
||||||
|
</li>
|
||||||
|
<li>Try signing in with GitHub again</li>
|
||||||
|
</ol>
|
||||||
|
<p className="text-amber-600 text-xs">
|
||||||
|
Alternatively, you can sign up using email and password instead.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<p className="text-gray-600 text-sm mt-6 text-center">
|
||||||
|
Redirecting to login page in 3 seconds...
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center items-center min-h-screen bg-gray-50">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="inline-block animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600 mb-4"></div>
|
||||||
|
<h2 className="text-xl font-semibold text-gray-900 mb-2">
|
||||||
|
Completing login...
|
||||||
|
</h2>
|
||||||
|
<p className="text-gray-600">Please wait</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CallbackPage;
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { useForgotPassword } from '@imphnen-frontend-service/service';
|
||||||
|
import { Link, useNavigate } from 'react-router';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import { Icon } from '@iconify/react';
|
||||||
|
|
||||||
|
export default function ForgotPasswordPage() {
|
||||||
|
const [email, setEmail] = useState('');
|
||||||
|
const [emailSent, setEmailSent] = useState(false);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const forgotPasswordMutation = useForgotPassword();
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (!email) {
|
||||||
|
toast.error('Please enter your email');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await forgotPasswordMutation.mutateAsync({ email });
|
||||||
|
|
||||||
|
setEmailSent(true);
|
||||||
|
toast.success('Password reset email sent! Check your inbox.');
|
||||||
|
} catch (err) {
|
||||||
|
toast.error((err as Error).message || 'Failed to send reset email');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (emailSent) {
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center items-center min-h-screen bg-gray-50 p-4">
|
||||||
|
<div className="bg-white w-full max-w-md p-8 rounded-2xl shadow-lg border border-gray-200 text-center">
|
||||||
|
<div className="mb-6">
|
||||||
|
<div className="mx-auto w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mb-4">
|
||||||
|
<span className="text-3xl">✓</span>
|
||||||
|
</div>
|
||||||
|
<h2 className="text-3xl font-bold text-gray-900 mb-2">
|
||||||
|
Check Your Email
|
||||||
|
</h2>
|
||||||
|
<p className="text-gray-600">
|
||||||
|
We've sent a password reset link to <strong>{email}</strong>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
Click the link in the email to reset your password. The link will
|
||||||
|
expire in 1 hour.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<Link to="/auth/login">
|
||||||
|
<button className="w-full py-3 bg-primary-600 text-white rounded-lg font-semibold hover:bg-primary-700 transition-colors">
|
||||||
|
Back to Login
|
||||||
|
</button>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => setEmailSent(false)}
|
||||||
|
className="w-full py-3 text-gray-600 hover:text-gray-900 transition-colors"
|
||||||
|
>
|
||||||
|
Send another email
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center items-center min-h-screen bg-gray-50 p-4">
|
||||||
|
<div className="bg-white w-full max-w-md p-8 rounded-2xl shadow-lg border border-gray-200">
|
||||||
|
<div className="flex items-center justify-between mb-6">
|
||||||
|
<button
|
||||||
|
onClick={() => navigate('/auth/login')}
|
||||||
|
className="cursor-pointer text-primary-500 hover:text-primary-600 text-base font-sans flex items-center"
|
||||||
|
>
|
||||||
|
<Icon icon="ic:baseline-chevron-left" width="24" height="24" />
|
||||||
|
Back to Login
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="text-center mb-8">
|
||||||
|
<h2 className="text-3xl font-bold text-gray-900 mb-2">
|
||||||
|
Forgot Password?
|
||||||
|
</h2>
|
||||||
|
<p className="text-gray-600">
|
||||||
|
No worries, we'll send you reset instructions
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
htmlFor="email"
|
||||||
|
className="block text-sm font-medium text-gray-700 mb-1"
|
||||||
|
>
|
||||||
|
Email Address
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="email"
|
||||||
|
type="email"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
placeholder="your@email.com"
|
||||||
|
disabled={forgotPasswordMutation.isPending}
|
||||||
|
className="bg-white text-gray-900 w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent disabled:bg-gray-100 disabled:cursor-not-allowed"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={forgotPasswordMutation.isPending}
|
||||||
|
className="w-full py-3 bg-primary-600 text-white rounded-lg font-semibold hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors"
|
||||||
|
>
|
||||||
|
{forgotPasswordMutation.isPending
|
||||||
|
? 'Sending...'
|
||||||
|
: 'Send Reset Link'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,234 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { GithubOutlined } from '@ant-design/icons';
|
||||||
|
import { useNavigate, Link } from 'react-router';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import { Icon } from '@iconify/react';
|
||||||
|
import { useAuthStore } from '../../features/auth/store/auth.store';
|
||||||
|
|
||||||
|
export default function LoginPage() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const login = useAuthStore((state) => state.login);
|
||||||
|
const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
|
||||||
|
|
||||||
|
const [isGithubLoading, setIsGithubLoading] = useState(false);
|
||||||
|
const [email, setEmail] = useState('');
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isAuthenticated) {
|
||||||
|
navigate('/');
|
||||||
|
}
|
||||||
|
}, [isAuthenticated, navigate]);
|
||||||
|
|
||||||
|
// Check for password reset tokens in URL and redirect to reset-password page
|
||||||
|
useEffect(() => {
|
||||||
|
const hashParams = new URLSearchParams(
|
||||||
|
globalThis.location.hash.substring(1)
|
||||||
|
);
|
||||||
|
const urlParams = new URLSearchParams(globalThis.location.search);
|
||||||
|
|
||||||
|
const accessToken =
|
||||||
|
hashParams.get('access_token') || urlParams.get('access_token');
|
||||||
|
const type = hashParams.get('type') || urlParams.get('type');
|
||||||
|
|
||||||
|
// If we have an access_token, this is likely a password reset redirect that landed on the wrong page
|
||||||
|
if (accessToken) {
|
||||||
|
console.log(
|
||||||
|
'[Login] Detected access_token, redirecting to reset-password page'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check if it's a password recovery
|
||||||
|
if (type === 'recovery' || type === 'magiclink' || !type) {
|
||||||
|
toast.info('Redirecting to password reset...');
|
||||||
|
navigate('/auth/reset-password?access_token=' + accessToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [navigate]);
|
||||||
|
|
||||||
|
const handleEmailLogin = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
if (!email || !password) {
|
||||||
|
setError('Please enter both email and password');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsSubmitting(true);
|
||||||
|
try {
|
||||||
|
const success = await login(email, password);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
toast.success('Login successful!');
|
||||||
|
navigate('/');
|
||||||
|
} else {
|
||||||
|
setError('Login failed. Please check your credentials.');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Login] Email login failed:', err);
|
||||||
|
setError('Login failed. Please try again.');
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleGithubLogin = async () => {
|
||||||
|
// TODO: Implement GitHub login with new auth service if needed
|
||||||
|
toast.info('GitHub login coming soon');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center items-center min-h-screen bg-gray-50 p-4">
|
||||||
|
<div className="bg-white w-full max-w-md p-8 rounded-2xl shadow-lg border border-gray-200">
|
||||||
|
{/* <div className="flex items-center justify-between mb-6">
|
||||||
|
<button
|
||||||
|
onClick={() => navigate('/')}
|
||||||
|
className="cursor-pointer text-primary-500 hover:text-primary-600 text-base font-sans flex items-center"
|
||||||
|
>
|
||||||
|
<Icon icon="ic:baseline-chevron-left" width="24" height="24" />
|
||||||
|
Back to Homepage
|
||||||
|
</button>
|
||||||
|
</div> */}
|
||||||
|
|
||||||
|
<div className="text-center mb-8">
|
||||||
|
<h2 className="text-3xl font-bold text-gray-900 mb-2">
|
||||||
|
Welcome Back
|
||||||
|
</h2>
|
||||||
|
<p className="text-gray-600">Sign in to continue to IMPHNEN</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="mb-6 p-3 bg-red-50 border border-red-200 rounded-lg">
|
||||||
|
<p className="text-red-600 text-sm">{error}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Traditional Login Form */}
|
||||||
|
<form onSubmit={handleEmailLogin} className="space-y-4 mb-6">
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
htmlFor="email"
|
||||||
|
className="block text-sm font-medium text-gray-700 mb-1"
|
||||||
|
>
|
||||||
|
Email
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="email"
|
||||||
|
type="email"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
placeholder="yourname@example.com"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
className="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent bg-white text-gray-900 placeholder:text-gray-400 disabled:bg-gray-100 disabled:cursor-not-allowed"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center justify-between mb-1">
|
||||||
|
<label
|
||||||
|
htmlFor="password"
|
||||||
|
className="block text-sm font-medium text-gray-700"
|
||||||
|
>
|
||||||
|
Password
|
||||||
|
</label>
|
||||||
|
<Link
|
||||||
|
to="/auth/forgot-password"
|
||||||
|
className="text-sm text-primary-600 hover:text-primary-700 font-medium"
|
||||||
|
>
|
||||||
|
Forgot password?
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div className="relative">
|
||||||
|
<input
|
||||||
|
id="password"
|
||||||
|
type={showPassword ? 'text' : 'password'}
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
placeholder="••••••••"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
className="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent bg-white text-gray-900 placeholder:text-gray-400 disabled:bg-gray-100 disabled:cursor-not-allowed pr-10"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowPassword(!showPassword)}
|
||||||
|
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-500 hover:text-gray-700"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon={showPassword ? 'mdi:eye-off' : 'mdi:eye'}
|
||||||
|
className="text-xl"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
className="w-full py-3 bg-primary-600 text-white rounded-lg font-semibold hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors cursor-pointer"
|
||||||
|
>
|
||||||
|
{isSubmitting ? 'Signing in...' : 'Sign in'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div className="relative my-6">
|
||||||
|
<div className="absolute inset-0 flex items-center">
|
||||||
|
<div className="w-full border-t border-gray-300"></div>
|
||||||
|
</div>
|
||||||
|
<div className="relative flex justify-center text-sm">
|
||||||
|
<span className="px-2 bg-white text-gray-500">
|
||||||
|
Or continue with
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={handleGithubLogin}
|
||||||
|
disabled={isGithubLoading}
|
||||||
|
type="button"
|
||||||
|
className="w-full py-3 flex items-center justify-center gap-2 bg-gray-100 border border-gray-300 rounded-lg font-semibold text-gray-900 hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 disabled:bg-gray-100 disabled:cursor-not-allowed transition-colors cursor-pointer"
|
||||||
|
>
|
||||||
|
<GithubOutlined className="text-xl" />
|
||||||
|
<span>
|
||||||
|
{isGithubLoading ? 'Connecting...' : 'Sign in with GitHub'}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<p className="mt-3 text-xs text-center text-gray-500">
|
||||||
|
Make sure your GitHub email is{' '}
|
||||||
|
<a
|
||||||
|
href="https://github.com/settings/emails"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-primary-600 hover:underline"
|
||||||
|
>
|
||||||
|
set to public
|
||||||
|
</a>{' '}
|
||||||
|
for GitHub sign in to work.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="mt-6 text-center">
|
||||||
|
<p className="text-gray-600 text-sm">
|
||||||
|
Don't have an account?{' '}
|
||||||
|
<Link
|
||||||
|
to="/auth/signup"
|
||||||
|
className="text-primary-600 hover:text-primary-700 font-semibold"
|
||||||
|
>
|
||||||
|
Sign up
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6 text-center">
|
||||||
|
<p className="text-gray-500 text-xs">
|
||||||
|
By signing in, you agree to our Terms of Service and Privacy Policy
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
useResetPassword,
|
||||||
|
useAuthStore,
|
||||||
|
} from '@imphnen-frontend-service/service';
|
||||||
|
import { useNavigate } from 'react-router';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import { Icon } from '@iconify/react';
|
||||||
|
|
||||||
|
export default function ResetPasswordPage() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { clearSession } = useAuthStore();
|
||||||
|
const resetPasswordMutation = useResetPassword();
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const [confirmPassword, setConfirmPassword] = useState('');
|
||||||
|
const [accessToken, setAccessToken] = useState<string | null>(null);
|
||||||
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
|
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Get the access_token from URL hash (Supabase sends it as hash fragment)
|
||||||
|
// or from query params (when redirected from callback page)
|
||||||
|
const hashParams = new URLSearchParams(
|
||||||
|
globalThis.location.hash.substring(1)
|
||||||
|
);
|
||||||
|
const queryParams = new URLSearchParams(globalThis.location.search);
|
||||||
|
const token =
|
||||||
|
hashParams.get('access_token') || queryParams.get('access_token');
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
setAccessToken(token);
|
||||||
|
} else {
|
||||||
|
toast.error('Invalid or expired reset link');
|
||||||
|
setTimeout(() => navigate('/auth/forgot-password'), 2000);
|
||||||
|
}
|
||||||
|
}, [navigate]);
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (password !== confirmPassword) {
|
||||||
|
toast.error('Passwords do not match');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password.length < 6) {
|
||||||
|
toast.error('Password must be at least 6 characters');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!accessToken) {
|
||||||
|
toast.error('Invalid reset token');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await resetPasswordMutation.mutateAsync({
|
||||||
|
access_token: accessToken,
|
||||||
|
new_password: password,
|
||||||
|
});
|
||||||
|
|
||||||
|
toast.success('Password updated successfully!');
|
||||||
|
|
||||||
|
// Clear session and redirect to login
|
||||||
|
clearSession();
|
||||||
|
navigate('/auth/login');
|
||||||
|
} catch (err) {
|
||||||
|
toast.error((err as Error).message || 'Failed to reset password');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!accessToken) {
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center items-center min-h-screen bg-gray-50">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="inline-block animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600 mb-4"></div>
|
||||||
|
<p className="text-gray-600">Verifying reset link...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center items-center min-h-screen bg-gray-50 p-4">
|
||||||
|
<div className="bg-white w-full max-w-md p-8 rounded-2xl shadow-lg border border-gray-200">
|
||||||
|
<div className="text-center mb-8">
|
||||||
|
<h2 className="text-3xl font-bold text-gray-900 mb-2">
|
||||||
|
Set New Password
|
||||||
|
</h2>
|
||||||
|
<p className="text-gray-600">Enter your new password below</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
htmlFor="password"
|
||||||
|
className="block text-sm font-medium text-gray-700 mb-1"
|
||||||
|
>
|
||||||
|
New Password
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<input
|
||||||
|
id="password"
|
||||||
|
type={showPassword ? 'text' : 'password'}
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
placeholder="••••••••"
|
||||||
|
disabled={resetPasswordMutation.isPending}
|
||||||
|
className="w-full px-4 py-2.5 pr-12 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent disabled:bg-gray-100 disabled:cursor-not-allowed bg-white text-gray-900"
|
||||||
|
required
|
||||||
|
minLength={6}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowPassword(!showPassword)}
|
||||||
|
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-500 hover:text-gray-700"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon={showPassword ? 'mdi:eye-off' : 'mdi:eye'}
|
||||||
|
className="text-xl"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
htmlFor="confirmPassword"
|
||||||
|
className="block text-sm font-medium text-gray-700 mb-1"
|
||||||
|
>
|
||||||
|
Confirm New Password
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<input
|
||||||
|
id="confirmPassword"
|
||||||
|
type={showConfirmPassword ? 'text' : 'password'}
|
||||||
|
value={confirmPassword}
|
||||||
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||||
|
placeholder="••••••••"
|
||||||
|
disabled={resetPasswordMutation.isPending}
|
||||||
|
className="w-full px-4 py-2.5 pr-12 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent disabled:bg-gray-100 disabled:cursor-not-allowed bg-white text-gray-900"
|
||||||
|
required
|
||||||
|
minLength={6}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
|
||||||
|
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-500 hover:text-gray-700"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon={showConfirmPassword ? 'mdi:eye-off' : 'mdi:eye'}
|
||||||
|
className="text-xl"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={resetPasswordMutation.isPending}
|
||||||
|
className="w-full py-3 bg-primary-600 text-white rounded-lg font-semibold hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors"
|
||||||
|
>
|
||||||
|
{resetPasswordMutation.isPending
|
||||||
|
? 'Updating...'
|
||||||
|
: 'Update Password'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,305 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { useGitHubAuth } from '@imphnen-frontend-service/service';
|
||||||
|
import { GithubOutlined } from '@ant-design/icons';
|
||||||
|
import { useNavigate, Link } from 'react-router';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import { Icon } from '@iconify/react';
|
||||||
|
import { useAuthStore } from '../../features/auth/store/auth.store';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
const signupSchema = z
|
||||||
|
.object({
|
||||||
|
fullname: z
|
||||||
|
.string()
|
||||||
|
.min(1, 'Full name is required')
|
||||||
|
.min(2, 'Full name must be at least 2 characters'),
|
||||||
|
email: z
|
||||||
|
.string()
|
||||||
|
.min(1, 'Email is required')
|
||||||
|
.email('Please enter a valid email address'),
|
||||||
|
password: z
|
||||||
|
.string()
|
||||||
|
.min(1, 'Password is required')
|
||||||
|
.min(6, 'Password must be at least 6 characters'),
|
||||||
|
confirmPassword: z.string().min(1, 'Please confirm your password'),
|
||||||
|
})
|
||||||
|
.refine((data) => data.password === data.confirmPassword, {
|
||||||
|
message: 'Passwords do not match',
|
||||||
|
path: ['confirmPassword'],
|
||||||
|
});
|
||||||
|
|
||||||
|
type SignupFormData = z.infer<typeof signupSchema>;
|
||||||
|
|
||||||
|
export default function SignupPage() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const registerUser = useAuthStore((state) => state.register);
|
||||||
|
|
||||||
|
const { signInWithGitHub } = useGitHubAuth();
|
||||||
|
const [isGithubLoading, setIsGithubLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
|
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
||||||
|
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
handleSubmit,
|
||||||
|
formState: { errors, isValid },
|
||||||
|
} = useForm<SignupFormData>({
|
||||||
|
resolver: zodResolver(signupSchema),
|
||||||
|
mode: 'onChange',
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = async (data: SignupFormData) => {
|
||||||
|
setError(null);
|
||||||
|
setIsSubmitting(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await registerUser(data.fullname, data.email, data.password);
|
||||||
|
toast.success('Registration successful! Redirecting...');
|
||||||
|
navigate('/');
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('[Signup] Email signup failed:', err);
|
||||||
|
// Construct a user-friendly error message
|
||||||
|
const errorMessage =
|
||||||
|
err.response?.data?.message || err.message || 'Signup failed';
|
||||||
|
setError(errorMessage);
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleGithubLogin = async () => {
|
||||||
|
try {
|
||||||
|
setIsGithubLoading(true);
|
||||||
|
|
||||||
|
const result = await signInWithGitHub();
|
||||||
|
|
||||||
|
if (result?.url) {
|
||||||
|
globalThis.location.href = result.url;
|
||||||
|
} else {
|
||||||
|
setIsGithubLoading(false);
|
||||||
|
setError('Failed to get GitHub OAuth URL');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Signup] GitHub login failed:', err);
|
||||||
|
setError((err as Error).message || 'GitHub login failed');
|
||||||
|
setIsGithubLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const inputBaseClass =
|
||||||
|
'w-full px-4 py-2.5 border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent bg-white text-gray-900 placeholder:text-gray-400 disabled:bg-gray-100 disabled:cursor-not-allowed';
|
||||||
|
const inputErrorClass = 'border-red-500';
|
||||||
|
const inputNormalClass = 'border-gray-300';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center items-center min-h-screen bg-gray-50 p-4">
|
||||||
|
<div className="bg-white w-full max-w-md p-8 rounded-2xl shadow-lg border border-gray-200">
|
||||||
|
<div className="flex items-center justify-between mb-6">
|
||||||
|
<button
|
||||||
|
onClick={() => navigate('/')}
|
||||||
|
className="cursor-pointer text-primary-500 hover:text-primary-600 text-base font-sans flex items-center"
|
||||||
|
>
|
||||||
|
<Icon icon="ic:baseline-chevron-left" width="24" height="24" />
|
||||||
|
Back to Homepage
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-center mb-8">
|
||||||
|
<h2 className="text-3xl font-bold text-gray-900 mb-2">
|
||||||
|
Create Account
|
||||||
|
</h2>
|
||||||
|
<p className="text-gray-600">Join IMPHNEN community</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="mb-6 p-3 bg-red-50 border border-red-200 rounded-lg">
|
||||||
|
<p className="text-red-600 text-sm">{error}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
htmlFor="fullname"
|
||||||
|
className="block text-sm font-medium text-gray-700 mb-1"
|
||||||
|
>
|
||||||
|
Full Name
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="fullname"
|
||||||
|
type="text"
|
||||||
|
{...register('fullname')}
|
||||||
|
placeholder="John Doe"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
className={`${inputBaseClass} ${
|
||||||
|
errors.fullname ? inputErrorClass : inputNormalClass
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
{errors.fullname && (
|
||||||
|
<p className="mt-1 text-sm text-red-500">
|
||||||
|
{errors.fullname.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
htmlFor="email"
|
||||||
|
className="block text-sm font-medium text-gray-700 mb-1"
|
||||||
|
>
|
||||||
|
Email
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="email"
|
||||||
|
type="email"
|
||||||
|
{...register('email')}
|
||||||
|
placeholder="yourname@example.com"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
className={`${inputBaseClass} ${
|
||||||
|
errors.email ? inputErrorClass : inputNormalClass
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
{errors.email && (
|
||||||
|
<p className="mt-1 text-sm text-red-500">
|
||||||
|
{errors.email.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
htmlFor="password"
|
||||||
|
className="block text-sm font-medium text-gray-700 mb-1"
|
||||||
|
>
|
||||||
|
Password
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<input
|
||||||
|
id="password"
|
||||||
|
type={showPassword ? 'text' : 'password'}
|
||||||
|
{...register('password')}
|
||||||
|
placeholder="••••••••"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
className={`${inputBaseClass} pr-12 ${
|
||||||
|
errors.password ? inputErrorClass : inputNormalClass
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowPassword(!showPassword)}
|
||||||
|
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-500 hover:text-gray-700"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon={showPassword ? 'mdi:eye-off' : 'mdi:eye'}
|
||||||
|
className="text-xl"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{errors.password && (
|
||||||
|
<p className="mt-1 text-sm text-red-500">
|
||||||
|
{errors.password.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
htmlFor="confirmPassword"
|
||||||
|
className="block text-sm font-medium text-gray-700 mb-1"
|
||||||
|
>
|
||||||
|
Confirm Password
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<input
|
||||||
|
id="confirmPassword"
|
||||||
|
type={showConfirmPassword ? 'text' : 'password'}
|
||||||
|
{...register('confirmPassword')}
|
||||||
|
placeholder="••••••••"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
className={`${inputBaseClass} pr-12 ${
|
||||||
|
errors.confirmPassword ? inputErrorClass : inputNormalClass
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
|
||||||
|
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-500 hover:text-gray-700"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon={showConfirmPassword ? 'mdi:eye-off' : 'mdi:eye'}
|
||||||
|
className="text-xl"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{errors.confirmPassword && (
|
||||||
|
<p className="mt-1 text-sm text-red-500">
|
||||||
|
{errors.confirmPassword.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={!isValid || isSubmitting}
|
||||||
|
className="w-full py-3 bg-primary-600 text-white rounded-lg font-semibold hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors cursor-pointer"
|
||||||
|
>
|
||||||
|
{isSubmitting ? 'Creating account...' : 'Create Account'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div className="my-6 flex items-center">
|
||||||
|
<div className="flex-1 border-t border-gray-300"></div>
|
||||||
|
<span className="px-4 text-sm text-gray-500">OR</span>
|
||||||
|
<div className="flex-1 border-t border-gray-300"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={handleGithubLogin}
|
||||||
|
disabled={isGithubLoading}
|
||||||
|
type="button"
|
||||||
|
className="w-full py-3 flex items-center justify-center gap-2 bg-gray-100 border border-gray-300 rounded-lg font-semibold text-gray-900 hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 disabled:bg-gray-100 disabled:cursor-not-allowed transition-colors cursor-pointer"
|
||||||
|
>
|
||||||
|
<GithubOutlined className="text-xl" />
|
||||||
|
<span>
|
||||||
|
{isGithubLoading ? 'Connecting...' : 'Sign up with GitHub'}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<p className="mt-3 text-xs text-center text-gray-500">
|
||||||
|
Make sure your GitHub email is{' '}
|
||||||
|
<a
|
||||||
|
href="https://github.com/settings/emails"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-primary-600 hover:underline"
|
||||||
|
>
|
||||||
|
set to public
|
||||||
|
</a>{' '}
|
||||||
|
for GitHub sign up to work.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="mt-6 text-center">
|
||||||
|
<p className="text-gray-600 text-sm">
|
||||||
|
Already have an account?{' '}
|
||||||
|
<Link
|
||||||
|
to="/auth/login"
|
||||||
|
className="text-primary-600 hover:text-primary-700 font-semibold"
|
||||||
|
>
|
||||||
|
Sign in
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6 text-center">
|
||||||
|
<p className="text-gray-500 text-xs">
|
||||||
|
By signing up, you agree to our Terms of Service and Privacy Policy
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { useRouteError, isRouteErrorResponse } from 'react-router-dom';
|
||||||
|
|
||||||
|
export default function ErrorPage() {
|
||||||
|
const error = useRouteError();
|
||||||
|
let errorMessage: string;
|
||||||
|
|
||||||
|
if (isRouteErrorResponse(error)) {
|
||||||
|
errorMessage = error.statusText;
|
||||||
|
} else if (error instanceof Error) {
|
||||||
|
errorMessage = error.message;
|
||||||
|
} else if (typeof error === 'string') {
|
||||||
|
errorMessage = error;
|
||||||
|
} else {
|
||||||
|
console.error(error);
|
||||||
|
errorMessage = 'Unknown error';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex items-center justify-center bg-gray-50">
|
||||||
|
<div className="text-center">
|
||||||
|
<h1 className="text-6xl font-bold text-red-600 mb-4">Oops!</h1>
|
||||||
|
<p className="text-xl text-gray-700 mb-2">
|
||||||
|
Sorry, an unexpected error has occurred.
|
||||||
|
</p>
|
||||||
|
<p className="text-gray-500 italic">{errorMessage}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import { api } from '../../auth/api/auth.service';
|
||||||
|
|
||||||
|
// Types
|
||||||
|
export interface Campaign {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
is_active: boolean;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateCampaignRequest {
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CampaignsResponse {
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
data: Campaign[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CampaignResponse {
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
data: Campaign;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DeleteResponse {
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const campaignService = {
|
||||||
|
getCampaigns: async (): Promise<Campaign[]> => {
|
||||||
|
const response = await api.get<CampaignsResponse>('/campaigns');
|
||||||
|
return response.data.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
createCampaign: async (data: CreateCampaignRequest): Promise<Campaign> => {
|
||||||
|
const response = await api.post<CampaignResponse>('/campaigns', data);
|
||||||
|
return response.data.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
activateCampaign: async (campaignId: string): Promise<Campaign> => {
|
||||||
|
const response = await api.put<CampaignResponse>(
|
||||||
|
`/campaigns/${campaignId}/activate`
|
||||||
|
);
|
||||||
|
return response.data.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteCampaign: async (campaignId: string): Promise<void> => {
|
||||||
|
await api.delete<DeleteResponse>(`/campaigns/${campaignId}`);
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { api } from '../../auth/api/auth.service';
|
||||||
|
|
||||||
|
// Types
|
||||||
|
export interface User {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
name: string;
|
||||||
|
role: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateUserRoleRequest {
|
||||||
|
role: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UsersResponse {
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
data: User[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UserResponse {
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
data: User;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DeleteResponse {
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const userService = {
|
||||||
|
getUsers: async (): Promise<User[]> => {
|
||||||
|
const response = await api.get<UsersResponse>('/users');
|
||||||
|
return response.data.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
updateUserRole: async (
|
||||||
|
userId: string,
|
||||||
|
role: string
|
||||||
|
): Promise<User> => {
|
||||||
|
const response = await api.put<UserResponse>(`/users/${userId}/role`, {
|
||||||
|
role,
|
||||||
|
});
|
||||||
|
return response.data.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteUser: async (userId: string): Promise<void> => {
|
||||||
|
await api.delete<DeleteResponse>(`/users/${userId}`);
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Navigate, useLocation } from 'react-router-dom';
|
||||||
|
import { useAuthStore } from '../../auth/store/auth.store';
|
||||||
|
|
||||||
|
interface RequireAdminProps {
|
||||||
|
children: JSX.Element;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RequireAdmin = ({ children }: RequireAdminProps) => {
|
||||||
|
const user = useAuthStore((state) => state.user);
|
||||||
|
const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
return <Navigate to="/login" state={{ from: location }} replace />;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user has admin role
|
||||||
|
// user.role is now an object { id, name, permissions }
|
||||||
|
const userRole = user?.role?.name;
|
||||||
|
if (userRole !== 'Admin' && userRole !== 'Super Admin') {
|
||||||
|
// Redirect non-admins to home
|
||||||
|
return <Navigate to="/" replace />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return children;
|
||||||
|
};
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import { Outlet, Link, useLocation } from 'react-router-dom';
|
||||||
|
import { useAuthStore } from '../../auth/store/auth.store';
|
||||||
|
|
||||||
|
export const AdminDashboard = () => {
|
||||||
|
const logout = useAuthStore((state) => state.logout);
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
const isActive = (path: string) => location.pathname.startsWith(path);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-slate-100 flex">
|
||||||
|
{/* Sidebar */}
|
||||||
|
<aside className="w-64 bg-white border-r border-slate-200 flex flex-col">
|
||||||
|
<div className="p-6 border-b border-slate-200">
|
||||||
|
<h1 className="text-xl font-bold text-slate-800">Admin Panel</h1>
|
||||||
|
<p className="text-xs text-slate-500 mt-1">QR Campaign Manager</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav className="flex-1 p-4 space-y-1">
|
||||||
|
<Link
|
||||||
|
to="/admin/campaigns"
|
||||||
|
className={`block px-4 py-2 rounded-md transition-colors ${
|
||||||
|
isActive('/admin/campaigns')
|
||||||
|
? 'bg-blue-50 text-blue-700'
|
||||||
|
: 'text-slate-600 hover:bg-slate-50'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Campaigns
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
to="/admin/users"
|
||||||
|
className={`block px-4 py-2 rounded-md transition-colors ${
|
||||||
|
isActive('/admin/users')
|
||||||
|
? 'bg-blue-50 text-blue-700'
|
||||||
|
: 'text-slate-600 hover:bg-slate-50'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Users
|
||||||
|
</Link>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div className="p-4 border-t border-slate-200">
|
||||||
|
<Link
|
||||||
|
to="/"
|
||||||
|
className="block px-4 py-2 text-sm text-slate-600 hover:text-slate-900 mb-2"
|
||||||
|
>
|
||||||
|
← Back to App
|
||||||
|
</Link>
|
||||||
|
<button
|
||||||
|
onClick={logout}
|
||||||
|
className="w-full px-4 py-2 text-sm text-red-600 hover:bg-red-50 rounded-md transition-colors text-left"
|
||||||
|
>
|
||||||
|
Logout
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
|
<main className="flex-1 p-8 overflow-auto">
|
||||||
|
<Outlet />
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,237 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
interface Campaign {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
image_url?: string; // QR code image URL if we want to show it
|
||||||
|
is_active: boolean;
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CreateCampaignInputs {
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CampaignManagement = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
handleSubmit,
|
||||||
|
reset,
|
||||||
|
formState: { errors },
|
||||||
|
} = useForm<CreateCampaignInputs>();
|
||||||
|
|
||||||
|
// Fetch Campaigns
|
||||||
|
const {
|
||||||
|
data: campaigns,
|
||||||
|
isLoading,
|
||||||
|
isError,
|
||||||
|
} = useQuery({
|
||||||
|
queryKey: ['campaigns'],
|
||||||
|
queryFn: async () => {
|
||||||
|
const res = await axios.get('http://localhost:8080/api/v1/campaigns');
|
||||||
|
return res.data.data as Campaign[];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create Campaign
|
||||||
|
const createMutation = useMutation({
|
||||||
|
mutationFn: async (data: CreateCampaignInputs) => {
|
||||||
|
await axios.post('http://localhost:8080/api/v1/campaigns', data);
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success('Campaign created successfully');
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['campaigns'] });
|
||||||
|
setIsCreateModalOpen(false);
|
||||||
|
reset();
|
||||||
|
},
|
||||||
|
onError: (error: any) => {
|
||||||
|
toast.error(error.response?.data?.message || 'Failed to create campaign');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Activate Campaign
|
||||||
|
const activateMutation = useMutation({
|
||||||
|
mutationFn: async (id: string) => {
|
||||||
|
await axios.put(`http://localhost:8080/api/v1/campaigns/${id}/activate`);
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success('Campaign activated');
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['campaigns'] });
|
||||||
|
// Also invalidate active QR for the main app
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['active-campaign-qr'] });
|
||||||
|
},
|
||||||
|
onError: () => toast.error('Failed to activate campaign'),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete Campaign
|
||||||
|
const deleteMutation = useMutation({
|
||||||
|
mutationFn: async (id: string) => {
|
||||||
|
await axios.delete(`http://localhost:8080/api/v1/campaigns/${id}`);
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success('Campaign deleted');
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['campaigns'] });
|
||||||
|
},
|
||||||
|
onError: () => toast.error('Failed to delete campaign'),
|
||||||
|
});
|
||||||
|
|
||||||
|
const onCreateSubmit = (data: CreateCampaignInputs) => {
|
||||||
|
createMutation.mutate(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLoading) return <div>Loading campaigns...</div>;
|
||||||
|
if (isError) return <div>Error loading campaigns.</div>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="flex justify-between items-center mb-6">
|
||||||
|
<h2 className="text-2xl font-bold text-slate-800">Campaigns</h2>
|
||||||
|
<button
|
||||||
|
onClick={() => setIsCreateModalOpen(true)}
|
||||||
|
className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 transition-colors"
|
||||||
|
>
|
||||||
|
+ New Campaign
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white rounded-lg shadow overflow-hidden">
|
||||||
|
<table className="w-full text-left">
|
||||||
|
<thead className="bg-slate-50 text-slate-500 text-xs uppercase font-medium">
|
||||||
|
<tr>
|
||||||
|
<th className="px-6 py-3">Name</th>
|
||||||
|
<th className="px-6 py-3">URL</th>
|
||||||
|
<th className="px-6 py-3">Status</th>
|
||||||
|
<th className="px-6 py-3">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-slate-200">
|
||||||
|
{campaigns?.map((campaign) => (
|
||||||
|
<tr key={campaign.id} className="hover:bg-slate-50">
|
||||||
|
<td className="px-6 py-4 font-medium text-slate-900">
|
||||||
|
{campaign.name}
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 text-slate-500 text-sm max-w-xs truncate">
|
||||||
|
{campaign.url}
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4">
|
||||||
|
{campaign.is_active ? (
|
||||||
|
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-sm font-medium bg-green-100 text-green-800">
|
||||||
|
Active
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-slate-100 text-slate-800">
|
||||||
|
Inactive
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 space-x-2">
|
||||||
|
{!campaign.is_active && (
|
||||||
|
<button
|
||||||
|
onClick={() => activateMutation.mutate(campaign.id)}
|
||||||
|
className="text-blue-600 hover:text-blue-800 text-sm font-medium"
|
||||||
|
>
|
||||||
|
Activate
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
if (
|
||||||
|
window.confirm(
|
||||||
|
'Are you sure you want to delete this campaign?'
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
deleteMutation.mutate(campaign.id);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="text-red-600 hover:text-red-800 text-sm font-medium"
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
{campaigns?.length === 0 && (
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
colSpan={4}
|
||||||
|
className="px-6 py-8 text-center text-slate-500"
|
||||||
|
>
|
||||||
|
No campaigns found. Create one to get started.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Basic Create Modal */}
|
||||||
|
{isCreateModalOpen && (
|
||||||
|
<div className="fixed inset-0 bg-black/50 flex items-center justify-center p-4 z-50">
|
||||||
|
<div className="bg-white rounded-lg shadow-xl max-w-md w-full p-6">
|
||||||
|
<h3 className="text-xl font-bold mb-4 text-slate-900">
|
||||||
|
Create New Campaign
|
||||||
|
</h3>
|
||||||
|
<form onSubmit={handleSubmit(onCreateSubmit)} className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 mb-1">
|
||||||
|
Campaign Name
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
{...register('name', { required: 'Name is required' })}
|
||||||
|
className="w-full px-3 py-2 border border-slate-300 rounded-md bg-white"
|
||||||
|
placeholder="e.g. Summer Sale 2026"
|
||||||
|
/>
|
||||||
|
{errors.name && (
|
||||||
|
<p className="text-red-500 text-sm mt-1">
|
||||||
|
{errors.name.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 mb-1">
|
||||||
|
Target URL
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
{...register('url', { required: 'URL is required' })}
|
||||||
|
className="w-full px-3 py-2 border border-slate-300 rounded-md bg-white"
|
||||||
|
placeholder="https://example.com/promo"
|
||||||
|
/>
|
||||||
|
{errors.url && (
|
||||||
|
<p className="text-red-500 text-sm mt-1">
|
||||||
|
{errors.url.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-end gap-2 mt-6">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsCreateModalOpen(false)}
|
||||||
|
className="px-4 py-2 text-slate-700 hover:bg-slate-100 rounded-md"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={createMutation.isPending}
|
||||||
|
className="px-4 py-2 bg-blue-600 text-white hover:bg-blue-700 rounded-md disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{createMutation.isPending ? 'Creating...' : 'Create Campaign'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
interface User {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
role: string;
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UserManagement = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
// Fetch Users
|
||||||
|
const {
|
||||||
|
data: users,
|
||||||
|
isLoading,
|
||||||
|
isError,
|
||||||
|
} = useQuery({
|
||||||
|
queryKey: ['users'],
|
||||||
|
queryFn: async () => {
|
||||||
|
const res = await axios.get('http://localhost:8080/api/v1/users');
|
||||||
|
return res.data.data as User[];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update Role
|
||||||
|
const updateRoleMutation = useMutation({
|
||||||
|
mutationFn: async ({ id, role }: { id: string; role: string }) => {
|
||||||
|
await axios.put(`http://localhost:8080/api/v1/users/${id}/role`, {
|
||||||
|
role,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success('User role updated');
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['users'] });
|
||||||
|
},
|
||||||
|
onError: () => toast.error('Failed to update user role'),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete User
|
||||||
|
const deleteMutation = useMutation({
|
||||||
|
mutationFn: async (id: string) => {
|
||||||
|
await axios.delete(`http://localhost:8080/api/v1/users/${id}`);
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success('User deleted');
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['users'] });
|
||||||
|
},
|
||||||
|
onError: () => toast.error('Failed to delete user'),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isLoading) return <div>Loading users...</div>;
|
||||||
|
if (isError) return <div>Error loading users.</div>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h2 className="text-2xl font-bold text-slate-800 mb-6">Users</h2>
|
||||||
|
|
||||||
|
<div className="bg-white rounded-lg shadow overflow-hidden">
|
||||||
|
<table className="w-full text-left">
|
||||||
|
<thead className="bg-slate-50 text-slate-500 text-xs uppercase font-medium">
|
||||||
|
<tr>
|
||||||
|
<th className="px-6 py-3">Name</th>
|
||||||
|
<th className="px-6 py-3">Email</th>
|
||||||
|
<th className="px-6 py-3">Role</th>
|
||||||
|
<th className="px-6 py-3">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-slate-200">
|
||||||
|
{users?.map((user) => (
|
||||||
|
<tr key={user.id} className="hover:bg-slate-50">
|
||||||
|
<td className="px-6 py-4 font-medium text-slate-900">
|
||||||
|
{user.name}
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 text-slate-500 text-sm">
|
||||||
|
{user.email}
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4">
|
||||||
|
<select
|
||||||
|
value={user.role}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateRoleMutation.mutate({
|
||||||
|
id: user.id,
|
||||||
|
role: e.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
disabled={user.email === 'admin@demo.com'} // Prevent changing main admin role for safety in demo
|
||||||
|
className="bg-transparent border border-slate-300 rounded text-sm px-2 py-1 text-slate-700 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
>
|
||||||
|
<option value="user">User</option>
|
||||||
|
<option value="admin">Admin</option>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4">
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
if (
|
||||||
|
window.confirm(
|
||||||
|
'Are you sure you want to delete this user?'
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
deleteMutation.mutate(user.id);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={user.email === 'admin@demo.com'}
|
||||||
|
className="text-red-600 hover:text-red-800 text-sm font-medium disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
// Define the base URL for the API
|
||||||
|
const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8080/api/v1';
|
||||||
|
|
||||||
|
// Create a configured axios instance
|
||||||
|
export const api = axios.create({
|
||||||
|
baseURL: API_URL,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add interceptor to add token to requests
|
||||||
|
api.interceptors.request.use(
|
||||||
|
(config) => {
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
if (token) {
|
||||||
|
config.headers['Authorization'] = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
(error) => Promise.reject(error)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Types
|
||||||
|
export interface LoginRequest {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RegisterRequest {
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backend user response
|
||||||
|
interface BackendUser {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
name: string;
|
||||||
|
role: string;
|
||||||
|
provider: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Frontend user type
|
||||||
|
export interface User {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
fullname: string;
|
||||||
|
role?: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
permissions: string[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backend auth response
|
||||||
|
interface BackendAuthResponse {
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
data: {
|
||||||
|
tokens: {
|
||||||
|
access_token: string;
|
||||||
|
refresh_token: string;
|
||||||
|
};
|
||||||
|
user: BackendUser;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuthResponse {
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
data: {
|
||||||
|
tokens: {
|
||||||
|
access_token: string;
|
||||||
|
refresh_token: string;
|
||||||
|
};
|
||||||
|
user: User;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to transform backend user to frontend user
|
||||||
|
const transformUser = (backendUser: BackendUser): User => {
|
||||||
|
return {
|
||||||
|
id: backendUser.id,
|
||||||
|
email: backendUser.email,
|
||||||
|
fullname: backendUser.name,
|
||||||
|
role: {
|
||||||
|
id: '',
|
||||||
|
name: backendUser.role === 'admin' ? 'Admin' : backendUser.role === 'user' ? 'User' : 'User',
|
||||||
|
permissions: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const authService = {
|
||||||
|
login: async (data: LoginRequest): Promise<AuthResponse> => {
|
||||||
|
const response = await api.post<BackendAuthResponse>('/auth/login', data);
|
||||||
|
return {
|
||||||
|
success: response.data.success,
|
||||||
|
message: response.data.message,
|
||||||
|
data: {
|
||||||
|
tokens: response.data.data.tokens,
|
||||||
|
user: transformUser(response.data.data.user),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
register: async (data: RegisterRequest): Promise<AuthResponse> => {
|
||||||
|
const response = await api.post<BackendAuthResponse>('/auth/register', data);
|
||||||
|
return {
|
||||||
|
success: response.data.success,
|
||||||
|
message: response.data.message,
|
||||||
|
data: {
|
||||||
|
tokens: response.data.data.tokens,
|
||||||
|
user: transformUser(response.data.data.user),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
getProfile: async (): Promise<User> => {
|
||||||
|
const response = await api.get<User>('/users/me');
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Navigate, useLocation } from 'react-router-dom';
|
||||||
|
import { useAuthStore } from '../../../features/auth/store/auth.store';
|
||||||
|
|
||||||
|
interface RequireAuthProps {
|
||||||
|
children: JSX.Element;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RequireAuth = ({ children }: RequireAuthProps) => {
|
||||||
|
const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
// Redirect them to the /login page, but save the current location they were
|
||||||
|
// trying to go to when they were redirected. This allows us to send them
|
||||||
|
// along to that page after they login, which is a nicer user experience.
|
||||||
|
return <Navigate to="/login" state={{ from: location }} replace />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return children;
|
||||||
|
};
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { useAuthStore } from '../store/auth.store';
|
||||||
|
import { useNavigate, useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
|
// Reusing UI components logic or standard HTML for now to keep it simple and dependency-free if UI lib issues arise
|
||||||
|
// But user mentioned shared UI libs, let's try to use standard Tailwind first to ensure speed.
|
||||||
|
|
||||||
|
interface LoginFormInputs {
|
||||||
|
email: string;
|
||||||
|
pass: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LoginPage = () => {
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
handleSubmit,
|
||||||
|
formState: { errors, isSubmitting },
|
||||||
|
} = useForm<LoginFormInputs>();
|
||||||
|
const login = useAuthStore((state) => state.login);
|
||||||
|
const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
const from = location.state?.from?.pathname || '/';
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isAuthenticated) {
|
||||||
|
navigate(from, { replace: true });
|
||||||
|
}
|
||||||
|
}, [isAuthenticated, navigate, from]);
|
||||||
|
|
||||||
|
const onSubmit = async (data: LoginFormInputs) => {
|
||||||
|
const success = await login(data.email, data.pass);
|
||||||
|
if (success) {
|
||||||
|
// Get user from store to check role
|
||||||
|
const user = useAuthStore.getState().user;
|
||||||
|
const userRole = user?.role?.name;
|
||||||
|
|
||||||
|
// Redirect admin to admin dashboard
|
||||||
|
if (userRole === 'Admin' || userRole === 'Super Admin') {
|
||||||
|
navigate('/admin/campaigns', { replace: true });
|
||||||
|
} else {
|
||||||
|
navigate(from, { replace: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex items-center justify-center bg-slate-50 p-4">
|
||||||
|
<div className="w-full max-w-md bg-white rounded-lg shadow-lg p-8">
|
||||||
|
<h1 className="text-2xl font-bold text-center mb-6 text-slate-800">
|
||||||
|
Login
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 mb-1">
|
||||||
|
Email
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
{...register('email', { required: 'Email is required' })}
|
||||||
|
className="w-full px-3 py-2 border border-slate-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
{errors.email && (
|
||||||
|
<p className="text-red-500 text-sm mt-1">
|
||||||
|
{errors.email.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 mb-1">
|
||||||
|
Password
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
{...register('pass', { required: 'Password is required' })}
|
||||||
|
className="w-full px-3 py-2 border border-slate-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
{errors.pass && (
|
||||||
|
<p className="text-red-500 text-sm mt-1">{errors.pass.message}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
className="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed font-medium"
|
||||||
|
>
|
||||||
|
{isSubmitting ? 'Logging in...' : 'Login'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div className="mt-4 text-center text-sm text-slate-500">
|
||||||
|
<p>Demo credentials available in backend seeder.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import { create } from 'zustand';
|
||||||
|
import { persist } from 'zustand/middleware';
|
||||||
|
import { authService, User } from '../api/auth.service';
|
||||||
|
|
||||||
|
interface AuthState {
|
||||||
|
user: User | null;
|
||||||
|
token: string | null;
|
||||||
|
isAuthenticated: boolean;
|
||||||
|
login: (email: string, pass: string) => Promise<boolean>;
|
||||||
|
register: (name: string, email: string, pass: string) => Promise<boolean>;
|
||||||
|
logout: () => void;
|
||||||
|
setUser: (user: User) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useAuthStore = create<AuthState>()(
|
||||||
|
persist(
|
||||||
|
(set) => ({
|
||||||
|
user: null,
|
||||||
|
token: null,
|
||||||
|
isAuthenticated: false,
|
||||||
|
|
||||||
|
login: async (email, password) => {
|
||||||
|
try {
|
||||||
|
const response = await authService.login({ email, password });
|
||||||
|
|
||||||
|
const token = response.data.tokens.access_token;
|
||||||
|
const refreshToken = response.data.tokens.refresh_token;
|
||||||
|
|
||||||
|
localStorage.setItem('token', token);
|
||||||
|
localStorage.setItem('refreshToken', refreshToken);
|
||||||
|
|
||||||
|
set({
|
||||||
|
user: response.data.user,
|
||||||
|
token: token,
|
||||||
|
isAuthenticated: true
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Login failed:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
register: async (name, email, password) => {
|
||||||
|
try {
|
||||||
|
const response = await authService.register({ name, email, password });
|
||||||
|
|
||||||
|
const token = response.data.tokens.access_token;
|
||||||
|
const refreshToken = response.data.tokens.refresh_token;
|
||||||
|
|
||||||
|
localStorage.setItem('token', token);
|
||||||
|
localStorage.setItem('refreshToken', refreshToken);
|
||||||
|
|
||||||
|
set({
|
||||||
|
user: response.data.user,
|
||||||
|
token: token,
|
||||||
|
isAuthenticated: true
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Registration failed:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
logout: () => {
|
||||||
|
localStorage.removeItem('token');
|
||||||
|
set({ user: null, token: null, isAuthenticated: false });
|
||||||
|
},
|
||||||
|
|
||||||
|
setUser: (user) => set({ user }),
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: 'auth-storage', // name of the item in the storage (must be unique)
|
||||||
|
partialize: (state) => ({
|
||||||
|
user: state.user,
|
||||||
|
token: state.token,
|
||||||
|
isAuthenticated: state.isAuthenticated
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
export const useActiveCampaignQR = () => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['active-campaign-qr'],
|
||||||
|
queryFn: async () => {
|
||||||
|
// Assuming backend is running on localhost:8080
|
||||||
|
// In production, this should be an env var or relative path if proxied
|
||||||
|
const response = await axios.get('http://localhost:8080/api/v1/campaigns/active/qr', {
|
||||||
|
responseType: 'blob',
|
||||||
|
});
|
||||||
|
return URL.createObjectURL(response.data);
|
||||||
|
},
|
||||||
|
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
import React, { useCallback, useState } from 'react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
interface DropzoneProps {
|
||||||
|
onImageDropped: (file: File) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Dropzone: React.FC<DropzoneProps> = ({ onImageDropped }) => {
|
||||||
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
|
|
||||||
|
const handleDragOver = useCallback((e: React.DragEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsDragging(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleDragLeave = useCallback((e: React.DragEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsDragging(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleDrop = useCallback(
|
||||||
|
(e: React.DragEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsDragging(false);
|
||||||
|
|
||||||
|
const files = Array.from(e.dataTransfer.files);
|
||||||
|
if (files.length === 0) return;
|
||||||
|
|
||||||
|
const file = files[0];
|
||||||
|
if (!file.type.startsWith('image/')) {
|
||||||
|
toast.error('Please upload an image file.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
onImageDropped(file);
|
||||||
|
},
|
||||||
|
[onImageDropped]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleFileInput = useCallback(
|
||||||
|
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const files = e.target.files;
|
||||||
|
if (files && files.length > 0) {
|
||||||
|
const file = files[0];
|
||||||
|
if (!file.type.startsWith('image/')) {
|
||||||
|
toast.error('Please upload an image file.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onImageDropped(file);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onImageDropped]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
onDragOver={handleDragOver}
|
||||||
|
onDragLeave={handleDragLeave}
|
||||||
|
onDrop={handleDrop}
|
||||||
|
className={`border-2 border-dashed rounded-lg p-12 text-center transition-colors cursor-pointer ${
|
||||||
|
isDragging
|
||||||
|
? 'border-blue-500 bg-blue-50'
|
||||||
|
: 'border-slate-300 hover:border-slate-400'
|
||||||
|
}`}
|
||||||
|
onClick={() => document.getElementById('file-upload')?.click()}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id="file-upload"
|
||||||
|
type="file"
|
||||||
|
className="hidden"
|
||||||
|
accept="image/png, image/jpeg, image/jpg"
|
||||||
|
onChange={handleFileInput}
|
||||||
|
/>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex justify-center">
|
||||||
|
{/* Simple upload icon */}
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
strokeWidth={1.5}
|
||||||
|
stroke="currentColor"
|
||||||
|
className="w-12 h-12 text-slate-400"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<p className="text-lg font-medium text-slate-700">
|
||||||
|
Drop your image here, or click to upload
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-slate-500">Supports JPG and PNG</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
import React, { useState, useRef, useEffect, useCallback } from 'react';
|
||||||
|
import html2canvas from 'html2canvas';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
interface WatermarkEditorProps {
|
||||||
|
imageFile: File;
|
||||||
|
qrCodeUrl: string;
|
||||||
|
onReset: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WatermarkEditor: React.FC<WatermarkEditorProps> = ({
|
||||||
|
imageFile,
|
||||||
|
qrCodeUrl,
|
||||||
|
onReset,
|
||||||
|
}) => {
|
||||||
|
const [imageUrl, setImageUrl] = useState<string | null>(null);
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const qrRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// State for QR code
|
||||||
|
const [position, setPosition] = useState({ x: 20, y: 20 });
|
||||||
|
const [size, setSize] = useState(100);
|
||||||
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
|
const [isResizing, setIsResizing] = useState(false);
|
||||||
|
const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 });
|
||||||
|
const [startResizePos, setStartResizePos] = useState({ x: 0, y: 0 });
|
||||||
|
const [startResizeSize, setStartResizeSize] = useState(100);
|
||||||
|
|
||||||
|
// Load image
|
||||||
|
useEffect(() => {
|
||||||
|
const url = URL.createObjectURL(imageFile);
|
||||||
|
setImageUrl(url);
|
||||||
|
return () => URL.revokeObjectURL(url);
|
||||||
|
}, [imageFile]);
|
||||||
|
|
||||||
|
// Drag handlers
|
||||||
|
const handleMouseDown = (e: React.MouseEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setIsDragging(true);
|
||||||
|
setDragOffset({
|
||||||
|
x: e.clientX - position.x,
|
||||||
|
y: e.clientY - position.y,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleResizeMouseDown = (e: React.MouseEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setIsResizing(true);
|
||||||
|
setStartResizePos({ x: e.clientX, y: e.clientY });
|
||||||
|
setStartResizeSize(size);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseMove = useCallback(
|
||||||
|
(e: MouseEvent) => {
|
||||||
|
if (isDragging) {
|
||||||
|
const newX = e.clientX - dragOffset.x;
|
||||||
|
const newY = e.clientY - dragOffset.y;
|
||||||
|
|
||||||
|
// Boundaries check (optional, but good UX)
|
||||||
|
if (containerRef.current) {
|
||||||
|
// const container = containerRef.current.getBoundingClientRect();
|
||||||
|
// Simple clamp? Or allow partial off-screen?
|
||||||
|
// Let's allow it to move freely within container
|
||||||
|
}
|
||||||
|
|
||||||
|
setPosition({ x: newX, y: newY });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isResizing) {
|
||||||
|
const deltaX = e.clientX - startResizePos.x;
|
||||||
|
const newSize = Math.max(50, startResizeSize + deltaX); // Min size 50px
|
||||||
|
setSize(newSize);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[isDragging, isResizing, dragOffset, startResizePos, startResizeSize]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleMouseUp = useCallback(() => {
|
||||||
|
setIsDragging(false);
|
||||||
|
setIsResizing(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isDragging || isResizing) {
|
||||||
|
window.addEventListener('mousemove', handleMouseMove);
|
||||||
|
window.addEventListener('mouseup', handleMouseUp);
|
||||||
|
} else {
|
||||||
|
window.removeEventListener('mousemove', handleMouseMove);
|
||||||
|
window.removeEventListener('mouseup', handleMouseUp);
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('mousemove', handleMouseMove);
|
||||||
|
window.removeEventListener('mouseup', handleMouseUp);
|
||||||
|
};
|
||||||
|
}, [isDragging, isResizing, handleMouseMove, handleMouseUp]);
|
||||||
|
|
||||||
|
const handleDownload = async () => {
|
||||||
|
if (!containerRef.current) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const canvas = await html2canvas(containerRef.current, {
|
||||||
|
useCORS: true, // Important for QR if from external URL
|
||||||
|
backgroundColor: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.download = `qr-campaign-${Date.now()}.png`;
|
||||||
|
link.href = canvas.toDataURL('image/png');
|
||||||
|
link.click();
|
||||||
|
toast.success('Image downloaded successfully!');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Download failed:', error);
|
||||||
|
toast.error('Failed to download image.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!imageUrl) return <div>Loading image...</div>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center gap-4 w-full h-full">
|
||||||
|
<div className="flex gap-2 mb-4">
|
||||||
|
<button
|
||||||
|
onClick={onReset}
|
||||||
|
className="px-4 py-2 bg-slate-200 text-slate-700 rounded hover:bg-slate-300 transition-colors"
|
||||||
|
>
|
||||||
|
Change Image
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleDownload}
|
||||||
|
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors shadow-lg"
|
||||||
|
>
|
||||||
|
Download Image
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border border-slate-200 shadow-xl rounded-lg overflow-hidden bg-slate-50 inline-block relative">
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className="relative inline-block"
|
||||||
|
style={{ lineHeight: 0 }}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={imageUrl}
|
||||||
|
alt="Uploaded"
|
||||||
|
className="max-h-[70vh] w-auto h-auto object-contain select-none"
|
||||||
|
draggable={false}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
ref={qrRef}
|
||||||
|
className="absolute cursor-move select-none group"
|
||||||
|
style={{
|
||||||
|
left: position.x,
|
||||||
|
top: position.y,
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
zIndex: 10,
|
||||||
|
}}
|
||||||
|
onMouseDown={handleMouseDown}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={qrCodeUrl}
|
||||||
|
alt="QR Code"
|
||||||
|
className="w-full h-full select-none pointer-events-none"
|
||||||
|
crossOrigin="anonymous" // Important for html2canvas
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Outline on hover/interaction */}
|
||||||
|
<div className="absolute inset-0 border-2 border-transparent group-hover:border-blue-400 group-active:border-blue-500 pointer-events-none rounded-sm transition-colors" />
|
||||||
|
|
||||||
|
{/* Resize handle */}
|
||||||
|
<div
|
||||||
|
className="absolute bottom-0 right-0 w-4 h-4 bg-blue-500 rounded-full cursor-nwse-resize opacity-0 group-hover:opacity-100 transition-opacity"
|
||||||
|
onMouseDown={handleResizeMouseDown}
|
||||||
|
style={{ transform: 'translate(50%, 50%)' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-sm text-slate-500 mt-2">
|
||||||
|
Drag to move the QR code. Drag the blue dot to resize.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import {
|
||||||
|
Outlet,
|
||||||
|
ScrollRestoration,
|
||||||
|
useLocation,
|
||||||
|
useNavigate,
|
||||||
|
} from 'react-router-dom';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useAuthStore } from './features/auth/store/auth.store';
|
||||||
|
import { Sidebar } from '../components/Sidebar';
|
||||||
|
import { MenuOutlined } from '@ant-design/icons';
|
||||||
|
|
||||||
|
// Helper to determine route types
|
||||||
|
const isPublicRoute = (pathname: string) => {
|
||||||
|
return pathname.startsWith('/auth') || pathname === '/auth/callback';
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function RootLayout() {
|
||||||
|
const location = useLocation();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { isAuthenticated } = useAuthStore();
|
||||||
|
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// If we're on a public route, no auth check needed
|
||||||
|
if (isPublicRoute(location.pathname)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// AUTH CHECK
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
// No session -> Redirect to login
|
||||||
|
navigate('/auth/login', { replace: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}, [location.pathname, navigate, isAuthenticated]);
|
||||||
|
|
||||||
|
// RENDER LOGIC
|
||||||
|
|
||||||
|
// 1. Public Pages (Full Layout Control)
|
||||||
|
if (isPublicRoute(location.pathname)) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Outlet />
|
||||||
|
<ScrollRestoration />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Protected Pages
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-screen bg-gray-50">
|
||||||
|
<Sidebar
|
||||||
|
isOpen={mobileSidebarOpen}
|
||||||
|
onClose={() => setMobileSidebarOpen(false)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex-1 flex flex-col min-w-0 overflow-hidden">
|
||||||
|
{/* Mobile Header */}
|
||||||
|
<header className="lg:hidden bg-white border-b border-gray-200 px-4 py-3 flex items-center justify-between sticky top-0 z-30">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<button
|
||||||
|
onClick={() => setMobileSidebarOpen(true)}
|
||||||
|
className="p-2 -ml-2 rounded-md hover:bg-gray-100 text-gray-700"
|
||||||
|
>
|
||||||
|
<MenuOutlined className="text-lg" />
|
||||||
|
</button>
|
||||||
|
<h1 className="font-semibold text-gray-900">QR Campaign</h1>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main className="flex-1 overflow-y-auto p-4 md:p-8">
|
||||||
|
<Outlet />
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ScrollRestoration />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { Dropzone } from './features/watermark/components/Dropzone';
|
||||||
|
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import { api } from './features/auth/api/auth.service';
|
||||||
|
|
||||||
|
export default function HomePage() {
|
||||||
|
const [imageFile, setImageFile] = useState<File | null>(null);
|
||||||
|
const [generatedImage, setGeneratedImage] = useState<string | null>(null);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const handleImageDropped = (file: File) => {
|
||||||
|
setImageFile(file);
|
||||||
|
setGeneratedImage(null); // Reset previous result
|
||||||
|
toast.success('Image selected ready for generation!');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReset = () => {
|
||||||
|
setImageFile(null);
|
||||||
|
setGeneratedImage(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleGenerate = async () => {
|
||||||
|
if (!imageFile) return;
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('image', imageFile);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await api.post('/campaigns/process-image', formData, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data',
|
||||||
|
},
|
||||||
|
responseType: 'blob',
|
||||||
|
});
|
||||||
|
|
||||||
|
const imageUrl = URL.createObjectURL(response.data);
|
||||||
|
setGeneratedImage(imageUrl);
|
||||||
|
toast.success('QR Code generated successfully!');
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(error);
|
||||||
|
const message =
|
||||||
|
error.response?.data?.message || 'Failed to generate QR code.';
|
||||||
|
toast.error(message);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDownload = () => {
|
||||||
|
if (!generatedImage) return;
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = generatedImage;
|
||||||
|
link.download = `qr-campaign-${Date.now()}.png`;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="max-w-4xl mx-auto py-8 px-4 flex flex-col gap-8">
|
||||||
|
<header className="text-center mb-8">
|
||||||
|
<h1 className="text-3xl font-bold text-gray-900 mb-2">
|
||||||
|
QR Code Generator
|
||||||
|
</h1>
|
||||||
|
<p className="text-gray-600">
|
||||||
|
Upload your image to add the campaign QR code watermark.
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="bg-white rounded-xl shadow-sm border border-gray-100 p-8">
|
||||||
|
{!imageFile ? (
|
||||||
|
<Dropzone onImageDropped={handleImageDropped} />
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col items-center gap-6">
|
||||||
|
<div className="relative w-full max-w-2xl bg-gray-50 rounded-lg overflow-hidden border border-gray-200">
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="flex flex-col items-center justify-center p-20 gap-4">
|
||||||
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600"></div>
|
||||||
|
<p className="text-gray-500 font-medium">
|
||||||
|
Processing image...
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : generatedImage ? (
|
||||||
|
<img
|
||||||
|
src={generatedImage}
|
||||||
|
alt="Generated with QR"
|
||||||
|
className="w-full h-auto object-contain max-h-[600px]"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="relative">
|
||||||
|
<img
|
||||||
|
src={URL.createObjectURL(imageFile)}
|
||||||
|
alt="Original"
|
||||||
|
className="w-full h-auto object-contain max-h-[400px]"
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-0 bg-black/5 flex items-center justify-center pointer-events-none">
|
||||||
|
<span className="bg-black/60 text-white px-3 py-1 rounded-2xl text-sm">
|
||||||
|
Original Image
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-4">
|
||||||
|
{!generatedImage && !isLoading && (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
variant="bordered"
|
||||||
|
onClick={handleReset}
|
||||||
|
className="w-32"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
onClick={handleGenerate}
|
||||||
|
className="w-40"
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
Generate QR
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{generatedImage && (
|
||||||
|
<>
|
||||||
|
<Button variant="bordered" onClick={handleReset}>
|
||||||
|
Upload Another
|
||||||
|
</Button>
|
||||||
|
<Button variant="primary" onClick={handleDownload}>
|
||||||
|
Download
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,229 @@
|
|||||||
|
import {
|
||||||
|
AppstoreOutlined,
|
||||||
|
UsergroupAddOutlined,
|
||||||
|
QrcodeOutlined,
|
||||||
|
LogoutOutlined,
|
||||||
|
DownOutlined,
|
||||||
|
RightOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||||
|
import { FC, ReactElement, useState } from 'react';
|
||||||
|
import { Link, useLocation } from 'react-router-dom';
|
||||||
|
import { cn, For } from '@imphnen-frontend-service/utils';
|
||||||
|
import { useAuthStore } from '../app/features/auth/store/auth.store';
|
||||||
|
|
||||||
|
type MenuItem = {
|
||||||
|
label: string;
|
||||||
|
href?: string;
|
||||||
|
icon?: ReactElement;
|
||||||
|
children?: Array<{ label: string; href: string; icon?: ReactElement }>;
|
||||||
|
roles?: string[]; // Roles that can see this menu
|
||||||
|
};
|
||||||
|
|
||||||
|
const MENUS: MenuItem[] = [
|
||||||
|
{
|
||||||
|
label: 'QR Generator',
|
||||||
|
href: '/',
|
||||||
|
icon: <QrcodeOutlined className="text-[20px]" />,
|
||||||
|
roles: ['User', 'Admin', 'Super Admin'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Campaign Management',
|
||||||
|
href: '/admin/campaigns',
|
||||||
|
icon: <AppstoreOutlined className="text-[20px]" />,
|
||||||
|
roles: ['Admin', 'Super Admin'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'User Management',
|
||||||
|
href: '/admin/users',
|
||||||
|
icon: <UsergroupAddOutlined className="text-[20px]" />,
|
||||||
|
roles: ['Admin', 'Super Admin'],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
interface SidebarProps {
|
||||||
|
isOpen?: boolean;
|
||||||
|
onClose?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Sidebar: FC<SidebarProps> = ({
|
||||||
|
isOpen = false,
|
||||||
|
onClose,
|
||||||
|
}): ReactElement => {
|
||||||
|
const { user, logout } = useAuthStore();
|
||||||
|
const location = useLocation();
|
||||||
|
const [openGroups, setOpenGroups] = useState<Record<string, boolean>>({});
|
||||||
|
|
||||||
|
const userRole = user?.role?.name || 'User';
|
||||||
|
|
||||||
|
const isActive = (path: string) => {
|
||||||
|
if (path === '/' && location.pathname !== '/') return false;
|
||||||
|
return location.pathname.startsWith(path);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleGroup = (groupLabel: string) => {
|
||||||
|
setOpenGroups((prev) => ({ ...prev, [groupLabel]: !prev[groupLabel] }));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Filter menus based on role
|
||||||
|
const filteredMenus = MENUS.filter((menu) => {
|
||||||
|
if (!menu.roles) return true;
|
||||||
|
return menu.roles.includes(userRole);
|
||||||
|
});
|
||||||
|
|
||||||
|
const sidebarContent = (
|
||||||
|
<div className="w-[280px] bg-white h-dvh px-7 shadow-xl flex flex-col border-r border-gray-100">
|
||||||
|
<div className="shrink-0 py-10 lg:py-[60px]">
|
||||||
|
<div className="flex justify-between lg:justify-center items-center w-full">
|
||||||
|
<img
|
||||||
|
src="/images/imphnen-logo.svg"
|
||||||
|
alt="IMPHNEN Logo"
|
||||||
|
className="w-[150px]"
|
||||||
|
/>
|
||||||
|
{onClose && (
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="lg:hidden p-2 rounded-lg hover:bg-gray-100 transition-colors cursor-pointer"
|
||||||
|
aria-label="Close sidebar"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="w-5 h-5 text-gray-500"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M6 18L18 6M6 6l12 12"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Navigation - Scrollable area */}
|
||||||
|
<nav className="flex-1 flex flex-col gap-4 w-full overflow-y-auto min-h-0 pb-4">
|
||||||
|
<For data={filteredMenus}>
|
||||||
|
{(menu) =>
|
||||||
|
menu.children && menu.children.length > 0 ? (
|
||||||
|
<div key={menu.label} className="w-full">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => toggleGroup(menu.label)}
|
||||||
|
className={cn(
|
||||||
|
'flex items-center justify-between w-full gap-3 px-2 py-2.5 rounded-md cursor-pointer',
|
||||||
|
openGroups[menu.label]
|
||||||
|
? 'bg-primary-50 text-primary-700'
|
||||||
|
: 'text-gray-700 hover:bg-gray-50'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
{menu.icon}
|
||||||
|
<span className="text-sm font-medium">{menu.label}</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-xs">
|
||||||
|
{openGroups[menu.label] ? (
|
||||||
|
<DownOutlined />
|
||||||
|
) : (
|
||||||
|
<RightOutlined />
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{openGroups[menu.label] && (
|
||||||
|
<div className="mt-2 ml-6 flex flex-col gap-2">
|
||||||
|
{menu.children.map((child) => (
|
||||||
|
<Link
|
||||||
|
key={child.href}
|
||||||
|
to={child.href}
|
||||||
|
className={cn(
|
||||||
|
'flex items-center gap-3 px-2 py-2.5 rounded-md',
|
||||||
|
isActive(child.href)
|
||||||
|
? 'bg-primary-100 text-primary-700'
|
||||||
|
: 'text-gray-600 hover:bg-gray-50'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{child.icon}
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
{child.label}
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Link
|
||||||
|
key={menu.href ?? menu.label}
|
||||||
|
to={menu.href ?? '#'}
|
||||||
|
className={cn(
|
||||||
|
'flex items-center justify-start gap-3 px-2 py-2.5 rounded-md transition-colors',
|
||||||
|
menu.href && isActive(menu.href)
|
||||||
|
? 'bg-primary-500 text-white hover:bg-primary-600 shadow-sm'
|
||||||
|
: 'text-gray-700 hover:bg-gray-100'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{menu.icon}
|
||||||
|
<span className="text-sm font-medium">{menu.label}</span>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</For>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{/* Footer - Fixed at bottom */}
|
||||||
|
<div className="shrink-0 w-full pb-10 lg:pb-[60px]">
|
||||||
|
<hr className="mb-5 border-gray-100" />
|
||||||
|
<div className="px-2 mb-4">
|
||||||
|
<div className="text-xs text-gray-500 font-medium uppercase mb-2 select-none">
|
||||||
|
Signed in as
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-8 h-8 rounded-full bg-primary-100 flex items-center justify-center text-primary-600 font-bold text-xs ring-2 ring-white">
|
||||||
|
{user?.fullname?.charAt(0) || 'U'}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col overflow-hidden">
|
||||||
|
<span className="text-sm font-medium truncate text-gray-900">
|
||||||
|
{user?.fullname}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-gray-500 truncate">
|
||||||
|
{user?.role?.name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={logout}
|
||||||
|
variant="text"
|
||||||
|
className="items-center justify-start gap-3 px-2 py-2.5 text-gray-600 hover:text-red-600 dark:hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-50 transition-colors w-full rounded-md"
|
||||||
|
>
|
||||||
|
<LogoutOutlined className="text-lg" />
|
||||||
|
<span className="text-sm font-medium">Log Out</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="hidden lg:block sticky top-0 h-screen overflow-y-auto border-r border-gray-200">
|
||||||
|
{sidebarContent}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isOpen && (
|
||||||
|
<div className="lg:hidden fixed inset-0 z-50">
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 bg-black/50 transition-opacity backdrop-blur-sm"
|
||||||
|
onClick={onClose}
|
||||||
|
/>
|
||||||
|
<div className="fixed inset-y-0 left-0 z-50 transform transition-transform duration-300 ease-in-out">
|
||||||
|
{sidebarContent}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
@import url('https://fonts.googleapis.com/css2?family=Bai+Jamjuree:ital,wght@0,200;0,300;0,400;0,500;0,600;0,700;1,200;1,300;1,400;1,500;1,600;1,700&display=swap');
|
||||||
|
@import 'tailwindcss';
|
||||||
|
@source "../../../libs/ui/**/*.{ts,tsx}";
|
||||||
|
|
||||||
|
@theme {
|
||||||
|
--color-primary-50: #f0f8ff;
|
||||||
|
--color-primary-100: #e1f0fd;
|
||||||
|
--color-primary-200: #bce1fb;
|
||||||
|
--color-primary-300: #81cbf8;
|
||||||
|
--color-primary-400: #3eb0f2;
|
||||||
|
--color-primary-500: #23a1eb;
|
||||||
|
--color-primary-600: #0877c1;
|
||||||
|
--color-primary-700: #085f9c;
|
||||||
|
--color-primary-800: #0b5181;
|
||||||
|
--color-primary-900: #0f446b;
|
||||||
|
--color-primary-950: #0a2b47;
|
||||||
|
|
||||||
|
--color-neutral-50: #f6f6f6;
|
||||||
|
--color-neutral-100: #e7e7e7;
|
||||||
|
--color-neutral-200: #d1d1d1;
|
||||||
|
--color-neutral-300: #b0b0b0;
|
||||||
|
--color-neutral-400: #888888;
|
||||||
|
--color-neutral-500: #6e6a86;
|
||||||
|
--color-neutral-600: #56526e;
|
||||||
|
--color-neutral-700: #44415a;
|
||||||
|
--color-neutral-800: #393552;
|
||||||
|
--color-neutral-900: #2a273f;
|
||||||
|
--color-neutral-950: #232136;
|
||||||
|
|
||||||
|
--color-success-100: #e0fbd8;
|
||||||
|
--color-success-200: #bcf8b0;
|
||||||
|
--color-success-300: #8eea85;
|
||||||
|
--color-success-400: #63d564;
|
||||||
|
--color-success-500: #35ba43;
|
||||||
|
--color-success-600: #269f3e;
|
||||||
|
--color-success-700: #1a8439;
|
||||||
|
--color-success-800: #106b32;
|
||||||
|
--color-success-900: #0b592f;
|
||||||
|
|
||||||
|
--color-info-100: #ccfcfe;
|
||||||
|
--color-info-200: #9bf3fd;
|
||||||
|
--color-info-300: #67e3fb;
|
||||||
|
--color-info-400: #42cdf8;
|
||||||
|
--color-info-500: #04acf3;
|
||||||
|
--color-info-600: #0185d0;
|
||||||
|
--color-info-700: #0264af;
|
||||||
|
--color-info-800: #01478d;
|
||||||
|
--color-info-900: #003375;
|
||||||
|
|
||||||
|
--color-warning-100: #fffcd3;
|
||||||
|
--color-warning-200: #fffaa9;
|
||||||
|
--color-warning-300: #fff67d;
|
||||||
|
--color-warning-400: #fff25d;
|
||||||
|
--color-warning-500: #ffed27;
|
||||||
|
--color-warning-600: #dbc91d;
|
||||||
|
--color-warning-700: #b7a714;
|
||||||
|
--color-warning-800: #93850b;
|
||||||
|
--color-warning-900: #7a6d07;
|
||||||
|
|
||||||
|
--color-danger-100: #ffe8da;
|
||||||
|
--color-danger-200: #ffcbb3;
|
||||||
|
--color-danger-300: #ffaa8d;
|
||||||
|
--color-danger-400: #ff8870;
|
||||||
|
--color-danger-500: #ff5242;
|
||||||
|
--color-danger-600: #da3030;
|
||||||
|
--color-danger-700: #b7212d;
|
||||||
|
--color-danger-800: #93152a;
|
||||||
|
--color-danger-900: #7a0c27;
|
||||||
|
|
||||||
|
--radius-none: 0px;
|
||||||
|
--radius-sm: 2px;
|
||||||
|
--radius-md: 4px;
|
||||||
|
--radius-lg: 8px;
|
||||||
|
--radius-xl: 12px;
|
||||||
|
--radius-2xl: 16px;
|
||||||
|
--radius-3xl: 24px;
|
||||||
|
--radius-full: 50%;
|
||||||
|
|
||||||
|
--font-bai-jamjuree: 'Bai Jamjuree', sans-serif;
|
||||||
|
|
||||||
|
--text-h1: 3.833rem;
|
||||||
|
/* ~46px */
|
||||||
|
--text-h2: 3.083rem;
|
||||||
|
/* ~37px */
|
||||||
|
--text-h3: 2.417rem;
|
||||||
|
/* ~29px */
|
||||||
|
--text-p1: 1.917rem;
|
||||||
|
/* ~23px */
|
||||||
|
--text-p2: 1.583rem;
|
||||||
|
/* ~19px */
|
||||||
|
--text-p3: 1.25rem;
|
||||||
|
/* 15px */
|
||||||
|
--text-label1: 1rem;
|
||||||
|
/* 12px */
|
||||||
|
--text-label2: 0.833rem;
|
||||||
|
/* ~10px */
|
||||||
|
--text-label3: 0.677rem;
|
||||||
|
/* ~8px */
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
|
||||||
|
*,
|
||||||
|
::after,
|
||||||
|
::before,
|
||||||
|
::backdrop,
|
||||||
|
::file-selector-button {
|
||||||
|
border-color: var(--color-neutral-200, currentColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom scrollbar */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: var(--color-neutral-50);
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: #cccccc;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: var(--color-neutral-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
font-family: 'Bai Jamjuree', sans-serif;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { createRoot } from 'react-dom/client';
|
||||||
|
import { StrictMode } from 'react';
|
||||||
|
import { createBrowserRouter, RouteObject, RouterProvider } from 'react-router';
|
||||||
|
import {
|
||||||
|
add404PageToRoutesChildren,
|
||||||
|
addErrorElementToRoutes,
|
||||||
|
convertPagesToRoute,
|
||||||
|
ModalLoginProvider,
|
||||||
|
QueryProvider,
|
||||||
|
} from '@imphnen-frontend-service/utils';
|
||||||
|
import { Toaster } from 'sonner';
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
|
const files = import.meta.glob('./app/**/*(page|layout).tsx');
|
||||||
|
const errorFiles = import.meta.glob('./app/**/*error.tsx');
|
||||||
|
const notFoundFiles = import.meta.glob('./app/**/*404.tsx');
|
||||||
|
const loadingFiles = import.meta.glob('./app/**/*loading.tsx');
|
||||||
|
|
||||||
|
const routes = convertPagesToRoute(files, loadingFiles) as RouteObject;
|
||||||
|
addErrorElementToRoutes(errorFiles, routes);
|
||||||
|
add404PageToRoutesChildren(notFoundFiles, routes);
|
||||||
|
|
||||||
|
const router = createBrowserRouter([
|
||||||
|
{
|
||||||
|
...routes,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const rootElement = document.getElementById('root');
|
||||||
|
|
||||||
|
if (!rootElement) throw new Error('Failed to find the root element');
|
||||||
|
|
||||||
|
createRoot(rootElement).render(
|
||||||
|
<StrictMode>
|
||||||
|
<QueryProvider>
|
||||||
|
<ModalLoginProvider>
|
||||||
|
<Toaster position="top-right" richColors />
|
||||||
|
<RouterProvider router={router} />
|
||||||
|
</ModalLoginProvider>
|
||||||
|
</QueryProvider>
|
||||||
|
</StrictMode>
|
||||||
|
);
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* You can add global styles to this file, and also import other style files */
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "../../dist/out-tsc",
|
||||||
|
"types": [
|
||||||
|
"node",
|
||||||
|
"@nx/react/typings/cssmodule.d.ts",
|
||||||
|
"@nx/react/typings/image.d.ts",
|
||||||
|
"vite/client"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"exclude": [
|
||||||
|
"src/**/*.spec.ts",
|
||||||
|
"src/**/*.test.ts",
|
||||||
|
"src/**/*.spec.tsx",
|
||||||
|
"src/**/*.test.tsx",
|
||||||
|
"src/**/*.spec.js",
|
||||||
|
"src/**/*.test.js",
|
||||||
|
"src/**/*.spec.jsx",
|
||||||
|
"src/**/*.test.jsx",
|
||||||
|
"vite.config.ts",
|
||||||
|
"vite.config.mts",
|
||||||
|
"vitest.config.ts",
|
||||||
|
"vitest.config.mts"
|
||||||
|
],
|
||||||
|
"include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"allowJs": false,
|
||||||
|
"esModuleInterop": false,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"strict": true,
|
||||||
|
"types": ["vite/client", "vitest"]
|
||||||
|
},
|
||||||
|
"files": [],
|
||||||
|
"include": [],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.app.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.spec.json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"extends": "../../tsconfig.base.json"
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "../../dist/out-tsc",
|
||||||
|
"types": [
|
||||||
|
"vitest/globals",
|
||||||
|
"vitest/importMeta",
|
||||||
|
"vite/client",
|
||||||
|
"node",
|
||||||
|
"vitest",
|
||||||
|
"@nx/react/typings/cssmodule.d.ts",
|
||||||
|
"@nx/react/typings/image.d.ts"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"vite.config.ts",
|
||||||
|
"vite.config.mts",
|
||||||
|
"vitest.config.ts",
|
||||||
|
"vitest.config.mts",
|
||||||
|
"src/**/*.test.ts",
|
||||||
|
"src/**/*.spec.ts",
|
||||||
|
"src/**/*.test.tsx",
|
||||||
|
"src/**/*.spec.tsx",
|
||||||
|
"src/**/*.test.js",
|
||||||
|
"src/**/*.spec.js",
|
||||||
|
"src/**/*.test.jsx",
|
||||||
|
"src/**/*.spec.jsx",
|
||||||
|
"src/**/*.d.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
/// <reference types='vitest' />
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
|
||||||
|
import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin';
|
||||||
|
|
||||||
|
export default defineConfig(() => ({
|
||||||
|
root: import.meta.dirname,
|
||||||
|
cacheDir: '../../node_modules/.vite/apps/qrcampaign',
|
||||||
|
server: {
|
||||||
|
port: 3008,
|
||||||
|
host: 'localhost',
|
||||||
|
},
|
||||||
|
preview: {
|
||||||
|
port: 3008,
|
||||||
|
host: 'localhost',
|
||||||
|
},
|
||||||
|
plugins: [react(), nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])],
|
||||||
|
// Uncomment this if you are using workers.
|
||||||
|
// worker: {
|
||||||
|
// plugins: [ nxViteTsPaths() ],
|
||||||
|
// },
|
||||||
|
build: {
|
||||||
|
outDir: '../../dist/apps/qrcampaign',
|
||||||
|
emptyOutDir: true,
|
||||||
|
reportCompressedSize: true,
|
||||||
|
commonjsOptions: {
|
||||||
|
transformMixedEsModules: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
test: {
|
||||||
|
name: 'qrcampaign',
|
||||||
|
watch: false,
|
||||||
|
globals: true,
|
||||||
|
environment: 'jsdom',
|
||||||
|
include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
|
||||||
|
reporters: ['default'],
|
||||||
|
coverage: {
|
||||||
|
reportsDirectory: '../../coverage/apps/qrcampaign',
|
||||||
|
provider: 'v8' as const,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
services:
|
||||||
|
qrcampaign:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: docker/qrcampaign.Dockerfile
|
||||||
|
ports:
|
||||||
|
- '127.0.0.1:3006:80'
|
||||||
|
restart: unless-stopped
|
||||||
|
container_name: qrcampaign
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
FROM node:22-alpine AS deps
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package.json package-lock.json ./
|
||||||
|
RUN npm ci --prefer-offline --no-audit --no-fund
|
||||||
|
|
||||||
|
FROM node:22-alpine AS builder
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
|
COPY . .
|
||||||
|
ENV NX_DAEMON=false
|
||||||
|
RUN npm run qrcampaign:build
|
||||||
|
|
||||||
|
FROM nginx:alpine AS runner
|
||||||
|
RUN rm -rf /usr/share/nginx/html/*
|
||||||
|
COPY --from=builder /app/dist/apps/qrcampaign /usr/share/nginx/html
|
||||||
|
EXPOSE 80
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
{
|
||||||
|
description = "Imphnen Frontend Service - Nx Monorepo";
|
||||||
|
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||||
|
flake-utils.url = "github:numtide/flake-utils";
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs = { self, nixpkgs, flake-utils }:
|
||||||
|
flake-utils.lib.eachDefaultSystem (system:
|
||||||
|
let
|
||||||
|
pkgs = import nixpkgs { inherit system; };
|
||||||
|
|
||||||
|
# NPM dependencies hash (update with: nix run nixpkgs#prefetch-npm-deps -- package-lock.json)
|
||||||
|
npmDepsHash = "sha256-xmG0ej6GJk1HIFFmrB9CpovNA20sJ7RanFLMBCxKdy0=";
|
||||||
|
|
||||||
|
# Import helpers
|
||||||
|
mkViteApp = import ./nix/mkViteApp.nix {
|
||||||
|
inherit pkgs npmDepsHash;
|
||||||
|
src = ./.;
|
||||||
|
};
|
||||||
|
|
||||||
|
landingApp = import ./nix/landing.nix {
|
||||||
|
inherit pkgs npmDepsHash;
|
||||||
|
src = ./.;
|
||||||
|
};
|
||||||
|
|
||||||
|
in {
|
||||||
|
packages = {
|
||||||
|
# Static Vite apps
|
||||||
|
backoffice = mkViteApp { name = "backoffice"; buildScript = "backoffice:build"; };
|
||||||
|
gacha = mkViteApp { name = "gacha"; buildScript = "gacha:build"; };
|
||||||
|
dimentorin = mkViteApp { name = "dimentorin"; buildScript = "dimentorin:build"; };
|
||||||
|
hackathon = mkViteApp { name = "hackathon"; buildScript = "hackathon:build"; };
|
||||||
|
infra = mkViteApp { name = "infra"; buildScript = "infra:build"; };
|
||||||
|
qrcampaign = mkViteApp { name = "qrcampaign"; buildScript = "qrcampaign:build"; };
|
||||||
|
|
||||||
|
# Next.js app
|
||||||
|
landing = landingApp;
|
||||||
|
|
||||||
|
# Default package
|
||||||
|
default = self.packages.${system}.landing;
|
||||||
|
};
|
||||||
|
|
||||||
|
# Development shell
|
||||||
|
devShells.default = pkgs.mkShell {
|
||||||
|
buildInputs = with pkgs; [
|
||||||
|
nodejs_22
|
||||||
|
nodePackages.npm
|
||||||
|
bun
|
||||||
|
|
||||||
|
# Useful dev tools
|
||||||
|
git
|
||||||
|
jq
|
||||||
|
];
|
||||||
|
|
||||||
|
shellHook = ''
|
||||||
|
echo "🚀 Imphnen Frontend Development Shell"
|
||||||
|
echo "Node.js: $(node --version)"
|
||||||
|
echo "npm: $(npm --version)"
|
||||||
|
echo ""
|
||||||
|
echo "Available commands:"
|
||||||
|
echo " npm install - Install dependencies"
|
||||||
|
echo " nx dev <app> - Start development server"
|
||||||
|
echo " nx build <app> - Build an app"
|
||||||
|
echo ""
|
||||||
|
echo "Apps: landing, backoffice, gacha, dimentorin, hackathon, qrcampaign"
|
||||||
|
echo ""
|
||||||
|
echo "Build packages with:"
|
||||||
|
echo " nix build .#landing"
|
||||||
|
echo " nix build .#backoffice"
|
||||||
|
echo " nix build .#gacha"
|
||||||
|
echo " nix build .#dimentorin"
|
||||||
|
echo " nix build .#hackathon"
|
||||||
|
echo " nix build .#qrcampaign"
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
) // {
|
||||||
|
# NixOS modules for deployment
|
||||||
|
nixosModules = {
|
||||||
|
landing = import ./nix/modules/landing.nix { inherit self; };
|
||||||
|
|
||||||
|
backoffice = import ./nix/modules/static-app.nix { inherit self; } { appName = "backoffice"; };
|
||||||
|
gacha = import ./nix/modules/static-app.nix { inherit self; } { appName = "gacha"; };
|
||||||
|
dimentorin = import ./nix/modules/static-app.nix { inherit self; } { appName = "dimentorin"; };
|
||||||
|
hackathon = import ./nix/modules/static-app.nix { inherit self; } { appName = "hackathon"; };
|
||||||
|
infra = import ./nix/modules/static-app.nix { inherit self; } { appName = "infra"; };
|
||||||
|
qrcampaign = import ./nix/modules/static-app.nix { inherit self; } { appName = "qrcampaign"; };
|
||||||
|
|
||||||
|
# All-in-one module that enables all apps
|
||||||
|
all = { config, lib, pkgs, ... }: {
|
||||||
|
imports = [
|
||||||
|
self.nixosModules.landing
|
||||||
|
self.nixosModules.backoffice
|
||||||
|
self.nixosModules.gacha
|
||||||
|
self.nixosModules.dimentorin
|
||||||
|
self.nixosModules.hackathon
|
||||||
|
self.nixosModules.infra
|
||||||
|
self.nixosModules.qrcampaign
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# Overlay for easy integration
|
||||||
|
overlays.default = final: prev: {
|
||||||
|
imphnen = {
|
||||||
|
landing = self.packages.${final.system}.landing;
|
||||||
|
backoffice = self.packages.${final.system}.backoffice;
|
||||||
|
gacha = self.packages.${final.system}.gacha;
|
||||||
|
dimentorin = self.packages.${final.system}.dimentorin;
|
||||||
|
hackathon = self.packages.${final.system}.hackathon;
|
||||||
|
infra = self.packages.${final.system}.infra;
|
||||||
|
qrcampaign = self.packages.${final.system}.qrcampaign;
|
||||||
|
# Function to build hackathon with custom environment variables (e.g., Supabase)
|
||||||
|
mkHackathonWithEnv = envVars: import ./nix/mkViteApp.nix {
|
||||||
|
pkgs = final;
|
||||||
|
src = self;
|
||||||
|
npmDepsHash = "sha256-xmG0ej6GJk1HIFFmrB9CpovNA20sJ7RanFLMBCxKdy0=";
|
||||||
|
} { name = "hackathon"; buildScript = "hackathon:build"; inherit envVars; };
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
import { createClient } from '@supabase/supabase-js';
|
|
||||||
|
|
||||||
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
|
|
||||||
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY;
|
|
||||||
|
|
||||||
if (!supabaseUrl || !supabaseAnonKey) {
|
|
||||||
throw new Error('Missing Supabase environment variables');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create Supabase client with proper session management enabled
|
|
||||||
export const supabase = createClient(supabaseUrl, supabaseAnonKey, {
|
|
||||||
auth: {
|
|
||||||
autoRefreshToken: true, // ✅ Auto-refresh expired tokens
|
|
||||||
persistSession: true, // ✅ Persist session in storage
|
|
||||||
detectSessionInUrl: true, // ✅ Auto-detect OAuth callback
|
|
||||||
storage: typeof window !== 'undefined' ? window.localStorage : undefined,
|
|
||||||
},
|
|
||||||
global: {
|
|
||||||
headers: {
|
|
||||||
'X-Client-Info': 'supabase-js-web',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Helper to create authenticated client (kept for backward compatibility)
|
|
||||||
// NOTE: With proper session management, this should no longer be needed
|
|
||||||
// Once session is set via supabase.auth.setSession(), the base client will have auth context
|
|
||||||
export const getAuthenticatedClient = (accessToken: string) => {
|
|
||||||
return createClient(supabaseUrl, supabaseAnonKey, {
|
|
||||||
auth: {
|
|
||||||
autoRefreshToken: false,
|
|
||||||
persistSession: false,
|
|
||||||
detectSessionInUrl: false,
|
|
||||||
},
|
|
||||||
global: {
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${accessToken}`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from './client';
|
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
# Example: How to use in imphnen-infrastructure
|
||||||
|
#
|
||||||
|
# In your imphnen-infrastructure flake.nix:
|
||||||
|
#
|
||||||
|
# {
|
||||||
|
# inputs = {
|
||||||
|
# nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||||
|
# imphnen-frontend = {
|
||||||
|
# url = "github:your-org/imphnen-frontend-service";
|
||||||
|
# # Or use local path during development:
|
||||||
|
# # url = "path:/path/to/imphnen-frontend-service";
|
||||||
|
# };
|
||||||
|
# };
|
||||||
|
#
|
||||||
|
# outputs = { self, nixpkgs, imphnen-frontend, ... }: {
|
||||||
|
# nixosConfigurations.your-server = nixpkgs.lib.nixosSystem {
|
||||||
|
# system = "x86_64-linux";
|
||||||
|
# modules = [
|
||||||
|
# # Import all modules at once
|
||||||
|
# imphnen-frontend.nixosModules.all
|
||||||
|
#
|
||||||
|
# # Or import individually:
|
||||||
|
# # imphnen-frontend.nixosModules.landing
|
||||||
|
# # imphnen-frontend.nixosModules.backoffice
|
||||||
|
#
|
||||||
|
# ./your-server-config.nix
|
||||||
|
# ];
|
||||||
|
# };
|
||||||
|
# };
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# Then in your-server-config.nix:
|
||||||
|
|
||||||
|
{ config, pkgs, ... }:
|
||||||
|
|
||||||
|
{
|
||||||
|
# Enable Landing (Next.js) - runs as systemd service
|
||||||
|
services.imphnen-landing = {
|
||||||
|
enable = true;
|
||||||
|
port = 3000;
|
||||||
|
hostname = "0.0.0.0";
|
||||||
|
openFirewall = true;
|
||||||
|
# environmentFile = /run/secrets/landing-env; # For secrets
|
||||||
|
};
|
||||||
|
|
||||||
|
# Enable Backoffice (Static) - served by nginx
|
||||||
|
services.imphnen-backoffice = {
|
||||||
|
enable = true;
|
||||||
|
domain = "backoffice.imphnen.dev";
|
||||||
|
enableSSL = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
# Enable Gacha (Static)
|
||||||
|
services.imphnen-gacha = {
|
||||||
|
enable = true;
|
||||||
|
domain = "gacha.imphnen.dev";
|
||||||
|
enableSSL = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
# Enable Dimentorin (Static)
|
||||||
|
services.imphnen-dimentorin = {
|
||||||
|
enable = true;
|
||||||
|
domain = "dimentorin.imphnen.dev";
|
||||||
|
enableSSL = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
# Enable Hackathon (Static)
|
||||||
|
services.imphnen-hackathon = {
|
||||||
|
enable = true;
|
||||||
|
domain = "hackathon.imphnen.dev";
|
||||||
|
enableSSL = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
# Nginx reverse proxy for landing (if needed)
|
||||||
|
services.nginx.virtualHosts."imphnen.dev" = {
|
||||||
|
forceSSL = true;
|
||||||
|
enableACME = true;
|
||||||
|
locations."/" = {
|
||||||
|
proxyPass = "http://127.0.0.1:3000";
|
||||||
|
proxyWebsockets = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# Enable ACME for SSL
|
||||||
|
security.acme = {
|
||||||
|
acceptTerms = true;
|
||||||
|
defaults.email = "admin@imphnen.dev";
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
# Next.js Landing App Package
|
||||||
|
{ pkgs, src, npmDepsHash }:
|
||||||
|
|
||||||
|
pkgs.buildNpmPackage {
|
||||||
|
pname = "imphnen-landing";
|
||||||
|
version = "0.0.1";
|
||||||
|
inherit src npmDepsHash;
|
||||||
|
|
||||||
|
npmFlags = [ "--legacy-peer-deps" ];
|
||||||
|
makeCacheWritable = true;
|
||||||
|
|
||||||
|
nativeBuildInputs = with pkgs; [
|
||||||
|
nodejs_22
|
||||||
|
nodePackages.npm
|
||||||
|
python3 # Required for node-gyp (sharp, etc.)
|
||||||
|
];
|
||||||
|
|
||||||
|
buildPhase = ''
|
||||||
|
runHook preBuild
|
||||||
|
export NX_DAEMON=false
|
||||||
|
export HOME=$TMPDIR
|
||||||
|
export CI=true
|
||||||
|
export NO_COLOR=1
|
||||||
|
export TERM=dumb
|
||||||
|
export NX_SKIP_NX_CACHE=true
|
||||||
|
export NX_TASKS_RUNNER_DYNAMIC_OUTPUT=false
|
||||||
|
export NX_NATIVE=false
|
||||||
|
./node_modules/.bin/nx build landing --output-style=static
|
||||||
|
runHook postBuild
|
||||||
|
'';
|
||||||
|
|
||||||
|
installPhase = ''
|
||||||
|
runHook preInstall
|
||||||
|
mkdir -p $out/bin $out/share/landing
|
||||||
|
|
||||||
|
# Copy standalone server
|
||||||
|
cp -r dist/apps/landing/.next/standalone/* $out/share/landing/
|
||||||
|
|
||||||
|
# Copy public assets
|
||||||
|
mkdir -p $out/share/landing/apps/landing/public
|
||||||
|
cp -r dist/apps/landing/public/* $out/share/landing/apps/landing/public/ || true
|
||||||
|
|
||||||
|
# Copy static files
|
||||||
|
mkdir -p $out/share/landing/dist/apps/landing/.next/static
|
||||||
|
cp -r dist/apps/landing/.next/static/* $out/share/landing/dist/apps/landing/.next/static/
|
||||||
|
|
||||||
|
# Create wrapper script
|
||||||
|
cat > $out/bin/imphnen-landing <<EOF
|
||||||
|
#!${pkgs.bash}/bin/bash
|
||||||
|
cd $out/share/landing
|
||||||
|
exec ${pkgs.nodejs_22}/bin/node --jitless apps/landing/server.js "\$@"
|
||||||
|
EOF
|
||||||
|
chmod +x $out/bin/imphnen-landing
|
||||||
|
|
||||||
|
runHook postInstall
|
||||||
|
'';
|
||||||
|
|
||||||
|
dontNpmBuild = true;
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
# Helper function to build Vite-based static apps
|
||||||
|
{ pkgs, src, npmDepsHash }:
|
||||||
|
|
||||||
|
{ name, buildScript, envVars ? {} }:
|
||||||
|
|
||||||
|
pkgs.buildNpmPackage {
|
||||||
|
pname = "imphnen-${name}";
|
||||||
|
version = "0.0.1";
|
||||||
|
inherit src npmDepsHash;
|
||||||
|
|
||||||
|
npmFlags = [ "--legacy-peer-deps" ];
|
||||||
|
makeCacheWritable = true;
|
||||||
|
|
||||||
|
nativeBuildInputs = with pkgs; [
|
||||||
|
nodejs_22
|
||||||
|
nodePackages.npm
|
||||||
|
util-linux # For script command to create pseudo-terminal
|
||||||
|
];
|
||||||
|
|
||||||
|
buildPhase = ''
|
||||||
|
runHook preBuild
|
||||||
|
export NX_DAEMON=false
|
||||||
|
export HOME=$TMPDIR
|
||||||
|
export CI=true
|
||||||
|
export NO_COLOR=1
|
||||||
|
export TERM=dumb
|
||||||
|
export NX_SKIP_NX_CACHE=true
|
||||||
|
export NX_TASKS_RUNNER_DYNAMIC_OUTPUT=false
|
||||||
|
export NX_NATIVE=false
|
||||||
|
${pkgs.lib.concatStringsSep "\n" (pkgs.lib.mapAttrsToList (k: v: "export ${k}=\"${v}\"") envVars)}
|
||||||
|
# Run nx build with pseudo-terminal to prevent terminal access errors
|
||||||
|
# Use script to create a pty, preventing Nx from crashing on raw terminal mode
|
||||||
|
script -q -c "./node_modules/.bin/nx build ${name} --output-style=static" /dev/null || true
|
||||||
|
# Verify the build output exists
|
||||||
|
test -d dist/apps/${name}
|
||||||
|
runHook postBuild
|
||||||
|
'';
|
||||||
|
|
||||||
|
installPhase = ''
|
||||||
|
runHook preInstall
|
||||||
|
mkdir -p $out
|
||||||
|
cp -r dist/apps/${name}/* $out/
|
||||||
|
runHook postInstall
|
||||||
|
'';
|
||||||
|
|
||||||
|
dontNpmBuild = true;
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
# NixOS Module for Imphnen Landing
|
||||||
|
{ self }:
|
||||||
|
|
||||||
|
{ config, lib, pkgs, ... }:
|
||||||
|
|
||||||
|
let
|
||||||
|
cfg = config.services.imphnen-landing;
|
||||||
|
in {
|
||||||
|
options.services.imphnen-landing = {
|
||||||
|
enable = lib.mkEnableOption "Imphnen Landing Page";
|
||||||
|
|
||||||
|
port = lib.mkOption {
|
||||||
|
type = lib.types.port;
|
||||||
|
default = 3000;
|
||||||
|
description = "Port to run the landing server on";
|
||||||
|
};
|
||||||
|
|
||||||
|
hostname = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "0.0.0.0";
|
||||||
|
description = "Hostname to bind to";
|
||||||
|
};
|
||||||
|
|
||||||
|
package = lib.mkOption {
|
||||||
|
type = lib.types.package;
|
||||||
|
default = self.packages.${pkgs.system}.landing;
|
||||||
|
description = "The landing package to use";
|
||||||
|
};
|
||||||
|
|
||||||
|
openFirewall = lib.mkOption {
|
||||||
|
type = lib.types.bool;
|
||||||
|
default = false;
|
||||||
|
description = "Open firewall for the landing port";
|
||||||
|
};
|
||||||
|
|
||||||
|
environmentFile = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.path;
|
||||||
|
default = null;
|
||||||
|
description = "Environment file for secrets";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = lib.mkIf cfg.enable {
|
||||||
|
systemd.services.imphnen-landing = {
|
||||||
|
description = "Imphnen Landing Page";
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
after = [ "network.target" ];
|
||||||
|
|
||||||
|
environment = {
|
||||||
|
NODE_ENV = "production";
|
||||||
|
PORT = toString cfg.port;
|
||||||
|
HOSTNAME = cfg.hostname;
|
||||||
|
};
|
||||||
|
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "simple";
|
||||||
|
ExecStart = "${cfg.package}/bin/imphnen-landing";
|
||||||
|
Restart = "on-failure";
|
||||||
|
RestartSec = "5s";
|
||||||
|
|
||||||
|
# Hardening
|
||||||
|
DynamicUser = true;
|
||||||
|
NoNewPrivileges = true;
|
||||||
|
ProtectSystem = "strict";
|
||||||
|
ProtectHome = true;
|
||||||
|
PrivateTmp = true;
|
||||||
|
ProtectKernelTunables = true;
|
||||||
|
ProtectKernelModules = true;
|
||||||
|
ProtectControlGroups = true;
|
||||||
|
RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ];
|
||||||
|
RestrictNamespaces = true;
|
||||||
|
LockPersonality = true;
|
||||||
|
MemoryDenyWriteExecute = true;
|
||||||
|
RestrictRealtime = true;
|
||||||
|
RestrictSUIDSGID = true;
|
||||||
|
} // lib.optionalAttrs (cfg.environmentFile != null) {
|
||||||
|
EnvironmentFile = cfg.environmentFile;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
networking.firewall.allowedTCPPorts = lib.mkIf cfg.openFirewall [ cfg.port ];
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
# NixOS Module for Static Vite Apps (nginx-based)
|
||||||
|
{ self }:
|
||||||
|
|
||||||
|
{ appName }:
|
||||||
|
|
||||||
|
{ config, lib, pkgs, ... }:
|
||||||
|
|
||||||
|
let
|
||||||
|
cfg = config.services."imphnen-${appName}";
|
||||||
|
in {
|
||||||
|
options.services."imphnen-${appName}" = {
|
||||||
|
enable = lib.mkEnableOption "Imphnen ${appName}";
|
||||||
|
|
||||||
|
domain = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
description = "Domain name for the app";
|
||||||
|
};
|
||||||
|
|
||||||
|
package = lib.mkOption {
|
||||||
|
type = lib.types.package;
|
||||||
|
default = self.packages.${pkgs.system}.${appName};
|
||||||
|
description = "The ${appName} package to use";
|
||||||
|
};
|
||||||
|
|
||||||
|
enableSSL = lib.mkOption {
|
||||||
|
type = lib.types.bool;
|
||||||
|
default = true;
|
||||||
|
description = "Enable ACME SSL";
|
||||||
|
};
|
||||||
|
|
||||||
|
extraLocations = lib.mkOption {
|
||||||
|
type = lib.types.attrsOf lib.types.anything;
|
||||||
|
default = {};
|
||||||
|
description = "Additional nginx locations";
|
||||||
|
};
|
||||||
|
|
||||||
|
extraConfig = lib.mkOption {
|
||||||
|
type = lib.types.lines;
|
||||||
|
default = "";
|
||||||
|
description = "Extra nginx configuration";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = lib.mkIf cfg.enable {
|
||||||
|
services.nginx = {
|
||||||
|
enable = true;
|
||||||
|
recommendedGzipSettings = true;
|
||||||
|
recommendedOptimisation = true;
|
||||||
|
recommendedProxySettings = true;
|
||||||
|
recommendedTlsSettings = true;
|
||||||
|
|
||||||
|
virtualHosts.${cfg.domain} = {
|
||||||
|
forceSSL = cfg.enableSSL;
|
||||||
|
enableACME = cfg.enableSSL;
|
||||||
|
root = cfg.package;
|
||||||
|
|
||||||
|
locations = {
|
||||||
|
"/" = {
|
||||||
|
tryFiles = "$uri $uri/ /index.html";
|
||||||
|
};
|
||||||
|
|
||||||
|
# Cache static assets
|
||||||
|
"~* \\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$" = {
|
||||||
|
tryFiles = "$uri =404";
|
||||||
|
extraConfig = ''
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
} // cfg.extraLocations;
|
||||||
|
|
||||||
|
extraConfig = cfg.extraConfig;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -53,6 +53,13 @@
|
|||||||
"options": {
|
"options": {
|
||||||
"targetName": "e2e"
|
"targetName": "e2e"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"plugin": "@nx/vitest",
|
||||||
|
"options": {
|
||||||
|
"testTargetName": "vitest:test",
|
||||||
|
"ciTargetName": "test-ci"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"generators": {
|
"generators": {
|
||||||
|
|||||||
Generated
+140
-163
@@ -16,7 +16,6 @@
|
|||||||
"@radix-ui/react-label": "^2.1.7",
|
"@radix-ui/react-label": "^2.1.7",
|
||||||
"@radix-ui/react-slot": "^1.2.0",
|
"@radix-ui/react-slot": "^1.2.0",
|
||||||
"@redocly/ajv": "^8.11.2",
|
"@redocly/ajv": "^8.11.2",
|
||||||
"@supabase/supabase-js": "^2.84.0",
|
|
||||||
"@tanstack/react-query": "^5.74.4",
|
"@tanstack/react-query": "^5.74.4",
|
||||||
"@tanstack/react-store": "^0.8.0",
|
"@tanstack/react-store": "^0.8.0",
|
||||||
"@tanstack/react-table": "^8.21.2",
|
"@tanstack/react-table": "^8.21.2",
|
||||||
@@ -28,7 +27,7 @@
|
|||||||
"graphql": "^16.11.0",
|
"graphql": "^16.11.0",
|
||||||
"html2canvas": "^1.4.1",
|
"html2canvas": "^1.4.1",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"next": "^16.0.8",
|
"next": "^16.0.10",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"openapi-fetch": "^0.15.0",
|
"openapi-fetch": "^0.15.0",
|
||||||
"openapi-react-query": "^0.5.0",
|
"openapi-react-query": "^0.5.0",
|
||||||
@@ -37,6 +36,7 @@
|
|||||||
"react-dom": "^19.2.1",
|
"react-dom": "^19.2.1",
|
||||||
"react-hook-form": "^7.56.4",
|
"react-hook-form": "^7.56.4",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
|
"react-is": "^19.2.3",
|
||||||
"react-router-dom": "^7.3.0",
|
"react-router-dom": "^7.3.0",
|
||||||
"recharts": "^3.1.2",
|
"recharts": "^3.1.2",
|
||||||
"sharp": "^0.34.1",
|
"sharp": "^0.34.1",
|
||||||
@@ -2587,6 +2587,7 @@
|
|||||||
"version": "4.9.0",
|
"version": "4.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz",
|
||||||
"integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==",
|
"integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"eslint-visitor-keys": "^3.4.3"
|
"eslint-visitor-keys": "^3.4.3"
|
||||||
@@ -2605,6 +2606,7 @@
|
|||||||
"version": "4.12.2",
|
"version": "4.12.2",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
|
||||||
"integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
|
"integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^12.0.0 || ^14.0.0 || >=16.0.0"
|
"node": "^12.0.0 || ^14.0.0 || >=16.0.0"
|
||||||
@@ -2635,6 +2637,7 @@
|
|||||||
"version": "0.21.1",
|
"version": "0.21.1",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz",
|
||||||
"integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==",
|
"integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==",
|
||||||
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint/object-schema": "^2.1.7",
|
"@eslint/object-schema": "^2.1.7",
|
||||||
@@ -2649,6 +2652,7 @@
|
|||||||
"version": "0.4.2",
|
"version": "0.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz",
|
||||||
"integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==",
|
"integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==",
|
||||||
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint/core": "^0.17.0"
|
"@eslint/core": "^0.17.0"
|
||||||
@@ -2661,6 +2665,7 @@
|
|||||||
"version": "0.17.0",
|
"version": "0.17.0",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz",
|
||||||
"integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==",
|
"integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/json-schema": "^7.0.15"
|
"@types/json-schema": "^7.0.15"
|
||||||
@@ -2736,6 +2741,7 @@
|
|||||||
"version": "3.3.1",
|
"version": "3.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz",
|
||||||
"integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==",
|
"integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ajv": "^6.12.4",
|
"ajv": "^6.12.4",
|
||||||
@@ -2759,6 +2765,7 @@
|
|||||||
"version": "9.39.1",
|
"version": "9.39.1",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz",
|
||||||
"integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==",
|
"integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==",
|
||||||
|
"dev": 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"
|
||||||
@@ -2771,6 +2778,7 @@
|
|||||||
"version": "2.1.7",
|
"version": "2.1.7",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz",
|
||||||
"integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==",
|
"integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==",
|
||||||
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
@@ -2780,6 +2788,7 @@
|
|||||||
"version": "0.4.1",
|
"version": "0.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz",
|
||||||
"integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==",
|
"integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==",
|
||||||
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint/core": "^0.17.0",
|
"@eslint/core": "^0.17.0",
|
||||||
@@ -2805,6 +2814,7 @@
|
|||||||
"version": "0.19.1",
|
"version": "0.19.1",
|
||||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
||||||
"integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
|
"integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
|
||||||
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.18.0"
|
"node": ">=18.18.0"
|
||||||
@@ -2814,6 +2824,7 @@
|
|||||||
"version": "0.16.7",
|
"version": "0.16.7",
|
||||||
"resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz",
|
"resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz",
|
||||||
"integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==",
|
"integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@humanfs/core": "^0.19.1",
|
"@humanfs/core": "^0.19.1",
|
||||||
@@ -2827,6 +2838,7 @@
|
|||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
|
||||||
"integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
|
"integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
|
||||||
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12.22"
|
"node": ">=12.22"
|
||||||
@@ -2840,6 +2852,7 @@
|
|||||||
"version": "0.4.3",
|
"version": "0.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
|
||||||
"integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
|
"integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.18"
|
"node": ">=18.18"
|
||||||
@@ -4953,9 +4966,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/env": {
|
"node_modules/@next/env": {
|
||||||
"version": "16.0.8",
|
"version": "16.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.0.10.tgz",
|
||||||
"integrity": "sha512-xP4WrQZuj9MdmLJy3eWFHepo+R3vznsMSS8Dy3wdA7FKpjCiesQ6DxZvdGziQisj0tEtCgBKJzjcAc4yZOgLEQ==",
|
"integrity": "sha512-8tuaQkyDVgeONQ1MeT9Mkk8pQmZapMKFh5B+OrFUlG3rVmYTXcXlBetBgTurKXGaIZvkoqRT9JL5K3phXcgang==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@next/eslint-plugin-next": {
|
"node_modules/@next/eslint-plugin-next": {
|
||||||
@@ -4969,9 +4982,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-darwin-arm64": {
|
"node_modules/@next/swc-darwin-arm64": {
|
||||||
"version": "16.0.8",
|
"version": "16.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.0.10.tgz",
|
||||||
"integrity": "sha512-yjVMvTQN21ZHOclQnhSFbjBTEizle+1uo4NV6L4rtS9WO3nfjaeJYw+H91G+nEf3Ef43TaEZvY5mPWfB/De7tA==",
|
"integrity": "sha512-4XgdKtdVsaflErz+B5XeG0T5PeXKDdruDf3CRpnhN+8UebNa5N2H58+3GDgpn/9GBurrQ1uWW768FfscwYkJRg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -4985,9 +4998,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-darwin-x64": {
|
"node_modules/@next/swc-darwin-x64": {
|
||||||
"version": "16.0.8",
|
"version": "16.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.0.10.tgz",
|
||||||
"integrity": "sha512-+zu2N3QQ0ZOb6RyqQKfcu/pn0UPGmg+mUDqpAAEviAcEVEYgDckemOpiMRsBP3IsEKpcoKuNzekDcPczEeEIzA==",
|
"integrity": "sha512-spbEObMvRKkQ3CkYVOME+ocPDFo5UqHb8EMTS78/0mQ+O1nqE8toHJVioZo4TvebATxgA8XMTHHrScPrn68OGw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -5001,9 +5014,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-linux-arm64-gnu": {
|
"node_modules/@next/swc-linux-arm64-gnu": {
|
||||||
"version": "16.0.8",
|
"version": "16.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.0.10.tgz",
|
||||||
"integrity": "sha512-LConttk+BeD0e6RG0jGEP9GfvdaBVMYsLJ5aDDweKiJVVCu6sGvo+Ohz9nQhvj7EQDVVRJMCGhl19DmJwGr6bQ==",
|
"integrity": "sha512-uQtWE3X0iGB8apTIskOMi2w/MKONrPOUCi5yLO+v3O8Mb5c7K4Q5KD1jvTpTF5gJKa3VH/ijKjKUq9O9UhwOYw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -5017,9 +5030,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-linux-arm64-musl": {
|
"node_modules/@next/swc-linux-arm64-musl": {
|
||||||
"version": "16.0.8",
|
"version": "16.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.0.10.tgz",
|
||||||
"integrity": "sha512-JaXFAlqn8fJV+GhhA9lpg6da/NCN/v9ub98n3HoayoUSPOVdoxEEt86iT58jXqQCs/R3dv5ZnxGkW8aF4obMrQ==",
|
"integrity": "sha512-llA+hiDTrYvyWI21Z0L1GiXwjQaanPVQQwru5peOgtooeJ8qx3tlqRV2P7uH2pKQaUfHxI/WVarvI5oYgGxaTw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -5033,9 +5046,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-linux-x64-gnu": {
|
"node_modules/@next/swc-linux-x64-gnu": {
|
||||||
"version": "16.0.8",
|
"version": "16.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.0.10.tgz",
|
||||||
"integrity": "sha512-O7M9it6HyNhsJp3HNAsJoHk5BUsfj7hRshfptpGcVsPZ1u0KQ/oVy8oxF7tlwxA5tR43VUP0yRmAGm1us514ng==",
|
"integrity": "sha512-AK2q5H0+a9nsXbeZ3FZdMtbtu9jxW4R/NgzZ6+lrTm3d6Zb7jYrWcgjcpM1k8uuqlSy4xIyPR2YiuUr+wXsavA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -5049,9 +5062,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-linux-x64-musl": {
|
"node_modules/@next/swc-linux-x64-musl": {
|
||||||
"version": "16.0.8",
|
"version": "16.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.0.10.tgz",
|
||||||
"integrity": "sha512-8+KClEC/GLI2dLYcrWwHu5JyC5cZYCFnccVIvmxpo6K+XQt4qzqM5L4coofNDZYkct/VCCyJWGbZZDsg6w6LFA==",
|
"integrity": "sha512-1TDG9PDKivNw5550S111gsO4RGennLVl9cipPhtkXIFVwo31YZ73nEbLjNC8qG3SgTz/QZyYyaFYMeY4BKZR/g==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -5065,9 +5078,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-win32-arm64-msvc": {
|
"node_modules/@next/swc-win32-arm64-msvc": {
|
||||||
"version": "16.0.8",
|
"version": "16.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.0.10.tgz",
|
||||||
"integrity": "sha512-rpQ/PgTEgH68SiXmhu/cJ2hk9aZ6YgFvspzQWe2I9HufY6g7V02DXRr/xrVqOaKm2lenBFPNQ+KAaeveywqV+A==",
|
"integrity": "sha512-aEZIS4Hh32xdJQbHz121pyuVZniSNoqDVx1yIr2hy+ZwJGipeqnMZBJHyMxv2tiuAXGx6/xpTcQJ6btIiBjgmg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -5081,9 +5094,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-win32-x64-msvc": {
|
"node_modules/@next/swc-win32-x64-msvc": {
|
||||||
"version": "16.0.8",
|
"version": "16.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.0.10.tgz",
|
||||||
"integrity": "sha512-jWpWjWcMQu2iZz4pEK2IktcfR+OA9+cCG8zenyLpcW8rN4rzjfOzH4yj/b1FiEAZHKS+5Vq8+bZyHi+2yqHbFA==",
|
"integrity": "sha512-E+njfCoFLb01RAFEnGZn6ERoOqhK1Gl3Lfz1Kjnj0Ulfu7oJbuMyvBKNj/bw8XZnenHDASlygTjZICQW+rYW1Q==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -6262,7 +6275,7 @@
|
|||||||
"version": "1.56.1",
|
"version": "1.56.1",
|
||||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.1.tgz",
|
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.1.tgz",
|
||||||
"integrity": "sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==",
|
"integrity": "sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==",
|
||||||
"devOptional": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright": "1.56.1"
|
"playwright": "1.56.1"
|
||||||
@@ -7945,106 +7958,6 @@
|
|||||||
"integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
|
"integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@supabase/auth-js": {
|
|
||||||
"version": "2.84.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.84.0.tgz",
|
|
||||||
"integrity": "sha512-J6XKbqqg1HQPMfYkAT9BrC8anPpAiifl7qoVLsYhQq5B/dnu/lxab1pabnxtJEsvYG5rwI5HEVEGXMjoQ6Wz2Q==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"tslib": "2.8.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@supabase/functions-js": {
|
|
||||||
"version": "2.84.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.84.0.tgz",
|
|
||||||
"integrity": "sha512-2oY5QBV4py/s64zMlhPEz+4RTdlwxzmfhM1k2xftD2v1DruRZKfoe7Yn9DCz1VondxX8evcvpc2udEIGzHI+VA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"tslib": "2.8.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@supabase/postgrest-js": {
|
|
||||||
"version": "2.84.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.84.0.tgz",
|
|
||||||
"integrity": "sha512-oplc/3jfJeVW4F0J8wqywHkjIZvOVHtqzF0RESijepDAv5Dn/LThlGW1ftysoP4+PXVIrnghAbzPHo88fNomPQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"tslib": "2.8.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@supabase/realtime-js": {
|
|
||||||
"version": "2.84.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.84.0.tgz",
|
|
||||||
"integrity": "sha512-ThqjxiCwWiZAroHnYPmnNl6tZk6jxGcG2a7Hp/3kcolPcMj89kWjUTA3cHmhdIWYsP84fHp8MAQjYWMLf7HEUg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/phoenix": "^1.6.6",
|
|
||||||
"@types/ws": "^8.18.1",
|
|
||||||
"tslib": "2.8.1",
|
|
||||||
"ws": "^8.18.2"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@supabase/realtime-js/node_modules/ws": {
|
|
||||||
"version": "8.18.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
|
|
||||||
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10.0.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"bufferutil": "^4.0.1",
|
|
||||||
"utf-8-validate": ">=5.0.2"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"bufferutil": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"utf-8-validate": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@supabase/storage-js": {
|
|
||||||
"version": "2.84.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.84.0.tgz",
|
|
||||||
"integrity": "sha512-vXvAJ1euCuhryOhC6j60dG8ky+lk0V06ubNo+CbhuoUv+sl39PyY0lc+k+qpQhTk/VcI6SiM0OECLN83+nyJ5A==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"tslib": "2.8.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@supabase/supabase-js": {
|
|
||||||
"version": "2.84.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.84.0.tgz",
|
|
||||||
"integrity": "sha512-byMqYBvb91sx2jcZsdp0qLpmd4Dioe80e4OU/UexXftCkpTcgrkoENXHf5dO8FCSai8SgNeq16BKg10QiDI6xg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@supabase/auth-js": "2.84.0",
|
|
||||||
"@supabase/functions-js": "2.84.0",
|
|
||||||
"@supabase/postgrest-js": "2.84.0",
|
|
||||||
"@supabase/realtime-js": "2.84.0",
|
|
||||||
"@supabase/storage-js": "2.84.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@svgr/babel-plugin-add-jsx-attribute": {
|
"node_modules/@svgr/babel-plugin-add-jsx-attribute": {
|
||||||
"version": "8.0.0",
|
"version": "8.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz",
|
||||||
@@ -9472,6 +9385,7 @@
|
|||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||||
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/express": {
|
"node_modules/@types/express": {
|
||||||
@@ -9583,6 +9497,7 @@
|
|||||||
"version": "7.0.15",
|
"version": "7.0.15",
|
||||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
||||||
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
|
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/json5": {
|
"node_modules/@types/json5": {
|
||||||
@@ -9610,6 +9525,7 @@
|
|||||||
"version": "22.19.1",
|
"version": "22.19.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.1.tgz",
|
||||||
"integrity": "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==",
|
"integrity": "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~6.21.0"
|
"undici-types": "~6.21.0"
|
||||||
@@ -9632,12 +9548,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/phoenix": {
|
|
||||||
"version": "1.6.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz",
|
|
||||||
"integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@types/qrcode": {
|
"node_modules/@types/qrcode": {
|
||||||
"version": "1.5.6",
|
"version": "1.5.6",
|
||||||
"resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.6.tgz",
|
"resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.6.tgz",
|
||||||
@@ -9666,7 +9576,7 @@
|
|||||||
"version": "19.2.7",
|
"version": "19.2.7",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz",
|
||||||
"integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
|
"integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
|
||||||
"devOptional": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.2.2"
|
"csstype": "^3.2.2"
|
||||||
@@ -9676,7 +9586,7 @@
|
|||||||
"version": "19.2.3",
|
"version": "19.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
|
||||||
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
||||||
"devOptional": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "^19.2.0"
|
"@types/react": "^19.2.0"
|
||||||
@@ -9776,6 +9686,7 @@
|
|||||||
"version": "8.18.1",
|
"version": "8.18.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
|
||||||
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
|
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
@@ -11664,6 +11575,7 @@
|
|||||||
"version": "8.15.0",
|
"version": "8.15.0",
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
@@ -11689,6 +11601,7 @@
|
|||||||
"version": "5.3.2",
|
"version": "5.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
|
||||||
"integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
|
"integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||||
@@ -11744,6 +11657,7 @@
|
|||||||
"version": "6.12.6",
|
"version": "6.12.6",
|
||||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fast-deep-equal": "^3.1.1",
|
"fast-deep-equal": "^3.1.1",
|
||||||
@@ -11805,6 +11719,7 @@
|
|||||||
"version": "0.4.1",
|
"version": "0.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
||||||
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
|
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/alien-signals": {
|
"node_modules/alien-signals": {
|
||||||
@@ -11940,6 +11855,7 @@
|
|||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||||
|
"dev": true,
|
||||||
"license": "Python-2.0"
|
"license": "Python-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/aria-hidden": {
|
"node_modules/aria-hidden": {
|
||||||
@@ -12502,6 +12418,7 @@
|
|||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/bare-events": {
|
"node_modules/bare-events": {
|
||||||
@@ -12890,6 +12807,7 @@
|
|||||||
"version": "1.1.12",
|
"version": "1.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"balanced-match": "^1.0.0",
|
"balanced-match": "^1.0.0",
|
||||||
@@ -13140,6 +13058,7 @@
|
|||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
||||||
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
|
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
@@ -13219,6 +13138,7 @@
|
|||||||
"version": "4.1.2",
|
"version": "4.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||||
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ansi-styles": "^4.1.0",
|
"ansi-styles": "^4.1.0",
|
||||||
@@ -13275,7 +13195,7 @@
|
|||||||
"version": "4.0.3",
|
"version": "4.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
||||||
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
|
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
|
||||||
"devOptional": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"readdirp": "^4.0.1"
|
"readdirp": "^4.0.1"
|
||||||
@@ -13679,6 +13599,7 @@
|
|||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/concat-with-sourcemaps": {
|
"node_modules/concat-with-sourcemaps": {
|
||||||
@@ -13927,6 +13848,7 @@
|
|||||||
"version": "7.0.6",
|
"version": "7.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||||
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"path-key": "^3.1.0",
|
"path-key": "^3.1.0",
|
||||||
@@ -14259,7 +14181,7 @@
|
|||||||
"version": "3.2.3",
|
"version": "3.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||||
"devOptional": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/d3-array": {
|
"node_modules/d3-array": {
|
||||||
@@ -14499,6 +14421,7 @@
|
|||||||
"version": "4.4.3",
|
"version": "4.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ms": "^2.1.3"
|
"ms": "^2.1.3"
|
||||||
@@ -14594,6 +14517,7 @@
|
|||||||
"version": "0.1.4",
|
"version": "0.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
|
||||||
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
|
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/deepmerge": {
|
"node_modules/deepmerge": {
|
||||||
@@ -15483,6 +15407,7 @@
|
|||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
||||||
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
|
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
@@ -15495,6 +15420,7 @@
|
|||||||
"version": "9.39.1",
|
"version": "9.39.1",
|
||||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz",
|
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz",
|
||||||
"integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
|
"integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.8.0",
|
"@eslint-community/eslint-utils": "^4.8.0",
|
||||||
@@ -16144,6 +16070,7 @@
|
|||||||
"version": "8.4.0",
|
"version": "8.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
|
||||||
"integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==",
|
"integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==",
|
||||||
|
"dev": true,
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esrecurse": "^4.3.0",
|
"esrecurse": "^4.3.0",
|
||||||
@@ -16160,6 +16087,7 @@
|
|||||||
"version": "3.4.3",
|
"version": "3.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
|
||||||
"integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
|
"integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
|
||||||
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||||
@@ -16172,6 +16100,7 @@
|
|||||||
"version": "4.2.1",
|
"version": "4.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
|
||||||
"integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
|
"integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
@@ -16184,6 +16113,7 @@
|
|||||||
"version": "10.4.0",
|
"version": "10.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
|
||||||
"integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
|
"integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"acorn": "^8.15.0",
|
"acorn": "^8.15.0",
|
||||||
@@ -16201,6 +16131,7 @@
|
|||||||
"version": "4.2.1",
|
"version": "4.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
|
||||||
"integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
|
"integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
@@ -16227,6 +16158,7 @@
|
|||||||
"version": "1.6.0",
|
"version": "1.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
|
||||||
"integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
|
"integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
|
||||||
|
"dev": true,
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"estraverse": "^5.1.0"
|
"estraverse": "^5.1.0"
|
||||||
@@ -16239,6 +16171,7 @@
|
|||||||
"version": "4.3.0",
|
"version": "4.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
|
||||||
"integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
|
"integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
|
||||||
|
"dev": true,
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"estraverse": "^5.2.0"
|
"estraverse": "^5.2.0"
|
||||||
@@ -16251,6 +16184,7 @@
|
|||||||
"version": "5.3.0",
|
"version": "5.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
|
||||||
"integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
|
"integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
|
||||||
|
"dev": true,
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=4.0"
|
"node": ">=4.0"
|
||||||
@@ -16267,6 +16201,7 @@
|
|||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
|
||||||
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
|
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
|
||||||
|
"dev": true,
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
@@ -16542,12 +16477,14 @@
|
|||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
|
||||||
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
|
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/fast-levenshtein": {
|
"node_modules/fast-levenshtein": {
|
||||||
"version": "2.0.6",
|
"version": "2.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
|
||||||
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
|
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/fast-uri": {
|
"node_modules/fast-uri": {
|
||||||
@@ -16644,6 +16581,7 @@
|
|||||||
"version": "8.0.0",
|
"version": "8.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
|
||||||
"integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
|
"integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"flat-cache": "^4.0.0"
|
"flat-cache": "^4.0.0"
|
||||||
@@ -16869,6 +16807,7 @@
|
|||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
|
||||||
"integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
|
"integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"locate-path": "^6.0.0",
|
"locate-path": "^6.0.0",
|
||||||
@@ -16911,6 +16850,7 @@
|
|||||||
"version": "4.0.1",
|
"version": "4.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
|
||||||
"integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
|
"integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"flatted": "^3.2.9",
|
"flatted": "^3.2.9",
|
||||||
@@ -16924,6 +16864,7 @@
|
|||||||
"version": "3.3.3",
|
"version": "3.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
|
||||||
"integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
|
"integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
|
||||||
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/follow-redirects": {
|
"node_modules/follow-redirects": {
|
||||||
@@ -17562,6 +17503,7 @@
|
|||||||
"version": "6.0.2",
|
"version": "6.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
||||||
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
|
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
|
||||||
|
"dev": true,
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"is-glob": "^4.0.3"
|
"is-glob": "^4.0.3"
|
||||||
@@ -17666,6 +17608,7 @@
|
|||||||
"version": "14.0.0",
|
"version": "14.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
|
||||||
"integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
|
"integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
@@ -18040,6 +17983,7 @@
|
|||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
@@ -18515,6 +18459,7 @@
|
|||||||
"version": "5.3.2",
|
"version": "5.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||||
"integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
|
"integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 4"
|
"node": ">= 4"
|
||||||
@@ -18548,7 +18493,7 @@
|
|||||||
"version": "5.1.4",
|
"version": "5.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz",
|
||||||
"integrity": "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==",
|
"integrity": "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==",
|
||||||
"devOptional": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/import-cwd": {
|
"node_modules/import-cwd": {
|
||||||
@@ -18568,6 +18513,7 @@
|
|||||||
"version": "3.3.1",
|
"version": "3.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
|
||||||
"integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
|
"integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"parent-module": "^1.0.0",
|
"parent-module": "^1.0.0",
|
||||||
@@ -18617,6 +18563,7 @@
|
|||||||
"version": "0.1.4",
|
"version": "0.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
|
||||||
"integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
|
"integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.8.19"
|
"node": ">=0.8.19"
|
||||||
@@ -18957,6 +18904,7 @@
|
|||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||||
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
@@ -19011,6 +18959,7 @@
|
|||||||
"version": "4.0.3",
|
"version": "4.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||||
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"is-extglob": "^2.1.1"
|
"is-extglob": "^2.1.1"
|
||||||
@@ -19429,6 +19378,7 @@
|
|||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||||
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/isobject": {
|
"node_modules/isobject": {
|
||||||
@@ -19702,7 +19652,7 @@
|
|||||||
"version": "2.4.2",
|
"version": "2.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz",
|
||||||
"integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==",
|
"integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==",
|
||||||
"devOptional": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
"jiti": "lib/jiti-cli.mjs"
|
"jiti": "lib/jiti-cli.mjs"
|
||||||
@@ -19745,6 +19695,7 @@
|
|||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
||||||
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
|
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"argparse": "^2.0.1"
|
"argparse": "^2.0.1"
|
||||||
@@ -19846,6 +19797,7 @@
|
|||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
|
||||||
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
|
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/json-parse-even-better-errors": {
|
"node_modules/json-parse-even-better-errors": {
|
||||||
@@ -19872,6 +19824,7 @@
|
|||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
|
||||||
"integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
|
"integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/json-stringify-safe": {
|
"node_modules/json-stringify-safe": {
|
||||||
@@ -20100,6 +20053,7 @@
|
|||||||
"version": "4.5.4",
|
"version": "4.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||||
"integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
|
"integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"json-buffer": "3.0.1"
|
"json-buffer": "3.0.1"
|
||||||
@@ -20339,6 +20293,7 @@
|
|||||||
"version": "0.4.1",
|
"version": "0.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
|
||||||
"integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
|
"integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"prelude-ls": "^1.2.1",
|
"prelude-ls": "^1.2.1",
|
||||||
@@ -20701,6 +20656,7 @@
|
|||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
|
||||||
"integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
|
"integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"p-locate": "^5.0.0"
|
"p-locate": "^5.0.0"
|
||||||
@@ -20803,6 +20759,7 @@
|
|||||||
"version": "4.6.2",
|
"version": "4.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
||||||
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
|
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/lodash.once": {
|
"node_modules/lodash.once": {
|
||||||
@@ -21239,6 +21196,7 @@
|
|||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||||
|
"dev": true,
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"brace-expansion": "^1.1.7"
|
"brace-expansion": "^1.1.7"
|
||||||
@@ -21341,6 +21299,7 @@
|
|||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/muggle-string": {
|
"node_modules/muggle-string": {
|
||||||
@@ -21412,6 +21371,7 @@
|
|||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
|
||||||
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
|
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/needle": {
|
"node_modules/needle": {
|
||||||
@@ -21450,12 +21410,12 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/next": {
|
"node_modules/next": {
|
||||||
"version": "16.0.8",
|
"version": "16.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/next/-/next-16.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/next/-/next-16.0.10.tgz",
|
||||||
"integrity": "sha512-LmcZzG04JuzNXi48s5P+TnJBsTGPJunViNKV/iE4uM6kstjTQsQhvsAv+xF6MJxU2Pr26tl15eVbp0jQnsv6/g==",
|
"integrity": "sha512-RtWh5PUgI+vxlV3HdR+IfWA1UUHu0+Ram/JBO4vWB54cVPentCD0e+lxyAYEsDTqGGMg7qpjhKh6dc6aW7W/sA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@next/env": "16.0.8",
|
"@next/env": "16.0.10",
|
||||||
"@swc/helpers": "0.5.15",
|
"@swc/helpers": "0.5.15",
|
||||||
"caniuse-lite": "^1.0.30001579",
|
"caniuse-lite": "^1.0.30001579",
|
||||||
"postcss": "8.4.31",
|
"postcss": "8.4.31",
|
||||||
@@ -21468,14 +21428,14 @@
|
|||||||
"node": ">=20.9.0"
|
"node": ">=20.9.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@next/swc-darwin-arm64": "16.0.8",
|
"@next/swc-darwin-arm64": "16.0.10",
|
||||||
"@next/swc-darwin-x64": "16.0.8",
|
"@next/swc-darwin-x64": "16.0.10",
|
||||||
"@next/swc-linux-arm64-gnu": "16.0.8",
|
"@next/swc-linux-arm64-gnu": "16.0.10",
|
||||||
"@next/swc-linux-arm64-musl": "16.0.8",
|
"@next/swc-linux-arm64-musl": "16.0.10",
|
||||||
"@next/swc-linux-x64-gnu": "16.0.8",
|
"@next/swc-linux-x64-gnu": "16.0.10",
|
||||||
"@next/swc-linux-x64-musl": "16.0.8",
|
"@next/swc-linux-x64-musl": "16.0.10",
|
||||||
"@next/swc-win32-arm64-msvc": "16.0.8",
|
"@next/swc-win32-arm64-msvc": "16.0.10",
|
||||||
"@next/swc-win32-x64-msvc": "16.0.8",
|
"@next/swc-win32-x64-msvc": "16.0.10",
|
||||||
"sharp": "^0.34.4"
|
"sharp": "^0.34.4"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
@@ -22207,6 +22167,7 @@
|
|||||||
"version": "0.9.4",
|
"version": "0.9.4",
|
||||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
||||||
"integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
|
"integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"deep-is": "^0.1.3",
|
"deep-is": "^0.1.3",
|
||||||
@@ -22285,6 +22246,7 @@
|
|||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
|
||||||
"integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
|
"integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"yocto-queue": "^0.1.0"
|
"yocto-queue": "^0.1.0"
|
||||||
@@ -22300,6 +22262,7 @@
|
|||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
|
||||||
"integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
|
"integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"p-limit": "^3.0.2"
|
"p-limit": "^3.0.2"
|
||||||
@@ -22386,6 +22349,7 @@
|
|||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
|
||||||
"integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
|
"integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"callsites": "^3.0.0"
|
"callsites": "^3.0.0"
|
||||||
@@ -22494,6 +22458,7 @@
|
|||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||||
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
|
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
@@ -22879,7 +22844,7 @@
|
|||||||
"version": "1.56.1",
|
"version": "1.56.1",
|
||||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz",
|
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz",
|
||||||
"integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==",
|
"integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==",
|
||||||
"devOptional": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.56.1"
|
"playwright-core": "1.56.1"
|
||||||
@@ -22898,7 +22863,7 @@
|
|||||||
"version": "1.56.1",
|
"version": "1.56.1",
|
||||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.1.tgz",
|
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.1.tgz",
|
||||||
"integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==",
|
"integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==",
|
||||||
"devOptional": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright-core": "cli.js"
|
"playwright-core": "cli.js"
|
||||||
@@ -23695,6 +23660,7 @@
|
|||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||||
"integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
|
"integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
@@ -23885,6 +23851,7 @@
|
|||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||||
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
|
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
@@ -24251,11 +24218,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-is": {
|
"node_modules/react-is": {
|
||||||
"version": "19.2.0",
|
"version": "19.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.3.tgz",
|
||||||
"integrity": "sha512-x3Ax3kNSMIIkyVYhWPyO09bu0uttcAIoecO/um/rKGQ4EltYWVYtyiGkS/3xMynrbVQdS69Jhlv8FXUEZehlzA==",
|
"integrity": "sha512-qJNJfu81ByyabuG7hPFEbXqNcWSU3+eVus+KJs+0ncpGfMyYdvSmxiJxbWR65lYi1I+/0HBcliO029gc4F+PnA==",
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/react-redux": {
|
"node_modules/react-redux": {
|
||||||
"version": "9.2.0",
|
"version": "9.2.0",
|
||||||
@@ -24435,7 +24401,7 @@
|
|||||||
"version": "4.1.2",
|
"version": "4.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
||||||
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
|
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
|
||||||
"devOptional": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 14.18.0"
|
"node": ">= 14.18.0"
|
||||||
@@ -24727,6 +24693,7 @@
|
|||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
||||||
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
|
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
@@ -26108,7 +26075,7 @@
|
|||||||
"version": "1.94.2",
|
"version": "1.94.2",
|
||||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.94.2.tgz",
|
"resolved": "https://registry.npmjs.org/sass/-/sass-1.94.2.tgz",
|
||||||
"integrity": "sha512-N+7WK20/wOr7CzA2snJcUSSNTCzeCGUTFY3OgeQP3mZ1aj9NMQ0mSTXwlrnd89j33zzQJGqIN52GIOmYrfq46A==",
|
"integrity": "sha512-N+7WK20/wOr7CzA2snJcUSSNTCzeCGUTFY3OgeQP3mZ1aj9NMQ0mSTXwlrnd89j33zzQJGqIN52GIOmYrfq46A==",
|
||||||
"devOptional": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chokidar": "^4.0.0",
|
"chokidar": "^4.0.0",
|
||||||
@@ -27193,6 +27160,7 @@
|
|||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||||
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
|
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"shebang-regex": "^3.0.0"
|
"shebang-regex": "^3.0.0"
|
||||||
@@ -27205,6 +27173,7 @@
|
|||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
|
||||||
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
|
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
@@ -27939,6 +27908,7 @@
|
|||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
|
||||||
"integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
|
"integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
@@ -28052,6 +28022,7 @@
|
|||||||
"version": "7.2.0",
|
"version": "7.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"has-flag": "^4.0.0"
|
"has-flag": "^4.0.0"
|
||||||
@@ -28908,6 +28879,7 @@
|
|||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
||||||
"integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
|
"integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"prelude-ls": "^1.2.1"
|
"prelude-ls": "^1.2.1"
|
||||||
@@ -29455,6 +29427,7 @@
|
|||||||
"version": "6.21.0",
|
"version": "6.21.0",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/unicode-canonical-property-names-ecmascript": {
|
"node_modules/unicode-canonical-property-names-ecmascript": {
|
||||||
@@ -29632,6 +29605,7 @@
|
|||||||
"version": "4.4.1",
|
"version": "4.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
|
||||||
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
|
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
|
||||||
|
"dev": true,
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"punycode": "^2.1.0"
|
"punycode": "^2.1.0"
|
||||||
@@ -30725,6 +30699,7 @@
|
|||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||||
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
||||||
|
"dev": true,
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"isexe": "^2.0.0"
|
"isexe": "^2.0.0"
|
||||||
@@ -30922,6 +30897,7 @@
|
|||||||
"version": "1.2.5",
|
"version": "1.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
|
||||||
"integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
|
"integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
@@ -31153,6 +31129,7 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
||||||
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
|
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
|
|||||||
+7
-3
@@ -27,6 +27,10 @@
|
|||||||
"landing:build": "nx build landing",
|
"landing:build": "nx build landing",
|
||||||
"landing:prod": "cd ./dist/apps/landing && npx next start",
|
"landing:prod": "cd ./dist/apps/landing && npx next start",
|
||||||
"landing:openapi": "npx openapi-typescript https://api.imphnen.dev/openapi.json -o ./apps/landing/src/openapi-types.ts",
|
"landing:openapi": "npx openapi-typescript https://api.imphnen.dev/openapi.json -o ./apps/landing/src/openapi-types.ts",
|
||||||
|
"qrcampaign:dev": "nx dev qrcampaign",
|
||||||
|
"qrcampaign:build": "nx build qrcampaign",
|
||||||
|
"qrcampaign:test": "nx test qrcampaign",
|
||||||
|
"qrcampaign:prod": "serve ./dist/apps/qrcampaign",
|
||||||
"ui:test": "nx test ui",
|
"ui:test": "nx test ui",
|
||||||
"ui:build": "nx build ui"
|
"ui:build": "nx build ui"
|
||||||
},
|
},
|
||||||
@@ -39,7 +43,6 @@
|
|||||||
"@radix-ui/react-label": "^2.1.7",
|
"@radix-ui/react-label": "^2.1.7",
|
||||||
"@radix-ui/react-slot": "^1.2.0",
|
"@radix-ui/react-slot": "^1.2.0",
|
||||||
"@redocly/ajv": "^8.11.2",
|
"@redocly/ajv": "^8.11.2",
|
||||||
"@supabase/supabase-js": "^2.84.0",
|
|
||||||
"@tanstack/react-query": "^5.74.4",
|
"@tanstack/react-query": "^5.74.4",
|
||||||
"@tanstack/react-store": "^0.8.0",
|
"@tanstack/react-store": "^0.8.0",
|
||||||
"@tanstack/react-table": "^8.21.2",
|
"@tanstack/react-table": "^8.21.2",
|
||||||
@@ -51,7 +54,7 @@
|
|||||||
"graphql": "^16.11.0",
|
"graphql": "^16.11.0",
|
||||||
"html2canvas": "^1.4.1",
|
"html2canvas": "^1.4.1",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"next": "^16.0.8",
|
"next": "^16.0.10",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"openapi-fetch": "^0.15.0",
|
"openapi-fetch": "^0.15.0",
|
||||||
"openapi-react-query": "^0.5.0",
|
"openapi-react-query": "^0.5.0",
|
||||||
@@ -60,6 +63,7 @@
|
|||||||
"react-dom": "^19.2.1",
|
"react-dom": "^19.2.1",
|
||||||
"react-hook-form": "^7.56.4",
|
"react-hook-form": "^7.56.4",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
|
"react-is": "^19.2.3",
|
||||||
"react-router-dom": "^7.3.0",
|
"react-router-dom": "^7.3.0",
|
||||||
"recharts": "^3.1.2",
|
"recharts": "^3.1.2",
|
||||||
"sharp": "^0.34.1",
|
"sharp": "^0.34.1",
|
||||||
@@ -94,7 +98,7 @@
|
|||||||
"@tailwindcss/postcss": "^4.0.13",
|
"@tailwindcss/postcss": "^4.0.13",
|
||||||
"@testing-library/dom": "10.4.0",
|
"@testing-library/dom": "10.4.0",
|
||||||
"@testing-library/jest-dom": "^6.6.3",
|
"@testing-library/jest-dom": "^6.6.3",
|
||||||
"@testing-library/react": "^16.1.0",
|
"@testing-library/react": "16.1.0",
|
||||||
"@testing-library/user-event": "^14.6.1",
|
"@testing-library/user-event": "^14.6.1",
|
||||||
"@types/js-cookie": "^3.0.6",
|
"@types/js-cookie": "^3.0.6",
|
||||||
"@types/node": "^22.12.0",
|
"@types/node": "^22.12.0",
|
||||||
|
|||||||
Reference in New Issue
Block a user