SCGR commited on
Commit
0ad5d66
·
1 Parent(s): 7104814

upload slice

Browse files
backend/handlers/caption.go ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package handlers
2
+
3
+ import (
4
+ "time"
5
+ "net/http"
6
+
7
+ "github.com/gin-gonic/gin"
8
+ "github.com/google/uuid"
9
+ )
10
+
11
+ // POST /api/maps/:id/caption
12
+ func (d *UploadDeps) CreateCaption(c *gin.Context) {
13
+ ctx := c.Request.Context()
14
+ mapID := c.Param("id")
15
+
16
+ // 1) look up the map’s file key
17
+ var key string
18
+ if err := d.DB.QueryRowContext(ctx,
19
+ `SELECT file_key FROM maps WHERE map_id = $1`, mapID).Scan(&key); err != nil {
20
+ c.JSON(http.StatusNotFound, gin.H{"error": "map not found"})
21
+ return
22
+ }
23
+
24
+ // 2) generate placeholder caption
25
+ text, model := d.Cap.Generate(ctx, key)
26
+
27
+ // 3) insert caption row
28
+ capID := uuid.New()
29
+ if _, err := d.DB.ExecContext(ctx, `
30
+ INSERT INTO captions (cap_id, map_id, generated, model, raw_json, created_at)
31
+ VALUES ($1,$2,$3,$4,$5,NOW())`,
32
+ capID, mapID, text, model, []byte("{}")); err != nil {
33
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "db insert failed"})
34
+ return
35
+ }
36
+
37
+ // 4) respond to the front‑end
38
+ c.JSON(http.StatusOK, gin.H{
39
+ "captionId": capID,
40
+ "generated": text,
41
+ })
42
+ }
43
+
44
+ func (d *UploadDeps) GetCaption(c *gin.Context) {
45
+ ctx := c.Request.Context()
46
+ capID := c.Param("id")
47
+
48
+ var key, text string
49
+ if err := d.DB.QueryRowContext(ctx, `
50
+ SELECT m.file_key, c.generated
51
+ FROM captions c
52
+ JOIN maps m ON c.map_id = m.id
53
+ WHERE c.id = $1`, capID).Scan(&key, &text); err != nil {
54
+ c.JSON(http.StatusNotFound, gin.H{"error": "caption not found"})
55
+ return
56
+ }
57
+
58
+ // turn object key into a 24‑hour presigned URL
59
+ url, _ := d.Storage.Link(ctx, key, 24*time.Hour)
60
+
61
+ c.JSON(http.StatusOK, gin.H{
62
+ "imageUrl": url,
63
+ "generated": text,
64
+ })
65
+ }
backend/handlers/metadata.go ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package handlers
2
+
3
+ import "github.com/gin-gonic/gin"
4
+
5
+ // PUT /api/maps/:id/metadata
6
+ func (d *UploadDeps) UpdateMapMetadata(c *gin.Context) {
7
+ mapID := c.Param("id")
8
+ var req struct {
9
+ Source string `json:"source"`
10
+ Region string `json:"region"`
11
+ Category string `json:"category"`
12
+ }
13
+ if err := c.BindJSON(&req); err != nil {
14
+ c.JSON(400, gin.H{"error": "invalid json"})
15
+ return
16
+ }
17
+ _, err := d.DB.Exec(`UPDATE maps
18
+ SET source=$1, region=$2, category=$3
19
+ WHERE id=$4`,
20
+ req.Source, req.Region, req.Category, mapID)
21
+ if err != nil {
22
+ c.JSON(500, gin.H{"error": "db update failed"})
23
+ return
24
+ }
25
+ c.Status(204)
26
+ }
backend/handlers/upload.go CHANGED
@@ -6,6 +6,7 @@ import (
6
  "database/sql"
7
  "encoding/hex"
8
  "io"
 
9
  "net/http"
10
  "time"
11
 
@@ -13,6 +14,7 @@ import (
13
  "github.com/google/uuid"
14
  "github.com/lib/pq"
15
  "github.com/SCGR-1/promptaid-backend/internal/storage"
 
16
  )
17
 
18
 
@@ -21,8 +23,8 @@ type UploadDeps struct {
21
  DB *sql.DB
22
  Storage storage.ObjectStore
23
  Bucket string
24
- RegionOK map[string]bool // in‑memory lookup, seeded at start
25
- // same for SourceOK, CategoryOK, CountryOK
26
  }
27
 
28
  func (d *UploadDeps) UploadMap(c *gin.Context) {
@@ -79,22 +81,25 @@ func (d *UploadDeps) UploadMap(c *gin.Context) {
79
 
80
  // ---- 5. Insert into maps -------------------------------------------
81
  mapID := uuid.New()
82
- _, err = d.DB.Exec(`
83
  INSERT INTO maps
84
- (id, file_key, sha256, source, region, category, created_at)
85
  VALUES ($1,$2,$3,$4,$5,$6,NOW())`,
86
  mapID, objKey, shaHex,
87
  params.Source, params.Region, params.Category,
88
  )
89
  if err != nil {
90
- c.JSON(http.StatusInternalServerError, gin.H{"error": "db insert failed"})
 
91
  return
92
  }
 
 
93
 
94
  // ---- 6. Insert any countries ---------------------------------------
95
  if len(params.Countries) > 0 {
96
  _, err = d.DB.Exec(`
97
- INSERT INTO map_countries (map_id, country_code)
98
  SELECT $1, UNNEST($2::char(2)[])
99
  ON CONFLICT DO NOTHING`,
100
  mapID, pq.Array(params.Countries),
 
6
  "database/sql"
7
  "encoding/hex"
8
  "io"
9
+ "log"
10
  "net/http"
11
  "time"
12
 
 
14
  "github.com/google/uuid"
15
  "github.com/lib/pq"
16
  "github.com/SCGR-1/promptaid-backend/internal/storage"
17
+ "github.com/SCGR-1/promptaid-backend/internal/captioner"
18
  )
19
 
20
 
 
23
  DB *sql.DB
24
  Storage storage.ObjectStore
25
  Bucket string
26
+ RegionOK map[string]bool
27
+ Cap captioner.Captioner
28
  }
29
 
30
  func (d *UploadDeps) UploadMap(c *gin.Context) {
 
81
 
82
  // ---- 5. Insert into maps -------------------------------------------
83
  mapID := uuid.New()
84
+ res, err := d.DB.Exec(`
85
  INSERT INTO maps
86
+ (map_id, file_key, sha256, source, region, category, created_at)
87
  VALUES ($1,$2,$3,$4,$5,$6,NOW())`,
88
  mapID, objKey, shaHex,
89
  params.Source, params.Region, params.Category,
90
  )
91
  if err != nil {
92
+ log.Printf("🔴 maps INSERT error: %v", err)
93
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
94
  return
95
  }
96
+ rows, _ := res.RowsAffected()
97
+ log.Printf("🟢 maps INSERT succeeded, rows affected: %d", rows)
98
 
99
  // ---- 6. Insert any countries ---------------------------------------
100
  if len(params.Countries) > 0 {
101
  _, err = d.DB.Exec(`
102
+ INSERT INTO map_countries (map_id, c_code)
103
  SELECT $1, UNNEST($2::char(2)[])
104
  ON CONFLICT DO NOTHING`,
105
  mapID, pq.Array(params.Countries),
backend/internal/captioner/stub.go ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package captioner
2
+
3
+ import "context"
4
+
5
+ // A pluggable captioner interface (so you can swap to GPT‑4o later).
6
+ type Captioner interface {
7
+ Generate(ctx context.Context, objectKey string) (text, model string)
8
+ }
9
+
10
+ // Minimal stub: always returns the same caption.
11
+ type Stub struct{}
12
+
13
+ func (Stub) Generate(_ context.Context, _ string) (string, string) {
14
+ return "🔧 Stub caption: replace me with GPT‑4o", "GPT‑4O"
15
+ }
16
+
backend/main.go CHANGED
@@ -10,6 +10,7 @@ import (
10
 
11
  "github.com/SCGR-1/promptaid-backend/internal/storage"
12
  "github.com/SCGR-1/promptaid-backend/handlers"
 
13
  )
14
 
15
  type Config struct {
@@ -26,12 +27,17 @@ func loadConfig() Config {
26
 
27
 
28
  func main() {
29
-
30
- cfg := loadConfig()
31
 
32
  // ---- 1. connect DB ----
33
- db, err := sql.Open("postgres", os.Getenv("DATABASE_URL"))
34
- if err != nil { log.Fatal(err) }
 
 
 
 
 
 
 
35
 
36
  // ---- 2. choose storage driver ----
37
  var store storage.ObjectStore
@@ -54,8 +60,9 @@ func main() {
54
  uploadDeps := handlers.UploadDeps{
55
  DB: db,
56
  Storage: store,
57
- Bucket: cfg.S3Bucket,
58
  RegionOK: make(map[string]bool),
 
59
  }
60
 
61
  // ---- 3. build server ----
@@ -65,7 +72,17 @@ func main() {
65
  r.Static("/static", l.Root()) // add Root() getter or hardcode "./uploads"
66
  }
67
 
68
- r.POST("/maps", uploadDeps.UploadMap)
 
 
 
 
 
 
 
 
 
 
69
 
70
  log.Fatal(r.Run(":8080"))
71
  }
 
10
 
11
  "github.com/SCGR-1/promptaid-backend/internal/storage"
12
  "github.com/SCGR-1/promptaid-backend/handlers"
13
+ "github.com/SCGR-1/promptaid-backend/internal/captioner"
14
  )
15
 
16
  type Config struct {
 
27
 
28
 
29
  func main() {
 
 
30
 
31
  // ---- 1. connect DB ----
32
+ dsn := os.Getenv("DATABASE_URL")
33
+ if dsn == "" {
34
+ dsn = "postgres://promptaid:promptaid@localhost:5432/promptaid?sslmode=disable"
35
+ }
36
+ db, err := sql.Open("postgres", dsn)
37
+ if err != nil {
38
+ log.Fatal(err)
39
+ }
40
+
41
 
42
  // ---- 2. choose storage driver ----
43
  var store storage.ObjectStore
 
60
  uploadDeps := handlers.UploadDeps{
61
  DB: db,
62
  Storage: store,
63
+ Bucket: os.Getenv("S3_BUCKET"),
64
  RegionOK: make(map[string]bool),
65
+ Cap: captioner.Stub{},
66
  }
67
 
68
  // ---- 3. build server ----
 
72
  r.Static("/static", l.Root()) // add Root() getter or hardcode "./uploads"
73
  }
74
 
75
+ api := r.Group("/api")
76
+
77
+ api.POST("/maps", uploadDeps.UploadMap)
78
+ api.POST("/maps/:id/caption", uploadDeps.CreateCaption)
79
+ api.PUT ("/maps/:id/metadata", uploadDeps.UpdateMapMetadata)
80
+ api.GET ("/captions/:id", uploadDeps.GetCaption)
81
+
82
+ uploadDeps.RegionOK = map[string]bool{
83
+ "_TBD_REGION": true,
84
+ "AFR": true, "AMR": true, "APA": true, "EUR": true, "MENA": true,
85
+ }
86
 
87
  log.Fatal(r.Run(":8080"))
88
  }
backend/migrations/0003_placeholder.sql ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -- +goose Up
2
+
3
+ INSERT INTO sources (s_code, label)
4
+ VALUES
5
+ ('_TBD_SOURCE', 'TBD placeholder')
6
+ ON CONFLICT (s_code) DO NOTHING;
7
+
8
+ INSERT INTO region (r_code, label)
9
+ VALUES
10
+ ('_TBD_REGION', 'TBD placeholder')
11
+ ON CONFLICT (r_code) DO NOTHING;
12
+
13
+ INSERT INTO category (cat_code, label)
14
+ VALUES
15
+ ('_TBD_CATEGORY', 'TBD placeholder')
16
+ ON CONFLICT (cat_code) DO NOTHING;
17
+
18
+ -- +goose Down
19
+
20
+ DELETE FROM category WHERE cat_code = '_TBD_CATEGORY';
21
+ DELETE FROM region WHERE r_code = '_TBD_REGION';
22
+ DELETE FROM sources WHERE s_code = '_TBD_SOURCE';
frontend/package-lock.json CHANGED
@@ -18,8 +18,10 @@
18
  "devDependencies": {
19
  "@eslint/js": "^9.30.1",
20
  "@tailwindcss/postcss": "^4.1.11",
 
21
  "@types/react": "^19.1.8",
22
  "@types/react-dom": "^19.1.6",
 
23
  "@vitejs/plugin-react-swc": "^3.10.2",
24
  "autoprefixer": "^10.4.21",
25
  "eslint": "^9.30.1",
@@ -29,8 +31,7 @@
29
  "postcss": "^8.5.6",
30
  "tailwindcss": "^4.1.11",
31
  "typescript": "~5.8.3",
32
- "typescript-eslint": "^8.35.1",
33
- "vite": "^7.0.4"
34
  }
35
  },
36
  "node_modules/@alloc/quick-lru": {
@@ -60,6 +61,260 @@
60
  "node": ">=6.0.0"
61
  }
62
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  "node_modules/@babel/runtime": {
64
  "version": "7.27.6",
65
  "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz",
@@ -81,6 +336,54 @@
81
  "node": ">=6.9.0"
82
  }
83
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
  "node_modules/@esbuild/aix-ppc64": {
85
  "version": "0.25.6",
86
  "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.6.tgz",
@@ -94,6 +397,7 @@
94
  "os": [
95
  "aix"
96
  ],
 
97
  "engines": {
98
  "node": ">=18"
99
  }
@@ -111,6 +415,7 @@
111
  "os": [
112
  "android"
113
  ],
 
114
  "engines": {
115
  "node": ">=18"
116
  }
@@ -128,6 +433,7 @@
128
  "os": [
129
  "android"
130
  ],
 
131
  "engines": {
132
  "node": ">=18"
133
  }
@@ -145,6 +451,7 @@
145
  "os": [
146
  "android"
147
  ],
 
148
  "engines": {
149
  "node": ">=18"
150
  }
@@ -162,6 +469,7 @@
162
  "os": [
163
  "darwin"
164
  ],
 
165
  "engines": {
166
  "node": ">=18"
167
  }
@@ -179,6 +487,7 @@
179
  "os": [
180
  "darwin"
181
  ],
 
182
  "engines": {
183
  "node": ">=18"
184
  }
@@ -196,6 +505,7 @@
196
  "os": [
197
  "freebsd"
198
  ],
 
199
  "engines": {
200
  "node": ">=18"
201
  }
@@ -213,6 +523,7 @@
213
  "os": [
214
  "freebsd"
215
  ],
 
216
  "engines": {
217
  "node": ">=18"
218
  }
@@ -230,6 +541,7 @@
230
  "os": [
231
  "linux"
232
  ],
 
233
  "engines": {
234
  "node": ">=18"
235
  }
@@ -247,6 +559,7 @@
247
  "os": [
248
  "linux"
249
  ],
 
250
  "engines": {
251
  "node": ">=18"
252
  }
@@ -264,6 +577,7 @@
264
  "os": [
265
  "linux"
266
  ],
 
267
  "engines": {
268
  "node": ">=18"
269
  }
@@ -281,6 +595,7 @@
281
  "os": [
282
  "linux"
283
  ],
 
284
  "engines": {
285
  "node": ">=18"
286
  }
@@ -298,6 +613,7 @@
298
  "os": [
299
  "linux"
300
  ],
 
301
  "engines": {
302
  "node": ">=18"
303
  }
@@ -315,6 +631,7 @@
315
  "os": [
316
  "linux"
317
  ],
 
318
  "engines": {
319
  "node": ">=18"
320
  }
@@ -332,6 +649,7 @@
332
  "os": [
333
  "linux"
334
  ],
 
335
  "engines": {
336
  "node": ">=18"
337
  }
@@ -349,6 +667,7 @@
349
  "os": [
350
  "linux"
351
  ],
 
352
  "engines": {
353
  "node": ">=18"
354
  }
@@ -366,6 +685,7 @@
366
  "os": [
367
  "linux"
368
  ],
 
369
  "engines": {
370
  "node": ">=18"
371
  }
@@ -383,6 +703,7 @@
383
  "os": [
384
  "netbsd"
385
  ],
 
386
  "engines": {
387
  "node": ">=18"
388
  }
@@ -400,6 +721,7 @@
400
  "os": [
401
  "netbsd"
402
  ],
 
403
  "engines": {
404
  "node": ">=18"
405
  }
@@ -417,6 +739,7 @@
417
  "os": [
418
  "openbsd"
419
  ],
 
420
  "engines": {
421
  "node": ">=18"
422
  }
@@ -434,6 +757,7 @@
434
  "os": [
435
  "openbsd"
436
  ],
 
437
  "engines": {
438
  "node": ">=18"
439
  }
@@ -451,6 +775,7 @@
451
  "os": [
452
  "openharmony"
453
  ],
 
454
  "engines": {
455
  "node": ">=18"
456
  }
@@ -468,6 +793,7 @@
468
  "os": [
469
  "sunos"
470
  ],
 
471
  "engines": {
472
  "node": ">=18"
473
  }
@@ -485,6 +811,7 @@
485
  "os": [
486
  "win32"
487
  ],
 
488
  "engines": {
489
  "node": ">=18"
490
  }
@@ -502,6 +829,7 @@
502
  "os": [
503
  "win32"
504
  ],
 
505
  "engines": {
506
  "node": ">=18"
507
  }
@@ -519,6 +847,7 @@
519
  "os": [
520
  "win32"
521
  ],
 
522
  "engines": {
523
  "node": ">=18"
524
  }
@@ -900,7 +1229,8 @@
900
  "optional": true,
901
  "os": [
902
  "android"
903
- ]
 
904
  },
905
  "node_modules/@rollup/rollup-android-arm64": {
906
  "version": "4.44.2",
@@ -914,7 +1244,8 @@
914
  "optional": true,
915
  "os": [
916
  "android"
917
- ]
 
918
  },
919
  "node_modules/@rollup/rollup-darwin-arm64": {
920
  "version": "4.44.2",
@@ -928,7 +1259,8 @@
928
  "optional": true,
929
  "os": [
930
  "darwin"
931
- ]
 
932
  },
933
  "node_modules/@rollup/rollup-darwin-x64": {
934
  "version": "4.44.2",
@@ -942,7 +1274,8 @@
942
  "optional": true,
943
  "os": [
944
  "darwin"
945
- ]
 
946
  },
947
  "node_modules/@rollup/rollup-freebsd-arm64": {
948
  "version": "4.44.2",
@@ -956,7 +1289,8 @@
956
  "optional": true,
957
  "os": [
958
  "freebsd"
959
- ]
 
960
  },
961
  "node_modules/@rollup/rollup-freebsd-x64": {
962
  "version": "4.44.2",
@@ -970,7 +1304,8 @@
970
  "optional": true,
971
  "os": [
972
  "freebsd"
973
- ]
 
974
  },
975
  "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
976
  "version": "4.44.2",
@@ -984,7 +1319,8 @@
984
  "optional": true,
985
  "os": [
986
  "linux"
987
- ]
 
988
  },
989
  "node_modules/@rollup/rollup-linux-arm-musleabihf": {
990
  "version": "4.44.2",
@@ -998,7 +1334,8 @@
998
  "optional": true,
999
  "os": [
1000
  "linux"
1001
- ]
 
1002
  },
1003
  "node_modules/@rollup/rollup-linux-arm64-gnu": {
1004
  "version": "4.44.2",
@@ -1012,7 +1349,8 @@
1012
  "optional": true,
1013
  "os": [
1014
  "linux"
1015
- ]
 
1016
  },
1017
  "node_modules/@rollup/rollup-linux-arm64-musl": {
1018
  "version": "4.44.2",
@@ -1026,7 +1364,8 @@
1026
  "optional": true,
1027
  "os": [
1028
  "linux"
1029
- ]
 
1030
  },
1031
  "node_modules/@rollup/rollup-linux-loongarch64-gnu": {
1032
  "version": "4.44.2",
@@ -1040,7 +1379,8 @@
1040
  "optional": true,
1041
  "os": [
1042
  "linux"
1043
- ]
 
1044
  },
1045
  "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
1046
  "version": "4.44.2",
@@ -1054,7 +1394,8 @@
1054
  "optional": true,
1055
  "os": [
1056
  "linux"
1057
- ]
 
1058
  },
1059
  "node_modules/@rollup/rollup-linux-riscv64-gnu": {
1060
  "version": "4.44.2",
@@ -1068,7 +1409,8 @@
1068
  "optional": true,
1069
  "os": [
1070
  "linux"
1071
- ]
 
1072
  },
1073
  "node_modules/@rollup/rollup-linux-riscv64-musl": {
1074
  "version": "4.44.2",
@@ -1082,7 +1424,8 @@
1082
  "optional": true,
1083
  "os": [
1084
  "linux"
1085
- ]
 
1086
  },
1087
  "node_modules/@rollup/rollup-linux-s390x-gnu": {
1088
  "version": "4.44.2",
@@ -1096,7 +1439,8 @@
1096
  "optional": true,
1097
  "os": [
1098
  "linux"
1099
- ]
 
1100
  },
1101
  "node_modules/@rollup/rollup-linux-x64-gnu": {
1102
  "version": "4.44.2",
@@ -1110,7 +1454,8 @@
1110
  "optional": true,
1111
  "os": [
1112
  "linux"
1113
- ]
 
1114
  },
1115
  "node_modules/@rollup/rollup-linux-x64-musl": {
1116
  "version": "4.44.2",
@@ -1124,7 +1469,8 @@
1124
  "optional": true,
1125
  "os": [
1126
  "linux"
1127
- ]
 
1128
  },
1129
  "node_modules/@rollup/rollup-win32-arm64-msvc": {
1130
  "version": "4.44.2",
@@ -1138,7 +1484,8 @@
1138
  "optional": true,
1139
  "os": [
1140
  "win32"
1141
- ]
 
1142
  },
1143
  "node_modules/@rollup/rollup-win32-ia32-msvc": {
1144
  "version": "4.44.2",
@@ -1152,7 +1499,8 @@
1152
  "optional": true,
1153
  "os": [
1154
  "win32"
1155
- ]
 
1156
  },
1157
  "node_modules/@rollup/rollup-win32-x64-msvc": {
1158
  "version": "4.44.2",
@@ -1166,7 +1514,8 @@
1166
  "optional": true,
1167
  "os": [
1168
  "win32"
1169
- ]
 
1170
  },
1171
  "node_modules/@swc/core": {
1172
  "version": "1.12.11",
@@ -1679,6 +2028,51 @@
1679
  "@babel/runtime-corejs3": "^7.22.6"
1680
  }
1681
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1682
  "node_modules/@types/estree": {
1683
  "version": "1.0.8",
1684
  "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
@@ -1693,6 +2087,16 @@
1693
  "dev": true,
1694
  "license": "MIT"
1695
  },
 
 
 
 
 
 
 
 
 
 
1696
  "node_modules/@types/react": {
1697
  "version": "19.1.8",
1698
  "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz",
@@ -1970,6 +2374,27 @@
1970
  "url": "https://opencollective.com/typescript-eslint"
1971
  }
1972
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1973
  "node_modules/@vitejs/plugin-react-swc": {
1974
  "version": "3.10.2",
1975
  "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.10.2.tgz",
@@ -1984,6 +2409,13 @@
1984
  "vite": "^4 || ^5 || ^6 || ^7.0.0-beta.0"
1985
  }
1986
  },
 
 
 
 
 
 
 
1987
  "node_modules/acorn": {
1988
  "version": "8.15.0",
1989
  "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
@@ -2246,6 +2678,13 @@
2246
  "dev": true,
2247
  "license": "MIT"
2248
  },
 
 
 
 
 
 
 
2249
  "node_modules/core-js-pure": {
2250
  "version": "3.44.0",
2251
  "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.44.0.tgz",
@@ -2424,6 +2863,7 @@
2424
  "dev": true,
2425
  "hasInstallScript": true,
2426
  "license": "MIT",
 
2427
  "bin": {
2428
  "esbuild": "bin/esbuild"
2429
  },
@@ -2821,10 +3261,21 @@
2821
  "os": [
2822
  "darwin"
2823
  ],
 
2824
  "engines": {
2825
  "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
2826
  }
2827
  },
 
 
 
 
 
 
 
 
 
 
2828
  "node_modules/get-nonce": {
2829
  "version": "1.0.1",
2830
  "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
@@ -3018,6 +3469,19 @@
3018
  "js-yaml": "bin/js-yaml.js"
3019
  }
3020
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
3021
  "node_modules/json-buffer": {
3022
  "version": "3.0.1",
3023
  "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
@@ -3039,6 +3503,19 @@
3039
  "dev": true,
3040
  "license": "MIT"
3041
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
3042
  "node_modules/keyv": {
3043
  "version": "4.5.4",
3044
  "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -3337,6 +3814,23 @@
3337
  "loose-envify": "cli.js"
3338
  }
3339
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3340
  "node_modules/lucide-react": {
3341
  "version": "0.525.0",
3342
  "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.525.0.tgz",
@@ -3777,6 +4271,16 @@
3777
  "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
3778
  "license": "MIT"
3779
  },
 
 
 
 
 
 
 
 
 
 
3780
  "node_modules/react-remove-scroll": {
3781
  "version": "2.7.1",
3782
  "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz",
@@ -3905,6 +4409,7 @@
3905
  "integrity": "sha512-PVoapzTwSEcelaWGth3uR66u7ZRo6qhPHc0f2uRO9fX6XDVNrIiGYS0Pj9+R8yIIYSD/mCx2b16Ws9itljKSPg==",
3906
  "dev": true,
3907
  "license": "MIT",
 
3908
  "dependencies": {
3909
  "@types/estree": "1.0.8"
3910
  },
@@ -4098,6 +4603,7 @@
4098
  "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==",
4099
  "dev": true,
4100
  "license": "MIT",
 
4101
  "dependencies": {
4102
  "fdir": "^6.4.4",
4103
  "picomatch": "^4.0.2"
@@ -4115,6 +4621,7 @@
4115
  "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==",
4116
  "dev": true,
4117
  "license": "MIT",
 
4118
  "peerDependencies": {
4119
  "picomatch": "^3 || ^4"
4120
  },
@@ -4130,6 +4637,7 @@
4130
  "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
4131
  "dev": true,
4132
  "license": "MIT",
 
4133
  "engines": {
4134
  "node": ">=12"
4135
  },
@@ -4219,6 +4727,13 @@
4219
  "typescript": ">=4.8.4 <5.9.0"
4220
  }
4221
  },
 
 
 
 
 
 
 
4222
  "node_modules/update-browserslist-db": {
4223
  "version": "1.1.3",
4224
  "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
@@ -4304,11 +4819,12 @@
4304
  }
4305
  },
4306
  "node_modules/vite": {
4307
- "version": "7.0.4",
4308
- "resolved": "https://registry.npmjs.org/vite/-/vite-7.0.4.tgz",
4309
- "integrity": "sha512-SkaSguuS7nnmV7mfJ8l81JGBFV7Gvzp8IzgE8A8t23+AxuNX61Q5H1Tpz5efduSN7NHC8nQXD3sKQKZAu5mNEA==",
4310
  "dev": true,
4311
  "license": "MIT",
 
4312
  "dependencies": {
4313
  "esbuild": "^0.25.0",
4314
  "fdir": "^6.4.6",
@@ -4384,6 +4900,7 @@
4384
  "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==",
4385
  "dev": true,
4386
  "license": "MIT",
 
4387
  "peerDependencies": {
4388
  "picomatch": "^3 || ^4"
4389
  },
@@ -4399,6 +4916,7 @@
4399
  "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
4400
  "dev": true,
4401
  "license": "MIT",
 
4402
  "engines": {
4403
  "node": ">=12"
4404
  },
 
18
  "devDependencies": {
19
  "@eslint/js": "^9.30.1",
20
  "@tailwindcss/postcss": "^4.1.11",
21
+ "@types/node": "^24.1.0",
22
  "@types/react": "^19.1.8",
23
  "@types/react-dom": "^19.1.6",
24
+ "@vitejs/plugin-react": "^4.7.0",
25
  "@vitejs/plugin-react-swc": "^3.10.2",
26
  "autoprefixer": "^10.4.21",
27
  "eslint": "^9.30.1",
 
31
  "postcss": "^8.5.6",
32
  "tailwindcss": "^4.1.11",
33
  "typescript": "~5.8.3",
34
+ "typescript-eslint": "^8.35.1"
 
35
  }
36
  },
37
  "node_modules/@alloc/quick-lru": {
 
61
  "node": ">=6.0.0"
62
  }
63
  },
64
+ "node_modules/@babel/code-frame": {
65
+ "version": "7.27.1",
66
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
67
+ "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
68
+ "dev": true,
69
+ "license": "MIT",
70
+ "dependencies": {
71
+ "@babel/helper-validator-identifier": "^7.27.1",
72
+ "js-tokens": "^4.0.0",
73
+ "picocolors": "^1.1.1"
74
+ },
75
+ "engines": {
76
+ "node": ">=6.9.0"
77
+ }
78
+ },
79
+ "node_modules/@babel/compat-data": {
80
+ "version": "7.28.0",
81
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz",
82
+ "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==",
83
+ "dev": true,
84
+ "license": "MIT",
85
+ "engines": {
86
+ "node": ">=6.9.0"
87
+ }
88
+ },
89
+ "node_modules/@babel/core": {
90
+ "version": "7.28.0",
91
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz",
92
+ "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==",
93
+ "dev": true,
94
+ "license": "MIT",
95
+ "dependencies": {
96
+ "@ampproject/remapping": "^2.2.0",
97
+ "@babel/code-frame": "^7.27.1",
98
+ "@babel/generator": "^7.28.0",
99
+ "@babel/helper-compilation-targets": "^7.27.2",
100
+ "@babel/helper-module-transforms": "^7.27.3",
101
+ "@babel/helpers": "^7.27.6",
102
+ "@babel/parser": "^7.28.0",
103
+ "@babel/template": "^7.27.2",
104
+ "@babel/traverse": "^7.28.0",
105
+ "@babel/types": "^7.28.0",
106
+ "convert-source-map": "^2.0.0",
107
+ "debug": "^4.1.0",
108
+ "gensync": "^1.0.0-beta.2",
109
+ "json5": "^2.2.3",
110
+ "semver": "^6.3.1"
111
+ },
112
+ "engines": {
113
+ "node": ">=6.9.0"
114
+ },
115
+ "funding": {
116
+ "type": "opencollective",
117
+ "url": "https://opencollective.com/babel"
118
+ }
119
+ },
120
+ "node_modules/@babel/core/node_modules/semver": {
121
+ "version": "6.3.1",
122
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
123
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
124
+ "dev": true,
125
+ "license": "ISC",
126
+ "bin": {
127
+ "semver": "bin/semver.js"
128
+ }
129
+ },
130
+ "node_modules/@babel/generator": {
131
+ "version": "7.28.0",
132
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz",
133
+ "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==",
134
+ "dev": true,
135
+ "license": "MIT",
136
+ "dependencies": {
137
+ "@babel/parser": "^7.28.0",
138
+ "@babel/types": "^7.28.0",
139
+ "@jridgewell/gen-mapping": "^0.3.12",
140
+ "@jridgewell/trace-mapping": "^0.3.28",
141
+ "jsesc": "^3.0.2"
142
+ },
143
+ "engines": {
144
+ "node": ">=6.9.0"
145
+ }
146
+ },
147
+ "node_modules/@babel/helper-compilation-targets": {
148
+ "version": "7.27.2",
149
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz",
150
+ "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==",
151
+ "dev": true,
152
+ "license": "MIT",
153
+ "dependencies": {
154
+ "@babel/compat-data": "^7.27.2",
155
+ "@babel/helper-validator-option": "^7.27.1",
156
+ "browserslist": "^4.24.0",
157
+ "lru-cache": "^5.1.1",
158
+ "semver": "^6.3.1"
159
+ },
160
+ "engines": {
161
+ "node": ">=6.9.0"
162
+ }
163
+ },
164
+ "node_modules/@babel/helper-compilation-targets/node_modules/semver": {
165
+ "version": "6.3.1",
166
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
167
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
168
+ "dev": true,
169
+ "license": "ISC",
170
+ "bin": {
171
+ "semver": "bin/semver.js"
172
+ }
173
+ },
174
+ "node_modules/@babel/helper-globals": {
175
+ "version": "7.28.0",
176
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
177
+ "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
178
+ "dev": true,
179
+ "license": "MIT",
180
+ "engines": {
181
+ "node": ">=6.9.0"
182
+ }
183
+ },
184
+ "node_modules/@babel/helper-module-imports": {
185
+ "version": "7.27.1",
186
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz",
187
+ "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==",
188
+ "dev": true,
189
+ "license": "MIT",
190
+ "dependencies": {
191
+ "@babel/traverse": "^7.27.1",
192
+ "@babel/types": "^7.27.1"
193
+ },
194
+ "engines": {
195
+ "node": ">=6.9.0"
196
+ }
197
+ },
198
+ "node_modules/@babel/helper-module-transforms": {
199
+ "version": "7.27.3",
200
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz",
201
+ "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==",
202
+ "dev": true,
203
+ "license": "MIT",
204
+ "dependencies": {
205
+ "@babel/helper-module-imports": "^7.27.1",
206
+ "@babel/helper-validator-identifier": "^7.27.1",
207
+ "@babel/traverse": "^7.27.3"
208
+ },
209
+ "engines": {
210
+ "node": ">=6.9.0"
211
+ },
212
+ "peerDependencies": {
213
+ "@babel/core": "^7.0.0"
214
+ }
215
+ },
216
+ "node_modules/@babel/helper-plugin-utils": {
217
+ "version": "7.27.1",
218
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz",
219
+ "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==",
220
+ "dev": true,
221
+ "license": "MIT",
222
+ "engines": {
223
+ "node": ">=6.9.0"
224
+ }
225
+ },
226
+ "node_modules/@babel/helper-string-parser": {
227
+ "version": "7.27.1",
228
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
229
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
230
+ "dev": true,
231
+ "license": "MIT",
232
+ "engines": {
233
+ "node": ">=6.9.0"
234
+ }
235
+ },
236
+ "node_modules/@babel/helper-validator-identifier": {
237
+ "version": "7.27.1",
238
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
239
+ "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
240
+ "dev": true,
241
+ "license": "MIT",
242
+ "engines": {
243
+ "node": ">=6.9.0"
244
+ }
245
+ },
246
+ "node_modules/@babel/helper-validator-option": {
247
+ "version": "7.27.1",
248
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
249
+ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
250
+ "dev": true,
251
+ "license": "MIT",
252
+ "engines": {
253
+ "node": ">=6.9.0"
254
+ }
255
+ },
256
+ "node_modules/@babel/helpers": {
257
+ "version": "7.27.6",
258
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz",
259
+ "integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==",
260
+ "dev": true,
261
+ "license": "MIT",
262
+ "dependencies": {
263
+ "@babel/template": "^7.27.2",
264
+ "@babel/types": "^7.27.6"
265
+ },
266
+ "engines": {
267
+ "node": ">=6.9.0"
268
+ }
269
+ },
270
+ "node_modules/@babel/parser": {
271
+ "version": "7.28.0",
272
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz",
273
+ "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==",
274
+ "dev": true,
275
+ "license": "MIT",
276
+ "dependencies": {
277
+ "@babel/types": "^7.28.0"
278
+ },
279
+ "bin": {
280
+ "parser": "bin/babel-parser.js"
281
+ },
282
+ "engines": {
283
+ "node": ">=6.0.0"
284
+ }
285
+ },
286
+ "node_modules/@babel/plugin-transform-react-jsx-self": {
287
+ "version": "7.27.1",
288
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
289
+ "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
290
+ "dev": true,
291
+ "license": "MIT",
292
+ "dependencies": {
293
+ "@babel/helper-plugin-utils": "^7.27.1"
294
+ },
295
+ "engines": {
296
+ "node": ">=6.9.0"
297
+ },
298
+ "peerDependencies": {
299
+ "@babel/core": "^7.0.0-0"
300
+ }
301
+ },
302
+ "node_modules/@babel/plugin-transform-react-jsx-source": {
303
+ "version": "7.27.1",
304
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
305
+ "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
306
+ "dev": true,
307
+ "license": "MIT",
308
+ "dependencies": {
309
+ "@babel/helper-plugin-utils": "^7.27.1"
310
+ },
311
+ "engines": {
312
+ "node": ">=6.9.0"
313
+ },
314
+ "peerDependencies": {
315
+ "@babel/core": "^7.0.0-0"
316
+ }
317
+ },
318
  "node_modules/@babel/runtime": {
319
  "version": "7.27.6",
320
  "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz",
 
336
  "node": ">=6.9.0"
337
  }
338
  },
339
+ "node_modules/@babel/template": {
340
+ "version": "7.27.2",
341
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
342
+ "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
343
+ "dev": true,
344
+ "license": "MIT",
345
+ "dependencies": {
346
+ "@babel/code-frame": "^7.27.1",
347
+ "@babel/parser": "^7.27.2",
348
+ "@babel/types": "^7.27.1"
349
+ },
350
+ "engines": {
351
+ "node": ">=6.9.0"
352
+ }
353
+ },
354
+ "node_modules/@babel/traverse": {
355
+ "version": "7.28.0",
356
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz",
357
+ "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==",
358
+ "dev": true,
359
+ "license": "MIT",
360
+ "dependencies": {
361
+ "@babel/code-frame": "^7.27.1",
362
+ "@babel/generator": "^7.28.0",
363
+ "@babel/helper-globals": "^7.28.0",
364
+ "@babel/parser": "^7.28.0",
365
+ "@babel/template": "^7.27.2",
366
+ "@babel/types": "^7.28.0",
367
+ "debug": "^4.3.1"
368
+ },
369
+ "engines": {
370
+ "node": ">=6.9.0"
371
+ }
372
+ },
373
+ "node_modules/@babel/types": {
374
+ "version": "7.28.1",
375
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.1.tgz",
376
+ "integrity": "sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ==",
377
+ "dev": true,
378
+ "license": "MIT",
379
+ "dependencies": {
380
+ "@babel/helper-string-parser": "^7.27.1",
381
+ "@babel/helper-validator-identifier": "^7.27.1"
382
+ },
383
+ "engines": {
384
+ "node": ">=6.9.0"
385
+ }
386
+ },
387
  "node_modules/@esbuild/aix-ppc64": {
388
  "version": "0.25.6",
389
  "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.6.tgz",
 
397
  "os": [
398
  "aix"
399
  ],
400
+ "peer": true,
401
  "engines": {
402
  "node": ">=18"
403
  }
 
415
  "os": [
416
  "android"
417
  ],
418
+ "peer": true,
419
  "engines": {
420
  "node": ">=18"
421
  }
 
433
  "os": [
434
  "android"
435
  ],
436
+ "peer": true,
437
  "engines": {
438
  "node": ">=18"
439
  }
 
451
  "os": [
452
  "android"
453
  ],
454
+ "peer": true,
455
  "engines": {
456
  "node": ">=18"
457
  }
 
469
  "os": [
470
  "darwin"
471
  ],
472
+ "peer": true,
473
  "engines": {
474
  "node": ">=18"
475
  }
 
487
  "os": [
488
  "darwin"
489
  ],
490
+ "peer": true,
491
  "engines": {
492
  "node": ">=18"
493
  }
 
505
  "os": [
506
  "freebsd"
507
  ],
508
+ "peer": true,
509
  "engines": {
510
  "node": ">=18"
511
  }
 
523
  "os": [
524
  "freebsd"
525
  ],
526
+ "peer": true,
527
  "engines": {
528
  "node": ">=18"
529
  }
 
541
  "os": [
542
  "linux"
543
  ],
544
+ "peer": true,
545
  "engines": {
546
  "node": ">=18"
547
  }
 
559
  "os": [
560
  "linux"
561
  ],
562
+ "peer": true,
563
  "engines": {
564
  "node": ">=18"
565
  }
 
577
  "os": [
578
  "linux"
579
  ],
580
+ "peer": true,
581
  "engines": {
582
  "node": ">=18"
583
  }
 
595
  "os": [
596
  "linux"
597
  ],
598
+ "peer": true,
599
  "engines": {
600
  "node": ">=18"
601
  }
 
613
  "os": [
614
  "linux"
615
  ],
616
+ "peer": true,
617
  "engines": {
618
  "node": ">=18"
619
  }
 
631
  "os": [
632
  "linux"
633
  ],
634
+ "peer": true,
635
  "engines": {
636
  "node": ">=18"
637
  }
 
649
  "os": [
650
  "linux"
651
  ],
652
+ "peer": true,
653
  "engines": {
654
  "node": ">=18"
655
  }
 
667
  "os": [
668
  "linux"
669
  ],
670
+ "peer": true,
671
  "engines": {
672
  "node": ">=18"
673
  }
 
685
  "os": [
686
  "linux"
687
  ],
688
+ "peer": true,
689
  "engines": {
690
  "node": ">=18"
691
  }
 
703
  "os": [
704
  "netbsd"
705
  ],
706
+ "peer": true,
707
  "engines": {
708
  "node": ">=18"
709
  }
 
721
  "os": [
722
  "netbsd"
723
  ],
724
+ "peer": true,
725
  "engines": {
726
  "node": ">=18"
727
  }
 
739
  "os": [
740
  "openbsd"
741
  ],
742
+ "peer": true,
743
  "engines": {
744
  "node": ">=18"
745
  }
 
757
  "os": [
758
  "openbsd"
759
  ],
760
+ "peer": true,
761
  "engines": {
762
  "node": ">=18"
763
  }
 
775
  "os": [
776
  "openharmony"
777
  ],
778
+ "peer": true,
779
  "engines": {
780
  "node": ">=18"
781
  }
 
793
  "os": [
794
  "sunos"
795
  ],
796
+ "peer": true,
797
  "engines": {
798
  "node": ">=18"
799
  }
 
811
  "os": [
812
  "win32"
813
  ],
814
+ "peer": true,
815
  "engines": {
816
  "node": ">=18"
817
  }
 
829
  "os": [
830
  "win32"
831
  ],
832
+ "peer": true,
833
  "engines": {
834
  "node": ">=18"
835
  }
 
847
  "os": [
848
  "win32"
849
  ],
850
+ "peer": true,
851
  "engines": {
852
  "node": ">=18"
853
  }
 
1229
  "optional": true,
1230
  "os": [
1231
  "android"
1232
+ ],
1233
+ "peer": true
1234
  },
1235
  "node_modules/@rollup/rollup-android-arm64": {
1236
  "version": "4.44.2",
 
1244
  "optional": true,
1245
  "os": [
1246
  "android"
1247
+ ],
1248
+ "peer": true
1249
  },
1250
  "node_modules/@rollup/rollup-darwin-arm64": {
1251
  "version": "4.44.2",
 
1259
  "optional": true,
1260
  "os": [
1261
  "darwin"
1262
+ ],
1263
+ "peer": true
1264
  },
1265
  "node_modules/@rollup/rollup-darwin-x64": {
1266
  "version": "4.44.2",
 
1274
  "optional": true,
1275
  "os": [
1276
  "darwin"
1277
+ ],
1278
+ "peer": true
1279
  },
1280
  "node_modules/@rollup/rollup-freebsd-arm64": {
1281
  "version": "4.44.2",
 
1289
  "optional": true,
1290
  "os": [
1291
  "freebsd"
1292
+ ],
1293
+ "peer": true
1294
  },
1295
  "node_modules/@rollup/rollup-freebsd-x64": {
1296
  "version": "4.44.2",
 
1304
  "optional": true,
1305
  "os": [
1306
  "freebsd"
1307
+ ],
1308
+ "peer": true
1309
  },
1310
  "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
1311
  "version": "4.44.2",
 
1319
  "optional": true,
1320
  "os": [
1321
  "linux"
1322
+ ],
1323
+ "peer": true
1324
  },
1325
  "node_modules/@rollup/rollup-linux-arm-musleabihf": {
1326
  "version": "4.44.2",
 
1334
  "optional": true,
1335
  "os": [
1336
  "linux"
1337
+ ],
1338
+ "peer": true
1339
  },
1340
  "node_modules/@rollup/rollup-linux-arm64-gnu": {
1341
  "version": "4.44.2",
 
1349
  "optional": true,
1350
  "os": [
1351
  "linux"
1352
+ ],
1353
+ "peer": true
1354
  },
1355
  "node_modules/@rollup/rollup-linux-arm64-musl": {
1356
  "version": "4.44.2",
 
1364
  "optional": true,
1365
  "os": [
1366
  "linux"
1367
+ ],
1368
+ "peer": true
1369
  },
1370
  "node_modules/@rollup/rollup-linux-loongarch64-gnu": {
1371
  "version": "4.44.2",
 
1379
  "optional": true,
1380
  "os": [
1381
  "linux"
1382
+ ],
1383
+ "peer": true
1384
  },
1385
  "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
1386
  "version": "4.44.2",
 
1394
  "optional": true,
1395
  "os": [
1396
  "linux"
1397
+ ],
1398
+ "peer": true
1399
  },
1400
  "node_modules/@rollup/rollup-linux-riscv64-gnu": {
1401
  "version": "4.44.2",
 
1409
  "optional": true,
1410
  "os": [
1411
  "linux"
1412
+ ],
1413
+ "peer": true
1414
  },
1415
  "node_modules/@rollup/rollup-linux-riscv64-musl": {
1416
  "version": "4.44.2",
 
1424
  "optional": true,
1425
  "os": [
1426
  "linux"
1427
+ ],
1428
+ "peer": true
1429
  },
1430
  "node_modules/@rollup/rollup-linux-s390x-gnu": {
1431
  "version": "4.44.2",
 
1439
  "optional": true,
1440
  "os": [
1441
  "linux"
1442
+ ],
1443
+ "peer": true
1444
  },
1445
  "node_modules/@rollup/rollup-linux-x64-gnu": {
1446
  "version": "4.44.2",
 
1454
  "optional": true,
1455
  "os": [
1456
  "linux"
1457
+ ],
1458
+ "peer": true
1459
  },
1460
  "node_modules/@rollup/rollup-linux-x64-musl": {
1461
  "version": "4.44.2",
 
1469
  "optional": true,
1470
  "os": [
1471
  "linux"
1472
+ ],
1473
+ "peer": true
1474
  },
1475
  "node_modules/@rollup/rollup-win32-arm64-msvc": {
1476
  "version": "4.44.2",
 
1484
  "optional": true,
1485
  "os": [
1486
  "win32"
1487
+ ],
1488
+ "peer": true
1489
  },
1490
  "node_modules/@rollup/rollup-win32-ia32-msvc": {
1491
  "version": "4.44.2",
 
1499
  "optional": true,
1500
  "os": [
1501
  "win32"
1502
+ ],
1503
+ "peer": true
1504
  },
1505
  "node_modules/@rollup/rollup-win32-x64-msvc": {
1506
  "version": "4.44.2",
 
1514
  "optional": true,
1515
  "os": [
1516
  "win32"
1517
+ ],
1518
+ "peer": true
1519
  },
1520
  "node_modules/@swc/core": {
1521
  "version": "1.12.11",
 
2028
  "@babel/runtime-corejs3": "^7.22.6"
2029
  }
2030
  },
