+5
.env.example
+5
.env.example
+70
.github/workflows/docker-image.yml
+70
.github/workflows/docker-image.yml
···
1
+
name: Docker
2
+
3
+
on:
4
+
schedule:
5
+
- cron: "38 9 * * *"
6
+
push:
7
+
branches: ["main"]
8
+
tags: ["v*.*.*"]
9
+
pull_request:
10
+
branches: ["main"]
11
+
release:
12
+
types: [published]
13
+
14
+
env:
15
+
REGISTRY: ghcr.io
16
+
IMAGE_NAME: ${{ github.repository }}
17
+
18
+
jobs:
19
+
build:
20
+
runs-on: self-hosted
21
+
permissions:
22
+
contents: read
23
+
packages: write
24
+
id-token: write
25
+
26
+
steps:
27
+
- name: Checkout repository
28
+
uses: actions/checkout@v3
29
+
30
+
- name: Install cosign
31
+
if: github.event_name != 'pull_request'
32
+
uses: sigstore/cosign-installer@v3.8.1
33
+
with:
34
+
cosign-release: "v2.4.3"
35
+
36
+
- name: Setup Docker buildx
37
+
uses: docker/setup-buildx-action@v3
38
+
39
+
- name: Log into registry ${{ env.REGISTRY }}
40
+
if: github.event_name != 'pull_request'
41
+
uses: docker/login-action@v3
42
+
with:
43
+
registry: ${{ env.REGISTRY }}
44
+
username: ${{ github.actor }}
45
+
password: ${{ secrets.GITHUB_TOKEN }}
46
+
47
+
- name: Log in to Docker Hub
48
+
if: github.event_name != 'pull_request'
49
+
uses: docker/login-action@v3
50
+
with:
51
+
username: ${{ secrets.DOCKER_USERNAME }}
52
+
password: ${{ secrets.DOCKER_PASSWORD }}
53
+
54
+
- name: Extract metadata (tags, labels) for Docker
55
+
id: meta
56
+
uses: docker/metadata-action@v5
57
+
with:
58
+
images: |
59
+
${{ env.IMAGE_NAME }}
60
+
${{ env.REGISTRY }}/${{ github.repository }}
61
+
62
+
- name: Build and push Docker image
63
+
uses: docker/build-push-action@v6
64
+
with:
65
+
context: .
66
+
file: ./Dockerfile
67
+
platforms: linux/amd64,linux/arm64
68
+
push: ${{ github.event_name != 'pull_request' }}
69
+
tags: ${{ steps.meta.outputs.tags }}
70
+
labels: ${{ steps.meta.outputs.labels }}
+10
-2
.gitignore
+10
-2
.gitignore
+20
.sqlx/query-fd64104d130b93dd5fc9414b8710ad5183b647eaaff90decbce15e10d83c7538.json
+20
.sqlx/query-fd64104d130b93dd5fc9414b8710ad5183b647eaaff90decbce15e10d83c7538.json
···
1
+
{
2
+
"db_name": "PostgreSQL",
3
+
"query": "SELECT COUNT(*) as count FROM users",
4
+
"describe": {
5
+
"columns": [
6
+
{
7
+
"ordinal": 0,
8
+
"name": "count",
9
+
"type_info": "Int8"
10
+
}
11
+
],
12
+
"parameters": {
13
+
"Left": []
14
+
},
15
+
"nullable": [
16
+
null
17
+
]
18
+
},
19
+
"hash": "fd64104d130b93dd5fc9414b8710ad5183b647eaaff90decbce15e10d83c7538"
20
+
}
+122
-220
Cargo.lock
+122
-220
Cargo.lock
···
607
607
]
608
608
609
609
[[package]]
610
-
name = "core-foundation"
611
-
version = "0.9.4"
612
-
source = "registry+https://github.com/rust-lang/crates.io-index"
613
-
checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
614
-
dependencies = [
615
-
"core-foundation-sys",
616
-
"libc",
617
-
]
618
-
619
-
[[package]]
620
610
name = "core-foundation-sys"
621
611
version = "0.8.7"
622
612
source = "registry+https://github.com/rust-lang/crates.io-index"
···
838
828
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
839
829
840
830
[[package]]
841
-
name = "foreign-types"
842
-
version = "0.3.2"
831
+
name = "foldhash"
832
+
version = "0.1.4"
843
833
source = "registry+https://github.com/rust-lang/crates.io-index"
844
-
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
845
-
dependencies = [
846
-
"foreign-types-shared",
847
-
]
848
-
849
-
[[package]]
850
-
name = "foreign-types-shared"
851
-
version = "0.1.1"
852
-
source = "registry+https://github.com/rust-lang/crates.io-index"
853
-
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
834
+
checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f"
854
835
855
836
[[package]]
856
837
name = "form_urlencoded"
···
859
840
checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
860
841
dependencies = [
861
842
"percent-encoding",
843
+
]
844
+
845
+
[[package]]
846
+
name = "futures"
847
+
version = "0.3.31"
848
+
source = "registry+https://github.com/rust-lang/crates.io-index"
849
+
checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
850
+
dependencies = [
851
+
"futures-channel",
852
+
"futures-core",
853
+
"futures-executor",
854
+
"futures-io",
855
+
"futures-sink",
856
+
"futures-task",
857
+
"futures-util",
862
858
]
863
859
864
860
[[package]]
···
906
902
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
907
903
908
904
[[package]]
905
+
name = "futures-macro"
906
+
version = "0.3.31"
907
+
source = "registry+https://github.com/rust-lang/crates.io-index"
908
+
checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
909
+
dependencies = [
910
+
"proc-macro2",
911
+
"quote",
912
+
"syn",
913
+
]
914
+
915
+
[[package]]
909
916
name = "futures-sink"
910
917
version = "0.3.31"
911
918
source = "registry+https://github.com/rust-lang/crates.io-index"
···
923
930
source = "registry+https://github.com/rust-lang/crates.io-index"
924
931
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
925
932
dependencies = [
933
+
"futures-channel",
926
934
"futures-core",
927
935
"futures-io",
936
+
"futures-macro",
928
937
"futures-sink",
929
938
"futures-task",
930
939
"memchr",
···
983
992
984
993
[[package]]
985
994
name = "hashbrown"
986
-
version = "0.14.5"
995
+
version = "0.15.2"
987
996
source = "registry+https://github.com/rust-lang/crates.io-index"
988
-
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
997
+
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
989
998
dependencies = [
990
-
"ahash",
991
999
"allocator-api2",
1000
+
"equivalent",
1001
+
"foldhash",
992
1002
]
993
1003
994
1004
[[package]]
995
-
name = "hashbrown"
996
-
version = "0.15.2"
997
-
source = "registry+https://github.com/rust-lang/crates.io-index"
998
-
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
999
-
1000
-
[[package]]
1001
1005
name = "hashlink"
1002
-
version = "0.9.1"
1006
+
version = "0.10.0"
1003
1007
source = "registry+https://github.com/rust-lang/crates.io-index"
1004
-
checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af"
1008
+
checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
1005
1009
dependencies = [
1006
-
"hashbrown 0.14.5",
1010
+
"hashbrown",
1007
1011
]
1008
1012
1009
1013
[[package]]
···
1249
1253
checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652"
1250
1254
dependencies = [
1251
1255
"equivalent",
1252
-
"hashbrown 0.15.2",
1256
+
"hashbrown",
1253
1257
]
1254
1258
1255
1259
[[package]]
···
1414
1418
]
1415
1419
1416
1420
[[package]]
1417
-
name = "minimal-lexical"
1418
-
version = "0.2.1"
1419
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1420
-
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
1421
-
1422
-
[[package]]
1423
1421
name = "miniz_oxide"
1424
1422
version = "0.8.3"
1425
1423
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1441
1439
]
1442
1440
1443
1441
[[package]]
1444
-
name = "native-tls"
1445
-
version = "0.2.12"
1446
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1447
-
checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466"
1448
-
dependencies = [
1449
-
"libc",
1450
-
"log",
1451
-
"openssl",
1452
-
"openssl-probe",
1453
-
"openssl-sys",
1454
-
"schannel",
1455
-
"security-framework",
1456
-
"security-framework-sys",
1457
-
"tempfile",
1458
-
]
1459
-
1460
-
[[package]]
1461
-
name = "nom"
1462
-
version = "7.1.3"
1463
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1464
-
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
1465
-
dependencies = [
1466
-
"memchr",
1467
-
"minimal-lexical",
1468
-
]
1469
-
1470
-
[[package]]
1471
1442
name = "nu-ansi-term"
1472
-
version = "0.46.0"
1443
+
version = "0.50.1"
1473
1444
source = "registry+https://github.com/rust-lang/crates.io-index"
1474
-
checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
1445
+
checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399"
1475
1446
dependencies = [
1476
-
"overload",
1477
-
"winapi",
1447
+
"windows-sys 0.52.0",
1478
1448
]
1479
1449
1480
1450
[[package]]
···
1554
1524
version = "1.20.2"
1555
1525
source = "registry+https://github.com/rust-lang/crates.io-index"
1556
1526
checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
1557
-
1558
-
[[package]]
1559
-
name = "openssl"
1560
-
version = "0.10.68"
1561
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1562
-
checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5"
1563
-
dependencies = [
1564
-
"bitflags",
1565
-
"cfg-if",
1566
-
"foreign-types",
1567
-
"libc",
1568
-
"once_cell",
1569
-
"openssl-macros",
1570
-
"openssl-sys",
1571
-
]
1572
-
1573
-
[[package]]
1574
-
name = "openssl-macros"
1575
-
version = "0.1.1"
1576
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1577
-
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
1578
-
dependencies = [
1579
-
"proc-macro2",
1580
-
"quote",
1581
-
"syn",
1582
-
]
1583
-
1584
-
[[package]]
1585
-
name = "openssl-probe"
1586
-
version = "0.1.6"
1587
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1588
-
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
1589
-
1590
-
[[package]]
1591
-
name = "openssl-sys"
1592
-
version = "0.9.104"
1593
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1594
-
checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741"
1595
-
dependencies = [
1596
-
"cc",
1597
-
"libc",
1598
-
"pkg-config",
1599
-
"vcpkg",
1600
-
]
1601
-
1602
-
[[package]]
1603
-
name = "overload"
1604
-
version = "0.1.1"
1605
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1606
-
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
1607
1527
1608
1528
[[package]]
1609
1529
name = "parking"
···
1824
1744
1825
1745
[[package]]
1826
1746
name = "ring"
1827
-
version = "0.17.8"
1747
+
version = "0.17.13"
1828
1748
source = "registry+https://github.com/rust-lang/crates.io-index"
1829
-
checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d"
1749
+
checksum = "70ac5d832aa16abd7d1def883a8545280c20a60f523a370aa3a9617c2b8550ee"
1830
1750
dependencies = [
1831
1751
"cc",
1832
1752
"cfg-if",
1833
1753
"getrandom",
1834
1754
"libc",
1835
-
"spin",
1836
1755
"untrusted",
1837
1756
"windows-sys 0.52.0",
1838
1757
]
···
1858
1777
]
1859
1778
1860
1779
[[package]]
1780
+
name = "rust-embed"
1781
+
version = "6.8.1"
1782
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1783
+
checksum = "a36224c3276f8c4ebc8c20f158eca7ca4359c8db89991c4925132aaaf6702661"
1784
+
dependencies = [
1785
+
"rust-embed-impl",
1786
+
"rust-embed-utils",
1787
+
"walkdir",
1788
+
]
1789
+
1790
+
[[package]]
1791
+
name = "rust-embed-impl"
1792
+
version = "6.8.1"
1793
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1794
+
checksum = "49b94b81e5b2c284684141a2fb9e2a31be90638caf040bf9afbc5a0416afe1ac"
1795
+
dependencies = [
1796
+
"proc-macro2",
1797
+
"quote",
1798
+
"rust-embed-utils",
1799
+
"syn",
1800
+
"walkdir",
1801
+
]
1802
+
1803
+
[[package]]
1804
+
name = "rust-embed-utils"
1805
+
version = "7.8.1"
1806
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1807
+
checksum = "9d38ff6bf570dc3bb7100fce9f7b60c33fa71d80e88da3f2580df4ff2bdded74"
1808
+
dependencies = [
1809
+
"sha2",
1810
+
"walkdir",
1811
+
]
1812
+
1813
+
[[package]]
1861
1814
name = "rustc-demangle"
1862
1815
version = "0.1.24"
1863
1816
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1898
1851
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
1899
1852
1900
1853
[[package]]
1901
-
name = "schannel"
1902
-
version = "0.1.27"
1854
+
name = "same-file"
1855
+
version = "1.0.6"
1903
1856
source = "registry+https://github.com/rust-lang/crates.io-index"
1904
-
checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d"
1857
+
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
1905
1858
dependencies = [
1906
-
"windows-sys 0.59.0",
1859
+
"winapi-util",
1907
1860
]
1908
1861
1909
1862
[[package]]
···
1913
1866
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
1914
1867
1915
1868
[[package]]
1916
-
name = "security-framework"
1917
-
version = "2.11.1"
1918
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1919
-
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
1920
-
dependencies = [
1921
-
"bitflags",
1922
-
"core-foundation",
1923
-
"core-foundation-sys",
1924
-
"libc",
1925
-
"security-framework-sys",
1926
-
]
1927
-
1928
-
[[package]]
1929
-
name = "security-framework-sys"
1930
-
version = "2.14.0"
1931
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1932
-
checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32"
1933
-
dependencies = [
1934
-
"core-foundation-sys",
1935
-
"libc",
1936
-
]
1937
-
1938
-
[[package]]
1939
1869
name = "semver"
1940
1870
version = "1.0.25"
1941
1871
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2066
1996
"chrono",
2067
1997
"clap",
2068
1998
"dotenv",
1999
+
"futures",
2069
2000
"jsonwebtoken",
2070
2001
"lazy_static",
2002
+
"mime_guess",
2003
+
"rand",
2071
2004
"regex",
2005
+
"rust-embed",
2072
2006
"serde",
2073
2007
"serde_json",
2074
2008
"sqlx",
···
2127
2061
]
2128
2062
2129
2063
[[package]]
2130
-
name = "sqlformat"
2131
-
version = "0.2.6"
2132
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2133
-
checksum = "7bba3a93db0cc4f7bdece8bb09e77e2e785c20bfebf79eb8340ed80708048790"
2134
-
dependencies = [
2135
-
"nom",
2136
-
"unicode_categories",
2137
-
]
2138
-
2139
-
[[package]]
2140
2064
name = "sqlx"
2141
-
version = "0.8.1"
2065
+
version = "0.8.3"
2142
2066
source = "registry+https://github.com/rust-lang/crates.io-index"
2143
-
checksum = "fcfa89bea9500db4a0d038513d7a060566bfc51d46d1c014847049a45cce85e8"
2067
+
checksum = "4410e73b3c0d8442c5f99b425d7a435b5ee0ae4167b3196771dd3f7a01be745f"
2144
2068
dependencies = [
2145
2069
"sqlx-core",
2146
2070
"sqlx-macros",
···
2151
2075
2152
2076
[[package]]
2153
2077
name = "sqlx-core"
2154
-
version = "0.8.1"
2078
+
version = "0.8.3"
2155
2079
source = "registry+https://github.com/rust-lang/crates.io-index"
2156
-
checksum = "d06e2f2bd861719b1f3f0c7dbe1d80c30bf59e76cf019f07d9014ed7eefb8e08"
2080
+
checksum = "6a007b6936676aa9ab40207cde35daab0a04b823be8ae004368c0793b96a61e0"
2157
2081
dependencies = [
2158
-
"atoi",
2159
-
"byteorder",
2160
2082
"bytes",
2161
2083
"chrono",
2162
2084
"crc",
2163
2085
"crossbeam-queue",
2164
2086
"either",
2165
2087
"event-listener",
2166
-
"futures-channel",
2167
2088
"futures-core",
2168
2089
"futures-intrusive",
2169
2090
"futures-io",
2170
2091
"futures-util",
2171
-
"hashbrown 0.14.5",
2092
+
"hashbrown",
2172
2093
"hashlink",
2173
-
"hex",
2174
2094
"indexmap",
2175
2095
"log",
2176
2096
"memchr",
2177
-
"native-tls",
2178
2097
"once_cell",
2179
-
"paste",
2180
2098
"percent-encoding",
2181
2099
"serde",
2182
2100
"serde_json",
2183
2101
"sha2",
2184
2102
"smallvec",
2185
-
"sqlformat",
2186
-
"thiserror 1.0.69",
2103
+
"thiserror 2.0.11",
2187
2104
"tokio",
2188
2105
"tokio-stream",
2189
2106
"tracing",
2190
2107
"url",
2191
-
"uuid",
2192
2108
]
2193
2109
2194
2110
[[package]]
2195
2111
name = "sqlx-macros"
2196
-
version = "0.8.1"
2112
+
version = "0.8.3"
2197
2113
source = "registry+https://github.com/rust-lang/crates.io-index"
2198
-
checksum = "2f998a9defdbd48ed005a89362bd40dd2117502f15294f61c8d47034107dbbdc"
2114
+
checksum = "3112e2ad78643fef903618d78cf0aec1cb3134b019730edb039b69eaf531f310"
2199
2115
dependencies = [
2200
2116
"proc-macro2",
2201
2117
"quote",
···
2206
2122
2207
2123
[[package]]
2208
2124
name = "sqlx-macros-core"
2209
-
version = "0.8.1"
2125
+
version = "0.8.3"
2210
2126
source = "registry+https://github.com/rust-lang/crates.io-index"
2211
-
checksum = "3d100558134176a2629d46cec0c8891ba0be8910f7896abfdb75ef4ab6f4e7ce"
2127
+
checksum = "4e9f90acc5ab146a99bf5061a7eb4976b573f560bc898ef3bf8435448dd5e7ad"
2212
2128
dependencies = [
2213
2129
"dotenvy",
2214
2130
"either",
···
2232
2148
2233
2149
[[package]]
2234
2150
name = "sqlx-mysql"
2235
-
version = "0.8.1"
2151
+
version = "0.8.3"
2236
2152
source = "registry+https://github.com/rust-lang/crates.io-index"
2237
-
checksum = "936cac0ab331b14cb3921c62156d913e4c15b74fb6ec0f3146bd4ef6e4fb3c12"
2153
+
checksum = "4560278f0e00ce64938540546f59f590d60beee33fffbd3b9cd47851e5fff233"
2238
2154
dependencies = [
2239
2155
"atoi",
2240
2156
"base64 0.22.1",
···
2268
2184
"smallvec",
2269
2185
"sqlx-core",
2270
2186
"stringprep",
2271
-
"thiserror 1.0.69",
2187
+
"thiserror 2.0.11",
2272
2188
"tracing",
2273
-
"uuid",
2274
2189
"whoami",
2275
2190
]
2276
2191
2277
2192
[[package]]
2278
2193
name = "sqlx-postgres"
2279
-
version = "0.8.1"
2194
+
version = "0.8.3"
2280
2195
source = "registry+https://github.com/rust-lang/crates.io-index"
2281
-
checksum = "9734dbce698c67ecf67c442f768a5e90a49b2a4d61a9f1d59f73874bd4cf0710"
2196
+
checksum = "c5b98a57f363ed6764d5b3a12bfedf62f07aa16e1856a7ddc2a0bb190a959613"
2282
2197
dependencies = [
2283
2198
"atoi",
2284
2199
"base64 0.22.1",
···
2290
2205
"etcetera",
2291
2206
"futures-channel",
2292
2207
"futures-core",
2293
-
"futures-io",
2294
2208
"futures-util",
2295
2209
"hex",
2296
2210
"hkdf",
···
2308
2222
"smallvec",
2309
2223
"sqlx-core",
2310
2224
"stringprep",
2311
-
"thiserror 1.0.69",
2225
+
"thiserror 2.0.11",
2312
2226
"tracing",
2313
-
"uuid",
2314
2227
"whoami",
2315
2228
]
2316
2229
2317
2230
[[package]]
2318
2231
name = "sqlx-sqlite"
2319
-
version = "0.8.1"
2232
+
version = "0.8.3"
2320
2233
source = "registry+https://github.com/rust-lang/crates.io-index"
2321
-
checksum = "a75b419c3c1b1697833dd927bdc4c6545a620bc1bbafabd44e1efbe9afcd337e"
2234
+
checksum = "f85ca71d3a5b24e64e1d08dd8fe36c6c95c339a896cc33068148906784620540"
2322
2235
dependencies = [
2323
2236
"atoi",
2324
2237
"chrono",
···
2336
2249
"sqlx-core",
2337
2250
"tracing",
2338
2251
"url",
2339
-
"uuid",
2340
2252
]
2341
2253
2342
2254
[[package]]
···
2512
2424
2513
2425
[[package]]
2514
2426
name = "tokio"
2515
-
version = "1.43.0"
2427
+
version = "1.43.1"
2516
2428
source = "registry+https://github.com/rust-lang/crates.io-index"
2517
-
checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e"
2429
+
checksum = "492a604e2fd7f814268a378409e6c92b5525d747d10db9a229723f55a417958c"
2518
2430
dependencies = [
2519
2431
"backtrace",
2520
2432
"bytes",
···
2609
2521
2610
2522
[[package]]
2611
2523
name = "tracing-subscriber"
2612
-
version = "0.3.19"
2524
+
version = "0.3.20"
2613
2525
source = "registry+https://github.com/rust-lang/crates.io-index"
2614
-
checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
2526
+
checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5"
2615
2527
dependencies = [
2616
2528
"nu-ansi-term",
2617
2529
"sharded-slab",
···
2661
2573
checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0"
2662
2574
2663
2575
[[package]]
2664
-
name = "unicode_categories"
2665
-
version = "0.1.1"
2666
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2667
-
checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e"
2668
-
2669
-
[[package]]
2670
2576
name = "untrusted"
2671
2577
version = "0.9.0"
2672
2578
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2708
2614
checksum = "b3758f5e68192bb96cc8f9b7e2c2cfdabb435499a28499a42f8f984092adad4b"
2709
2615
dependencies = [
2710
2616
"getrandom",
2711
-
"serde",
2712
2617
]
2713
2618
2714
2619
[[package]]
···
2734
2639
version = "0.9.5"
2735
2640
source = "registry+https://github.com/rust-lang/crates.io-index"
2736
2641
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
2642
+
2643
+
[[package]]
2644
+
name = "walkdir"
2645
+
version = "2.5.0"
2646
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2647
+
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
2648
+
dependencies = [
2649
+
"same-file",
2650
+
"winapi-util",
2651
+
]
2737
2652
2738
2653
[[package]]
2739
2654
name = "wasi"
···
2816
2731
]
2817
2732
2818
2733
[[package]]
2819
-
name = "winapi"
2820
-
version = "0.3.9"
2734
+
name = "winapi-util"
2735
+
version = "0.1.9"
2821
2736
source = "registry+https://github.com/rust-lang/crates.io-index"
2822
-
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
2737
+
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
2823
2738
dependencies = [
2824
-
"winapi-i686-pc-windows-gnu",
2825
-
"winapi-x86_64-pc-windows-gnu",
2739
+
"windows-sys 0.59.0",
2826
2740
]
2827
-
2828
-
[[package]]
2829
-
name = "winapi-i686-pc-windows-gnu"
2830
-
version = "0.4.0"
2831
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2832
-
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
2833
-
2834
-
[[package]]
2835
-
name = "winapi-x86_64-pc-windows-gnu"
2836
-
version = "0.4.0"
2837
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2838
-
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
2839
2741
2840
2742
[[package]]
2841
2743
name = "windows-core"
+7
-3
Cargo.toml
+7
-3
Cargo.toml
···
8
8
path = "src/lib.rs"
9
9
10
10
[dependencies]
11
+
rust-embed = "6.8"
11
12
jsonwebtoken = "9"
12
13
actix-web = "4.4"
13
14
actix-files = "0.6"
14
15
actix-cors = "0.6"
15
-
tokio = { version = "1.36", features = ["full"] }
16
-
sqlx = { version = "0.8", features = ["runtime-tokio-native-tls", "postgres", "uuid", "chrono"] }
16
+
tokio = { version = "1.43", features = ["rt-multi-thread", "macros"] }
17
+
sqlx = { version = "0.8", features = ["runtime-tokio", "postgres", "sqlite", "chrono"] }
17
18
serde = { version = "1.0", features = ["derive"] }
18
19
serde_json = "1.0"
19
20
anyhow = "1.0"
20
21
thiserror = "1.0"
21
22
tracing = "0.1"
22
23
tracing-subscriber = "0.3"
23
-
uuid = { version = "1.7", features = ["v4", "serde"] }
24
+
uuid = { version = "1.7", features = ["v4"] } # Remove serde if not using UUID serialization
24
25
base62 = "2.0"
25
26
clap = { version = "4.5", features = ["derive"] }
26
27
dotenv = "0.15"
···
28
29
regex = "1.10"
29
30
lazy_static = "1.4"
30
31
argon2 = "0.5.3"
32
+
rand = { version = "0.8", features = ["std"] }
33
+
mime_guess = "2.0.5"
34
+
futures = "0.3.31"
+30
-7
Dockerfile
+30
-7
Dockerfile
···
1
-
# Build stage
2
-
FROM rust:latest as builder
1
+
# Frontend build stage
2
+
FROM oven/bun:latest AS frontend-builder
3
+
4
+
WORKDIR /usr/src/frontend
5
+
6
+
# Copy frontend files
7
+
COPY frontend/package.json ./
8
+
RUN bun install
9
+
10
+
COPY frontend/ ./
11
+
12
+
# Build frontend with production configuration
13
+
ARG API_URL=http://localhost:8080
14
+
ENV VITE_API_URL=${API_URL}
15
+
RUN bun run build
16
+
17
+
# Rust build stage
18
+
FROM rust:latest AS backend-builder
3
19
4
20
# Install PostgreSQL client libraries and SSL dependencies
5
21
RUN apt-get update && \
···
16
32
COPY migrations/ migrations/
17
33
COPY .sqlx/ .sqlx/
18
34
19
-
# Build your application
35
+
# Create static directory and copy frontend build
36
+
COPY --from=frontend-builder /usr/src/frontend/dist/ static/
37
+
38
+
# Build the application
20
39
RUN cargo build --release
21
40
22
41
# Runtime stage
···
30
49
WORKDIR /app
31
50
32
51
# Copy the binary from builder
33
-
COPY --from=builder /usr/src/app/target/release/simplelink /app/simplelink
52
+
COPY --from=backend-builder /usr/src/app/target/release/simplelink /app/simplelink
53
+
34
54
# Copy migrations folder for SQLx
35
-
COPY --from=builder /usr/src/app/migrations /app/migrations
55
+
COPY --from=backend-builder /usr/src/app/migrations /app/migrations
56
+
57
+
# Copy static files
58
+
COPY --from=backend-builder /usr/src/app/static /app/static
36
59
37
-
# Expose the port (this is just documentation)
60
+
# Expose the port
38
61
EXPOSE 8080
39
62
40
63
# Set default network configuration
···
42
65
ENV SERVER_PORT=8080
43
66
44
67
# Run the binary
45
-
CMD ["./simplelink"]
68
+
CMD ["./simplelink"]
+80
README.md
+80
README.md
···
1
+
# SimpleLink
2
+
3
+
A very performant and light (2MB in memory) link shortener and tracker. Written in Rust and React and uses Postgres or SQLite.
4
+
5
+

6
+
7
+

8
+
9
+
## How to Run
10
+
11
+
### From Docker
12
+
13
+
```bash
14
+
docker run -p 8080:8080 \
15
+
-e JWT_SECRET=change-me-in-production \
16
+
-e SIMPLELINK_USER=admin@example.com \
17
+
-e SIMPLELINK_PASS=your-secure-password \
18
+
-v simplelink_data:/data \
19
+
ghcr.io/waveringana/simplelink:v2.2
20
+
```
21
+
22
+
### Environment Variables
23
+
24
+
- `JWT_SECRET`: Required. Used for JWT token generation
25
+
- `SIMPLELINK_USER`: Optional. If set along with SIMPLELINK_PASS, creates an admin user on first run
26
+
- `SIMPLELINK_PASS`: Optional. Admin user password
27
+
- `DATABASE_URL`: Optional. Postgres connection string. If not set, uses SQLite
28
+
- `INITIAL_LINKS`: Optional. Semicolon-separated list of initial links in format "url,code;url2,code2"
29
+
- `SERVER_HOST`: Optional. Default: "127.0.0.1"
30
+
- `SERVER_PORT`: Optional. Default: "8080"
31
+
32
+
If `SIMPLELINK_USER` and `SIMPLELINK_PASS` are not passed, an admin-setup-token is pasted to the console and as a text file in the project root.
33
+
34
+
### From Docker Compose
35
+
36
+
Edit the docker-compose.yml file. It comes included with a PostgreSQL db configuration.
37
+
38
+
## Build
39
+
40
+
### From Source
41
+
42
+
First configure .env.example and save it to .env
43
+
44
+
```bash
45
+
git clone https://github.com/waveringana/simplelink && cd simplelink
46
+
./build.sh
47
+
cargo run
48
+
```
49
+
50
+
Alternatively for a binary build:
51
+
52
+
```bash
53
+
./build.sh --binary
54
+
```
55
+
56
+
then check /target/release for the binary named `SimpleGit`
57
+
58
+
### From Docker
59
+
60
+
```bash
61
+
docker build -t simplelink .
62
+
docker run -p 8080:8080 \
63
+
-e JWT_SECRET=change-me-in-production \
64
+
-e SIMPLELINK_USER=admin@example.com \
65
+
-e SIMPLELINK_PASS=your-secure-password \
66
+
-v simplelink_data:/data \
67
+
simplelink
68
+
```
69
+
70
+
### From Docker Compose
71
+
72
+
Adjust the included docker-compose.yml to your liking; it includes a postgres config as well.
73
+
74
+
## Features
75
+
76
+
- Support for both PostgreSQL and SQLite databases
77
+
- Initial links can be configured via environment variables
78
+
- Admin user can be created on first run via environment variables
79
+
- Link click tracking and statistics
80
+
- Lightweight and performant
+89
build.sh
+89
build.sh
···
1
+
#!/bin/bash
2
+
3
+
# Default values
4
+
#API_URL="http://localhost:8080"
5
+
RELEASE_MODE=false
6
+
BINARY_MODE=false
7
+
8
+
# Parse command line arguments
9
+
for arg in "$@"
10
+
do
11
+
case $arg in
12
+
#api-domain=*)
13
+
#API_URL="${arg#*=}"
14
+
#shift
15
+
#;;
16
+
--release)
17
+
RELEASE_MODE=true
18
+
shift
19
+
;;
20
+
--binary)
21
+
BINARY_MODE=true
22
+
shift
23
+
;;
24
+
esac
25
+
done
26
+
27
+
#echo "Building project with API_URL: $API_URL"
28
+
echo "Release mode: $RELEASE_MODE"
29
+
30
+
# Check if cargo is installed
31
+
if ! command -v cargo &> /dev/null; then
32
+
echo "cargo is not installed. Please install Rust and cargo first."
33
+
exit 1
34
+
fi
35
+
36
+
# Check if npm is installed
37
+
if ! command -v npm &> /dev/null; then
38
+
echo "npm is not installed. Please install Node.js and npm first."
39
+
exit 1
40
+
fi
41
+
42
+
# Build frontend
43
+
echo "Building frontend..."
44
+
# Create .env file for Vite
45
+
#echo "VITE_API_URL=$API_URL" > frontend/.env
46
+
47
+
# Install frontend dependencies and build
48
+
cd frontend
49
+
npm install
50
+
npm run build
51
+
cd ..
52
+
53
+
# Create static directory and copy frontend build
54
+
mkdir -p static
55
+
rm -rf static/*
56
+
cp -r frontend/dist/* static/
57
+
58
+
# Build Rust project
59
+
echo "Building Rust project..."
60
+
if [ "$RELEASE_MODE" = true ]; then
61
+
cargo build --release
62
+
63
+
# Create release directory
64
+
mkdir -p release
65
+
66
+
# Copy only the binary to release directory
67
+
cp target/release/simplelink release/
68
+
cp .env.example release/.env
69
+
70
+
# Create a tar archive
71
+
tar -czf release.tar.gz release/
72
+
73
+
echo "Release archive created: release.tar.gz"
74
+
elif [ "$BINARY_MODE" = true ]; then
75
+
cargo build --release
76
+
else
77
+
cargo build
78
+
fi
79
+
80
+
echo "Build complete!"
81
+
echo "To run the project:"
82
+
if [ "$RELEASE_MODE" = true ]; then
83
+
echo "1. Extract release.tar.gz"
84
+
echo "2. Configure .env file"
85
+
echo "3. Run ./simplelink"
86
+
else
87
+
echo "1. Configure .env file"
88
+
echo "2. Run 'cargo run'"
89
+
fi
+33
-2
docker-compose.yml
+33
-2
docker-compose.yml
···
1
-
version: '3.8'
2
1
services:
3
2
db:
4
3
image: postgres:15-alpine
···
16
15
interval: 5s
17
16
timeout: 5s
18
17
retries: 5
18
+
networks:
19
+
- shortener-network
20
+
21
+
app:
22
+
image: ghcr.io/waveringana/simplelink:v2.2
23
+
container_name: shortener-app
24
+
ports:
25
+
- "8080:8080"
26
+
environment:
27
+
- DATABASE_URL=postgresql://shortener:shortener123@db:5432/shortener
28
+
- SERVER_HOST=0.0.0.0
29
+
- SERVER_PORT=8080
30
+
- JWT_SECRET=change-me-in-production
31
+
depends_on:
32
+
db:
33
+
condition: service_healthy
34
+
healthcheck:
35
+
test: ["CMD", "curl", "-f", "http://localhost:8080/api/health"]
36
+
interval: 30s
37
+
timeout: 10s
38
+
retries: 3
39
+
start_period: 40s
40
+
networks:
41
+
- shortener-network
42
+
deploy:
43
+
restart_policy:
44
+
condition: on-failure
45
+
max_attempts: 3
46
+
window: 120s
47
+
48
+
networks:
49
+
shortener-network:
50
+
driver: bridge
19
51
20
52
volumes:
21
53
shortener-data:
22
-
-855
frontend/bun.lock
-855
frontend/bun.lock
···
1
-
{
2
-
"lockfileVersion": 1,
3
-
"workspaces": {
4
-
"": {
5
-
"name": "frontend",
6
-
"dependencies": {
7
-
"@emotion/react": "^11.14.0",
8
-
"@hookform/resolvers": "^3.10.0",
9
-
"@mantine/core": "^7.16.1",
10
-
"@mantine/form": "^7.16.1",
11
-
"@mantine/hooks": "^7.16.1",
12
-
"@radix-ui/react-dialog": "^1.1.5",
13
-
"@radix-ui/react-dropdown-menu": "^2.1.5",
14
-
"@radix-ui/react-label": "^2.1.1",
15
-
"@radix-ui/react-slot": "^1.1.1",
16
-
"@radix-ui/react-tabs": "^1.1.2",
17
-
"@radix-ui/react-toast": "^1.2.5",
18
-
"@tailwindcss/vite": "^4.0.0",
19
-
"axios": "^1.7.9",
20
-
"class-variance-authority": "^0.7.1",
21
-
"clsx": "^2.1.1",
22
-
"lucide-react": "^0.474.0",
23
-
"react": "^18.3.1",
24
-
"react-dom": "^18.3.1",
25
-
"react-hook-form": "^7.54.2",
26
-
"recharts": "^2.15.0",
27
-
"tailwind-merge": "^2.6.0",
28
-
"tailwindcss-animate": "^1.0.7",
29
-
"zod": "^3.24.1",
30
-
},
31
-
"devDependencies": {
32
-
"@eslint/js": "^9.17.0",
33
-
"@tailwindcss/postcss": "^4.0.0",
34
-
"@types/node": "^22.10.10",
35
-
"@types/react": "^18.3.18",
36
-
"@types/react-dom": "^18.3.5",
37
-
"@vitejs/plugin-react": "^4.3.4",
38
-
"eslint": "^9.17.0",
39
-
"eslint-plugin-react-hooks": "^5.0.0",
40
-
"eslint-plugin-react-refresh": "^0.4.16",
41
-
"globals": "^15.14.0",
42
-
"postcss": "^8.5.1",
43
-
"tailwindcss": "^4.0.0",
44
-
"typescript": "~5.6.2",
45
-
"typescript-eslint": "^8.18.2",
46
-
"vite": "^6.0.5",
47
-
},
48
-
},
49
-
},
50
-
"packages": {
51
-
"@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
52
-
53
-
"@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="],
54
-
55
-
"@babel/code-frame": ["@babel/code-frame@7.26.2", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.25.9", "js-tokens": "^4.0.0", "picocolors": "^1.0.0" } }, "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ=="],
56
-
57
-
"@babel/compat-data": ["@babel/compat-data@7.26.5", "", {}, "sha512-XvcZi1KWf88RVbF9wn8MN6tYFloU5qX8KjuF3E1PVBmJ9eypXfs4GRiJwLuTZL0iSnJUKn1BFPa5BPZZJyFzPg=="],
58
-
59
-
"@babel/core": ["@babel/core@7.26.7", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.26.2", "@babel/generator": "^7.26.5", "@babel/helper-compilation-targets": "^7.26.5", "@babel/helper-module-transforms": "^7.26.0", "@babel/helpers": "^7.26.7", "@babel/parser": "^7.26.7", "@babel/template": "^7.25.9", "@babel/traverse": "^7.26.7", "@babel/types": "^7.26.7", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-SRijHmF0PSPgLIBYlWnG0hyeJLwXE2CgpsXaMOrtt2yp9/86ALw6oUlj9KYuZ0JN07T4eBMVIW4li/9S1j2BGA=="],
60
-
61
-
"@babel/generator": ["@babel/generator@7.26.5", "", { "dependencies": { "@babel/parser": "^7.26.5", "@babel/types": "^7.26.5", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" } }, "sha512-2caSP6fN9I7HOe6nqhtft7V4g7/V/gfDsC3Ag4W7kEzzvRGKqiv0pu0HogPiZ3KaVSoNDhUws6IJjDjpfmYIXw=="],
62
-
63
-
"@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.26.5", "", { "dependencies": { "@babel/compat-data": "^7.26.5", "@babel/helper-validator-option": "^7.25.9", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA=="],
64
-
65
-
"@babel/helper-module-imports": ["@babel/helper-module-imports@7.25.9", "", { "dependencies": { "@babel/traverse": "^7.25.9", "@babel/types": "^7.25.9" } }, "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw=="],
66
-
67
-
"@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.26.0", "", { "dependencies": { "@babel/helper-module-imports": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9", "@babel/traverse": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw=="],
68
-
69
-
"@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.26.5", "", {}, "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg=="],
70
-
71
-
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.25.9", "", {}, "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA=="],
72
-
73
-
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.25.9", "", {}, "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ=="],
74
-
75
-
"@babel/helper-validator-option": ["@babel/helper-validator-option@7.25.9", "", {}, "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw=="],
76
-
77
-
"@babel/helpers": ["@babel/helpers@7.26.7", "", { "dependencies": { "@babel/template": "^7.25.9", "@babel/types": "^7.26.7" } }, "sha512-8NHiL98vsi0mbPQmYAGWwfcFaOy4j2HY49fXJCfuDcdE7fMIsH9a7GdaeXpIBsbT7307WU8KCMp5pUVDNL4f9A=="],
78
-
79
-
"@babel/parser": ["@babel/parser@7.26.7", "", { "dependencies": { "@babel/types": "^7.26.7" }, "bin": "./bin/babel-parser.js" }, "sha512-kEvgGGgEjRUutvdVvZhbn/BxVt+5VSpwXz1j3WYXQbXDo8KzFOPNG2GQbdAiNq8g6wn1yKk7C/qrke03a84V+w=="],
80
-
81
-
"@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg=="],
82
-
83
-
"@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg=="],
84
-
85
-
"@babel/runtime": ["@babel/runtime@7.26.7", "", { "dependencies": { "regenerator-runtime": "^0.14.0" } }, "sha512-AOPI3D+a8dXnja+iwsUqGRjr1BbZIe771sXdapOtYI531gSqpi92vXivKcq2asu/DFpdl1ceFAKZyRzK2PCVcQ=="],
86
-
87
-
"@babel/template": ["@babel/template@7.25.9", "", { "dependencies": { "@babel/code-frame": "^7.25.9", "@babel/parser": "^7.25.9", "@babel/types": "^7.25.9" } }, "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg=="],
88
-
89
-
"@babel/traverse": ["@babel/traverse@7.26.7", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "@babel/generator": "^7.26.5", "@babel/parser": "^7.26.7", "@babel/template": "^7.25.9", "@babel/types": "^7.26.7", "debug": "^4.3.1", "globals": "^11.1.0" } }, "sha512-1x1sgeyRLC3r5fQOM0/xtQKsYjyxmFjaOrLJNtZ81inNjyJHGIolTULPiSc/2qe1/qfpFLisLQYFnnZl7QoedA=="],
90
-
91
-
"@babel/types": ["@babel/types@7.26.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" } }, "sha512-t8kDRGrKXyp6+tjUh7hw2RLyclsW4TRoRvRHtSyAX9Bb5ldlFh+90YAYY6awRXrlB4G5G2izNeGySpATlFzmOg=="],
92
-
93
-
"@emotion/babel-plugin": ["@emotion/babel-plugin@11.13.5", "", { "dependencies": { "@babel/helper-module-imports": "^7.16.7", "@babel/runtime": "^7.18.3", "@emotion/hash": "^0.9.2", "@emotion/memoize": "^0.9.0", "@emotion/serialize": "^1.3.3", "babel-plugin-macros": "^3.1.0", "convert-source-map": "^1.5.0", "escape-string-regexp": "^4.0.0", "find-root": "^1.1.0", "source-map": "^0.5.7", "stylis": "4.2.0" } }, "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ=="],
94
-
95
-
"@emotion/cache": ["@emotion/cache@11.14.0", "", { "dependencies": { "@emotion/memoize": "^0.9.0", "@emotion/sheet": "^1.4.0", "@emotion/utils": "^1.4.2", "@emotion/weak-memoize": "^0.4.0", "stylis": "4.2.0" } }, "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA=="],
96
-
97
-
"@emotion/hash": ["@emotion/hash@0.9.2", "", {}, "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g=="],
98
-
99
-
"@emotion/memoize": ["@emotion/memoize@0.9.0", "", {}, "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ=="],
100
-
101
-
"@emotion/react": ["@emotion/react@11.14.0", "", { "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", "@emotion/cache": "^11.14.0", "@emotion/serialize": "^1.3.3", "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", "@emotion/utils": "^1.4.2", "@emotion/weak-memoize": "^0.4.0", "hoist-non-react-statics": "^3.3.1" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA=="],
102
-
103
-
"@emotion/serialize": ["@emotion/serialize@1.3.3", "", { "dependencies": { "@emotion/hash": "^0.9.2", "@emotion/memoize": "^0.9.0", "@emotion/unitless": "^0.10.0", "@emotion/utils": "^1.4.2", "csstype": "^3.0.2" } }, "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA=="],
104
-
105
-
"@emotion/sheet": ["@emotion/sheet@1.4.0", "", {}, "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg=="],
106
-
107
-
"@emotion/unitless": ["@emotion/unitless@0.10.0", "", {}, "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg=="],
108
-
109
-
"@emotion/use-insertion-effect-with-fallbacks": ["@emotion/use-insertion-effect-with-fallbacks@1.2.0", "", { "peerDependencies": { "react": ">=16.8.0" } }, "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg=="],
110
-
111
-
"@emotion/utils": ["@emotion/utils@1.4.2", "", {}, "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA=="],
112
-
113
-
"@emotion/weak-memoize": ["@emotion/weak-memoize@0.4.0", "", {}, "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg=="],
114
-
115
-
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.24.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA=="],
116
-
117
-
"@esbuild/android-arm": ["@esbuild/android-arm@0.24.2", "", { "os": "android", "cpu": "arm" }, "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q=="],
118
-
119
-
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.24.2", "", { "os": "android", "cpu": "arm64" }, "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg=="],
120
-
121
-
"@esbuild/android-x64": ["@esbuild/android-x64@0.24.2", "", { "os": "android", "cpu": "x64" }, "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw=="],
122
-
123
-
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.24.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA=="],
124
-
125
-
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.24.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA=="],
126
-
127
-
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.24.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg=="],
128
-
129
-
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.24.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q=="],
130
-
131
-
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.24.2", "", { "os": "linux", "cpu": "arm" }, "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA=="],
132
-
133
-
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.24.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg=="],
134
-
135
-
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.24.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw=="],
136
-
137
-
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.24.2", "", { "os": "linux", "cpu": "none" }, "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ=="],
138
-
139
-
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.24.2", "", { "os": "linux", "cpu": "none" }, "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw=="],
140
-
141
-
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.24.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw=="],
142
-
143
-
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.24.2", "", { "os": "linux", "cpu": "none" }, "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q=="],
144
-
145
-
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.24.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw=="],
146
-
147
-
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.24.2", "", { "os": "linux", "cpu": "x64" }, "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q=="],
148
-
149
-
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.24.2", "", { "os": "none", "cpu": "arm64" }, "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw=="],
150
-
151
-
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.24.2", "", { "os": "none", "cpu": "x64" }, "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw=="],
152
-
153
-
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.24.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A=="],
154
-
155
-
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.24.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA=="],
156
-
157
-
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.24.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig=="],
158
-
159
-
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.24.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ=="],
160
-
161
-
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.24.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA=="],
162
-
163
-
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.24.2", "", { "os": "win32", "cpu": "x64" }, "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg=="],
164
-
165
-
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.4.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA=="],
166
-
167
-
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.1", "", {}, "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ=="],
168
-
169
-
"@eslint/config-array": ["@eslint/config-array@0.19.1", "", { "dependencies": { "@eslint/object-schema": "^2.1.5", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-fo6Mtm5mWyKjA/Chy1BYTdn5mGJoDNjC7C64ug20ADsRDGrA85bN3uK3MaKbeRkRuuIEAR5N33Jr1pbm411/PA=="],
170
-
171
-
"@eslint/core": ["@eslint/core@0.10.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-gFHJ+xBOo4G3WRlR1e/3G8A6/KZAH6zcE/hkLRCZTi/B9avAG365QhFA8uOGzTMqgTghpn7/fSnscW++dpMSAw=="],
172
-
173
-
"@eslint/eslintrc": ["@eslint/eslintrc@3.2.0", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w=="],
174
-
175
-
"@eslint/js": ["@eslint/js@9.19.0", "", {}, "sha512-rbq9/g38qjfqFLOVPvwjIvFFdNziEC5S65jmjPw5r6A//QH+W91akh9irMwjDN8zKUTak6W9EsAv4m/7Wnw0UQ=="],
176
-
177
-
"@eslint/object-schema": ["@eslint/object-schema@2.1.5", "", {}, "sha512-o0bhxnL89h5Bae5T318nFoFzGy+YE5i/gGkoPAgkmTVdRKTiv3p8JHevPiPaMwoloKfEiiaHlawCqaZMqRm+XQ=="],
178
-
179
-
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.2.5", "", { "dependencies": { "@eslint/core": "^0.10.0", "levn": "^0.4.1" } }, "sha512-lB05FkqEdUg2AA0xEbUz0SnkXT1LcCTa438W4IWTUh4hdOnVbQyOJ81OrDXsJk/LSiJHubgGEFoR5EHq1NsH1A=="],
180
-
181
-
"@floating-ui/core": ["@floating-ui/core@1.6.9", "", { "dependencies": { "@floating-ui/utils": "^0.2.9" } }, "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw=="],
182
-
183
-
"@floating-ui/dom": ["@floating-ui/dom@1.6.13", "", { "dependencies": { "@floating-ui/core": "^1.6.0", "@floating-ui/utils": "^0.2.9" } }, "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w=="],
184
-
185
-
"@floating-ui/react": ["@floating-ui/react@0.26.28", "", { "dependencies": { "@floating-ui/react-dom": "^2.1.2", "@floating-ui/utils": "^0.2.8", "tabbable": "^6.0.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw=="],
186
-
187
-
"@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.2", "", { "dependencies": { "@floating-ui/dom": "^1.0.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A=="],
188
-
189
-
"@floating-ui/utils": ["@floating-ui/utils@0.2.9", "", {}, "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg=="],
190
-
191
-
"@hookform/resolvers": ["@hookform/resolvers@3.10.0", "", { "peerDependencies": { "react-hook-form": "^7.0.0" } }, "sha512-79Dv+3mDF7i+2ajj7SkypSKHhl1cbln1OGavqrsF7p6mbUv11xpqpacPsGDCTRvCSjEEIez2ef1NveSVL3b0Ag=="],
192
-
193
-
"@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
194
-
195
-
"@humanfs/node": ["@humanfs/node@0.16.6", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.3.0" } }, "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw=="],
196
-
197
-
"@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="],
198
-
199
-
"@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.1", "", {}, "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA=="],
200
-
201
-
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.8", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA=="],
202
-
203
-
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
204
-
205
-
"@jridgewell/set-array": ["@jridgewell/set-array@1.2.1", "", {}, "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A=="],
206
-
207
-
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="],
208
-
209
-
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
210
-
211
-
"@mantine/core": ["@mantine/core@7.16.1", "", { "dependencies": { "@floating-ui/react": "^0.26.28", "clsx": "^2.1.1", "react-number-format": "^5.4.3", "react-remove-scroll": "^2.6.2", "react-textarea-autosize": "8.5.6", "type-fest": "^4.27.0" }, "peerDependencies": { "@mantine/hooks": "7.16.1", "react": "^18.x || ^19.x", "react-dom": "^18.x || ^19.x" } }, "sha512-HYdjCeMU3dUJbc1CrAAedeAASTG5kVyL/qsiuYh5b7BoG0qsRtK8WJxBpUjW6VqtJpUaE94c5tlBJ8MgAmPHTQ=="],
212
-
213
-
"@mantine/form": ["@mantine/form@7.16.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "klona": "^2.0.6" }, "peerDependencies": { "react": "^18.x || ^19.x" } }, "sha512-SZfOlmO14oAYdqo3SJKJlPrSNaeWyTPIPV/cur/4sPf114cAyggEZHoHJEjy2yA8UccfwYZx39yWrwxQCb8J8w=="],
214
-
215
-
"@mantine/hooks": ["@mantine/hooks@7.16.1", "", { "peerDependencies": { "react": "^18.x || ^19.x" } }, "sha512-+hER8E4d2ByfQ/DKIXGM3Euxb7IH5ArSjzzzoF21sG095iXIryOCob22ZanrmiXCoAzKKdxqgVj4Di67ikLYSQ=="],
216
-
217
-
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
218
-
219
-
"@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="],
220
-
221
-
"@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
222
-
223
-
"@radix-ui/primitive": ["@radix-ui/primitive@1.1.1", "", {}, "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA=="],
224
-
225
-
"@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.1", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-NaVpZfmv8SKeZbn4ijN2V3jlHA9ngBG16VnIIm22nUR0Yk8KUALyBxT3KYEUnNuch9sTE8UTsS3whzBgKOL30w=="],
226
-
227
-
"@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-slot": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-LwT3pSho9Dljg+wY2KN2mrrh6y3qELfftINERIzBUO9e0N+t0oMTyn3k9iv+ZqgrwGkRnLpNJrsMv9BZlt2yuA=="],
228
-
229
-
"@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw=="],
230
-
231
-
"@radix-ui/react-context": ["@radix-ui/react-context@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q=="],
232
-
233
-
"@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.5", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.4", "@radix-ui/react-focus-guards": "1.1.1", "@radix-ui/react-focus-scope": "1.1.1", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-portal": "1.1.3", "@radix-ui/react-presence": "1.1.2", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-slot": "1.1.1", "@radix-ui/react-use-controllable-state": "1.1.0", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-LaO3e5h/NOEL4OfXjxD43k9Dx+vn+8n+PCFt6uhX/BADFflllyv3WJG6rgvvSVBxpTch938Qq/LGc2MMxipXPw=="],
234
-
235
-
"@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg=="],
236
-
237
-
"@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.4", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-escape-keydown": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-XDUI0IVYVSwjMXxM6P4Dfti7AH+Y4oS/TB+sglZ/EXc7cqLwGAmp1NlMrcUjj7ks6R5WTZuWKv44FBbLpwU3sA=="],
238
-
239
-
"@radix-ui/react-dropdown-menu": ["@radix-ui/react-dropdown-menu@2.1.5", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-menu": "2.1.5", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-use-controllable-state": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-50ZmEFL1kOuLalPKHrLWvPFMons2fGx9TqQCWlPwDVpbAnaUJ1g4XNcKqFNMQymYU0kKWR4MDDi+9vUQBGFgcQ=="],
240
-
241
-
"@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg=="],
242
-
243
-
"@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-use-callback-ref": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-01omzJAYRxXdG2/he/+xy+c8a8gCydoQ1yOxnWNcRhrrBW5W+RQJ22EK1SaO8tb3WoUsuEw7mJjBozPzihDFjA=="],
244
-
245
-
"@radix-ui/react-id": ["@radix-ui/react-id@1.1.0", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA=="],
246
-
247
-
"@radix-ui/react-label": ["@radix-ui/react-label@2.1.1", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-UUw5E4e/2+4kFMH7+YxORXGWggtY6sM8WIwh5RZchhLuUg2H1hc98Py+pr8HMz6rdaYrK2t296ZEjYLOCO5uUw=="],
248
-
249
-
"@radix-ui/react-menu": ["@radix-ui/react-menu@2.1.5", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-collection": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-direction": "1.1.0", "@radix-ui/react-dismissable-layer": "1.1.4", "@radix-ui/react-focus-guards": "1.1.1", "@radix-ui/react-focus-scope": "1.1.1", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-popper": "1.2.1", "@radix-ui/react-portal": "1.1.3", "@radix-ui/react-presence": "1.1.2", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-roving-focus": "1.1.1", "@radix-ui/react-slot": "1.1.1", "@radix-ui/react-use-callback-ref": "1.1.0", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-uH+3w5heoMJtqVCgYOtYVMECk1TOrkUn0OG0p5MqXC0W2ppcuVeESbou8PTHoqAjbdTEK19AGXBWcEtR5WpEQg=="],
250
-
251
-
"@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.1", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0", "@radix-ui/react-use-rect": "1.1.0", "@radix-ui/react-use-size": "1.1.0", "@radix-ui/rect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-3kn5Me69L+jv82EKRuQCXdYyf1DqHwD2U/sxoNgBGCB7K9TRc3bQamQ+5EPM9EvyPdli0W41sROd+ZU1dTCztw=="],
252
-
253
-
"@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.3", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-NciRqhXnGojhT93RPyDaMPfLH3ZSl4jjIFbZQ1b/vxvZEdHsBZ49wP9w8L3HzUQwep01LcWtkUvm0OVB5JAHTw=="],
254
-
255
-
"@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.2", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg=="],
256
-
257
-
"@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.0.1", "", { "dependencies": { "@radix-ui/react-slot": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg=="],
258
-
259
-
"@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.1", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-collection": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-direction": "1.1.0", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-controllable-state": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-QE1RoxPGJ/Nm8Qmk0PxP8ojmoaS67i0s7hVssS7KuI2FQoc/uzVlZsqKfQvxPE6D8hICCPHJ4D88zNhT3OOmkw=="],
260
-
261
-
"@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g=="],
262
-
263
-
"@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.2", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-direction": "1.1.0", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-presence": "1.1.2", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-roving-focus": "1.1.1", "@radix-ui/react-use-controllable-state": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9u/tQJMcC2aGq7KXpGivMm1mgq7oRJKXphDwdypPd/j21j/2znamPU8WkXgnhUaTrSFNIt8XhOyCAupg8/GbwQ=="],
264
-
265
-
"@radix-ui/react-toast": ["@radix-ui/react-toast@1.2.5", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-collection": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.4", "@radix-ui/react-portal": "1.1.3", "@radix-ui/react-presence": "1.1.2", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-controllable-state": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0", "@radix-ui/react-visually-hidden": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-ZzUsAaOx8NdXZZKcFNDhbSlbsCUy8qQWmzTdgrlrhhZAOx2ofLtKrBDW9fkqhFvXgmtv560Uj16pkLkqML7SHA=="],
266
-
267
-
"@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw=="],
268
-
269
-
"@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.1.0", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw=="],
270
-
271
-
"@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.0", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw=="],
272
-
273
-
"@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w=="],
274
-
275
-
"@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.1.0", "", { "dependencies": { "@radix-ui/rect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ=="],
276
-
277
-
"@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.0", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw=="],
278
-
279
-
"@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.1.1", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-vVfA2IZ9q/J+gEamvj761Oq1FpWgCDaNOOIfbPVp2MVPLEomUr5+Vf7kJGwQ24YxZSlQVar7Bes8kyTo5Dshpg=="],
280
-
281
-
"@radix-ui/rect": ["@radix-ui/rect@1.1.0", "", {}, "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg=="],
282
-
283
-
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.32.0", "", { "os": "android", "cpu": "arm" }, "sha512-G2fUQQANtBPsNwiVFg4zKiPQyjVKZCUdQUol53R8E71J7AsheRMV/Yv/nB8giOcOVqP7//eB5xPqieBYZe9bGg=="],
284
-
285
-
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-qhFwQ+ljoymC+j5lXRv8DlaJYY/+8vyvYmVx074zrLsu5ZGWYsJNLjPPVJJjhZQpyAKUGPydOq9hRLLNvh1s3A=="],
286
-
287
-
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-44n/X3lAlWsEY6vF8CzgCx+LQaoqWGN7TzUfbJDiTIOjJm4+L2Yq+r5a8ytQRGyPqgJDs3Rgyo8eVL7n9iW6AQ=="],
288
-
289
-
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-F9ct0+ZX5Np6+ZDztxiGCIvlCaW87HBdHcozUfsHnj1WCUTBUubAoanhHUfnUHZABlElyRikI0mgcw/qdEm2VQ=="],
290
-
291
-
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.32.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-JpsGxLBB2EFXBsTLHfkZDsXSpSmKD3VxXCgBQtlPcuAqB8TlqtLcbeMhxXQkCDv1avgwNjF8uEIbq5p+Cee0PA=="],
292
-
293
-
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-wegiyBT6rawdpvnD9lmbOpx5Sph+yVZKHbhnSP9MqUEDX08G4UzMU+D87jrazGE7lRSyTRs6NEYHtzfkJ3FjjQ=="],
294
-
295
-
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-3pA7xecItbgOs1A5H58dDvOUEboG5UfpTq3WzAdF54acBbUM+olDJAPkgj1GRJ4ZqE12DZ9/hNS2QZk166v92A=="],
296
-
297
-
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-Y7XUZEVISGyge51QbYyYAEHwpGgmRrAxQXO3siyYo2kmaj72USSG8LtlQQgAtlGfxYiOwu+2BdbPjzEpcOpRmQ=="],
298
-
299
-
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-r7/OTF5MqeBrZo5omPXcTnjvv1GsrdH8a8RerARvDFiDwFpDVDnJyByYM/nX+mvks8XXsgPUxkwe/ltaX2VH7w=="],
300
-
301
-
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-HJbifC9vex9NqnlodV2BHVFNuzKL5OnsV2dvTw6e1dpZKkNjPG6WUq+nhEYV6Hv2Bv++BXkwcyoGlXnPrjAKXw=="],
302
-
303
-
"@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.32.0", "", { "os": "linux", "cpu": "none" }, "sha512-VAEzZTD63YglFlWwRj3taofmkV1V3xhebDXffon7msNz4b14xKsz7utO6F8F4cqt8K/ktTl9rm88yryvDpsfOw=="],
304
-
305
-
"@rollup/rollup-linux-powerpc64le-gnu": ["@rollup/rollup-linux-powerpc64le-gnu@4.32.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Sts5DST1jXAc9YH/iik1C9QRsLcCoOScf3dfbY5i4kH9RJpKxiTBXqm7qU5O6zTXBTEZry69bGszr3SMgYmMcQ=="],
306
-
307
-
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.32.0", "", { "os": "linux", "cpu": "none" }, "sha512-qhlXeV9AqxIyY9/R1h1hBD6eMvQCO34ZmdYvry/K+/MBs6d1nRFLm6BOiITLVI+nFAAB9kUB6sdJRKyVHXnqZw=="],
308
-
309
-
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.32.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-8ZGN7ExnV0qjXa155Rsfi6H8M4iBBwNLBM9lcVS+4NcSzOFaNqmt7djlox8pN1lWrRPMRRQ8NeDlozIGx3Omsw=="],
310
-
311
-
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-VDzNHtLLI5s7xd/VubyS10mq6TxvZBp+4NRWoW+Hi3tgV05RtVm4qK99+dClwTN1McA6PHwob6DEJ6PlXbY83A=="],
312
-
313
-
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-qcb9qYDlkxz9DxJo7SDhWxTWV1gFuwznjbTiov289pASxlfGbaOD54mgbs9+z94VwrXtKTu+2RqwlSTbiOqxGg=="],
314
-
315
-
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-pFDdotFDMXW2AXVbfdUEfidPAk/OtwE/Hd4eYMTNVVaCQ6Yl8et0meDaKNL63L44Haxv4UExpv9ydSf3aSayDg=="],
316
-
317
-
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.32.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-/TG7WfrCAjeRNDvI4+0AAMoHxea/USWhAzf9PVDFHbcqrQ7hMMKp4jZIy4VEjk72AAfN5k4TiSMRXRKf/0akSw=="],
318
-
319
-
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-5hqO5S3PTEO2E5VjCePxv40gIgyS2KvO7E7/vvC/NbIW4SIRamkMr1hqj+5Y67fbBWv/bQLB6KelBQmXlyCjWA=="],
320
-
321
-
"@tailwindcss/node": ["@tailwindcss/node@4.0.0", "", { "dependencies": { "enhanced-resolve": "^5.18.0", "jiti": "^2.4.2", "tailwindcss": "4.0.0" } }, "sha512-tfG2uBvo6j6kDIPmntxwXggCOZAt7SkpAXJ6pTIYirNdk5FBqh/CZZ9BZPpgcl/tNFLs6zc4yghM76sqiELG9g=="],
322
-
323
-
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.0.0", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.0.0", "@tailwindcss/oxide-darwin-arm64": "4.0.0", "@tailwindcss/oxide-darwin-x64": "4.0.0", "@tailwindcss/oxide-freebsd-x64": "4.0.0", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.0.0", "@tailwindcss/oxide-linux-arm64-gnu": "4.0.0", "@tailwindcss/oxide-linux-arm64-musl": "4.0.0", "@tailwindcss/oxide-linux-x64-gnu": "4.0.0", "@tailwindcss/oxide-linux-x64-musl": "4.0.0", "@tailwindcss/oxide-win32-arm64-msvc": "4.0.0", "@tailwindcss/oxide-win32-x64-msvc": "4.0.0" } }, "sha512-W3FjpJgy4VV1JiL7iBYDf2n/WkeDg1Il+0Q7eWnqPyvkPPCo/Mbwc5BiaT7dfBNV6tQKAhVE34rU5xl8pSl50w=="],
324
-
325
-
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.0.0", "", { "os": "android", "cpu": "arm64" }, "sha512-EAhjU0+FIdyGPR+7MbBWubLLPtmOu+p7c2egTTFBRk/n//zYjNvVK0WhcBK5Y7oUB5mo4EjA2mCbY7dcEMWSRw=="],
326
-
327
-
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.0.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-hdz4xnSWS11cIp+7ye+3dGHqs0X33z+BXXTtgPOguDWVa+TdXUzwxonklSzf5wlJFuot3dv5eWzhlNai0oYYQg=="],
328
-
329
-
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.0.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-+dOUUaXTkPKKhtUI9QtVaYg+MpmLh2CN0dHohiYXaBirEyPMkjaT0zbRgzQlNnQWjCVVXPQluIEb0OMEjSTH+Q=="],
330
-
331
-
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.0.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-CJhGDhxnrmu4SwyC62fA+wP24MhA/TZlIhRHqg1kRuIHoGoVR2uSSm1qxTxU37tSSZj8Up0q6jsBJCAP4k7rgQ=="],
332
-
333
-
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.0.0", "", { "os": "linux", "cpu": "arm" }, "sha512-Wy7Av0xzXfY2ujZBcYy4+7GQm25/J1iHvlQU2CfwdDCuPWfIjYzR6kggz+uVdSJyKV2s64znchBxRE8kV4uXSA=="],
334
-
335
-
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.0.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-srwBo2l6pvM0swBntc1ucuhGsfFOLkqPRFQ3dWARRTfSkL1U9nAsob2MKc/n47Eva/W9pZZgMOuf7rDw8pK1Ew=="],
336
-
337
-
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.0.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-abhusswkduYWuezkBmgo0K0/erGq3M4Se5xP0fhc/0dKs0X/rJUYYCFWntHb3IGh3aVzdQ0SXJs93P76DbUqtw=="],
338
-
339
-
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.0.0", "", { "os": "linux", "cpu": "x64" }, "sha512-hGtRYIUEx377/HlU49+jvVKKwU1MDSKYSMMs0JFO2Wp7LGxk5+0j5+RBk9NFnmp/lbp32yPTgIOO5m1BmDq36A=="],
340
-
341
-
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.0.0", "", { "os": "linux", "cpu": "x64" }, "sha512-7xgQgSAThs0I14VAgmxpJnK6XFSZBxHMGoDXkLyYkEnu+8WRQMbCP93dkCUn2PIv+Q+JulRgc00PJ09uORSLXQ=="],
342
-
343
-
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.0.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-qEcgTIPcWY5ZE7f6VxQ/JPrSFMcehzVIlZj7sGE3mVd5YWreAT+Fl1vSP8q2pjnWXn0avZG3Iw7a2hJQAm+fTQ=="],
344
-
345
-
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.0.0", "", { "os": "win32", "cpu": "x64" }, "sha512-bqT0AY8RXb8GMDy28JtngvqaOSB2YixbLPLvUo6I6lkvvUwA6Eqh2Tj60e2Lh7O/k083f8tYiB0WEK4wmTI7Jg=="],
346
-
347
-
"@tailwindcss/postcss": ["@tailwindcss/postcss@4.0.0", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "^4.0.0", "@tailwindcss/oxide": "^4.0.0", "lightningcss": "^1.29.1", "postcss": "^8.4.41", "tailwindcss": "4.0.0" } }, "sha512-lI2bPk4TvwavHdehjr5WiC6HnZ59hacM6ySEo4RM/H7tsjWd8JpqiNW9ThH7rO/yKtrn4mGBoXshpvn8clXjPg=="],
348
-
349
-
"@tailwindcss/vite": ["@tailwindcss/vite@4.0.0", "", { "dependencies": { "@tailwindcss/node": "^4.0.0", "@tailwindcss/oxide": "^4.0.0", "lightningcss": "^1.29.1", "tailwindcss": "4.0.0" }, "peerDependencies": { "vite": "^5.2.0 || ^6" } }, "sha512-4uukMiU9gHui8KMPMdWic5SP1O/tmQ1NFSRNrQWmcop5evAVl/LZ6/LuWL3quEiecp2RBcRWwqJrG+mFXlRlew=="],
350
-
351
-
"@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
352
-
353
-
"@types/babel__generator": ["@types/babel__generator@7.6.8", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw=="],
354
-
355
-
"@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="],
356
-
357
-
"@types/babel__traverse": ["@types/babel__traverse@7.20.6", "", { "dependencies": { "@babel/types": "^7.20.7" } }, "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg=="],
358
-
359
-
"@types/d3-array": ["@types/d3-array@3.2.1", "", {}, "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg=="],
360
-
361
-
"@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="],
362
-
363
-
"@types/d3-ease": ["@types/d3-ease@3.0.2", "", {}, "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="],
364
-
365
-
"@types/d3-interpolate": ["@types/d3-interpolate@3.0.4", "", { "dependencies": { "@types/d3-color": "*" } }, "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA=="],
366
-
367
-
"@types/d3-path": ["@types/d3-path@3.1.0", "", {}, "sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ=="],
368
-
369
-
"@types/d3-scale": ["@types/d3-scale@4.0.8", "", { "dependencies": { "@types/d3-time": "*" } }, "sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ=="],
370
-
371
-
"@types/d3-shape": ["@types/d3-shape@3.1.7", "", { "dependencies": { "@types/d3-path": "*" } }, "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg=="],
372
-
373
-
"@types/d3-time": ["@types/d3-time@3.0.4", "", {}, "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="],
374
-
375
-
"@types/d3-timer": ["@types/d3-timer@3.0.2", "", {}, "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="],
376
-
377
-
"@types/estree": ["@types/estree@1.0.6", "", {}, "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="],
378
-
379
-
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
380
-
381
-
"@types/node": ["@types/node@22.10.10", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-X47y/mPNzxviAGY5TcYPtYL8JsY3kAq2n8fMmKoRCxq/c4v4pyGNCzM2R6+M5/umG4ZfHuT+sgqDYqWc9rJ6ww=="],
382
-
383
-
"@types/parse-json": ["@types/parse-json@4.0.2", "", {}, "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw=="],
384
-
385
-
"@types/prop-types": ["@types/prop-types@15.7.14", "", {}, "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ=="],
386
-
387
-
"@types/react": ["@types/react@18.3.18", "", { "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" } }, "sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ=="],
388
-
389
-
"@types/react-dom": ["@types/react-dom@18.3.5", "", { "peerDependencies": { "@types/react": "^18.0.0" } }, "sha512-P4t6saawp+b/dFrUr2cvkVsfvPguwsxtH6dNIYRllMsefqFzkZk5UIjzyDOv5g1dXIPdG4Sp1yCR4Z6RCUsG/Q=="],
390
-
391
-
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.21.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.21.0", "@typescript-eslint/type-utils": "8.21.0", "@typescript-eslint/utils": "8.21.0", "@typescript-eslint/visitor-keys": "8.21.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", "ts-api-utils": "^2.0.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-eTH+UOR4I7WbdQnG4Z48ebIA6Bgi7WO8HvFEneeYBxG8qCOYgTOFPSg6ek9ITIDvGjDQzWHcoWHCDO2biByNzA=="],
392
-
393
-
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.21.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.21.0", "@typescript-eslint/types": "8.21.0", "@typescript-eslint/typescript-estree": "8.21.0", "@typescript-eslint/visitor-keys": "8.21.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-Wy+/sdEH9kI3w9civgACwabHbKl+qIOu0uFZ9IMKzX3Jpv9og0ZBJrZExGrPpFAY7rWsXuxs5e7CPPP17A4eYA=="],
394
-
395
-
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.21.0", "", { "dependencies": { "@typescript-eslint/types": "8.21.0", "@typescript-eslint/visitor-keys": "8.21.0" } }, "sha512-G3IBKz0/0IPfdeGRMbp+4rbjfSSdnGkXsM/pFZA8zM9t9klXDnB/YnKOBQ0GoPmoROa4bCq2NeHgJa5ydsQ4mA=="],
396
-
397
-
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.21.0", "", { "dependencies": { "@typescript-eslint/typescript-estree": "8.21.0", "@typescript-eslint/utils": "8.21.0", "debug": "^4.3.4", "ts-api-utils": "^2.0.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-95OsL6J2BtzoBxHicoXHxgk3z+9P3BEcQTpBKriqiYzLKnM2DeSqs+sndMKdamU8FosiadQFT3D+BSL9EKnAJQ=="],
398
-
399
-
"@typescript-eslint/types": ["@typescript-eslint/types@8.21.0", "", {}, "sha512-PAL6LUuQwotLW2a8VsySDBwYMm129vFm4tMVlylzdoTybTHaAi0oBp7Ac6LhSrHHOdLM3efH+nAR6hAWoMF89A=="],
400
-
401
-
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.21.0", "", { "dependencies": { "@typescript-eslint/types": "8.21.0", "@typescript-eslint/visitor-keys": "8.21.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.0.0" }, "peerDependencies": { "typescript": ">=4.8.4 <5.8.0" } }, "sha512-x+aeKh/AjAArSauz0GiQZsjT8ciadNMHdkUSwBB9Z6PrKc/4knM4g3UfHml6oDJmKC88a6//cdxnO/+P2LkMcg=="],
402
-
403
-
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.21.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@typescript-eslint/scope-manager": "8.21.0", "@typescript-eslint/types": "8.21.0", "@typescript-eslint/typescript-estree": "8.21.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-xcXBfcq0Kaxgj7dwejMbFyq7IOHgpNMtVuDveK7w3ZGwG9owKzhALVwKpTF2yrZmEwl9SWdetf3fxNzJQaVuxw=="],
404
-
405
-
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.21.0", "", { "dependencies": { "@typescript-eslint/types": "8.21.0", "eslint-visitor-keys": "^4.2.0" } }, "sha512-BkLMNpdV6prozk8LlyK/SOoWLmUFi+ZD+pcqti9ILCbVvHGk1ui1g4jJOc2WDLaeExz2qWwojxlPce5PljcT3w=="],
406
-
407
-
"@vitejs/plugin-react": ["@vitejs/plugin-react@4.3.4", "", { "dependencies": { "@babel/core": "^7.26.0", "@babel/plugin-transform-react-jsx-self": "^7.25.9", "@babel/plugin-transform-react-jsx-source": "^7.25.9", "@types/babel__core": "^7.20.5", "react-refresh": "^0.14.2" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0" } }, "sha512-SCCPBJtYLdE8PX/7ZQAs1QAZ8Jqwih+0VBLum1EGqmCCQal+MIUqLCzj3ZUy8ufbC0cAM4LRlSTm7IQJwWT4ug=="],
408
-
409
-
"acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="],
410
-
411
-
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
412
-
413
-
"ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
414
-
415
-
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
416
-
417
-
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
418
-
419
-
"aria-hidden": ["aria-hidden@1.2.4", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A=="],
420
-
421
-
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
422
-
423
-
"axios": ["axios@1.7.9", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } }, "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw=="],
424
-
425
-
"babel-plugin-macros": ["babel-plugin-macros@3.1.0", "", { "dependencies": { "@babel/runtime": "^7.12.5", "cosmiconfig": "^7.0.0", "resolve": "^1.19.0" } }, "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg=="],
426
-
427
-
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
428
-
429
-
"brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="],
430
-
431
-
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
432
-
433
-
"browserslist": ["browserslist@4.24.4", "", { "dependencies": { "caniuse-lite": "^1.0.30001688", "electron-to-chromium": "^1.5.73", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.1" }, "bin": { "browserslist": "cli.js" } }, "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A=="],
434
-
435
-
"callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
436
-
437
-
"caniuse-lite": ["caniuse-lite@1.0.30001695", "", {}, "sha512-vHyLade6wTgI2u1ec3WQBxv+2BrTERV28UXQu9LO6lZ9pYeMk34vjXFLOxo1A4UBA8XTL4njRQZdno/yYaSmWw=="],
438
-
439
-
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
440
-
441
-
"class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="],
442
-
443
-
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
444
-
445
-
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
446
-
447
-
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
448
-
449
-
"combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
450
-
451
-
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
452
-
453
-
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
454
-
455
-
"cosmiconfig": ["cosmiconfig@7.1.0", "", { "dependencies": { "@types/parse-json": "^4.0.0", "import-fresh": "^3.2.1", "parse-json": "^5.0.0", "path-type": "^4.0.0", "yaml": "^1.10.0" } }, "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA=="],
456
-
457
-
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
458
-
459
-
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
460
-
461
-
"d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="],
462
-
463
-
"d3-color": ["d3-color@3.1.0", "", {}, "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="],
464
-
465
-
"d3-ease": ["d3-ease@3.0.1", "", {}, "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w=="],
466
-
467
-
"d3-format": ["d3-format@3.1.0", "", {}, "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA=="],
468
-
469
-
"d3-interpolate": ["d3-interpolate@3.0.1", "", { "dependencies": { "d3-color": "1 - 3" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="],
470
-
471
-
"d3-path": ["d3-path@3.1.0", "", {}, "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ=="],
472
-
473
-
"d3-scale": ["d3-scale@4.0.2", "", { "dependencies": { "d3-array": "2.10.0 - 3", "d3-format": "1 - 3", "d3-interpolate": "1.2.0 - 3", "d3-time": "2.1.1 - 3", "d3-time-format": "2 - 4" } }, "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ=="],
474
-
475
-
"d3-shape": ["d3-shape@3.2.0", "", { "dependencies": { "d3-path": "^3.1.0" } }, "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA=="],
476
-
477
-
"d3-time": ["d3-time@3.1.0", "", { "dependencies": { "d3-array": "2 - 3" } }, "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q=="],
478
-
479
-
"d3-time-format": ["d3-time-format@4.1.0", "", { "dependencies": { "d3-time": "1 - 3" } }, "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg=="],
480
-
481
-
"d3-timer": ["d3-timer@3.0.1", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="],
482
-
483
-
"debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="],
484
-
485
-
"decimal.js-light": ["decimal.js-light@2.5.1", "", {}, "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="],
486
-
487
-
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
488
-
489
-
"delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
490
-
491
-
"detect-libc": ["detect-libc@1.0.3", "", { "bin": { "detect-libc": "./bin/detect-libc.js" } }, "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg=="],
492
-
493
-
"detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="],
494
-
495
-
"dom-helpers": ["dom-helpers@5.2.1", "", { "dependencies": { "@babel/runtime": "^7.8.7", "csstype": "^3.0.2" } }, "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA=="],
496
-
497
-
"electron-to-chromium": ["electron-to-chromium@1.5.88", "", {}, "sha512-K3C2qf1o+bGzbilTDCTBhTQcMS9KW60yTAaTeeXsfvQuTDDwlokLam/AdqlqcSy9u4UainDgsHV23ksXAOgamw=="],
498
-
499
-
"enhanced-resolve": ["enhanced-resolve@5.18.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-0/r0MySGYG8YqlayBZ6MuCfECmHFdJ5qyPh8s8wa5Hnm6SaFLSK1VYCbj+NKp090Nm1caZhD+QTnmxO7esYGyQ=="],
500
-
501
-
"error-ex": ["error-ex@1.3.2", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g=="],
502
-
503
-
"esbuild": ["esbuild@0.24.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.24.2", "@esbuild/android-arm": "0.24.2", "@esbuild/android-arm64": "0.24.2", "@esbuild/android-x64": "0.24.2", "@esbuild/darwin-arm64": "0.24.2", "@esbuild/darwin-x64": "0.24.2", "@esbuild/freebsd-arm64": "0.24.2", "@esbuild/freebsd-x64": "0.24.2", "@esbuild/linux-arm": "0.24.2", "@esbuild/linux-arm64": "0.24.2", "@esbuild/linux-ia32": "0.24.2", "@esbuild/linux-loong64": "0.24.2", "@esbuild/linux-mips64el": "0.24.2", "@esbuild/linux-ppc64": "0.24.2", "@esbuild/linux-riscv64": "0.24.2", "@esbuild/linux-s390x": "0.24.2", "@esbuild/linux-x64": "0.24.2", "@esbuild/netbsd-arm64": "0.24.2", "@esbuild/netbsd-x64": "0.24.2", "@esbuild/openbsd-arm64": "0.24.2", "@esbuild/openbsd-x64": "0.24.2", "@esbuild/sunos-x64": "0.24.2", "@esbuild/win32-arm64": "0.24.2", "@esbuild/win32-ia32": "0.24.2", "@esbuild/win32-x64": "0.24.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA=="],
504
-
505
-
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
506
-
507
-
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
508
-
509
-
"eslint": ["eslint@9.19.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.19.0", "@eslint/core": "^0.10.0", "@eslint/eslintrc": "^3.2.0", "@eslint/js": "9.19.0", "@eslint/plugin-kit": "^0.2.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.1", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.2.0", "eslint-visitor-keys": "^4.2.0", "espree": "^10.3.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-ug92j0LepKlbbEv6hD911THhoRHmbdXt2gX+VDABAW/Ir7D3nqKdv5Pf5vtlyY6HQMTEP2skXY43ueqTCWssEA=="],
510
-
511
-
"eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@5.1.0", "", { "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-mpJRtPgHN2tNAvZ35AMfqeB3Xqeo273QxrHJsbBEPWODRM4r0yB6jfoROqKEYrOn27UtRPpcpHc2UqyBSuUNTw=="],
512
-
513
-
"eslint-plugin-react-refresh": ["eslint-plugin-react-refresh@0.4.18", "", { "peerDependencies": { "eslint": ">=8.40" } }, "sha512-IRGEoFn3OKalm3hjfolEWGqoF/jPqeEYFp+C8B0WMzwGwBMvlRDQd06kghDhF0C61uJ6WfSDhEZE/sAQjduKgw=="],
514
-
515
-
"eslint-scope": ["eslint-scope@8.2.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A=="],
516
-
517
-
"eslint-visitor-keys": ["eslint-visitor-keys@4.2.0", "", {}, "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw=="],
518
-
519
-
"espree": ["espree@10.3.0", "", { "dependencies": { "acorn": "^8.14.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.0" } }, "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg=="],
520
-
521
-
"esquery": ["esquery@1.6.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg=="],
522
-
523
-
"esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
524
-
525
-
"estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
526
-
527
-
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
528
-
529
-
"eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="],
530
-
531
-
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
532
-
533
-
"fast-equals": ["fast-equals@5.2.2", "", {}, "sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw=="],
534
-
535
-
"fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
536
-
537
-
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
538
-
539
-
"fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
540
-
541
-
"fastq": ["fastq@1.18.0", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw=="],
542
-
543
-
"file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
544
-
545
-
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
546
-
547
-
"find-root": ["find-root@1.1.0", "", {}, "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng=="],
548
-
549
-
"find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
550
-
551
-
"flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="],
552
-
553
-
"flatted": ["flatted@3.3.2", "", {}, "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA=="],
554
-
555
-
"follow-redirects": ["follow-redirects@1.15.9", "", {}, "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ=="],
556
-
557
-
"form-data": ["form-data@4.0.1", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "mime-types": "^2.1.12" } }, "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw=="],
558
-
559
-
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
560
-
561
-
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
562
-
563
-
"gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
564
-
565
-
"get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="],
566
-
567
-
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
568
-
569
-
"globals": ["globals@15.14.0", "", {}, "sha512-OkToC372DtlQeje9/zHIo5CT8lRP/FUgEOKBEhU4e0abL7J7CD24fD9ohiLN5hagG/kWCYj4K5oaxxtj2Z0Dig=="],
570
-
571
-
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
572
-
573
-
"graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="],
574
-
575
-
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
576
-
577
-
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
578
-
579
-
"hoist-non-react-statics": ["hoist-non-react-statics@3.3.2", "", { "dependencies": { "react-is": "^16.7.0" } }, "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw=="],
580
-
581
-
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
582
-
583
-
"import-fresh": ["import-fresh@3.3.0", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw=="],
584
-
585
-
"imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
586
-
587
-
"internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="],
588
-
589
-
"is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="],
590
-
591
-
"is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="],
592
-
593
-
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
594
-
595
-
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
596
-
597
-
"is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
598
-
599
-
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
600
-
601
-
"jiti": ["jiti@2.4.2", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A=="],
602
-
603
-
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
604
-
605
-
"js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="],
606
-
607
-
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
608
-
609
-
"json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
610
-
611
-
"json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="],
612
-
613
-
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
614
-
615
-
"json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
616
-
617
-
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
618
-
619
-
"keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
620
-
621
-
"klona": ["klona@2.0.6", "", {}, "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA=="],
622
-
623
-
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
624
-
625
-
"lightningcss": ["lightningcss@1.29.1", "", { "dependencies": { "detect-libc": "^1.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.29.1", "lightningcss-darwin-x64": "1.29.1", "lightningcss-freebsd-x64": "1.29.1", "lightningcss-linux-arm-gnueabihf": "1.29.1", "lightningcss-linux-arm64-gnu": "1.29.1", "lightningcss-linux-arm64-musl": "1.29.1", "lightningcss-linux-x64-gnu": "1.29.1", "lightningcss-linux-x64-musl": "1.29.1", "lightningcss-win32-arm64-msvc": "1.29.1", "lightningcss-win32-x64-msvc": "1.29.1" } }, "sha512-FmGoeD4S05ewj+AkhTY+D+myDvXI6eL27FjHIjoyUkO/uw7WZD1fBVs0QxeYWa7E17CUHJaYX/RUGISCtcrG4Q=="],
626
-
627
-
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.29.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-HtR5XJ5A0lvCqYAoSv2QdZZyoHNttBpa5EP9aNuzBQeKGfbyH5+UipLWvVzpP4Uml5ej4BYs5I9Lco9u1fECqw=="],
628
-
629
-
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.29.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-k33G9IzKUpHy/J/3+9MCO4e+PzaFblsgBjSGlpAaFikeBFm8B/CkO3cKU9oI4g+fjS2KlkLM/Bza9K/aw8wsNA=="],
630
-
631
-
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.29.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-0SUW22fv/8kln2LnIdOCmSuXnxgxVC276W5KLTwoehiO0hxkacBxjHOL5EtHD8BAXg2BvuhsJPmVMasvby3LiQ=="],
632
-
633
-
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.29.1", "", { "os": "linux", "cpu": "arm" }, "sha512-sD32pFvlR0kDlqsOZmYqH/68SqUMPNj+0pucGxToXZi4XZgZmqeX/NkxNKCPsswAXU3UeYgDSpGhu05eAufjDg=="],
634
-
635
-
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.29.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-0+vClRIZ6mmJl/dxGuRsE197o1HDEeeRk6nzycSy2GofC2JsY4ifCRnvUWf/CUBQmlrvMzt6SMQNMSEu22csWQ=="],
636
-
637
-
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.29.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-UKMFrG4rL/uHNgelBsDwJcBqVpzNJbzsKkbI3Ja5fg00sgQnHw/VrzUTEc4jhZ+AN2BvQYz/tkHu4vt1kLuJyw=="],
638
-
639
-
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.29.1", "", { "os": "linux", "cpu": "x64" }, "sha512-u1S+xdODy/eEtjADqirA774y3jLcm8RPtYztwReEXoZKdzgsHYPl0s5V52Tst+GKzqjebkULT86XMSxejzfISw=="],
640
-
641
-
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.29.1", "", { "os": "linux", "cpu": "x64" }, "sha512-L0Tx0DtaNUTzXv0lbGCLB/c/qEADanHbu4QdcNOXLIe1i8i22rZRpbT3gpWYsCh9aSL9zFujY/WmEXIatWvXbw=="],
642
-
643
-
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.29.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-QoOVnkIEFfbW4xPi+dpdft/zAKmgLgsRHfJalEPYuJDOWf7cLQzYg0DEh8/sn737FaeMJxHZRc1oBreiwZCjog=="],
644
-
645
-
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.29.1", "", { "os": "win32", "cpu": "x64" }, "sha512-NygcbThNBe4JElP+olyTI/doBNGJvLs3bFCRPdvuCcxZCcCZ71B858IHpdm7L1btZex0FvCmM17FK98Y9MRy1Q=="],
646
-
647
-
"lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
648
-
649
-
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
650
-
651
-
"lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
652
-
653
-
"lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
654
-
655
-
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
656
-
657
-
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
658
-
659
-
"lucide-react": ["lucide-react@0.474.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-CmghgHkh0OJNmxGKWc0qfPJCYHASPMVSyGY8fj3xgk4v84ItqDg64JNKFZn5hC6E0vHi6gxnbCgwhyVB09wQtA=="],
660
-
661
-
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
662
-
663
-
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
664
-
665
-
"mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
666
-
667
-
"mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
668
-
669
-
"minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
670
-
671
-
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
672
-
673
-
"nanoid": ["nanoid@3.3.8", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w=="],
674
-
675
-
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
676
-
677
-
"node-releases": ["node-releases@2.0.19", "", {}, "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw=="],
678
-
679
-
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
680
-
681
-
"optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
682
-
683
-
"p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
684
-
685
-
"p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
686
-
687
-
"parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
688
-
689
-
"parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="],
690
-
691
-
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
692
-
693
-
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
694
-
695
-
"path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="],
696
-
697
-
"path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="],
698
-
699
-
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
700
-
701
-
"picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
702
-
703
-
"postcss": ["postcss@8.5.1", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ=="],
704
-
705
-
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
706
-
707
-
"prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="],
708
-
709
-
"proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="],
710
-
711
-
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
712
-
713
-
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
714
-
715
-
"react": ["react@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ=="],
716
-
717
-
"react-dom": ["react-dom@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" }, "peerDependencies": { "react": "^18.3.1" } }, "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw=="],
718
-
719
-
"react-hook-form": ["react-hook-form@7.54.2", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-eHpAUgUjWbZocoQYUHposymRb4ZP6d0uwUnooL2uOybA9/3tPUvoAKqEWK1WaSiTxxOfTpffNZP7QwlnM3/gEg=="],
720
-
721
-
"react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="],
722
-
723
-
"react-number-format": ["react-number-format@5.4.3", "", { "peerDependencies": { "react": "^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-VCY5hFg/soBighAoGcdE+GagkJq0230qN6jcS5sp8wQX1qy1fYN/RX7/BXkrs0oyzzwqR8/+eSUrqXbGeywdUQ=="],
724
-
725
-
"react-refresh": ["react-refresh@0.14.2", "", {}, "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA=="],
726
-
727
-
"react-remove-scroll": ["react-remove-scroll@2.6.3", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ=="],
728
-
729
-
"react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="],
730
-
731
-
"react-smooth": ["react-smooth@4.0.4", "", { "dependencies": { "fast-equals": "^5.0.1", "prop-types": "^15.8.1", "react-transition-group": "^4.4.5" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q=="],
732
-
733
-
"react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="],
734
-
735
-
"react-textarea-autosize": ["react-textarea-autosize@8.5.6", "", { "dependencies": { "@babel/runtime": "^7.20.13", "use-composed-ref": "^1.3.0", "use-latest": "^1.2.1" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-aT3ioKXMa8f6zHYGebhbdMD2L00tKeRX1zuVuDx9YQK/JLLRSaSxq3ugECEmUB9z2kvk6bFSIoRHLkkUv0RJiw=="],
736
-
737
-
"react-transition-group": ["react-transition-group@4.4.5", "", { "dependencies": { "@babel/runtime": "^7.5.5", "dom-helpers": "^5.0.1", "loose-envify": "^1.4.0", "prop-types": "^15.6.2" }, "peerDependencies": { "react": ">=16.6.0", "react-dom": ">=16.6.0" } }, "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g=="],
738
-
739
-
"recharts": ["recharts@2.15.0", "", { "dependencies": { "clsx": "^2.0.0", "eventemitter3": "^4.0.1", "lodash": "^4.17.21", "react-is": "^18.3.1", "react-smooth": "^4.0.0", "recharts-scale": "^0.4.4", "tiny-invariant": "^1.3.1", "victory-vendor": "^36.6.8" }, "peerDependencies": { "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-cIvMxDfpAmqAmVgc4yb7pgm/O1tmmkl/CjrvXuW+62/+7jj/iF9Ykm+hb/UJt42TREHMyd3gb+pkgoa2MxgDIw=="],
740
-
741
-
"recharts-scale": ["recharts-scale@0.4.5", "", { "dependencies": { "decimal.js-light": "^2.4.1" } }, "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w=="],
742
-
743
-
"regenerator-runtime": ["regenerator-runtime@0.14.1", "", {}, "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="],
744
-
745
-
"resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="],
746
-
747
-
"resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
748
-
749
-
"reusify": ["reusify@1.0.4", "", {}, "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw=="],
750
-
751
-
"rollup": ["rollup@4.32.0", "", { "dependencies": { "@types/estree": "1.0.6" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.32.0", "@rollup/rollup-android-arm64": "4.32.0", "@rollup/rollup-darwin-arm64": "4.32.0", "@rollup/rollup-darwin-x64": "4.32.0", "@rollup/rollup-freebsd-arm64": "4.32.0", "@rollup/rollup-freebsd-x64": "4.32.0", "@rollup/rollup-linux-arm-gnueabihf": "4.32.0", "@rollup/rollup-linux-arm-musleabihf": "4.32.0", "@rollup/rollup-linux-arm64-gnu": "4.32.0", "@rollup/rollup-linux-arm64-musl": "4.32.0", "@rollup/rollup-linux-loongarch64-gnu": "4.32.0", "@rollup/rollup-linux-powerpc64le-gnu": "4.32.0", "@rollup/rollup-linux-riscv64-gnu": "4.32.0", "@rollup/rollup-linux-s390x-gnu": "4.32.0", "@rollup/rollup-linux-x64-gnu": "4.32.0", "@rollup/rollup-linux-x64-musl": "4.32.0", "@rollup/rollup-win32-arm64-msvc": "4.32.0", "@rollup/rollup-win32-ia32-msvc": "4.32.0", "@rollup/rollup-win32-x64-msvc": "4.32.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-JmrhfQR31Q4AuNBjjAX4s+a/Pu/Q8Q9iwjWBsjRH1q52SPFE2NqRMK6fUZKKnvKO6id+h7JIRf0oYsph53eATg=="],
752
-
753
-
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
754
-
755
-
"scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="],
756
-
757
-
"semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
758
-
759
-
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
760
-
761
-
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
762
-
763
-
"source-map": ["source-map@0.5.7", "", {}, "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ=="],
764
-
765
-
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
766
-
767
-
"strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
768
-
769
-
"stylis": ["stylis@4.2.0", "", {}, "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw=="],
770
-
771
-
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
772
-
773
-
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
774
-
775
-
"tabbable": ["tabbable@6.2.0", "", {}, "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew=="],
776
-
777
-
"tailwind-merge": ["tailwind-merge@2.6.0", "", {}, "sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA=="],
778
-
779
-
"tailwindcss": ["tailwindcss@4.0.0", "", {}, "sha512-ULRPI3A+e39T7pSaf1xoi58AqqJxVCLg8F/uM5A3FadUbnyDTgltVnXJvdkTjwCOGA6NazqHVcwPJC5h2vRYVQ=="],
780
-
781
-
"tailwindcss-animate": ["tailwindcss-animate@1.0.7", "", { "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders" } }, "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA=="],
782
-
783
-
"tapable": ["tapable@2.2.1", "", {}, "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ=="],
784
-
785
-
"tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
786
-
787
-
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
788
-
789
-
"ts-api-utils": ["ts-api-utils@2.0.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-xCt/TOAc+EOHS1XPnijD3/yzpH6qg2xppZO1YDqGoVsNXfQfzHpOdNuXwrwOU8u4ITXJyDCTyt8w5g1sZv9ynQ=="],
790
-
791
-
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
792
-
793
-
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
794
-
795
-
"type-fest": ["type-fest@4.33.0", "", {}, "sha512-s6zVrxuyKbbAsSAD5ZPTB77q4YIdRctkTbJ2/Dqlinwz+8ooH2gd+YA7VA6Pa93KML9GockVvoxjZ2vHP+mu8g=="],
796
-
797
-
"typescript": ["typescript@5.6.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw=="],
798
-
799
-
"typescript-eslint": ["typescript-eslint@8.21.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.21.0", "@typescript-eslint/parser": "8.21.0", "@typescript-eslint/utils": "8.21.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-txEKYY4XMKwPXxNkN8+AxAdX6iIJAPiJbHE/FpQccs/sxw8Lf26kqwC3cn0xkHlW8kEbLhkhCsjWuMveaY9Rxw=="],
800
-
801
-
"undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
802
-
803
-
"update-browserslist-db": ["update-browserslist-db@1.1.2", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg=="],
804
-
805
-
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
806
-
807
-
"use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="],
808
-
809
-
"use-composed-ref": ["use-composed-ref@1.4.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-djviaxuOOh7wkj0paeO1Q/4wMZ8Zrnag5H6yBvzN7AKKe8beOaED9SF5/ByLqsku8NP4zQqsvM2u3ew/tJK8/w=="],
810
-
811
-
"use-isomorphic-layout-effect": ["use-isomorphic-layout-effect@1.2.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-q6ayo8DWoPZT0VdG4u3D3uxcgONP3Mevx2i2b0434cwWBoL+aelL1DzkXI6w3PhTZzUeR2kaVlZn70iCiseP6w=="],
812
-
813
-
"use-latest": ["use-latest@1.3.0", "", { "dependencies": { "use-isomorphic-layout-effect": "^1.1.1" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-mhg3xdm9NaM8q+gLT8KryJPnRFOz1/5XPBhmDEVZK1webPzDjrPk7f/mbpeLqTgB9msytYWANxgALOCJKnLvcQ=="],
814
-
815
-
"use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="],
816
-
817
-
"victory-vendor": ["victory-vendor@36.9.2", "", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ=="],
818
-
819
-
"vite": ["vite@6.0.11", "", { "dependencies": { "esbuild": "^0.24.2", "postcss": "^8.4.49", "rollup": "^4.23.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-4VL9mQPKoHy4+FE0NnRE/kbY51TOfaknxAjt3fJbGJxhIpBZiqVzlZDEesWWsuREXHwNdAoOFZ9MkPEVXczHwg=="],
820
-
821
-
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
822
-
823
-
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
824
-
825
-
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
826
-
827
-
"yaml": ["yaml@1.10.2", "", {}, "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="],
828
-
829
-
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
830
-
831
-
"zod": ["zod@3.24.1", "", {}, "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A=="],
832
-
833
-
"@babel/traverse/globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="],
834
-
835
-
"@emotion/babel-plugin/convert-source-map": ["convert-source-map@1.9.0", "", {}, "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="],
836
-
837
-
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
838
-
839
-
"@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="],
840
-
841
-
"@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="],
842
-
843
-
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
844
-
845
-
"@typescript-eslint/typescript-estree/semver": ["semver@7.6.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="],
846
-
847
-
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
848
-
849
-
"hoist-non-react-statics/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
850
-
851
-
"prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
852
-
853
-
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
854
-
}
855
-
}
+14
-11
frontend/index.html
+14
-11
frontend/index.html
···
1
1
<!doctype html>
2
2
<html lang="en">
3
-
<head>
4
-
<meta charset="UTF-8" />
5
-
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
-
<title>Vite + React + TS</title>
8
-
</head>
9
-
<body>
10
-
<div id="root"></div>
11
-
<script type="module" src="/src/main.tsx"></script>
12
-
</body>
13
-
</html>
3
+
4
+
<head>
5
+
<meta charset="UTF-8" />
6
+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
7
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
8
+
<title>SimpleLink</title>
9
+
</head>
10
+
11
+
<body>
12
+
<div id="root"></div>
13
+
<script type="module" src="/src/main.tsx"></script>
14
+
</body>
15
+
16
+
</html>
+1
frontend/package.json
+1
frontend/package.json
+52
-51
frontend/src/App.tsx
+52
-51
frontend/src/App.tsx
···
2
2
import { LinkForm } from './components/LinkForm'
3
3
import { LinkList } from './components/LinkList'
4
4
import { AuthForms } from './components/AuthForms'
5
+
import { Footer } from './components/Footer'
5
6
import { AuthProvider, useAuth } from './context/AuthContext'
6
7
import { Button } from "@/components/ui/button"
7
8
import { Toaster } from './components/ui/toaster'
···
9
10
import { useState } from 'react'
10
11
11
12
function AppContent() {
12
-
const { user, logout } = useAuth()
13
-
const [refreshCounter, setRefreshCounter] = useState(0)
14
-
15
-
const handleLinkCreated = () => {
16
-
setRefreshCounter(prev => prev + 1)
17
-
}
13
+
const { user, logout } = useAuth()
14
+
const [refreshCounter, setRefreshCounter] = useState(0)
15
+
16
+
const handleLinkCreated = () => {
17
+
setRefreshCounter(prev => prev + 1)
18
+
}
18
19
19
-
return (
20
-
<div className="min-h-screen bg-background flex flex-col">
21
-
<header className="border-b">
22
-
<div className="container max-w-6xl mx-auto flex h-16 items-center justify-between px-4">
23
-
<h1 className="text-2xl font-bold">SimpleLink</h1>
24
-
<div className="flex items-center gap-4">
25
-
{user ? (
26
-
<>
27
-
<span className="text-sm text-muted-foreground">Welcome, {user.email}</span>
28
-
<Button variant="outline" size="sm" onClick={logout}>
29
-
Logout
30
-
</Button>
31
-
</>
32
-
) : (
33
-
<span className="text-sm text-muted-foreground">A link shortening and tracking service</span>
34
-
)}
35
-
<ModeToggle />
36
-
</div>
37
-
</div>
38
-
</header>
39
-
40
-
<main className="flex-1 flex flex-col">
41
-
<div className="container max-w-6xl mx-auto px-4 py-8 flex-1 flex flex-col">
42
-
<div className="space-y-8 flex-1 flex flex-col justify-center">
43
-
{user ? (
44
-
<>
45
-
<LinkForm onSuccess={handleLinkCreated} />
46
-
<LinkList refresh={refreshCounter} />
47
-
</>
48
-
) : (
49
-
<AuthForms />
50
-
)}
51
-
</div>
52
-
</div>
53
-
</main>
54
-
</div>
55
-
)
20
+
return (
21
+
<div className="min-h-screen bg-background flex flex-col">
22
+
<header className="border-b">
23
+
<div className="container max-w-6xl mx-auto flex h-16 items-center justify-between px-4">
24
+
<h1 className="text-2xl font-bold">SimpleLink</h1>
25
+
<div className="flex items-center space-x-2 sm:space-x-4">
26
+
{user ? (
27
+
<>
28
+
<span className="text-sm text-muted-foreground hidden sm:inline">Welcome, {user.email}</span>
29
+
<Button variant="outline" size="sm" onClick={logout}>
30
+
Logout
31
+
</Button>
32
+
</>
33
+
) : (
34
+
<span className="text-sm text-muted-foreground mr-2">A link shortening service</span>
35
+
)}
36
+
<ModeToggle />
37
+
</div>
38
+
</div>
39
+
</header>
40
+
<main className="flex-1 flex flex-col">
41
+
<div className="container max-w-6xl mx-auto px-4 py-8 flex-1 flex flex-col">
42
+
<div className="space-y-8 flex-1 flex flex-col justify-center">
43
+
{user ? (
44
+
<>
45
+
<LinkForm onSuccess={handleLinkCreated} />
46
+
<LinkList refresh={refreshCounter} />
47
+
</>
48
+
) : (
49
+
<AuthForms />
50
+
)}
51
+
</div>
52
+
</div>
53
+
</main>
54
+
<Footer />
55
+
</div>
56
+
)
56
57
}
57
58
58
59
function App() {
59
-
return (
60
-
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
61
-
<AuthProvider>
62
-
<AppContent />
63
-
<Toaster />
64
-
</AuthProvider>
65
-
</ThemeProvider>
66
-
)
60
+
return (
61
+
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
62
+
<AuthProvider>
63
+
<AppContent />
64
+
<Toaster />
65
+
</AuthProvider>
66
+
</ThemeProvider>
67
+
)
67
68
}
68
69
69
70
export default App
+42
-5
frontend/src/api/client.ts
+42
-5
frontend/src/api/client.ts
···
15
15
return config;
16
16
});
17
17
18
+
api.interceptors.response.use(
19
+
(response) => response,
20
+
(error) => {
21
+
if (error.response?.status === 401) {
22
+
localStorage.removeItem('token');
23
+
localStorage.removeItem('user');
24
+
25
+
window.dispatchEvent(new Event('unauthorized'));
26
+
}
27
+
return Promise.reject(error);
28
+
}
29
+
);
30
+
31
+
18
32
// Auth endpoints
19
33
export const login = async (email: string, password: string) => {
20
34
const response = await api.post<AuthResponse>('/auth/login', {
···
24
38
return response.data;
25
39
};
26
40
27
-
export const register = async (email: string, password: string) => {
41
+
export const register = async (email: string, password: string, adminToken: string) => {
28
42
const response = await api.post<AuthResponse>('/auth/register', {
29
43
email,
30
44
password,
45
+
admin_token: adminToken,
31
46
});
32
47
return response.data;
33
48
};
···
43
58
return response.data;
44
59
};
45
60
61
+
export const editLink = async (id: number, data: Partial<CreateLinkRequest>) => {
62
+
const response = await api.patch<Link>(`/links/${id}`, data);
63
+
return response.data;
64
+
};
65
+
66
+
46
67
export const deleteLink = async (id: number) => {
47
68
await api.delete(`/links/${id}`);
48
69
};
49
70
50
71
export const getLinkClickStats = async (id: number) => {
51
-
const response = await api.get<ClickStats[]>(`/links/${id}/clicks`);
52
-
return response.data;
72
+
try {
73
+
const response = await api.get<ClickStats[]>(`/links/${id}/clicks`);
74
+
return response.data;
75
+
} catch (error) {
76
+
console.error('Error fetching click stats:', error);
77
+
throw error;
78
+
}
53
79
};
54
80
55
81
export const getLinkSourceStats = async (id: number) => {
56
-
const response = await api.get<SourceStats[]>(`/links/${id}/sources`);
57
-
return response.data;
82
+
try {
83
+
const response = await api.get<SourceStats[]>(`/links/${id}/sources`);
84
+
return response.data;
85
+
} catch (error) {
86
+
console.error('Error fetching source stats:', error);
87
+
throw error;
88
+
}
89
+
};
90
+
91
+
92
+
export const checkFirstUser = async () => {
93
+
const response = await api.get<{ isFirstUser: boolean }>('/auth/check-first-user');
94
+
return response.data.isFirstUser;
58
95
};
59
96
60
97
export { api };
+87
-49
frontend/src/components/AuthForms.tsx
+87
-49
frontend/src/components/AuthForms.tsx
···
1
-
import { useState } from 'react'
1
+
import { useState, useEffect } from 'react'
2
2
import { useForm } from 'react-hook-form'
3
3
import { z } from 'zod'
4
4
import { zodResolver } from '@hookform/resolvers/zod'
···
6
6
import { Button } from '@/components/ui/button'
7
7
import { Input } from '@/components/ui/input'
8
8
import { Card } from '@/components/ui/card'
9
-
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
10
9
import {
11
10
Form,
12
11
FormControl,
···
16
15
FormMessage,
17
16
} from '@/components/ui/form'
18
17
import { useToast } from '@/hooks/use-toast'
18
+
import { checkFirstUser } from '../api/client'
19
19
20
20
const formSchema = z.object({
21
21
email: z.string().email('Invalid email address'),
22
22
password: z.string().min(6, 'Password must be at least 6 characters long'),
23
+
adminToken: z.string().optional(),
23
24
})
24
25
25
26
type FormValues = z.infer<typeof formSchema>
26
27
27
28
export function AuthForms() {
28
-
const [activeTab, setActiveTab] = useState<'login' | 'register'>('login')
29
+
const [isFirstUser, setIsFirstUser] = useState<boolean | null>(null)
29
30
const { login, register } = useAuth()
30
31
const { toast } = useToast()
31
32
···
34
35
defaultValues: {
35
36
email: '',
36
37
password: '',
38
+
adminToken: '',
37
39
},
38
40
})
39
41
42
+
useEffect(() => {
43
+
const init = async () => {
44
+
try {
45
+
const isFirst = await checkFirstUser()
46
+
setIsFirstUser(isFirst)
47
+
} catch (err) {
48
+
console.error('Error checking first user:', err)
49
+
setIsFirstUser(false)
50
+
}
51
+
}
52
+
53
+
init()
54
+
}, [])
55
+
40
56
const onSubmit = async (values: FormValues) => {
41
57
try {
42
-
if (activeTab === 'login') {
58
+
if (isFirstUser) {
59
+
await register(values.email, values.password, values.adminToken || '')
60
+
} else {
43
61
await login(values.email, values.password)
44
-
} else {
45
-
await register(values.email, values.password)
46
62
}
47
63
form.reset()
48
64
} catch (err: any) {
49
65
toast({
50
66
variant: 'destructive',
51
67
title: 'Error',
52
-
description: err.response?.data?.error || 'An error occurred',
68
+
description: err.response?.data || 'An error occurred',
53
69
})
54
70
}
71
+
}
72
+
73
+
if (isFirstUser === null) {
74
+
return <div>Loading...</div>
55
75
}
56
76
57
77
return (
58
78
<Card className="w-full max-w-md mx-auto p-6">
59
-
<Tabs value={activeTab} onValueChange={(value: string) => setActiveTab(value as 'login' | 'register')}>
60
-
<TabsList className="grid w-full grid-cols-2">
61
-
<TabsTrigger value="login">Login</TabsTrigger>
62
-
<TabsTrigger value="register">Register</TabsTrigger>
63
-
</TabsList>
79
+
<div className="mb-6 text-center">
80
+
<h2 className="text-2xl font-bold">
81
+
{isFirstUser ? 'Create Admin Account' : 'Login'}
82
+
</h2>
83
+
<p className="text-sm text-muted-foreground mt-1">
84
+
{isFirstUser
85
+
? 'Set up your admin account to get started'
86
+
: 'Welcome back! Please login to your account'}
87
+
</p>
88
+
</div>
64
89
65
-
<TabsContent value={activeTab}>
66
-
<Form {...form}>
67
-
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
68
-
<FormField
69
-
control={form.control}
70
-
name="email"
71
-
render={({ field }) => (
72
-
<FormItem>
73
-
<FormLabel>Email</FormLabel>
74
-
<FormControl>
75
-
<Input type="email" {...field} />
76
-
</FormControl>
77
-
<FormMessage />
78
-
</FormItem>
79
-
)}
80
-
/>
90
+
<Form {...form}>
91
+
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
92
+
<FormField
93
+
control={form.control}
94
+
name="email"
95
+
render={({ field }) => (
96
+
<FormItem>
97
+
<FormLabel>Email</FormLabel>
98
+
<FormControl>
99
+
<Input type="email" {...field} />
100
+
</FormControl>
101
+
<FormMessage />
102
+
</FormItem>
103
+
)}
104
+
/>
81
105
82
-
<FormField
83
-
control={form.control}
84
-
name="password"
85
-
render={({ field }) => (
86
-
<FormItem>
87
-
<FormLabel>Password</FormLabel>
88
-
<FormControl>
89
-
<Input type="password" {...field} />
90
-
</FormControl>
91
-
<FormMessage />
92
-
</FormItem>
93
-
)}
94
-
/>
106
+
<FormField
107
+
control={form.control}
108
+
name="password"
109
+
render={({ field }) => (
110
+
<FormItem>
111
+
<FormLabel>Password</FormLabel>
112
+
<FormControl>
113
+
<Input type="password" {...field} />
114
+
</FormControl>
115
+
<FormMessage />
116
+
</FormItem>
117
+
)}
118
+
/>
95
119
96
-
<Button type="submit" className="w-full">
97
-
{activeTab === 'login' ? 'Sign in' : 'Create account'}
98
-
</Button>
99
-
</form>
100
-
</Form>
101
-
</TabsContent>
102
-
</Tabs>
120
+
{isFirstUser && (
121
+
<FormField
122
+
control={form.control}
123
+
name="adminToken"
124
+
render={({ field }) => (
125
+
<FormItem>
126
+
<FormLabel>Admin Setup Token</FormLabel>
127
+
<FormControl>
128
+
<Input type="text" {...field} />
129
+
</FormControl>
130
+
<FormMessage />
131
+
</FormItem>
132
+
)}
133
+
/>
134
+
)}
135
+
136
+
<Button type="submit" className="w-full">
137
+
{isFirstUser ? 'Create Account' : 'Sign in'}
138
+
</Button>
139
+
</form>
140
+
</Form>
103
141
</Card>
104
142
)
105
-
}
143
+
}
+139
frontend/src/components/EditModal.tsx
+139
frontend/src/components/EditModal.tsx
···
1
+
// src/components/EditModal.tsx
2
+
import { useState } from 'react';
3
+
import { useForm } from 'react-hook-form';
4
+
import { zodResolver } from '@hookform/resolvers/zod';
5
+
import * as z from 'zod';
6
+
import { Link } from '../types/api';
7
+
import { editLink } from '../api/client';
8
+
import { useToast } from '@/hooks/use-toast';
9
+
import {
10
+
Dialog,
11
+
DialogContent,
12
+
DialogHeader,
13
+
DialogTitle,
14
+
DialogFooter,
15
+
} from '@/components/ui/dialog';
16
+
import { Button } from '@/components/ui/button';
17
+
import { Input } from '@/components/ui/input';
18
+
import {
19
+
Form,
20
+
FormControl,
21
+
FormField,
22
+
FormItem,
23
+
FormLabel,
24
+
FormMessage,
25
+
} from '@/components/ui/form';
26
+
27
+
const formSchema = z.object({
28
+
url: z
29
+
.string()
30
+
.min(1, 'URL is required')
31
+
.url('Must be a valid URL')
32
+
.refine((val) => val.startsWith('http://') || val.startsWith('https://'), {
33
+
message: 'URL must start with http:// or https://',
34
+
}),
35
+
custom_code: z
36
+
.string()
37
+
.regex(/^[a-zA-Z0-9_-]{1,32}$/, {
38
+
message:
39
+
'Custom code must be 1-32 characters and contain only letters, numbers, underscores, and hyphens',
40
+
})
41
+
.optional(),
42
+
});
43
+
44
+
interface EditModalProps {
45
+
isOpen: boolean;
46
+
onClose: () => void;
47
+
link: Link;
48
+
onSuccess: () => void;
49
+
}
50
+
51
+
export function EditModal({ isOpen, onClose, link, onSuccess }: EditModalProps) {
52
+
const [loading, setLoading] = useState(false);
53
+
const { toast } = useToast();
54
+
55
+
const form = useForm<z.infer<typeof formSchema>>({
56
+
resolver: zodResolver(formSchema),
57
+
defaultValues: {
58
+
url: link.original_url,
59
+
custom_code: link.short_code,
60
+
},
61
+
});
62
+
63
+
const onSubmit = async (values: z.infer<typeof formSchema>) => {
64
+
try {
65
+
setLoading(true);
66
+
await editLink(link.id, values);
67
+
toast({
68
+
description: 'Link updated successfully',
69
+
});
70
+
onSuccess();
71
+
onClose();
72
+
} catch (err: unknown) {
73
+
const error = err as { response?: { data?: { error?: string } } };
74
+
toast({
75
+
variant: 'destructive',
76
+
title: 'Error',
77
+
description: error.response?.data?.error || 'Failed to update link',
78
+
});
79
+
} finally {
80
+
setLoading(false);
81
+
}
82
+
};
83
+
84
+
return (
85
+
<Dialog open={isOpen} onOpenChange={onClose}>
86
+
<DialogContent>
87
+
<DialogHeader>
88
+
<DialogTitle>Edit Link</DialogTitle>
89
+
</DialogHeader>
90
+
91
+
<Form {...form}>
92
+
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
93
+
<FormField
94
+
control={form.control}
95
+
name="url"
96
+
render={({ field }) => (
97
+
<FormItem>
98
+
<FormLabel>Destination URL</FormLabel>
99
+
<FormControl>
100
+
<Input placeholder="https://example.com" {...field} />
101
+
</FormControl>
102
+
<FormMessage />
103
+
</FormItem>
104
+
)}
105
+
/>
106
+
107
+
<FormField
108
+
control={form.control}
109
+
name="custom_code"
110
+
render={({ field }) => (
111
+
<FormItem>
112
+
<FormLabel>Short Code</FormLabel>
113
+
<FormControl>
114
+
<Input placeholder="custom-code" {...field} />
115
+
</FormControl>
116
+
<FormMessage />
117
+
</FormItem>
118
+
)}
119
+
/>
120
+
121
+
<DialogFooter>
122
+
<Button
123
+
type="button"
124
+
variant="outline"
125
+
onClick={onClose}
126
+
disabled={loading}
127
+
>
128
+
Cancel
129
+
</Button>
130
+
<Button type="submit" disabled={loading}>
131
+
{loading ? 'Saving...' : 'Save Changes'}
132
+
</Button>
133
+
</DialogFooter>
134
+
</form>
135
+
</Form>
136
+
</DialogContent>
137
+
</Dialog>
138
+
);
139
+
}
+47
-14
frontend/src/components/LinkList.tsx
+47
-14
frontend/src/components/LinkList.tsx
···
1
-
import { useEffect, useState } from 'react'
1
+
import { useCallback, useEffect, useState } from 'react'
2
2
import { Link } from '../types/api'
3
3
import { getAllLinks, deleteLink } from '../api/client'
4
4
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
···
12
12
} from "@/components/ui/table"
13
13
import { Button } from "@/components/ui/button"
14
14
import { useToast } from "@/hooks/use-toast"
15
-
import { Copy, Trash2, BarChart2 } from "lucide-react"
15
+
import { Copy, Trash2, BarChart2, Pencil } from "lucide-react"
16
16
import {
17
17
Dialog,
18
18
DialogContent,
···
23
23
} from "@/components/ui/dialog"
24
24
25
25
import { StatisticsModal } from "./StatisticsModal"
26
+
import { EditModal } from './EditModal'
26
27
27
28
interface LinkListProps {
28
29
refresh?: number;
···
39
40
isOpen: false,
40
41
linkId: null,
41
42
});
43
+
const [editModal, setEditModal] = useState<{ isOpen: boolean; link: Link | null }>({
44
+
isOpen: false,
45
+
link: null,
46
+
});
42
47
const { toast } = useToast()
43
48
44
-
const fetchLinks = async () => {
49
+
const fetchLinks = useCallback(async () => {
45
50
try {
46
51
setLoading(true)
47
52
const data = await getAllLinks()
48
53
setLinks(data)
49
-
} catch (err) {
54
+
} catch (err: unknown) {
55
+
const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
50
56
toast({
51
57
title: "Error",
52
-
description: "Failed to load links",
58
+
description: `Failed to load links: ${errorMessage}`,
53
59
variant: "destructive",
54
60
})
55
61
} finally {
56
62
setLoading(false)
57
63
}
58
-
}
64
+
}, [toast, setLinks, setLoading])
59
65
60
66
useEffect(() => {
61
67
fetchLinks()
62
-
}, [refresh]) // Re-fetch when refresh counter changes
68
+
}, [fetchLinks, refresh]) // Re-fetch when refresh counter changes
63
69
64
70
const handleDelete = async () => {
65
71
if (!deleteModal.linkId) return
···
71
77
toast({
72
78
description: "Link deleted successfully",
73
79
})
74
-
} catch (err) {
80
+
} catch (err: unknown) {
81
+
const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
75
82
toast({
76
83
title: "Error",
77
-
description: "Failed to delete link",
84
+
description: `Failed to delete link: ${errorMessage}`,
78
85
variant: "destructive",
79
86
})
80
87
}
81
88
}
82
89
83
90
const handleCopy = (shortCode: string) => {
84
-
navigator.clipboard.writeText(`http://localhost:8080/${shortCode}`)
91
+
// Use import.meta.env.VITE_BASE_URL or fall back to window.location.origin
92
+
const baseUrl = window.location.origin
93
+
navigator.clipboard.writeText(`${baseUrl}/${shortCode}`)
85
94
toast({
86
-
description: "Link copied to clipboard",
95
+
description: (
96
+
<>
97
+
Link copied to clipboard
98
+
<br />
99
+
You can add ?source=TextHere to the end of the link to track the source of clicks
100
+
</>
101
+
),
87
102
})
88
103
}
89
104
···
119
134
</CardHeader>
120
135
<CardContent>
121
136
<div className="rounded-md border">
137
+
122
138
<Table>
123
139
<TableHeader>
124
140
<TableRow>
···
126
142
<TableHead className="hidden md:table-cell">Original URL</TableHead>
127
143
<TableHead>Clicks</TableHead>
128
144
<TableHead className="hidden md:table-cell">Created</TableHead>
129
-
<TableHead>Actions</TableHead>
145
+
<TableHead className="w-[1%] whitespace-nowrap pr-4">Actions</TableHead>
130
146
</TableRow>
131
147
</TableHeader>
132
148
<TableBody>
···
140
156
<TableCell className="hidden md:table-cell">
141
157
{new Date(link.created_at).toLocaleDateString()}
142
158
</TableCell>
143
-
<TableCell>
144
-
<div className="flex gap-2">
159
+
<TableCell className="p-2 pr-4">
160
+
<div className="flex items-center gap-1">
145
161
<Button
146
162
variant="ghost"
147
163
size="icon"
···
163
179
<Button
164
180
variant="ghost"
165
181
size="icon"
182
+
className="h-8 w-8"
183
+
onClick={() => setEditModal({ isOpen: true, link })}
184
+
>
185
+
<Pencil className="h-4 w-4" />
186
+
<span className="sr-only">Edit Link</span>
187
+
</Button>
188
+
<Button
189
+
variant="ghost"
190
+
size="icon"
166
191
className="h-8 w-8 text-destructive"
167
192
onClick={() => setDeleteModal({ isOpen: true, linkId: link.id })}
168
193
>
···
183
208
onClose={() => setStatsModal({ isOpen: false, linkId: null })}
184
209
linkId={statsModal.linkId!}
185
210
/>
211
+
{editModal.link && (
212
+
<EditModal
213
+
isOpen={editModal.isOpen}
214
+
onClose={() => setEditModal({ isOpen: false, link: null })}
215
+
link={editModal.link}
216
+
onSuccess={fetchLinks}
217
+
/>
218
+
)}
186
219
</>
187
220
)
188
221
}
+39
frontend/src/components/PrivacyModal.tsx
+39
frontend/src/components/PrivacyModal.tsx
···
1
+
import {
2
+
Dialog,
3
+
DialogContent,
4
+
DialogDescription,
5
+
DialogFooter,
6
+
DialogHeader,
7
+
DialogTitle,
8
+
} from "@/components/ui/dialog"
9
+
10
+
import { Button } from "@/components/ui/button"
11
+
12
+
interface PrivacyModalProps {
13
+
isOpen: boolean;
14
+
onClose: () => void;
15
+
}
16
+
17
+
export function PrivacyModal({ isOpen, onClose }: PrivacyModalProps) {
18
+
return (
19
+
<Dialog open={isOpen}>
20
+
<DialogContent className="max-w-md">
21
+
<DialogHeader>
22
+
<DialogTitle>Privacy Policy</DialogTitle>
23
+
<DialogDescription>
24
+
Simplelink's data collection and usage policies
25
+
</DialogDescription>
26
+
</DialogHeader>
27
+
<div className="text-sm text-muted-foreground">
28
+
<p>Simplelink shortens URLs and tracks only two pieces of information: the time each link is clicked and the source of the link through a ?source= query tag. We do not collect any personal information such as IP addresses or any other data.</p>
29
+
</div>
30
+
<DialogFooter>
31
+
<Button variant="outline" onClick={onClose}>
32
+
Close
33
+
</Button>
34
+
</DialogFooter>
35
+
</DialogContent>
36
+
</Dialog>
37
+
)
38
+
}
39
+
+160
-98
frontend/src/components/StatisticsModal.tsx
+160
-98
frontend/src/components/StatisticsModal.tsx
···
1
1
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
2
2
import {
3
-
LineChart,
4
-
Line,
5
-
XAxis,
6
-
YAxis,
7
-
CartesianGrid,
8
-
Tooltip,
9
-
ResponsiveContainer,
3
+
LineChart,
4
+
Line,
5
+
XAxis,
6
+
YAxis,
7
+
CartesianGrid,
8
+
Tooltip,
9
+
ResponsiveContainer,
10
10
} from "recharts";
11
11
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
12
-
import { useState, useEffect } from "react";
12
+
import { toast } from "@/hooks/use-toast";
13
+
import { useState, useEffect, useMemo } from "react";
13
14
14
-
import { getLinkClickStats, getLinkSourceStats } from '../api/client';
15
-
import { ClickStats, SourceStats } from '../types/api';
15
+
import { getLinkClickStats, getLinkSourceStats } from "../api/client";
16
+
import { ClickStats, SourceStats } from "../types/api";
16
17
17
18
interface StatisticsModalProps {
18
-
isOpen: boolean;
19
-
onClose: () => void;
20
-
linkId: number;
19
+
isOpen: boolean;
20
+
onClose: () => void;
21
+
linkId: number;
21
22
}
22
23
24
+
interface EnhancedClickStats extends ClickStats {
25
+
sources?: { source: string; count: number }[];
26
+
}
27
+
28
+
const CustomTooltip = ({
29
+
active,
30
+
payload,
31
+
label,
32
+
}: {
33
+
active?: boolean;
34
+
payload?: { value: number; payload: EnhancedClickStats }[];
35
+
label?: string;
36
+
}) => {
37
+
if (active && payload && payload.length > 0) {
38
+
const data = payload[0].payload;
39
+
return (
40
+
<div className="bg-background text-foreground p-4 rounded-lg shadow-lg border">
41
+
<p className="font-medium">{label}</p>
42
+
<p className="text-sm">Clicks: {data.clicks}</p>
43
+
{data.sources && data.sources.length > 0 && (
44
+
<div className="mt-2">
45
+
<p className="font-medium text-sm">Sources:</p>
46
+
<ul className="text-sm">
47
+
{data.sources.map((source: { source: string; count: number }) => (
48
+
<li key={source.source}>
49
+
{source.source}: {source.count}
50
+
</li>
51
+
))}
52
+
</ul>
53
+
</div>
54
+
)}
55
+
</div>
56
+
);
57
+
}
58
+
return null;
59
+
};
60
+
23
61
export function StatisticsModal({ isOpen, onClose, linkId }: StatisticsModalProps) {
24
-
const [clicksOverTime, setClicksOverTime] = useState<ClickStats[]>([]);
25
-
const [sourcesData, setSourcesData] = useState<SourceStats[]>([]);
26
-
const [loading, setLoading] = useState(true);
62
+
const [clicksOverTime, setClicksOverTime] = useState<EnhancedClickStats[]>([]);
63
+
const [sourcesData, setSourcesData] = useState<SourceStats[]>([]);
64
+
const [loading, setLoading] = useState(true);
27
65
28
-
useEffect(() => {
29
-
if (isOpen && linkId) {
30
-
const fetchData = async () => {
31
-
try {
32
-
setLoading(true);
33
-
const [clicksData, sourcesData] = await Promise.all([
34
-
getLinkClickStats(linkId),
35
-
getLinkSourceStats(linkId),
36
-
]);
37
-
setClicksOverTime(clicksData);
38
-
setSourcesData(sourcesData);
39
-
} catch (error) {
40
-
console.error("Failed to fetch statistics:", error);
41
-
} finally {
42
-
setLoading(false);
43
-
}
44
-
};
66
+
useEffect(() => {
67
+
if (isOpen && linkId) {
68
+
const fetchData = async () => {
69
+
try {
70
+
setLoading(true);
71
+
const [clicksData, sourcesData] = await Promise.all([
72
+
getLinkClickStats(linkId),
73
+
getLinkSourceStats(linkId),
74
+
]);
45
75
46
-
fetchData();
47
-
}
48
-
}, [isOpen, linkId]);
76
+
// Enhance clicks data with source information
77
+
const enhancedClicksData = clicksData.map((clickData) => ({
78
+
...clickData,
79
+
sources: sourcesData.filter((source) => source.date === clickData.date),
80
+
}));
49
81
50
-
return (
51
-
<Dialog open={isOpen} onOpenChange={onClose}>
52
-
<DialogContent className="max-w-3xl">
53
-
<DialogHeader>
54
-
<DialogTitle>Link Statistics</DialogTitle>
55
-
</DialogHeader>
82
+
setClicksOverTime(enhancedClicksData);
83
+
setSourcesData(sourcesData);
84
+
} catch (error: unknown) {
85
+
console.error("Failed to fetch statistics:", error);
86
+
toast({
87
+
variant: "destructive",
88
+
title: "Error",
89
+
description: error instanceof Error ? error.message : "Failed to load statistics",
90
+
});
91
+
} finally {
92
+
setLoading(false);
93
+
}
94
+
};
56
95
57
-
{loading ? (
58
-
<div className="flex items-center justify-center h-64">Loading...</div>
59
-
) : (
60
-
<div className="grid gap-4">
61
-
<Card>
62
-
<CardHeader>
63
-
<CardTitle>Clicks Over Time</CardTitle>
64
-
</CardHeader>
65
-
<CardContent>
66
-
<div className="h-[300px]">
67
-
<ResponsiveContainer width="100%" height="100%">
68
-
<LineChart data={clicksOverTime}>
69
-
<CartesianGrid strokeDasharray="3 3" />
70
-
<XAxis dataKey="date" />
71
-
<YAxis />
72
-
<Tooltip />
73
-
<Line
74
-
type="monotone"
75
-
dataKey="clicks"
76
-
stroke="#8884d8"
77
-
strokeWidth={2}
78
-
/>
79
-
</LineChart>
80
-
</ResponsiveContainer>
81
-
</div>
82
-
</CardContent>
83
-
</Card>
96
+
fetchData();
97
+
}
98
+
}, [isOpen, linkId]);
84
99
85
-
<Card>
86
-
<CardHeader>
87
-
<CardTitle>Top Sources</CardTitle>
88
-
</CardHeader>
89
-
<CardContent>
90
-
<ul className="space-y-2">
91
-
{sourcesData.map((source, index) => (
92
-
<li
93
-
key={source.source}
94
-
className="flex items-center justify-between py-2 border-b last:border-0"
95
-
>
96
-
<span className="text-sm">
97
-
<span className="font-medium text-muted-foreground mr-2">
98
-
{index + 1}.
99
-
</span>
100
-
{source.source}
101
-
</span>
102
-
<span className="text-sm font-medium">
103
-
{source.count} clicks
104
-
</span>
105
-
</li>
106
-
))}
107
-
</ul>
108
-
</CardContent>
109
-
</Card>
110
-
</div>
111
-
)}
112
-
</DialogContent>
113
-
</Dialog>
100
+
const aggregatedSources = useMemo(() => {
101
+
const sourceMap = sourcesData.reduce<Record<string, number>>(
102
+
(acc, { source, count }) => ({
103
+
...acc,
104
+
[source]: (acc[source] || 0) + count
105
+
}),
106
+
{}
114
107
);
108
+
109
+
return Object.entries(sourceMap)
110
+
.map(([source, count]) => ({ source, count }))
111
+
.sort((a, b) => b.count - a.count);
112
+
}, [sourcesData]);
113
+
114
+
return (
115
+
<Dialog open={isOpen} onOpenChange={onClose}>
116
+
<DialogContent className="max-w-3xl">
117
+
<DialogHeader>
118
+
<DialogTitle>Link Statistics</DialogTitle>
119
+
</DialogHeader>
120
+
121
+
{loading ? (
122
+
<div className="flex items-center justify-center h-64">Loading...</div>
123
+
) : (
124
+
<div className="grid gap-4">
125
+
<Card>
126
+
<CardHeader>
127
+
<CardTitle>Clicks Over Time</CardTitle>
128
+
</CardHeader>
129
+
<CardContent>
130
+
<div className="h-[300px]">
131
+
<ResponsiveContainer width="100%" height="100%">
132
+
<LineChart data={clicksOverTime}>
133
+
<CartesianGrid strokeDasharray="3 3" />
134
+
<XAxis dataKey="date" />
135
+
<YAxis />
136
+
<Tooltip content={<CustomTooltip />} />
137
+
<Line
138
+
type="monotone"
139
+
dataKey="clicks"
140
+
stroke="#8884d8"
141
+
strokeWidth={2}
142
+
/>
143
+
</LineChart>
144
+
</ResponsiveContainer>
145
+
</div>
146
+
</CardContent>
147
+
</Card>
148
+
149
+
<Card>
150
+
<CardHeader>
151
+
<CardTitle>Top Sources</CardTitle>
152
+
</CardHeader>
153
+
<CardContent>
154
+
<ul className="space-y-2">
155
+
{aggregatedSources.map((source, index) => (
156
+
<li
157
+
key={source.source}
158
+
className="flex items-center justify-between py-2 border-b last:border-0"
159
+
>
160
+
<span className="text-sm">
161
+
<span className="font-medium text-muted-foreground mr-2">
162
+
{index + 1}.
163
+
</span>
164
+
{source.source}
165
+
</span>
166
+
<span className="text-sm font-medium">{source.count} clicks</span>
167
+
</li>
168
+
))}
169
+
</ul>
170
+
</CardContent>
171
+
</Card>
172
+
</div>
173
+
)}
174
+
</DialogContent>
175
+
</Dialog>
176
+
);
115
177
}
+13
-3
frontend/src/context/AuthContext.tsx
+13
-3
frontend/src/context/AuthContext.tsx
···
5
5
interface AuthContextType {
6
6
user: User | null;
7
7
login: (email: string, password: string) => Promise<void>;
8
-
register: (email: string, password: string) => Promise<void>;
8
+
register: (email: string, password: string, adminToken: string) => Promise<void>;
9
9
logout: () => void;
10
10
isLoading: boolean;
11
11
}
···
23
23
setUser(userData);
24
24
}
25
25
setIsLoading(false);
26
+
27
+
const handleUnauthorized = () => {
28
+
setUser(null);
29
+
};
30
+
31
+
window.addEventListener('unauthorized', handleUnauthorized);
32
+
33
+
return () => {
34
+
window.removeEventListener('unauthorized', handleUnauthorized);
35
+
};
26
36
}, []);
27
37
28
38
const login = async (email: string, password: string) => {
···
33
43
setUser(user);
34
44
};
35
45
36
-
const register = async (email: string, password: string) => {
37
-
const response = await api.register(email, password);
46
+
const register = async (email: string, password: string, adminToken: string) => {
47
+
const response = await api.register(email, password, adminToken);
38
48
const { token, user } = response;
39
49
localStorage.setItem('token', token);
40
50
localStorage.setItem('user', JSON.stringify(user));
+7
frontend/src/types/api.ts
+7
frontend/src/types/api.ts
+28
-19
frontend/vite.config.ts
+28
-19
frontend/vite.config.ts
···
3
3
import tailwindcss from '@tailwindcss/vite'
4
4
import path from "path"
5
5
6
-
export default defineConfig({
7
-
plugins: [
8
-
react(),
9
-
tailwindcss(),
10
-
],
11
-
server: {
12
-
proxy: {
13
-
'/api': {
14
-
target: 'http://localhost:8080',
15
-
changeOrigin: true,
6
+
export default defineConfig(({ command }) => {
7
+
if (command === 'serve') { //command == 'dev'
8
+
return {
9
+
server: {
10
+
proxy: {
11
+
'/api': {
12
+
target: process.env.VITE_API_URL || 'http://localhost:8080',
13
+
changeOrigin: true,
14
+
},
15
+
},
16
+
},
17
+
plugins: [react(), tailwindcss()],
18
+
resolve: {
19
+
alias: {
20
+
"@": path.resolve(__dirname, "./src"),
21
+
},
22
+
},
23
+
}
24
+
} else { //command === 'build'
25
+
return {
26
+
plugins: [react(), tailwindcss()],
27
+
resolve: {
28
+
alias: {
29
+
"@": path.resolve(__dirname, "./src"),
30
+
},
16
31
},
17
-
},
18
-
},
19
-
resolve: {
20
-
alias: {
21
-
"@": path.resolve(__dirname, "./src"),
22
-
},
23
-
},
24
-
})
25
-
32
+
}
33
+
}
34
+
})
+3
migrations/20250219000000_extend_short_code.sql
+3
migrations/20250219000000_extend_short_code.sql
+42
migrations/sqlite/20250125000000_init.sql
+42
migrations/sqlite/20250125000000_init.sql
···
1
+
-- Enable foreign key support
2
+
PRAGMA foreign_keys = ON;
3
+
4
+
-- Add Migration Version
5
+
CREATE TABLE IF NOT EXISTS _sqlx_migrations (
6
+
version INTEGER PRIMARY KEY,
7
+
description TEXT NOT NULL,
8
+
installed_on TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
9
+
);
10
+
11
+
-- Create users table
12
+
CREATE TABLE users (
13
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
14
+
email VARCHAR(255) NOT NULL UNIQUE,
15
+
password_hash TEXT NOT NULL
16
+
);
17
+
18
+
-- Create links table
19
+
CREATE TABLE links (
20
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
21
+
original_url TEXT NOT NULL,
22
+
short_code VARCHAR(8) NOT NULL UNIQUE,
23
+
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
24
+
clicks INTEGER NOT NULL DEFAULT 0,
25
+
user_id INTEGER,
26
+
FOREIGN KEY (user_id) REFERENCES users(id)
27
+
);
28
+
29
+
-- Create clicks table
30
+
CREATE TABLE clicks (
31
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
32
+
link_id INTEGER,
33
+
source TEXT,
34
+
query_source TEXT,
35
+
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
36
+
FOREIGN KEY (link_id) REFERENCES links(id)
37
+
);
38
+
39
+
-- Create indexes
40
+
CREATE INDEX idx_short_code ON links(short_code);
41
+
CREATE INDEX idx_user_id ON links(user_id);
42
+
CREATE INDEX idx_link_id ON clicks(link_id);
readme_img/mainview.jpg
readme_img/mainview.jpg
This is a binary file and will not be displayed.
readme_img/statview.jpg
readme_img/statview.jpg
This is a binary file and will not be displayed.
+8
-7
src/auth.rs
+8
-7
src/auth.rs
···
1
+
use crate::{error::AppError, models::Claims};
1
2
use actix_web::{dev::Payload, FromRequest, HttpRequest};
2
3
use jsonwebtoken::{decode, DecodingKey, Validation};
3
4
use std::future::{ready, Ready};
4
-
use crate::{error::AppError, models::Claims};
5
5
6
6
pub struct AuthenticatedUser {
7
7
pub user_id: i32,
···
12
12
type Future = Ready<Result<Self, Self::Error>>;
13
13
14
14
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
15
-
let auth_header = req.headers()
15
+
let auth_header = req
16
+
.headers()
16
17
.get("Authorization")
17
18
.and_then(|h| h.to_str().ok());
18
19
19
20
if let Some(auth_header) = auth_header {
20
21
if auth_header.starts_with("Bearer ") {
21
22
let token = &auth_header[7..];
22
-
let secret = std::env::var("JWT_SECRET").unwrap_or_else(|_| "default_secret".to_string());
23
-
23
+
let secret =
24
+
std::env::var("JWT_SECRET").unwrap_or_else(|_| "default_secret".to_string());
24
25
match decode::<Claims>(
25
26
token,
26
27
&DecodingKey::from_secret(secret.as_bytes()),
27
-
&Validation::default()
28
+
&Validation::default(),
28
29
) {
29
30
Ok(token_data) => {
30
31
return ready(Ok(AuthenticatedUser {
···
35
36
}
36
37
}
37
38
}
38
-
39
39
ready(Err(AppError::Unauthorized))
40
40
}
41
-
}
41
+
}
42
+
+600
-151
src/handlers.rs
+600
-151
src/handlers.rs
···
2
2
use crate::{
3
3
error::AppError,
4
4
models::{
5
-
AuthResponse, Claims, ClickStats, CreateLink, Link, LoginRequest, RegisterRequest,
6
-
SourceStats, User, UserResponse,
5
+
AuthResponse, Claims, ClickStats, CreateLink, DatabasePool, Link, LoginRequest,
6
+
RegisterRequest, SourceStats, User, UserResponse,
7
7
},
8
8
AppState,
9
9
};
···
16
16
use jsonwebtoken::{encode, EncodingKey, Header};
17
17
use lazy_static::lazy_static;
18
18
use regex::Regex;
19
+
use serde_json::json;
20
+
use sqlx::{Postgres, Sqlite};
19
21
20
22
lazy_static! {
21
23
static ref VALID_CODE_REGEX: Regex = Regex::new(r"^[a-zA-Z0-9_-]{1,32}$").unwrap();
···
27
29
payload: web::Json<CreateLink>,
28
30
) -> Result<impl Responder, AppError> {
29
31
tracing::debug!("Creating short URL with user_id: {}", user.user_id);
30
-
31
32
validate_url(&payload.url)?;
32
33
33
34
let short_code = if let Some(ref custom_code) = payload.custom_code {
34
35
validate_custom_code(custom_code)?;
35
36
36
-
tracing::debug!("Checking if custom code {} exists", custom_code);
37
-
// Check if code is already taken
38
-
if let Some(_) = sqlx::query_as::<_, Link>("SELECT * FROM links WHERE short_code = $1")
39
-
.bind(custom_code)
40
-
.fetch_optional(&state.db)
41
-
.await?
42
-
{
37
+
// Check if code exists using match on pool type
38
+
let exists = match &state.db {
39
+
DatabasePool::Postgres(pool) => {
40
+
sqlx::query_as::<_, Link>("SELECT * FROM links WHERE short_code = $1")
41
+
.bind(custom_code)
42
+
.fetch_optional(pool)
43
+
.await?
44
+
}
45
+
DatabasePool::Sqlite(pool) => {
46
+
sqlx::query_as::<_, Link>("SELECT * FROM links WHERE short_code = ?1")
47
+
.bind(custom_code)
48
+
.fetch_optional(pool)
49
+
.await?
50
+
}
51
+
};
52
+
53
+
if exists.is_some() {
43
54
return Err(AppError::InvalidInput(
44
55
"Custom code already taken".to_string(),
45
56
));
46
57
}
47
-
48
58
custom_code.clone()
49
59
} else {
50
60
generate_short_code()
51
61
};
52
62
53
-
// Start transaction
54
-
let mut tx = state.db.begin().await?;
63
+
// Start transaction based on pool type
64
+
let result = match &state.db {
65
+
DatabasePool::Postgres(pool) => {
66
+
let mut tx = pool.begin().await?;
55
67
56
-
tracing::debug!("Inserting new link with short_code: {}", short_code);
57
-
let link = sqlx::query_as::<_, Link>(
58
-
"INSERT INTO links (original_url, short_code, user_id) VALUES ($1, $2, $3) RETURNING *",
59
-
)
60
-
.bind(&payload.url)
61
-
.bind(&short_code)
62
-
.bind(user.user_id)
63
-
.fetch_one(&mut *tx)
64
-
.await?;
68
+
let link = sqlx::query_as::<_, Link>(
69
+
"INSERT INTO links (original_url, short_code, user_id) VALUES ($1, $2, $3) RETURNING *"
70
+
)
71
+
.bind(&payload.url)
72
+
.bind(&short_code)
73
+
.bind(user.user_id)
74
+
.fetch_one(&mut *tx)
75
+
.await?;
65
76
66
-
if let Some(ref source) = payload.source {
67
-
tracing::debug!("Adding click source: {}", source);
68
-
sqlx::query("INSERT INTO clicks (link_id, source) VALUES ($1, $2)")
69
-
.bind(link.id)
70
-
.bind(source)
71
-
.execute(&mut *tx)
77
+
if let Some(ref source) = payload.source {
78
+
sqlx::query("INSERT INTO clicks (link_id, source) VALUES ($1, $2)")
79
+
.bind(link.id)
80
+
.bind(source)
81
+
.execute(&mut *tx)
82
+
.await?;
83
+
}
84
+
85
+
tx.commit().await?;
86
+
link
87
+
}
88
+
DatabasePool::Sqlite(pool) => {
89
+
let mut tx = pool.begin().await?;
90
+
91
+
let link = sqlx::query_as::<_, Link>(
92
+
"INSERT INTO links (original_url, short_code, user_id) VALUES (?1, ?2, ?3) RETURNING *"
93
+
)
94
+
.bind(&payload.url)
95
+
.bind(&short_code)
96
+
.bind(user.user_id)
97
+
.fetch_one(&mut *tx)
72
98
.await?;
73
-
}
99
+
100
+
if let Some(ref source) = payload.source {
101
+
sqlx::query("INSERT INTO clicks (link_id, source) VALUES (?1, ?2)")
102
+
.bind(link.id)
103
+
.bind(source)
104
+
.execute(&mut *tx)
105
+
.await?;
106
+
}
74
107
75
-
tx.commit().await?;
76
-
Ok(HttpResponse::Created().json(link))
108
+
tx.commit().await?;
109
+
link
110
+
}
111
+
};
112
+
113
+
Ok(HttpResponse::Created().json(result))
77
114
}
78
115
79
116
fn validate_custom_code(code: &str) -> Result<(), AppError> {
···
94
131
Ok(())
95
132
}
96
133
97
-
fn validate_url(url: &String) -> Result<(), AppError> {
134
+
fn validate_url(url: &str) -> Result<(), AppError> {
98
135
if url.is_empty() {
99
136
return Err(AppError::InvalidInput("URL cannot be empty".to_string()));
100
137
}
···
120
157
.and_then(|q| web::Query::<std::collections::HashMap<String, String>>::from_query(q).ok())
121
158
.and_then(|params| params.get("source").cloned());
122
159
123
-
let mut tx = state.db.begin().await?;
124
-
125
-
let link = sqlx::query_as::<_, Link>(
126
-
"UPDATE links SET clicks = clicks + 1 WHERE short_code = $1 RETURNING *",
127
-
)
128
-
.bind(&short_code)
129
-
.fetch_optional(&mut *tx)
130
-
.await?;
160
+
let link = match &state.db {
161
+
DatabasePool::Postgres(pool) => {
162
+
let mut tx = pool.begin().await?;
163
+
let link = sqlx::query_as::<_, Link>(
164
+
"UPDATE links SET clicks = clicks + 1 WHERE short_code = $1 RETURNING *",
165
+
)
166
+
.bind(&short_code)
167
+
.fetch_optional(&mut *tx)
168
+
.await?;
169
+
tx.commit().await?;
170
+
link
171
+
}
172
+
DatabasePool::Sqlite(pool) => {
173
+
let mut tx = pool.begin().await?;
174
+
let link = sqlx::query_as::<_, Link>(
175
+
"UPDATE links SET clicks = clicks + 1 WHERE short_code = ?1 RETURNING *",
176
+
)
177
+
.bind(&short_code)
178
+
.fetch_optional(&mut *tx)
179
+
.await?;
180
+
tx.commit().await?;
181
+
link
182
+
}
183
+
};
131
184
132
185
match link {
133
186
Some(link) => {
134
-
// Record click with both user agent and query source
135
-
let user_agent = req
136
-
.headers()
137
-
.get("user-agent")
138
-
.and_then(|h| h.to_str().ok())
139
-
.unwrap_or("unknown")
140
-
.to_string();
187
+
// Handle click recording based on database type
188
+
match &state.db {
189
+
DatabasePool::Postgres(pool) => {
190
+
let mut tx = pool.begin().await?;
191
+
let user_agent = req
192
+
.headers()
193
+
.get("user-agent")
194
+
.and_then(|h| h.to_str().ok())
195
+
.unwrap_or("unknown")
196
+
.to_string();
141
197
142
-
sqlx::query("INSERT INTO clicks (link_id, source, query_source) VALUES ($1, $2, $3)")
143
-
.bind(link.id)
144
-
.bind(user_agent)
145
-
.bind(query_source)
146
-
.execute(&mut *tx)
147
-
.await?;
198
+
sqlx::query(
199
+
"INSERT INTO clicks (link_id, source, query_source) VALUES ($1, $2, $3)",
200
+
)
201
+
.bind(link.id)
202
+
.bind(user_agent)
203
+
.bind(query_source)
204
+
.execute(&mut *tx)
205
+
.await?;
206
+
207
+
tx.commit().await?;
208
+
}
209
+
DatabasePool::Sqlite(pool) => {
210
+
let mut tx = pool.begin().await?;
211
+
let user_agent = req
212
+
.headers()
213
+
.get("user-agent")
214
+
.and_then(|h| h.to_str().ok())
215
+
.unwrap_or("unknown")
216
+
.to_string();
217
+
218
+
sqlx::query(
219
+
"INSERT INTO clicks (link_id, source, query_source) VALUES (?1, ?2, ?3)",
220
+
)
221
+
.bind(link.id)
222
+
.bind(user_agent)
223
+
.bind(query_source)
224
+
.execute(&mut *tx)
225
+
.await?;
148
226
149
-
tx.commit().await?;
227
+
tx.commit().await?;
228
+
}
229
+
};
150
230
151
231
Ok(HttpResponse::TemporaryRedirect()
152
232
.append_header(("Location", link.original_url))
···
160
240
state: web::Data<AppState>,
161
241
user: AuthenticatedUser,
162
242
) -> Result<impl Responder, AppError> {
163
-
let links = sqlx::query_as::<_, Link>(
164
-
"SELECT * FROM links WHERE user_id = $1 ORDER BY created_at DESC",
165
-
)
166
-
.bind(user.user_id)
167
-
.fetch_all(&state.db)
168
-
.await?;
243
+
let links = match &state.db {
244
+
DatabasePool::Postgres(pool) => {
245
+
sqlx::query_as::<_, Link>(
246
+
"SELECT * FROM links WHERE user_id = $1 ORDER BY created_at DESC",
247
+
)
248
+
.bind(user.user_id)
249
+
.fetch_all(pool)
250
+
.await?
251
+
}
252
+
DatabasePool::Sqlite(pool) => {
253
+
sqlx::query_as::<_, Link>(
254
+
"SELECT * FROM links WHERE user_id = ?1 ORDER BY created_at DESC",
255
+
)
256
+
.bind(user.user_id)
257
+
.fetch_all(pool)
258
+
.await?
259
+
}
260
+
};
169
261
170
262
Ok(HttpResponse::Ok().json(links))
171
263
}
172
264
173
265
pub async fn health_check(state: web::Data<AppState>) -> impl Responder {
174
-
match sqlx::query("SELECT 1").execute(&state.db).await {
175
-
Ok(_) => HttpResponse::Ok().json("Healthy"),
176
-
Err(_) => HttpResponse::ServiceUnavailable().json("Database unavailable"),
266
+
let is_healthy = match &state.db {
267
+
DatabasePool::Postgres(pool) => sqlx::query("SELECT 1").execute(pool).await.is_ok(),
268
+
DatabasePool::Sqlite(pool) => sqlx::query("SELECT 1").execute(pool).await.is_ok(),
269
+
};
270
+
271
+
if is_healthy {
272
+
HttpResponse::Ok().json("Healthy")
273
+
} else {
274
+
HttpResponse::ServiceUnavailable().json("Database unavailable")
177
275
}
178
276
}
179
277
···
182
280
use uuid::Uuid;
183
281
184
282
let uuid = Uuid::new_v4();
185
-
encode(uuid.as_u128() as u64).chars().take(8).collect()
283
+
encode(uuid.as_u128() as u64).chars().take(32).collect()
186
284
}
187
285
188
286
pub async fn register(
189
287
state: web::Data<AppState>,
190
288
payload: web::Json<RegisterRequest>,
191
289
) -> Result<impl Responder, AppError> {
192
-
let exists = sqlx::query!("SELECT id FROM users WHERE email = $1", payload.email)
193
-
.fetch_optional(&state.db)
194
-
.await?;
290
+
// Check if any users exist
291
+
let user_count = match &state.db {
292
+
DatabasePool::Postgres(pool) => {
293
+
let mut tx = pool.begin().await?;
294
+
let count = sqlx::query_as::<Postgres, (i64,)>("SELECT COUNT(*)::bigint FROM users")
295
+
.fetch_one(&mut *tx)
296
+
.await?
297
+
.0;
298
+
tx.commit().await?;
299
+
count
300
+
}
301
+
DatabasePool::Sqlite(pool) => {
302
+
let mut tx = pool.begin().await?;
303
+
let count = sqlx::query_as::<Sqlite, (i64,)>("SELECT COUNT(*) FROM users")
304
+
.fetch_one(&mut *tx)
305
+
.await?
306
+
.0;
307
+
tx.commit().await?;
308
+
count
309
+
}
310
+
};
311
+
312
+
// If users exist, registration is closed - no exceptions
313
+
if user_count > 0 {
314
+
return Err(AppError::Auth("Registration is closed".to_string()));
315
+
}
316
+
317
+
// Verify admin token for first user
318
+
match (&state.admin_token, &payload.admin_token) {
319
+
(Some(stored_token), Some(provided_token)) if stored_token == provided_token => {
320
+
// Token matches, proceed with registration
321
+
}
322
+
_ => return Err(AppError::Auth("Invalid admin setup token".to_string())),
323
+
}
324
+
325
+
// Check if email already exists
326
+
let exists = match &state.db {
327
+
DatabasePool::Postgres(pool) => {
328
+
let mut tx = pool.begin().await?;
329
+
let exists =
330
+
sqlx::query_as::<Postgres, (i32,)>("SELECT id FROM users WHERE email = $1")
331
+
.bind(&payload.email)
332
+
.fetch_optional(&mut *tx)
333
+
.await?;
334
+
tx.commit().await?;
335
+
exists
336
+
}
337
+
DatabasePool::Sqlite(pool) => {
338
+
let mut tx = pool.begin().await?;
339
+
let exists = sqlx::query_as::<Sqlite, (i32,)>("SELECT id FROM users WHERE email = ?")
340
+
.bind(&payload.email)
341
+
.fetch_optional(&mut *tx)
342
+
.await?;
343
+
tx.commit().await?;
344
+
exists
345
+
}
346
+
};
195
347
196
348
if exists.is_some() {
197
349
return Err(AppError::Auth("Email already registered".to_string()));
···
204
356
.map_err(|e| AppError::Auth(e.to_string()))?
205
357
.to_string();
206
358
207
-
let user = sqlx::query_as!(
208
-
User,
209
-
"INSERT INTO users (email, password_hash) VALUES ($1, $2) RETURNING *",
210
-
payload.email,
211
-
password_hash
212
-
)
213
-
.fetch_one(&state.db)
214
-
.await?;
359
+
// Insert new user
360
+
let user = match &state.db {
361
+
DatabasePool::Postgres(pool) => {
362
+
let mut tx = pool.begin().await?;
363
+
let user = sqlx::query_as::<Postgres, User>(
364
+
"INSERT INTO users (email, password_hash) VALUES ($1, $2) RETURNING *",
365
+
)
366
+
.bind(&payload.email)
367
+
.bind(&password_hash)
368
+
.fetch_one(&mut *tx)
369
+
.await?;
370
+
tx.commit().await?;
371
+
user
372
+
}
373
+
DatabasePool::Sqlite(pool) => {
374
+
let mut tx = pool.begin().await?;
375
+
let user = sqlx::query_as::<Sqlite, User>(
376
+
"INSERT INTO users (email, password_hash) VALUES (?, ?) RETURNING *",
377
+
)
378
+
.bind(&payload.email)
379
+
.bind(&password_hash)
380
+
.fetch_one(&mut *tx)
381
+
.await?;
382
+
tx.commit().await?;
383
+
user
384
+
}
385
+
};
215
386
216
387
let claims = Claims::new(user.id);
217
388
let secret = std::env::var("JWT_SECRET").unwrap_or_else(|_| "default_secret".to_string());
···
235
406
state: web::Data<AppState>,
236
407
payload: web::Json<LoginRequest>,
237
408
) -> Result<impl Responder, AppError> {
238
-
let user = sqlx::query_as!(User, "SELECT * FROM users WHERE email = $1", payload.email)
239
-
.fetch_optional(&state.db)
240
-
.await?
241
-
.ok_or_else(|| AppError::Auth("Invalid credentials".to_string()))?;
409
+
let user = match &state.db {
410
+
DatabasePool::Postgres(pool) => {
411
+
let mut tx = pool.begin().await?;
412
+
let user = sqlx::query_as::<Postgres, User>("SELECT * FROM users WHERE email = $1")
413
+
.bind(&payload.email)
414
+
.fetch_optional(&mut *tx)
415
+
.await?;
416
+
tx.commit().await?;
417
+
user
418
+
}
419
+
DatabasePool::Sqlite(pool) => {
420
+
let mut tx = pool.begin().await?;
421
+
let user = sqlx::query_as::<Sqlite, User>("SELECT * FROM users WHERE email = ?")
422
+
.bind(&payload.email)
423
+
.fetch_optional(&mut *tx)
424
+
.await?;
425
+
tx.commit().await?;
426
+
user
427
+
}
428
+
}
429
+
.ok_or_else(|| AppError::Auth("Invalid credentials".to_string()))?;
242
430
243
431
let argon2 = Argon2::default();
244
432
let parsed_hash =
···
269
457
}))
270
458
}
271
459
460
+
pub async fn edit_link(
461
+
state: web::Data<AppState>,
462
+
user: AuthenticatedUser,
463
+
path: web::Path<i32>,
464
+
payload: web::Json<CreateLink>,
465
+
) -> Result<impl Responder, AppError> {
466
+
let link_id: i32 = path.into_inner();
467
+
468
+
// Validate the new URL if provided
469
+
validate_url(&payload.url)?;
470
+
471
+
// Validate custom code if provided
472
+
if let Some(ref custom_code) = payload.custom_code {
473
+
validate_custom_code(custom_code)?;
474
+
475
+
// Check if the custom code is already taken by another link
476
+
let existing_link = match &state.db {
477
+
DatabasePool::Postgres(pool) => {
478
+
sqlx::query_as::<_, Link>("SELECT * FROM links WHERE short_code = $1 AND id != $2")
479
+
.bind(custom_code)
480
+
.bind(link_id)
481
+
.fetch_optional(pool)
482
+
.await?
483
+
}
484
+
DatabasePool::Sqlite(pool) => {
485
+
sqlx::query_as::<_, Link>("SELECT * FROM links WHERE short_code = ?1 AND id != ?2")
486
+
.bind(custom_code)
487
+
.bind(link_id)
488
+
.fetch_optional(pool)
489
+
.await?
490
+
}
491
+
};
492
+
493
+
if existing_link.is_some() {
494
+
return Err(AppError::InvalidInput(
495
+
"Custom code already taken".to_string(),
496
+
));
497
+
}
498
+
}
499
+
500
+
// Update the link
501
+
let updated_link = match &state.db {
502
+
DatabasePool::Postgres(pool) => {
503
+
let mut tx = pool.begin().await?;
504
+
505
+
// First verify the link belongs to the user
506
+
let link =
507
+
sqlx::query_as::<_, Link>("SELECT * FROM links WHERE id = $1 AND user_id = $2")
508
+
.bind(link_id)
509
+
.bind(user.user_id)
510
+
.fetch_optional(&mut *tx)
511
+
.await?;
512
+
513
+
if link.is_none() {
514
+
return Err(AppError::NotFound);
515
+
}
516
+
517
+
// Update the link
518
+
let updated = sqlx::query_as::<_, Link>(
519
+
r#"
520
+
UPDATE links
521
+
SET
522
+
original_url = $1,
523
+
short_code = COALESCE($2, short_code)
524
+
WHERE id = $3 AND user_id = $4
525
+
RETURNING *
526
+
"#,
527
+
)
528
+
.bind(&payload.url)
529
+
.bind(&payload.custom_code)
530
+
.bind(link_id)
531
+
.bind(user.user_id)
532
+
.fetch_one(&mut *tx)
533
+
.await?;
534
+
535
+
// If source is provided, add a click record
536
+
if let Some(ref source) = payload.source {
537
+
sqlx::query("INSERT INTO clicks (link_id, source) VALUES ($1, $2)")
538
+
.bind(link_id)
539
+
.bind(source)
540
+
.execute(&mut *tx)
541
+
.await?;
542
+
}
543
+
544
+
tx.commit().await?;
545
+
updated
546
+
}
547
+
DatabasePool::Sqlite(pool) => {
548
+
let mut tx = pool.begin().await?;
549
+
550
+
// First verify the link belongs to the user
551
+
let link =
552
+
sqlx::query_as::<_, Link>("SELECT * FROM links WHERE id = ?1 AND user_id = ?2")
553
+
.bind(link_id)
554
+
.bind(user.user_id)
555
+
.fetch_optional(&mut *tx)
556
+
.await?;
557
+
558
+
if link.is_none() {
559
+
return Err(AppError::NotFound);
560
+
}
561
+
562
+
// Update the link
563
+
let updated = sqlx::query_as::<_, Link>(
564
+
r#"
565
+
UPDATE links
566
+
SET
567
+
original_url = ?1,
568
+
short_code = COALESCE(?2, short_code)
569
+
WHERE id = ?3 AND user_id = ?4
570
+
RETURNING *
571
+
"#,
572
+
)
573
+
.bind(&payload.url)
574
+
.bind(&payload.custom_code)
575
+
.bind(link_id)
576
+
.bind(user.user_id)
577
+
.fetch_one(&mut *tx)
578
+
.await?;
579
+
580
+
// If source is provided, add a click record
581
+
if let Some(ref source) = payload.source {
582
+
sqlx::query("INSERT INTO clicks (link_id, source) VALUES (?1, ?2)")
583
+
.bind(link_id)
584
+
.bind(source)
585
+
.execute(&mut *tx)
586
+
.await?;
587
+
}
588
+
589
+
tx.commit().await?;
590
+
updated
591
+
}
592
+
};
593
+
594
+
Ok(HttpResponse::Ok().json(updated_link))
595
+
}
596
+
272
597
pub async fn delete_link(
273
598
state: web::Data<AppState>,
274
599
user: AuthenticatedUser,
275
600
path: web::Path<i32>,
276
601
) -> Result<impl Responder, AppError> {
277
-
let link_id = path.into_inner();
602
+
let link_id: i32 = path.into_inner();
278
603
279
-
// Start transaction
280
-
let mut tx = state.db.begin().await?;
604
+
match &state.db {
605
+
DatabasePool::Postgres(pool) => {
606
+
let mut tx = pool.begin().await?;
281
607
282
-
// Verify the link belongs to the user
283
-
let link = sqlx::query!(
284
-
"SELECT id FROM links WHERE id = $1 AND user_id = $2",
285
-
link_id,
286
-
user.user_id
287
-
)
288
-
.fetch_optional(&mut *tx)
289
-
.await?;
608
+
// Verify the link belongs to the user
609
+
let link = sqlx::query_as::<Postgres, (i32,)>(
610
+
"SELECT id FROM links WHERE id = $1 AND user_id = $2",
611
+
)
612
+
.bind(link_id)
613
+
.bind(user.user_id)
614
+
.fetch_optional(&mut *tx)
615
+
.await?;
290
616
291
-
if link.is_none() {
292
-
return Err(AppError::NotFound);
293
-
}
617
+
if link.is_none() {
618
+
return Err(AppError::NotFound);
619
+
}
294
620
295
-
// Delete associated clicks first due to foreign key constraint
296
-
sqlx::query!("DELETE FROM clicks WHERE link_id = $1", link_id)
297
-
.execute(&mut *tx)
298
-
.await?;
621
+
// Delete associated clicks first due to foreign key constraint
622
+
sqlx::query("DELETE FROM clicks WHERE link_id = $1")
623
+
.bind(link_id)
624
+
.execute(&mut *tx)
625
+
.await?;
626
+
627
+
// Delete the link
628
+
sqlx::query("DELETE FROM links WHERE id = $1")
629
+
.bind(link_id)
630
+
.execute(&mut *tx)
631
+
.await?;
632
+
633
+
tx.commit().await?;
634
+
}
635
+
DatabasePool::Sqlite(pool) => {
636
+
let mut tx = pool.begin().await?;
637
+
638
+
// Verify the link belongs to the user
639
+
let link = sqlx::query_as::<Sqlite, (i32,)>(
640
+
"SELECT id FROM links WHERE id = ? AND user_id = ?",
641
+
)
642
+
.bind(link_id)
643
+
.bind(user.user_id)
644
+
.fetch_optional(&mut *tx)
645
+
.await?;
299
646
300
-
// Delete the link
301
-
sqlx::query!("DELETE FROM links WHERE id = $1", link_id)
302
-
.execute(&mut *tx)
303
-
.await?;
647
+
if link.is_none() {
648
+
return Err(AppError::NotFound);
649
+
}
650
+
651
+
// Delete associated clicks first due to foreign key constraint
652
+
sqlx::query("DELETE FROM clicks WHERE link_id = ?")
653
+
.bind(link_id)
654
+
.execute(&mut *tx)
655
+
.await?;
656
+
657
+
// Delete the link
658
+
sqlx::query("DELETE FROM links WHERE id = ?")
659
+
.bind(link_id)
660
+
.execute(&mut *tx)
661
+
.await?;
304
662
305
-
tx.commit().await?;
663
+
tx.commit().await?;
664
+
}
665
+
}
306
666
307
667
Ok(HttpResponse::NoContent().finish())
308
668
}
···
314
674
) -> Result<impl Responder, AppError> {
315
675
let link_id = path.into_inner();
316
676
317
-
// Verify the link belongs to the user
318
-
let link = sqlx::query!(
319
-
"SELECT id FROM links WHERE id = $1 AND user_id = $2",
320
-
link_id,
321
-
user.user_id
322
-
)
323
-
.fetch_optional(&state.db)
324
-
.await?;
677
+
// First verify the link belongs to the user
678
+
let link = match &state.db {
679
+
DatabasePool::Postgres(pool) => {
680
+
sqlx::query_as::<_, (i32,)>("SELECT id FROM links WHERE id = $1 AND user_id = $2")
681
+
.bind(link_id)
682
+
.bind(user.user_id)
683
+
.fetch_optional(pool)
684
+
.await?
685
+
}
686
+
DatabasePool::Sqlite(pool) => {
687
+
sqlx::query_as::<_, (i32,)>("SELECT id FROM links WHERE id = ? AND user_id = ?")
688
+
.bind(link_id)
689
+
.bind(user.user_id)
690
+
.fetch_optional(pool)
691
+
.await?
692
+
}
693
+
};
325
694
326
695
if link.is_none() {
327
696
return Err(AppError::NotFound);
328
697
}
329
698
330
-
let clicks = sqlx::query_as!(
331
-
ClickStats,
332
-
r#"
333
-
SELECT
334
-
DATE(created_at)::date as "date!",
335
-
COUNT(*)::bigint as "clicks!"
336
-
FROM clicks
337
-
WHERE link_id = $1
338
-
GROUP BY DATE(created_at)
339
-
ORDER BY DATE(created_at) ASC -- Changed from DESC to ASC
340
-
LIMIT 30
341
-
"#,
342
-
link_id
343
-
)
344
-
.fetch_all(&state.db)
345
-
.await?;
699
+
let clicks = match &state.db {
700
+
DatabasePool::Postgres(pool) => {
701
+
sqlx::query_as::<_, ClickStats>(
702
+
r#"
703
+
SELECT
704
+
DATE(created_at)::text as date,
705
+
COUNT(*)::bigint as clicks
706
+
FROM clicks
707
+
WHERE link_id = $1
708
+
GROUP BY DATE(created_at)
709
+
ORDER BY DATE(created_at) ASC
710
+
"#,
711
+
)
712
+
.bind(link_id)
713
+
.fetch_all(pool)
714
+
.await?
715
+
}
716
+
DatabasePool::Sqlite(pool) => {
717
+
sqlx::query_as::<_, ClickStats>(
718
+
r#"
719
+
SELECT
720
+
DATE(created_at) as date,
721
+
COUNT(*) as clicks
722
+
FROM clicks
723
+
WHERE link_id = ?
724
+
GROUP BY DATE(created_at)
725
+
ORDER BY DATE(created_at) ASC
726
+
"#,
727
+
)
728
+
.bind(link_id)
729
+
.fetch_all(pool)
730
+
.await?
731
+
}
732
+
};
346
733
347
734
Ok(HttpResponse::Ok().json(clicks))
348
735
}
···
355
742
let link_id = path.into_inner();
356
743
357
744
// Verify the link belongs to the user
358
-
let link = sqlx::query!(
359
-
"SELECT id FROM links WHERE id = $1 AND user_id = $2",
360
-
link_id,
361
-
user.user_id
362
-
)
363
-
.fetch_optional(&state.db)
364
-
.await?;
745
+
let link = match &state.db {
746
+
DatabasePool::Postgres(pool) => {
747
+
let mut tx = pool.begin().await?;
748
+
let link = sqlx::query_as::<Postgres, (i32,)>(
749
+
"SELECT id FROM links WHERE id = $1 AND user_id = $2",
750
+
)
751
+
.bind(link_id)
752
+
.bind(user.user_id)
753
+
.fetch_optional(&mut *tx)
754
+
.await?;
755
+
tx.commit().await?;
756
+
link
757
+
}
758
+
DatabasePool::Sqlite(pool) => {
759
+
let mut tx = pool.begin().await?;
760
+
let link = sqlx::query_as::<Sqlite, (i32,)>(
761
+
"SELECT id FROM links WHERE id = ? AND user_id = ?",
762
+
)
763
+
.bind(link_id)
764
+
.bind(user.user_id)
765
+
.fetch_optional(&mut *tx)
766
+
.await?;
767
+
tx.commit().await?;
768
+
link
769
+
}
770
+
};
365
771
366
772
if link.is_none() {
367
773
return Err(AppError::NotFound);
368
774
}
369
775
370
-
let sources = sqlx::query_as!(
371
-
SourceStats,
372
-
r#"
373
-
SELECT
374
-
query_source as "source!",
375
-
COUNT(*)::bigint as "count!"
376
-
FROM clicks
377
-
WHERE link_id = $1
378
-
AND query_source IS NOT NULL
379
-
AND query_source != ''
380
-
GROUP BY query_source
381
-
ORDER BY COUNT(*) DESC
382
-
LIMIT 10
383
-
"#,
384
-
link_id
385
-
)
386
-
.fetch_all(&state.db)
387
-
.await?;
776
+
let sources = match &state.db {
777
+
DatabasePool::Postgres(pool) => {
778
+
sqlx::query_as::<_, SourceStats>(
779
+
r#"
780
+
SELECT
781
+
DATE(created_at)::text as date,
782
+
query_source as source,
783
+
COUNT(*)::bigint as count
784
+
FROM clicks
785
+
WHERE link_id = $1
786
+
AND query_source IS NOT NULL
787
+
AND query_source != ''
788
+
GROUP BY DATE(created_at), query_source
789
+
ORDER BY DATE(created_at) ASC, COUNT(*) DESC
790
+
"#,
791
+
)
792
+
.bind(link_id)
793
+
.fetch_all(pool)
794
+
.await?
795
+
}
796
+
DatabasePool::Sqlite(pool) => {
797
+
sqlx::query_as::<_, SourceStats>(
798
+
r#"
799
+
SELECT
800
+
DATE(created_at) as date,
801
+
query_source as source,
802
+
COUNT(*) as count
803
+
FROM clicks
804
+
WHERE link_id = ?
805
+
AND query_source IS NOT NULL
806
+
AND query_source != ''
807
+
GROUP BY DATE(created_at), query_source
808
+
ORDER BY DATE(created_at) ASC, COUNT(*) DESC
809
+
"#,
810
+
)
811
+
.bind(link_id)
812
+
.fetch_all(pool)
813
+
.await?
814
+
}
815
+
};
388
816
389
817
Ok(HttpResponse::Ok().json(sources))
390
818
}
819
+
820
+
pub async fn check_first_user(state: web::Data<AppState>) -> Result<impl Responder, AppError> {
821
+
let user_count = match &state.db {
822
+
DatabasePool::Postgres(pool) => {
823
+
sqlx::query_as::<Postgres, (i64,)>("SELECT COUNT(*)::bigint FROM users")
824
+
.fetch_one(pool)
825
+
.await?
826
+
.0
827
+
}
828
+
DatabasePool::Sqlite(pool) => {
829
+
sqlx::query_as::<Sqlite, (i64,)>("SELECT COUNT(*) FROM users")
830
+
.fetch_one(pool)
831
+
.await?
832
+
.0
833
+
}
834
+
};
835
+
836
+
Ok(HttpResponse::Ok().json(json!({
837
+
"isFirstUser": user_count == 0
838
+
})))
839
+
}
+127
-2
src/lib.rs
+127
-2
src/lib.rs
···
1
-
use sqlx::PgPool;
1
+
use anyhow::Result;
2
+
use rand::Rng;
3
+
use sqlx::migrate::MigrateDatabase;
4
+
use sqlx::postgres::PgPoolOptions;
5
+
use sqlx::{Postgres, Sqlite};
6
+
use std::fs::File;
7
+
use std::io::Write;
8
+
use tracing::info;
9
+
10
+
use models::DatabasePool;
2
11
3
12
pub mod auth;
4
13
pub mod error;
···
7
16
8
17
#[derive(Clone)]
9
18
pub struct AppState {
10
-
pub db: PgPool,
19
+
pub db: DatabasePool,
20
+
pub admin_token: Option<String>,
21
+
}
22
+
23
+
pub async fn create_db_pool() -> Result<DatabasePool> {
24
+
let database_url = std::env::var("DATABASE_URL").ok();
25
+
26
+
match database_url {
27
+
Some(url) if url.starts_with("postgres://") || url.starts_with("postgresql://") => {
28
+
info!("Using PostgreSQL database");
29
+
let pool = PgPoolOptions::new()
30
+
.max_connections(5)
31
+
.acquire_timeout(std::time::Duration::from_secs(3))
32
+
.connect(&url)
33
+
.await?;
34
+
35
+
Ok(DatabasePool::Postgres(pool))
36
+
}
37
+
_ => {
38
+
info!("No PostgreSQL connection string found, using SQLite");
39
+
40
+
// Get the project root directory
41
+
let project_root = std::env::current_dir()?;
42
+
let data_dir = project_root.join("data");
43
+
44
+
// Create a data directory if it doesn't exist
45
+
if !data_dir.exists() {
46
+
std::fs::create_dir_all(&data_dir)?;
47
+
}
48
+
49
+
let db_path = data_dir.join("simplelink.db");
50
+
let sqlite_url = format!("sqlite://{}", db_path.display());
51
+
52
+
// Check if database exists and create it if it doesn't
53
+
if !Sqlite::database_exists(&sqlite_url).await.unwrap_or(false) {
54
+
info!("Creating new SQLite database at {}", db_path.display());
55
+
Sqlite::create_database(&sqlite_url).await?;
56
+
info!("Database created successfully");
57
+
} else {
58
+
info!("Database already exists");
59
+
}
60
+
61
+
let pool = sqlx::sqlite::SqlitePoolOptions::new()
62
+
.max_connections(5)
63
+
.connect(&sqlite_url)
64
+
.await?;
65
+
66
+
Ok(DatabasePool::Sqlite(pool))
67
+
}
68
+
}
69
+
}
70
+
71
+
pub async fn run_migrations(pool: &DatabasePool) -> Result<()> {
72
+
match pool {
73
+
DatabasePool::Postgres(pool) => {
74
+
// Use the root migrations directory for postgres
75
+
sqlx::migrate!().run(pool).await?;
76
+
}
77
+
DatabasePool::Sqlite(pool) => {
78
+
sqlx::migrate!("./migrations/sqlite").run(pool).await?;
79
+
}
80
+
}
81
+
Ok(())
82
+
}
83
+
84
+
pub async fn check_and_generate_admin_token(db: &DatabasePool) -> anyhow::Result<Option<String>> {
85
+
// Check if any users exist
86
+
let user_count = match db {
87
+
DatabasePool::Postgres(pool) => {
88
+
let mut tx = pool.begin().await?;
89
+
let count = sqlx::query_as::<Postgres, (i64,)>("SELECT COUNT(*)::bigint FROM users")
90
+
.fetch_one(&mut *tx)
91
+
.await?
92
+
.0;
93
+
tx.commit().await?;
94
+
count
95
+
}
96
+
DatabasePool::Sqlite(pool) => {
97
+
let mut tx = pool.begin().await?;
98
+
let count = sqlx::query_as::<Sqlite, (i64,)>("SELECT COUNT(*) FROM users")
99
+
.fetch_one(&mut *tx)
100
+
.await?
101
+
.0;
102
+
tx.commit().await?;
103
+
count
104
+
}
105
+
};
106
+
107
+
if user_count == 0 {
108
+
let token: String = (0..32)
109
+
.map(|_| {
110
+
let idx = rand::thread_rng().gen_range(0..62);
111
+
match idx {
112
+
0..=9 => (b'0' + idx as u8) as char,
113
+
10..=35 => (b'a' + (idx - 10) as u8) as char,
114
+
_ => (b'A' + (idx - 36) as u8) as char,
115
+
}
116
+
})
117
+
.collect();
118
+
119
+
// Get the project root directory
120
+
let project_root = std::env::current_dir()?;
121
+
let token_path = project_root.join("admin-setup-token.txt");
122
+
123
+
// Save token to file
124
+
let mut file = File::create(token_path)?;
125
+
writeln!(file, "{}", token)?;
126
+
127
+
info!("No users found - generated admin setup token");
128
+
info!("Token has been saved to admin-setup-token.txt");
129
+
info!("Use this token to create the admin user");
130
+
info!("Admin setup token: {}", token);
131
+
132
+
Ok(Some(token))
133
+
} else {
134
+
Ok(None)
135
+
}
11
136
}
+195
-14
src/main.rs
+195
-14
src/main.rs
···
1
1
use actix_cors::Cors;
2
-
use actix_web::{web, App, HttpServer};
2
+
use actix_web::{web, App, HttpResponse, HttpServer};
3
3
use anyhow::Result;
4
+
use clap::Parser;
5
+
use rust_embed::RustEmbed;
6
+
use simplelink::check_and_generate_admin_token;
7
+
use simplelink::models::DatabasePool;
8
+
use simplelink::{create_db_pool, run_migrations};
4
9
use simplelink::{handlers, AppState};
5
-
use sqlx::postgres::PgPoolOptions;
6
-
use tracing::info;
10
+
use sqlx::{Postgres, Sqlite};
11
+
use tracing::{error, info};
12
+
13
+
#[derive(Parser, Debug)]
14
+
#[command(author, version, about, long_about = None)]
15
+
#[derive(RustEmbed)]
16
+
#[folder = "static/"]
17
+
struct Asset;
18
+
19
+
async fn serve_static_file(path: &str) -> HttpResponse {
20
+
match Asset::get(path) {
21
+
Some(content) => {
22
+
let mime = mime_guess::from_path(path).first_or_octet_stream();
23
+
HttpResponse::Ok()
24
+
.content_type(mime.as_ref())
25
+
.body(content.data.into_owned())
26
+
}
27
+
None => HttpResponse::NotFound().body("404 Not Found"),
28
+
}
29
+
}
30
+
31
+
async fn create_initial_links(pool: &DatabasePool) -> Result<()> {
32
+
if let Ok(links) = std::env::var("INITIAL_LINKS") {
33
+
for link_entry in links.split(';') {
34
+
let parts: Vec<&str> = link_entry.split(',').collect();
35
+
if parts.len() >= 2 {
36
+
let url = parts[0];
37
+
let code = parts[1];
38
+
39
+
match pool {
40
+
DatabasePool::Postgres(pool) => {
41
+
sqlx::query(
42
+
"INSERT INTO links (original_url, short_code, user_id)
43
+
VALUES ($1, $2, $3)
44
+
ON CONFLICT (short_code)
45
+
DO UPDATE SET short_code = EXCLUDED.short_code
46
+
WHERE links.original_url = EXCLUDED.original_url",
47
+
)
48
+
.bind(url)
49
+
.bind(code)
50
+
.bind(1)
51
+
.execute(pool)
52
+
.await?;
53
+
}
54
+
DatabasePool::Sqlite(pool) => {
55
+
// First check if the exact combination exists
56
+
let exists = sqlx::query_scalar::<_, bool>(
57
+
"SELECT EXISTS(
58
+
SELECT 1 FROM links
59
+
WHERE original_url = ?1
60
+
AND short_code = ?2
61
+
)",
62
+
)
63
+
.bind(url)
64
+
.bind(code)
65
+
.fetch_one(pool)
66
+
.await?;
67
+
68
+
// Only insert if the exact combination doesn't exist
69
+
if !exists {
70
+
sqlx::query(
71
+
"INSERT INTO links (original_url, short_code, user_id)
72
+
VALUES (?1, ?2, ?3)",
73
+
)
74
+
.bind(url)
75
+
.bind(code)
76
+
.bind(1)
77
+
.execute(pool)
78
+
.await?;
79
+
info!("Created initial link: {} -> {} for user_id: 1", code, url);
80
+
} else {
81
+
info!("Skipped existing link: {} -> {} for user_id: 1", code, url);
82
+
}
83
+
}
84
+
}
85
+
}
86
+
}
87
+
}
88
+
Ok(())
89
+
}
90
+
91
+
async fn create_admin_user(pool: &DatabasePool, email: &str, password: &str) -> Result<()> {
92
+
use argon2::{
93
+
password_hash::{rand_core::OsRng, SaltString},
94
+
Argon2, PasswordHasher,
95
+
};
96
+
97
+
let salt = SaltString::generate(&mut OsRng);
98
+
let argon2 = Argon2::default();
99
+
let password_hash = argon2
100
+
.hash_password(password.as_bytes(), &salt)
101
+
.map_err(|e| anyhow::anyhow!("Password hashing error: {}", e))?
102
+
.to_string();
103
+
104
+
match pool {
105
+
DatabasePool::Postgres(pool) => {
106
+
sqlx::query(
107
+
"INSERT INTO users (email, password_hash)
108
+
VALUES ($1, $2)
109
+
ON CONFLICT (email) DO NOTHING",
110
+
)
111
+
.bind(email)
112
+
.bind(&password_hash)
113
+
.execute(pool)
114
+
.await?;
115
+
}
116
+
DatabasePool::Sqlite(pool) => {
117
+
sqlx::query(
118
+
"INSERT OR IGNORE INTO users (email, password_hash)
119
+
VALUES (?1, ?2)",
120
+
)
121
+
.bind(email)
122
+
.bind(&password_hash)
123
+
.execute(pool)
124
+
.await?;
125
+
}
126
+
}
127
+
info!("Created admin user: {}", email);
128
+
Ok(())
129
+
}
7
130
8
131
#[actix_web::main]
9
132
async fn main() -> Result<()> {
···
13
136
// Initialize logging
14
137
tracing_subscriber::fmt::init();
15
138
16
-
// Database connection string from environment
17
-
let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set");
18
-
19
139
// Create database connection pool
20
-
let pool = PgPoolOptions::new()
21
-
.max_connections(5)
22
-
.acquire_timeout(std::time::Duration::from_secs(3))
23
-
.connect(&database_url)
24
-
.await?;
140
+
let pool = create_db_pool().await?;
141
+
run_migrations(&pool).await?;
25
142
26
-
// Run database migrations
27
-
sqlx::migrate!("./migrations").run(&pool).await?;
143
+
// First check if admin credentials are provided in environment variables
144
+
let admin_credentials = match (
145
+
std::env::var("SIMPLELINK_USER"),
146
+
std::env::var("SIMPLELINK_PASS"),
147
+
) {
148
+
(Ok(user), Ok(pass)) => Some((user, pass)),
149
+
_ => None,
150
+
};
28
151
29
-
let state = AppState { db: pool };
152
+
if let Some((email, password)) = admin_credentials {
153
+
// Now check for existing users
154
+
let user_count = match &pool {
155
+
DatabasePool::Postgres(pool) => {
156
+
let mut tx = pool.begin().await?;
157
+
let count =
158
+
sqlx::query_as::<Postgres, (i64,)>("SELECT COUNT(*)::bigint FROM users")
159
+
.fetch_one(&mut *tx)
160
+
.await?
161
+
.0;
162
+
tx.commit().await?;
163
+
count
164
+
}
165
+
DatabasePool::Sqlite(pool) => {
166
+
let mut tx = pool.begin().await?;
167
+
let count = sqlx::query_as::<Sqlite, (i64,)>("SELECT COUNT(*) FROM users")
168
+
.fetch_one(&mut *tx)
169
+
.await?
170
+
.0;
171
+
tx.commit().await?;
172
+
count
173
+
}
174
+
};
175
+
176
+
if user_count == 0 {
177
+
info!("No users found, creating admin user: {}", email);
178
+
match create_admin_user(&pool, &email, &password).await {
179
+
Ok(_) => info!("Successfully created admin user"),
180
+
Err(e) => {
181
+
error!("Failed to create admin user: {}", e);
182
+
return Err(anyhow::anyhow!("Failed to create admin user: {}", e));
183
+
}
184
+
}
185
+
}
186
+
} else {
187
+
info!(
188
+
"No admin credentials provided in environment variables, skipping admin user creation"
189
+
);
190
+
}
191
+
192
+
// Create initial links from environment variables
193
+
create_initial_links(&pool).await?;
194
+
195
+
let admin_token = check_and_generate_admin_token(&pool).await?;
196
+
197
+
let state = AppState {
198
+
db: pool,
199
+
admin_token,
200
+
};
30
201
31
202
let host = std::env::var("SERVER_HOST").unwrap_or_else(|_| "127.0.0.1".to_string());
32
203
let port = std::env::var("SERVER_PORT").unwrap_or_else(|_| "8080".to_string());
···
56
227
"/links/{id}/sources",
57
228
web::get().to(handlers::get_link_sources),
58
229
)
230
+
.route("/links/{id}", web::patch().to(handlers::edit_link))
59
231
.route("/auth/register", web::post().to(handlers::register))
60
232
.route("/auth/login", web::post().to(handlers::login))
233
+
.route(
234
+
"/auth/check-first-user",
235
+
web::get().to(handlers::check_first_user),
236
+
)
61
237
.route("/health", web::get().to(handlers::health_check)),
62
238
)
63
239
.service(web::resource("/{short_code}").route(web::get().to(handlers::redirect_to_url)))
240
+
.default_service(web::route().to(|req: actix_web::HttpRequest| async move {
241
+
let path = req.path().trim_start_matches('/');
242
+
let path = if path.is_empty() { "index.html" } else { path };
243
+
serve_static_file(path).await
244
+
}))
64
245
})
65
246
.workers(2)
66
247
.backlog(10_000)
+78
-5
src/models.rs
+78
-5
src/models.rs
···
1
+
use anyhow::Result;
2
+
use futures::future::BoxFuture;
3
+
use serde::{Deserialize, Serialize};
4
+
use sqlx::postgres::PgRow;
5
+
use sqlx::sqlite::SqliteRow;
6
+
use sqlx::FromRow;
7
+
use sqlx::Pool;
8
+
use sqlx::Postgres;
9
+
use sqlx::Sqlite;
10
+
use sqlx::Transaction;
1
11
use std::time::{SystemTime, UNIX_EPOCH};
2
12
3
-
use chrono::NaiveDate;
4
-
use serde::{Deserialize, Serialize};
5
-
use sqlx::FromRow;
13
+
#[derive(Clone)]
14
+
pub enum DatabasePool {
15
+
Postgres(Pool<Postgres>),
16
+
Sqlite(Pool<Sqlite>),
17
+
}
18
+
19
+
impl DatabasePool {
20
+
pub async fn begin(&self) -> Result<Box<dyn std::any::Any + Send>> {
21
+
match self {
22
+
DatabasePool::Postgres(pool) => Ok(Box::new(pool.begin().await?)),
23
+
DatabasePool::Sqlite(pool) => Ok(Box::new(pool.begin().await?)),
24
+
}
25
+
}
26
+
27
+
pub async fn fetch_optional<T>(&self, pg_query: &str, sqlite_query: &str) -> Result<Option<T>>
28
+
where
29
+
T: for<'r> FromRow<'r, PgRow> + for<'r> FromRow<'r, SqliteRow> + Send + Sync + Unpin,
30
+
{
31
+
match self {
32
+
DatabasePool::Postgres(pool) => {
33
+
Ok(sqlx::query_as(pg_query).fetch_optional(pool).await?)
34
+
}
35
+
DatabasePool::Sqlite(pool) => {
36
+
Ok(sqlx::query_as(sqlite_query).fetch_optional(pool).await?)
37
+
}
38
+
}
39
+
}
40
+
41
+
pub async fn execute(&self, pg_query: &str, sqlite_query: &str) -> Result<()> {
42
+
match self {
43
+
DatabasePool::Postgres(pool) => {
44
+
sqlx::query(pg_query).execute(pool).await?;
45
+
Ok(())
46
+
}
47
+
DatabasePool::Sqlite(pool) => {
48
+
sqlx::query(sqlite_query).execute(pool).await?;
49
+
Ok(())
50
+
}
51
+
}
52
+
}
53
+
54
+
pub async fn transaction<'a, F, R>(&'a self, f: F) -> Result<R>
55
+
where
56
+
F: for<'c> Fn(&'c mut Transaction<'_, Postgres>) -> BoxFuture<'c, Result<R>>
57
+
+ for<'c> Fn(&'c mut Transaction<'_, Sqlite>) -> BoxFuture<'c, Result<R>>
58
+
+ Copy,
59
+
R: Send + 'static,
60
+
{
61
+
match self {
62
+
DatabasePool::Postgres(pool) => {
63
+
let mut tx = pool.begin().await?;
64
+
let result = f(&mut tx).await?;
65
+
tx.commit().await?;
66
+
Ok(result)
67
+
}
68
+
DatabasePool::Sqlite(pool) => {
69
+
let mut tx = pool.begin().await?;
70
+
let result = f(&mut tx).await?;
71
+
tx.commit().await?;
72
+
Ok(result)
73
+
}
74
+
}
75
+
}
76
+
}
6
77
7
78
#[derive(Debug, Serialize, Deserialize)]
8
79
pub struct Claims {
···
16
87
.duration_since(UNIX_EPOCH)
17
88
.unwrap()
18
89
.as_secs() as usize
19
-
+ 24 * 60 * 60; // 24 hours from now
90
+
+ 14 * 24 * 60 * 60; // 2 weeks from now
20
91
21
92
Self { sub: user_id, exp }
22
93
}
···
49
120
pub struct RegisterRequest {
50
121
pub email: String,
51
122
pub password: String,
123
+
pub admin_token: Option<String>,
52
124
}
53
125
54
126
#[derive(Serialize)]
···
72
144
73
145
#[derive(sqlx::FromRow, Serialize)]
74
146
pub struct ClickStats {
75
-
pub date: NaiveDate,
147
+
pub date: String,
76
148
pub clicks: i64,
77
149
}
78
150
79
151
#[derive(sqlx::FromRow, Serialize)]
80
152
pub struct SourceStats {
153
+
pub date: String,
81
154
pub source: String,
82
155
pub count: i64,
83
156
}
+175
test/.gitignore
+175
test/.gitignore
···
1
+
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
2
+
3
+
# Logs
4
+
5
+
logs
6
+
_.log
7
+
npm-debug.log_
8
+
yarn-debug.log*
9
+
yarn-error.log*
10
+
lerna-debug.log*
11
+
.pnpm-debug.log*
12
+
13
+
# Caches
14
+
15
+
.cache
16
+
17
+
# Diagnostic reports (https://nodejs.org/api/report.html)
18
+
19
+
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
20
+
21
+
# Runtime data
22
+
23
+
pids
24
+
_.pid
25
+
_.seed
26
+
*.pid.lock
27
+
28
+
# Directory for instrumented libs generated by jscoverage/JSCover
29
+
30
+
lib-cov
31
+
32
+
# Coverage directory used by tools like istanbul
33
+
34
+
coverage
35
+
*.lcov
36
+
37
+
# nyc test coverage
38
+
39
+
.nyc_output
40
+
41
+
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
42
+
43
+
.grunt
44
+
45
+
# Bower dependency directory (https://bower.io/)
46
+
47
+
bower_components
48
+
49
+
# node-waf configuration
50
+
51
+
.lock-wscript
52
+
53
+
# Compiled binary addons (https://nodejs.org/api/addons.html)
54
+
55
+
build/Release
56
+
57
+
# Dependency directories
58
+
59
+
node_modules/
60
+
jspm_packages/
61
+
62
+
# Snowpack dependency directory (https://snowpack.dev/)
63
+
64
+
web_modules/
65
+
66
+
# TypeScript cache
67
+
68
+
*.tsbuildinfo
69
+
70
+
# Optional npm cache directory
71
+
72
+
.npm
73
+
74
+
# Optional eslint cache
75
+
76
+
.eslintcache
77
+
78
+
# Optional stylelint cache
79
+
80
+
.stylelintcache
81
+
82
+
# Microbundle cache
83
+
84
+
.rpt2_cache/
85
+
.rts2_cache_cjs/
86
+
.rts2_cache_es/
87
+
.rts2_cache_umd/
88
+
89
+
# Optional REPL history
90
+
91
+
.node_repl_history
92
+
93
+
# Output of 'npm pack'
94
+
95
+
*.tgz
96
+
97
+
# Yarn Integrity file
98
+
99
+
.yarn-integrity
100
+
101
+
# dotenv environment variable files
102
+
103
+
.env
104
+
.env.development.local
105
+
.env.test.local
106
+
.env.production.local
107
+
.env.local
108
+
109
+
# parcel-bundler cache (https://parceljs.org/)
110
+
111
+
.parcel-cache
112
+
113
+
# Next.js build output
114
+
115
+
.next
116
+
out
117
+
118
+
# Nuxt.js build / generate output
119
+
120
+
.nuxt
121
+
dist
122
+
123
+
# Gatsby files
124
+
125
+
# Comment in the public line in if your project uses Gatsby and not Next.js
126
+
127
+
# https://nextjs.org/blog/next-9-1#public-directory-support
128
+
129
+
# public
130
+
131
+
# vuepress build output
132
+
133
+
.vuepress/dist
134
+
135
+
# vuepress v2.x temp and cache directory
136
+
137
+
.temp
138
+
139
+
# Docusaurus cache and generated files
140
+
141
+
.docusaurus
142
+
143
+
# Serverless directories
144
+
145
+
.serverless/
146
+
147
+
# FuseBox cache
148
+
149
+
.fusebox/
150
+
151
+
# DynamoDB Local files
152
+
153
+
.dynamodb/
154
+
155
+
# TernJS port file
156
+
157
+
.tern-port
158
+
159
+
# Stores VSCode versions used for testing VSCode extensions
160
+
161
+
.vscode-test
162
+
163
+
# yarn v2
164
+
165
+
.yarn/cache
166
+
.yarn/unplugged
167
+
.yarn/build-state.yml
168
+
.yarn/install-state.gz
169
+
.pnp.*
170
+
171
+
# IntelliJ based IDEs
172
+
.idea
173
+
174
+
# Finder (MacOS) folder config
175
+
.DS_Store
+15
test/README.md
+15
test/README.md
+32
test/bun.lock
+32
test/bun.lock
···
1
+
{
2
+
"lockfileVersion": 1,
3
+
"workspaces": {
4
+
"": {
5
+
"name": "test",
6
+
"dependencies": {
7
+
"k6": "^0.0.0",
8
+
},
9
+
"devDependencies": {
10
+
"@types/bun": "latest",
11
+
},
12
+
"peerDependencies": {
13
+
"typescript": "^5.0.0",
14
+
},
15
+
},
16
+
},
17
+
"packages": {
18
+
"@types/bun": ["@types/bun@1.2.0", "", { "dependencies": { "bun-types": "1.2.0" } }, "sha512-5N1JqdahfpBlAv4wy6svEYcd/YfO2GNrbL95JOmFx8nkE6dbK4R0oSE5SpBA4vBRqgrOUAXF8Dpiz+gi7r80SA=="],
19
+
20
+
"@types/node": ["@types/node@22.10.10", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-X47y/mPNzxviAGY5TcYPtYL8JsY3kAq2n8fMmKoRCxq/c4v4pyGNCzM2R6+M5/umG4ZfHuT+sgqDYqWc9rJ6ww=="],
21
+
22
+
"@types/ws": ["@types/ws@8.5.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw=="],
23
+
24
+
"bun-types": ["bun-types@1.2.0", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-KEaJxyZfbV/c4eyG0vyehDpYmBGreNiQbZIqvVHJwZ4BmeuWlNZ7EAzMN2Zcd7ailmS/tGVW0BgYbGf+lGEpWw=="],
25
+
26
+
"k6": ["k6@0.0.0", "", {}, "sha512-GAQSWayS2+LjbH5bkRi+pMPYyP1JSp7o+4j58ANZ762N/RH/SdlAT3CHHztnn8s/xgg8kYNM24Gd2IPo9b5W+g=="],
27
+
28
+
"typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="],
29
+
30
+
"undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
31
+
}
32
+
}
+1
test/index.ts
+1
test/index.ts
···
1
+
console.log("Hello via Bun!");
+39
test/mikubeam.js
+39
test/mikubeam.js
···
1
+
import http from "k6/http";
2
+
import { check, sleep } from "k6";
3
+
4
+
// Test configuration
5
+
export const options = {
6
+
stages: [
7
+
{ duration: "30s", target: 50 }, // Ramp up to 50 users
8
+
{ duration: "1m", target: 50 }, // Stay at 50 users for 1 minute
9
+
{ duration: "30s", target: 100 }, // Ramp up to 100 users
10
+
{ duration: "1m", target: 100 }, // Stay at 100 users for 1 minute
11
+
{ duration: "30s", target: 0 }, // Ramp down to 0 users
12
+
],
13
+
thresholds: {
14
+
http_req_duration: ["p(95)<500"], // 95% of requests should be below 500ms
15
+
"checks{type:redirect}": ["rate>0.95"], // 95% success rate
16
+
},
17
+
};
18
+
19
+
const SHORTENED_URL = "http://localhost:8080/mikubeam";
20
+
21
+
export default function () {
22
+
const res = http.get(SHORTENED_URL, {
23
+
tags: { type: "redirect" },
24
+
redirects: 0, // Don't follow redirects to measure just the redirect response
25
+
});
26
+
27
+
// Check if we got a redirect status (307)
28
+
check(
29
+
res,
30
+
{
31
+
"status is 307": (r) => r.status === 307,
32
+
"has location header": (r) => r.headers["Location"] !== undefined,
33
+
},
34
+
{ type: "redirect" }
35
+
);
36
+
37
+
sleep(1); // Add some think time between requests
38
+
}
39
+
+14
test/package.json
+14
test/package.json
+27
test/tsconfig.json
+27
test/tsconfig.json
···
1
+
{
2
+
"compilerOptions": {
3
+
// Enable latest features
4
+
"lib": ["ESNext", "DOM"],
5
+
"target": "ESNext",
6
+
"module": "ESNext",
7
+
"moduleDetection": "force",
8
+
"jsx": "react-jsx",
9
+
"allowJs": true,
10
+
11
+
// Bundler mode
12
+
"moduleResolution": "bundler",
13
+
"allowImportingTsExtensions": true,
14
+
"verbatimModuleSyntax": true,
15
+
"noEmit": true,
16
+
17
+
// Best practices
18
+
"strict": true,
19
+
"skipLibCheck": true,
20
+
"noFallthroughCasesInSwitch": true,
21
+
22
+
// Some stricter flags (disabled by default)
23
+
"noUnusedLocals": false,
24
+
"noUnusedParameters": false,
25
+
"noPropertyAccessFromIndexSignature": false
26
+
}
27
+
}