2031
+ "node_modules/@types/babel__core": {
2032
+ "version": "7.20.5",
2033
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
2034
+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
2035
+ "dev": true,
2036
+ "license": "MIT",
2037
+ "dependencies": {
2038
+ "@babel/parser": "^7.20.7",
2039
+ "@babel/types": "^7.20.7",
2040
+ "@types/babel__generator": "*",
2041
+ "@types/babel__template": "*",
2042
+ "@types/babel__traverse": "*"
2043
+ }
2044
+ },
2045
+ "node_modules/@types/babel__generator": {
2046
+ "version": "7.27.0",
2047
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
2048
+ "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
2049
+ "dev": true,
2050
+ "license": "MIT",
2051
+ "dependencies": {
2052
+ "@babel/types": "^7.0.0"
2053
+ }
2054
+ },
2055
+ "node_modules/@types/babel__template": {
2056
+ "version": "7.4.4",
2057
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
2058
+ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
2059
+ "dev": true,
2060
+ "license": "MIT",
2061
+ "dependencies": {
2062
+ "@babel/parser": "^7.1.0",
2063
+ "@babel/types": "^7.0.0"
2064
+ }
2065
+ },
2066
+ "node_modules/@types/babel__traverse": {
2067
+ "version": "7.20.7",
2068
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz",
2069
+ "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==",
2070
+ "dev": true,
2071
+ "license": "MIT",
2072
+ "dependencies": {
2073
+ "@babel/types": "^7.20.7"
2074
+ }
2075
+ },
2076
  "node_modules/@types/estree": {
2077
  "version": "1.0.8",
2078
  "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
 
2087
  "dev": true,
2088
  "license": "MIT"
2089
  },
2090
+ "node_modules/@types/node": {
2091
+ "version": "24.1.0",
2092
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.1.0.tgz",
2093
+ "integrity": "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==",
2094
+ "dev": true,
2095
+ "license": "MIT",
2096
+ "dependencies": {
2097
+ "undici-types": "~7.8.0"
2098
+ }
2099
+ },
2100
  "node_modules/@types/react": {
2101
  "version": "19.1.8",
2102
  "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz",
 
2374
  "url": "https://opencollective.com/typescript-eslint"
2375
  }
2376
  },
2377
+ "node_modules/@vitejs/plugin-react": {
2378
+ "version": "4.7.0",
2379
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
2380
+ "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
2381
+ "dev": true,
2382
+ "license": "MIT",
2383
+ "dependencies": {
2384
+ "@babel/core": "^7.28.0",
2385
+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
2386
+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
2387
+ "@rolldown/pluginutils": "1.0.0-beta.27",
2388
+ "@types/babel__core": "^7.20.5",
2389
+ "react-refresh": "^0.17.0"
2390
+ },
2391
+ "engines": {
2392
+ "node": "^14.18.0 || >=16.0.0"
2393
+ },
2394
+ "peerDependencies": {
2395
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
2396
+ }
2397
+ },
2398
  "node_modules/@vitejs/plugin-react-swc": {
2399
  "version": "3.10.2",
2400
  "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.10.2.tgz",
 
2409
  "vite": "^4 || ^5 || ^6 || ^7.0.0-beta.0"
2410
  }
2411
  },
2412
+ "node_modules/@vitejs/plugin-react/node_modules/@rolldown/pluginutils": {
2413
+ "version": "1.0.0-beta.27",
2414
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
2415
+ "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
2416
+ "dev": true,
2417
+ "license": "MIT"
2418
+ },
2419
  "node_modules/acorn": {
2420
  "version": "8.15.0",
2421
  "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
 
2678
  "dev": true,
2679
  "license": "MIT"
2680
  },
2681
+ "node_modules/convert-source-map": {
2682
+ "version": "2.0.0",
2683
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
2684
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
2685
+ "dev": true,
2686
+ "license": "MIT"
2687
+ },
2688
  "node_modules/core-js-pure": {
2689
  "version": "3.44.0",
2690
  "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.44.0.tgz",
 
2863
  "dev": true,
2864
  "hasInstallScript": true,
2865
  "license": "MIT",
2866
+ "peer": true,
2867
  "bin": {
2868
  "esbuild": "bin/esbuild"
2869
  },
 
3261
  "os": [
3262
  "darwin"
3263
  ],
3264
+ "peer": true,
3265
  "engines": {
3266
  "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
3267
  }
3268
  },
3269
+ "node_modules/gensync": {
3270
+ "version": "1.0.0-beta.2",
3271
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
3272
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
3273
+ "dev": true,
3274
+ "license": "MIT",
3275
+ "engines": {
3276
+ "node": ">=6.9.0"
3277
+ }
3278
+ },
3279
  "node_modules/get-nonce": {
3280
  "version": "1.0.1",
3281
  "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
 
3469
  "js-yaml": "bin/js-yaml.js"
3470
  }
3471
  },
3472
+ "node_modules/jsesc": {
3473
+ "version": "3.1.0",
3474
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
3475
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
3476
+ "dev": true,
3477
+ "license": "MIT",
3478
+ "bin": {
3479
+ "jsesc": "bin/jsesc"
3480
+ },
3481
+ "engines": {
3482
+ "node": ">=6"
3483
+ }
3484
+ },
3485
  "node_modules/json-buffer": {
3486
  "version": "3.0.1",
3487
  "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
 
3503
  "dev": true,
3504
  "license": "MIT"
3505
  },
3506
+ "node_modules/json5": {
3507
+ "version": "2.2.3",
3508
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
3509
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
3510
+ "dev": true,
3511
+ "license": "MIT",
3512
+ "bin": {
3513
+ "json5": "lib/cli.js"
3514
+ },
3515
+ "engines": {
3516
+ "node": ">=6"
3517
+ }
3518
+ },
3519
  "node_modules/keyv": {
3520
  "version": "4.5.4",
3521
  "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
 
3814
  "loose-envify": "cli.js"
3815
  }
3816
  },
3817
+ "node_modules/lru-cache": {
3818
+ "version": "5.1.1",
3819
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
3820
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
3821
+ "dev": true,
3822
+ "license": "ISC",
3823
+ "dependencies": {
3824
+ "yallist": "^3.0.2"
3825
+ }
3826
+ },
3827
+ "node_modules/lru-cache/node_modules/yallist": {
3828
+ "version": "3.1.1",
3829
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
3830
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
3831
+ "dev": true,
3832
+ "license": "ISC"
3833
+ },
3834
  "node_modules/lucide-react": {
3835
  "version": "0.525.0",
3836
  "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.525.0.tgz",
 
4271
  "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
4272
  "license": "MIT"
4273
  },
4274
+ "node_modules/react-refresh": {
4275
+ "version": "0.17.0",
4276
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
4277
+ "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
4278
+ "dev": true,
4279
+ "license": "MIT",
4280
+ "engines": {
4281
+ "node": ">=0.10.0"
4282
+ }
4283
+ },
4284
  "node_modules/react-remove-scroll": {
4285
  "version": "2.7.1",
4286
  "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz",
 
4409
  "integrity": "sha512-PVoapzTwSEcelaWGth3uR66u7ZRo6qhPHc0f2uRO9fX6XDVNrIiGYS0Pj9+R8yIIYSD/mCx2b16Ws9itljKSPg==",
4410
  "dev": true,
4411
  "license": "MIT",
4412
+ "peer": true,
4413
  "dependencies": {
4414
  "@types/estree": "1.0.8"
4415
  },
 
4603
  "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==",
4604
  "dev": true,
4605
  "license": "MIT",
4606
+ "peer": true,
4607
  "dependencies": {
4608
  "fdir": "^6.4.4",
4609
  "picomatch": "^4.0.2"
 
4621
  "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==",
4622
  "dev": true,
4623
  "license": "MIT",
4624
+ "peer": true,
4625
  "peerDependencies": {
4626
  "picomatch": "^3 || ^4"
4627
  },
 
4637
  "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
4638
  "dev": true,
4639
  "license": "MIT",
4640
+ "peer": true,
4641
  "engines": {
4642
  "node": ">=12"
4643
  },
 
4727
  "typescript": ">=4.8.4 <5.9.0"
4728
  }
4729
  },
4730
+ "node_modules/undici-types": {
4731
+ "version": "7.8.0",
4732
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz",
4733
+ "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==",
4734
+ "dev": true,
4735
+ "license": "MIT"
4736
+ },
4737
  "node_modules/update-browserslist-db": {
4738
  "version": "1.1.3",
4739
  "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
 
4819
  }
4820
  },
4821
  "node_modules/vite": {
4822
+ "version": "7.0.5",
4823
+ "resolved": "https://registry.npmjs.org/vite/-/vite-7.0.5.tgz",
4824
+ "integrity": "sha512-1mncVwJxy2C9ThLwz0+2GKZyEXuC3MyWtAAlNftlZZXZDP3AJt5FmwcMit/IGGaNZ8ZOB2BNO/HFUB+CpN0NQw==",
4825
  "dev": true,
4826
  "license": "MIT",
4827
+ "peer": true,
4828
  "dependencies": {
4829
  "esbuild": "^0.25.0",
4830
  "fdir": "^6.4.6",
 
4900
  "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==",
4901
  "dev": true,
4902
  "license": "MIT",
4903
+ "peer": true,
4904
  "peerDependencies": {
4905
  "picomatch": "^3 || ^4"
4906
  },
 
4916
  "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
4917
  "dev": true,
4918
  "license": "MIT",
4919
+ "peer": true,
4920
  "engines": {
4921
  "node": ">=12"
4922
  },
frontend/package.json CHANGED
@@ -12,8 +12,10 @@
12
  "devDependencies": {
13
  "@eslint/js": "^9.30.1",
14
  "@tailwindcss/postcss": "^4.1.11",
 
15
  "@types/react": "^19.1.8",
16
  "@types/react-dom": "^19.1.6",
 
17
  "@vitejs/plugin-react-swc": "^3.10.2",
18
  "autoprefixer": "^10.4.21",
19
  "eslint": "^9.30.1",
@@ -23,8 +25,7 @@
23
  "postcss": "^8.5.6",
24
  "tailwindcss": "^4.1.11",
25
  "typescript": "~5.8.3",
26
- "typescript-eslint": "^8.35.1",
27
- "vite": "^7.0.4"
28
  },
29
  "dependencies": {
30
  "@ifrc-go/icons": "^2.0.1",
 
12
  "devDependencies": {
13
  "@eslint/js": "^9.30.1",
14
  "@tailwindcss/postcss": "^4.1.11",
15
+ "@types/node": "^24.1.0",
16
  "@types/react": "^19.1.8",
17
  "@types/react-dom": "^19.1.6",
18
+ "@vitejs/plugin-react": "^4.7.0",
19
  "@vitejs/plugin-react-swc": "^3.10.2",
20
  "autoprefixer": "^10.4.21",
21
  "eslint": "^9.30.1",
 
25
  "postcss": "^8.5.6",
26
  "tailwindcss": "^4.1.11",
27
  "typescript": "~5.8.3",
28
+ "typescript-eslint": "^8.35.1"
 
29
  },
30
  "dependencies": {
31
  "@ifrc-go/icons": "^2.0.1",
frontend/src/App.tsx CHANGED
@@ -1,7 +1,7 @@
1
- /* src/App.tsx */
2
  import { createBrowserRouter, RouterProvider } from 'react-router-dom';
3
  import RootLayout from './layouts/RootLayout';
4
  import UploadPage from './pages/UploadPage';
 
5
  import AnalyticsPage from './pages/AnalyticsPage';
6
  import ExplorePage from './pages/ExplorePage';
7
  import HelpPage from './pages/HelpPage';
@@ -12,6 +12,7 @@ const router = createBrowserRouter([
12
  children: [
13
  { path: '/', element: <UploadPage /> },
14
  { path: '/upload', element: <UploadPage /> },
 
15
  { path: '/analytics', element: <AnalyticsPage /> },
16
  { path: '/explore', element: <ExplorePage /> },
17
  { path: '/help', element: <HelpPage /> },
 
 
1
  import { createBrowserRouter, RouterProvider } from 'react-router-dom';
2
  import RootLayout from './layouts/RootLayout';
3
  import UploadPage from './pages/UploadPage';
4
+ import ReviewPage from './pages/ReviewPage';
5
  import AnalyticsPage from './pages/AnalyticsPage';
6
  import ExplorePage from './pages/ExplorePage';
7
  import HelpPage from './pages/HelpPage';
 
12
  children: [
13
  { path: '/', element: <UploadPage /> },
14
  { path: '/upload', element: <UploadPage /> },
15
+ { path: '/review/:id', element: <ReviewPage /> },
16
  { path: '/analytics', element: <AnalyticsPage /> },
17
  { path: '/explore', element: <ExplorePage /> },
18
  { path: '/help', element: <HelpPage /> },
frontend/src/pages/ReviewPage.tsx ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // src/pages/ReviewPage.tsx
2
+ import { useParams } from "react-router-dom";
3
+ import { useEffect, useState } from "react";
4
+
5
+ export default function ReviewPage() {
6
+ const { id } = useParams(); // captionId
7
+ const [data, setData] = useState<any>(null);
8
+ const [draft, setDraft] = useState("");
9
+
10
+ useEffect(() => {
11
+ (async () => {
12
+ const res = await fetch(`/api/captions/${id}`);
13
+ const json = await res.json();
14
+ setData(json);
15
+ setDraft(json.generated || "");
16
+ })();
17
+ }, [id]);
18
+
19
+ if (!data) return <p className="p-6">Loading…</p>;
20
+
21
+ return (
22
+ <main className="p-6 space-y-6">
23
+ <img src={data.imageUrl} className="max-w-full rounded-xl shadow" />
24
+ <textarea
25
+ value={draft}
26
+ onChange={e => setDraft(e.target.value)}
27
+ className="w-full border rounded p-3 font-mono" rows={5}
28
+ />
29
+ {/* sliders for accuracy/context/usability */}
30
+ {/* Save button calls PUT /api/captions/{id} */}
31
+ </main>
32
+ );
33
+ }
frontend/src/pages/UploadPage.tsx CHANGED
@@ -1,4 +1,4 @@
1
- import { useCallback, useState } from 'react';
2
  import type { DragEvent } from 'react';
3
  import {
4
  PageContainer, Heading, Button,
@@ -12,14 +12,24 @@ import {
12
  import { Link, useNavigate } from 'react-router-dom';
13
 
14
  export default function UploadPage() {
 
 
15
  /* ---------------- local state ----------------- */
16
  const navigate = useNavigate();
17
 
 
 
 
 
18
  const [file, setFile] = useState<File | null>(null);
19
- const [source, setSource] = useState('');
20
- const [region, setRegion] = useState('');
21
- const [category, setCategory] = useState('');
 
 
 
22
  const [countries, setCountries] = useState<string[]>([]);
 
23
 
24
  // Wrapper functions to handle OptionKey to string conversion
25
  const handleSourceChange = (value: any) => setSource(String(value));
@@ -27,6 +37,13 @@ export default function UploadPage() {
27
  const handleCategoryChange = (value: any) => setCategory(String(value));
28
  const handleCountriesChange = (value: any) => setCountries(Array.isArray(value) ? value.map(String) : []);
29
 
 
 
 
 
 
 
 
30
  /* ---- drag-and-drop + file-picker handlers -------------------------- */
31
  const onDrop = useCallback((e: DragEvent<HTMLDivElement>) => {
32
  e.preventDefault();
@@ -38,6 +55,27 @@ export default function UploadPage() {
38
  if (file) setFile(file);
39
  }, []);
40
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  /* ---- generate handler --------------------------------------------- */
42
  async function handleGenerate() {
43
  if (!file) return;
@@ -50,12 +88,24 @@ export default function UploadPage() {
50
  countries.forEach((c) => fd.append('countries', c));
51
 
52
  try {
53
- const res = await fetch('/api/maps', { method: 'POST', body: fd });
54
- const json = await res.json();
55
- if (!res.ok) throw new Error(json.error || 'Upload failed');
56
- navigate(`/review/${json.mapId}`);
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  } catch (err) {
58
- // basic alert for now; replace with toast later
59
  alert((err as Error).message);
60
  }
61
  }
@@ -65,121 +115,186 @@ export default function UploadPage() {
65
  <PageContainer>
66
  <div className="mx-auto max-w-3xl text-center px-4 py-10">
67
  {/* Title & intro copy */}
68
- <Heading level={2}>Upload Your Crisis Map</Heading>
69
-
70
- <p className="mt-3 text-gray-700 leading-relaxed">
71
- This app evaluates how well multimodal AI models turn emergency maps
72
- into meaningful text. Upload your map, let the AI generate a
73
- description, then review and rate the result based on your expertise.
74
- </p>
75
-
76
- {/* “More »” link */}
77
- <div className="mt-2">
78
- <Link
79
- to="/help"
80
- className="text-ifrcRed text-xs hover:underline flex items-center gap-1"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
  >
82
- More <ArrowRightLineIcon className="w-3 h-3" />
83
- </Link>
84
- </div>
85
 
86
- {/* ----- metadata form ----------------------------------------- */}
87
- <div className="grid gap-4 mt-8 text-left sm:grid-cols-2">
88
- <SelectInput
89
- label="Source"
90
- name="source"
91
- value={source}
92
- onChange={handleSourceChange}
93
- options={[
94
- { value: 'UNOSAT', label: 'UNOSAT' },
95
- { value: 'FIELD', label: 'Field HQ' },
96
- ]}
97
- keySelector={(option) => option.value}
98
- labelSelector={(option) => option.label}
99
- required
100
- />
101
- <SelectInput
102
- label="Region"
103
- name="region"
104
- value={region}
105
- onChange={handleRegionChange}
106
- options={[
107
- { value: 'AFR', label: 'Africa' },
108
- { value: 'AMR', label: 'Americas' },
109
- { value: 'APA', label: 'Asia-Pacific' },
110
- { value: 'EUR', label: 'Europe' },
111
- { value: 'MENA', label: 'Middle East & North Africa' },
112
- ]}
113
- keySelector={(option) => option.value}
114
- labelSelector={(option) => option.label}
115
- required
116
- />
117
- <SelectInput
118
- label="Category"
119
- name="category"
120
- value={category}
121
- onChange={handleCategoryChange}
122
- options={[
123
- { value: 'FLOOD', label: 'Flood' },
124
- { value: 'WILDFIRE', label: 'Wildfire' },
125
- { value: 'EARTHQUAKE', label: 'Earthquake' },
126
- ]}
127
- keySelector={(option) => option.value}
128
- labelSelector={(option) => option.label}
129
- required
130
- />
131
- <MultiSelectInput
132
- label="Countries (optional)"
133
- name="countries"
134
- value={countries}
135
- onChange={handleCountriesChange}
136
- options={[
137
- { value: 'PH', label: 'Philippines' },
138
- { value: 'ID', label: 'Indonesia' },
139
- { value: 'VN', label: 'Vietnam' },
140
- ]}
141
- keySelector={(option) => option.value}
142
- labelSelector={(option) => option.label}
143
- placeholder="Select one or more"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
144
  />
 
145
  </div>
 
 
146
 
147
- {/* Drop-zone */}
148
- <div
149
- className="mt-10 border-2 border-dashed border-gray-300 bg-gray-50 rounded-xl p-10 flex flex-col items-center gap-4 hover:bg-gray-100 transition-colors"
150
- onDragOver={(e) => e.preventDefault()}
151
- onDrop={onDrop}
152
- >
153
- <UploadCloudLineIcon className="w-10 h-10 text-ifrcRed" />
154
-
155
- {file ? (
156
- <p className="text-sm font-medium text-gray-800">
157
- Selected file: {file.name}
158
- </p>
159
- ) : (
160
- <>
161
- <p className="text-sm text-gray-600">Drag &amp; Drop a file here</p>
162
- <p className="text-xs text-gray-500">or</p>
163
-
164
- {/* File-picker button */}
165
- <RawFileInput name="file" accept="image/*" onChange={onFileChange}>
166
- <Button name="upload" size={1}>
167
- Upload
168
- </Button>
169
- </RawFileInput>
170
- </>
171
- )}
172
- </div>
173
 
174
- {/* Generate button */}
175
- <Button
176
- name="generate"
177
- className="mt-8"
178
- disabled={!file || !source || !region || !category}
179
- onClick={handleGenerate}
180
- >
181
- Generate
182
- </Button>
 
 
 
 
 
183
  </div>
184
  </PageContainer>
185
  );
 
1
+ import { useCallback, useState, useEffect } from 'react';
2
  import type { DragEvent } from 'react';
3
  import {
4
  PageContainer, Heading, Button,
 
12
  import { Link, useNavigate } from 'react-router-dom';
13
 
14
  export default function UploadPage() {
15
+ const [step, setStep] = useState<1 | 2>(1);
16
+ const [preview, setPreview] = useState<string | null>(null);
17
  /* ---------------- local state ----------------- */
18
  const navigate = useNavigate();
19
 
20
+ const PH_SOURCE = "_TBD_SOURCE";
21
+ const PH_REGION = "_TBD_REGION";
22
+ const PH_CATEGORY = "_TBD_CATEGORY";
23
+
24
  const [file, setFile] = useState<File | null>(null);
25
+ //const [source, setSource] = useState('');
26
+ //const [region, setRegion] = useState('');
27
+ //const [category, setCategory] = useState('');
28
+ const [source, setSource] = useState(PH_SOURCE);
29
+ const [region, setRegion] = useState(PH_REGION);
30
+ const [category, setCategory] = useState(PH_CATEGORY);
31
  const [countries, setCountries] = useState<string[]>([]);
32
+ const [captionId, setCaptionId] = useState<string | null>(null);
33
 
34
  // Wrapper functions to handle OptionKey to string conversion
35
  const handleSourceChange = (value: any) => setSource(String(value));
 
37
  const handleCategoryChange = (value: any) => setCategory(String(value));
38
  const handleCountriesChange = (value: any) => setCountries(Array.isArray(value) ? value.map(String) : []);
39
 
40
+ const [draft, setDraft] = useState('');
41
+ const [scores, setScores] = useState({
42
+ accuracy: 50,
43
+ context: 50,
44
+ usability: 50,
45
+ });
46
+
47
  /* ---- drag-and-drop + file-picker handlers -------------------------- */
48
  const onDrop = useCallback((e: DragEvent<HTMLDivElement>) => {
49
  e.preventDefault();
 
55
  if (file) setFile(file);
56
  }, []);
57
 
58
+ // blob URL / preview
59
+ useEffect(() => {
60
+ if (!file) {
61
+ setPreview(null);
62
+ return;
63
+ }
64
+ const url = URL.createObjectURL(file);
65
+ setPreview(url);
66
+ return () => URL.revokeObjectURL(url);
67
+ }, [file]);
68
+
69
+
70
+ async function readJsonSafely(res: Response): Promise<any> {
71
+ const text = await res.text(); // get raw body
72
+ try {
73
+ return text ? JSON.parse(text) : {}; // valid JSON or empty object
74
+ } catch {
75
+ return { error: text }; // plain text fallback
76
+ }
77
+ }
78
+
79
  /* ---- generate handler --------------------------------------------- */
80
  async function handleGenerate() {
81
  if (!file) return;
 
88
  countries.forEach((c) => fd.append('countries', c));
89
 
90
  try {
91
+ /* 1) upload */
92
+ const mapRes = await fetch('/api/maps', { method: 'POST', body: fd });
93
+ const mapJson = await readJsonSafely(mapRes);
94
+ if (!mapRes.ok) throw new Error(mapJson.error || 'Upload failed');
95
+
96
+ /* 2) caption */
97
+ const capRes = await fetch(
98
+ `/api/maps/${mapJson.mapId}/caption`,
99
+ { method: 'POST' },
100
+ );
101
+ const capJson = await readJsonSafely(capRes);
102
+ if (!capRes.ok) throw new Error(capJson.error || 'Caption failed');
103
+
104
+ /* 3) continue workflow */
105
+ setCaptionId(capJson.captionId);
106
+ setDraft(capJson.generated);
107
+ setStep(2);
108
  } catch (err) {
 
109
  alert((err as Error).message);
110
  }
111
  }
 
115
  <PageContainer>
116
  <div className="mx-auto max-w-3xl text-center px-4 py-10">
117
  {/* Title & intro copy */}
118
+ {step === 1 && <>
119
+ <Heading level={2}>Upload Your Crisis Map</Heading>
120
+ <p className="mt-3 text-gray-700 leading-relaxed">
121
+ This app evaluates how well multimodal AI models turn emergency maps
122
+ into meaningful text. Upload your map, let the AI generate a
123
+ description, then review and rate the result based on your expertise.
124
+ </p>
125
+ {/* “More »” link */}
126
+ <div className="mt-2">
127
+ <Link
128
+ to="/help"
129
+ className="text-ifrcRed text-xs hover:underline flex items-center gap-1"
130
+ >
131
+ More <ArrowRightLineIcon className="w-3 h-3" />
132
+ </Link>
133
+ </div>
134
+ </>}
135
+
136
+ {/* Show uploaded image in step 2 */}
137
+ {step === 2 && preview &&(
138
+ <div className="mt-6 flex justify-center">
139
+ <img
140
+ src={preview}
141
+ alt="Uploaded map preview"
142
+ className="max-h-80 rounded shadow"
143
+ />
144
+ </div>
145
+ )}
146
+
147
+ {/* Drop-zone */}
148
+ {step === 1 && (
149
+ <div
150
+ className="mt-10 border-2 border-dashed border-gray-300 bg-gray-50 rounded-xl p-10 flex flex-col items-center gap-4 hover:bg-gray-100 transition-colors"
151
+ onDragOver={(e) => e.preventDefault()}
152
+ onDrop={onDrop}
153
  >
154
+ <UploadCloudLineIcon className="w-10 h-10 text-ifrcRed" />
 
 
155
 
156
+ {file ? (
157
+ <p className="text-sm font-medium text-gray-800">
158
+ Selected file: {file.name}
159
+ </p>
160
+ ) : (
161
+ <>
162
+ <p className="text-sm text-gray-600">Drag &amp; Drop a file here</p>
163
+ <p className="text-xs text-gray-500">or</p>
164
+
165
+ {/* File-picker button */}
166
+ <RawFileInput name="file" accept="image/*" onChange={onFileChange}>
167
+ <Button name="upload" size={1}>
168
+ Upload
169
+ </Button>
170
+ </RawFileInput>
171
+ </>
172
+ )}
173
+ </div>
174
+ )}
175
+
176
+ {/* Generate button */}
177
+ {step === 1 && (
178
+ <Button
179
+ name="generate"
180
+ className="mt-8"
181
+ disabled={!file}
182
+ onClick={handleGenerate}
183
+ >
184
+ Generate
185
+ </Button>
186
+ )}
187
+
188
+
189
+ {step === 2 && (
190
+ <div className="space-y-10">
191
+ {/* ────── METADATA FORM ────── */}
192
+ <div className="grid gap-4 text-left sm:grid-cols-2">
193
+ <SelectInput
194
+ label="Source"
195
+ name="source"
196
+ value={source}
197
+ onChange={handleSourceChange}
198
+ options={[
199
+ { value: 'UNOSAT', label: 'UNOSAT' },
200
+ { value: 'FIELD', label: 'Field HQ' },
201
+ ]}
202
+ keySelector={(o) => o.value}
203
+ labelSelector={(o) => o.label}
204
+ required
205
+ />
206
+ <SelectInput
207
+ label="Region"
208
+ name="region"
209
+ value={region}
210
+ onChange={handleRegionChange}
211
+ options={[
212
+ { value: 'AFR', label: 'Africa' },
213
+ { value: 'AMR', label: 'Americas' },
214
+ { value: 'APA', label: 'Asia‑Pacific' },
215
+ { value: 'EUR', label: 'Europe' },
216
+ { value: 'MENA', label: 'Middle East & N Africa' },
217
+ ]}
218
+ keySelector={(o) => o.value}
219
+ labelSelector={(o) => o.label}
220
+ required
221
+ />
222
+ <SelectInput
223
+ label="Category"
224
+ name="category"
225
+ value={category}
226
+ onChange={handleCategoryChange}
227
+ options={[
228
+ { value: 'FLOOD', label: 'Flood' },
229
+ { value: 'WILDFIRE', label: 'Wildfire' },
230
+ { value: 'EARTHQUAKE', label: 'Earthquake' },
231
+ ]}
232
+ keySelector={(o) => o.value}
233
+ labelSelector={(o) => o.label}
234
+ required
235
+ />
236
+ <MultiSelectInput
237
+ label="Countries (optional)"
238
+ name="countries"
239
+ value={countries}
240
+ onChange={handleCountriesChange}
241
+ options={[
242
+ { value: 'PH', label: 'Philippines' },
243
+ { value: 'ID', label: 'Indonesia' },
244
+ { value: 'VN', label: 'Vietnam' },
245
+ ]}
246
+ keySelector={(o) => o.value}
247
+ labelSelector={(o) => o.label}
248
+ placeholder="Select one or more"
249
+ />
250
+ </div>
251
+
252
+ {/* ────── RATING SLIDERS ────── */}
253
+ <div className="text-left">
254
+ <Heading level={3}>How well did the AI perform on the task?</Heading>
255
+ {(['accuracy', 'context', 'usability'] as const).map((k) => (
256
+ <div key={k} className="mt-6 flex items-center gap-4">
257
+ <label className="block text-sm font-medium capitalize w-28">{k}</label>
258
+ <input
259
+ type="range"
260
+ min={0}
261
+ max={100}
262
+ value={scores[k]}
263
+ onChange={(e) =>
264
+ setScores((s) => ({ ...s, [k]: Number(e.target.value) }))
265
+ }
266
+ className="w-full accent-ifrcRed"
267
  />
268
+ <span className="ml-2 w-10 text-right tabular-nums">{scores[k]}</span>
269
  </div>
270
+ ))}
271
+ </div>
272
 
273
+ {/* ────── AI‑GENERATED CAPTION ────── */}
274
+ <div className="text-left">
275
+ <Heading level={3}>AI‑Generated Caption</Heading>
276
+ <textarea
277
+ className="w-full border rounded p-3 font-mono mt-2"
278
+ rows={5}
279
+ value={draft}
280
+ onChange={(e) => setDraft(e.target.value)}
281
+ />
282
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
283
 
284
+ {/* ────── SUBMIT BUTTON ────── */}
285
+ <Button
286
+ name="submit"
287
+ className="mt-10"
288
+ onClick={() =>
289
+ alert('Stub saved – wire PUT /api/captions/:id later')
290
+ }
291
+ >
292
+ Submit
293
+ </Button>
294
+ </div>
295
+ )}
296
+
297
+
298
  </div>
299
  </PageContainer>
300
  );
frontend/vite.config.ts CHANGED
@@ -1,7 +1,20 @@
 
 
1
  import { defineConfig } from 'vite'
2
- import react from '@vitejs/plugin-react-swc'
3
 
4
- // https://vite.dev/config/
5
  export default defineConfig({
 
 
 
 
 
 
 
 
 
 
6
  plugins: [react()],
7
  })
 
 
 
1
+ console.log('⚙️ VITE CONFIG LOADED')
2
+
3
  import { defineConfig } from 'vite'
4
+ import react from '@vitejs/plugin-react'
5
 
 
6
  export default defineConfig({
7
+ server: {
8
+ port: 5173,
9
+ proxy: {
10
+ '/api': {
11
+ target: 'http://localhost:8080',
12
+ changeOrigin: true,
13
+ secure: false,
14
+ },
15
+ },
16
+ },
17
  plugins: [react()],
18
  })
19
+
20
+