+85
bun.lock
+85
bun.lock
···
12
12
"@elysiajs/eden": "^1.4.3",
13
13
"@elysiajs/openapi": "^1.4.11",
14
14
"@elysiajs/static": "^1.4.2",
15
+
"@radix-ui/react-dialog": "^1.1.15",
16
+
"@radix-ui/react-label": "^2.1.7",
17
+
"@radix-ui/react-radio-group": "^1.3.8",
18
+
"@radix-ui/react-slot": "^1.2.3",
19
+
"@radix-ui/react-tabs": "^1.1.13",
15
20
"@tanstack/react-query": "^5.90.2",
21
+
"class-variance-authority": "^0.7.1",
16
22
"clsx": "^2.1.1",
17
23
"elysia": "latest",
18
24
"iron-session": "^8.0.4",
25
+
"lucide-react": "^0.546.0",
19
26
"react": "^19.2.0",
20
27
"react-dom": "^19.2.0",
28
+
"tailwind-merge": "^3.3.1",
21
29
"tailwindcss": "4",
30
+
"tw-animate-css": "^1.4.0",
22
31
},
23
32
"devDependencies": {
24
33
"@types/react": "^19.2.2",
···
129
138
130
139
"@oven/bun-windows-x64-baseline": ["@oven/bun-windows-x64-baseline@1.3.0", "", { "os": "win32", "cpu": "x64" }, "sha512-/jVZ8eYjpYHLDFNoT86cP+AjuWvpkzFY+0R0a1bdeu0sQ6ILuy1FV6hz1hUAP390E09VCo5oP76fnx29giHTtA=="],
131
140
141
+
"@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="],
142
+
143
+
"@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "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-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw=="],
144
+
145
+
"@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
146
+
147
+
"@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
148
+
149
+
"@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "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-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw=="],
150
+
151
+
"@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="],
152
+
153
+
"@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "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-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg=="],
154
+
155
+
"@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw=="],
156
+
157
+
"@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "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-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="],
158
+
159
+
"@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="],
160
+
161
+
"@radix-ui/react-label": ["@radix-ui/react-label@2.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "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-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ=="],
162
+
163
+
"@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "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-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="],
164
+
165
+
"@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "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-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="],
166
+
167
+
"@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "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-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
168
+
169
+
"@radix-ui/react-radio-group": ["@radix-ui/react-radio-group@1.3.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "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-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ=="],
170
+
171
+
"@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.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-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA=="],
172
+
173
+
"@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
174
+
175
+
"@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.13", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.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-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A=="],
176
+
177
+
"@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="],
178
+
179
+
"@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="],
180
+
181
+
"@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA=="],
182
+
183
+
"@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.1", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g=="],
184
+
185
+
"@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
186
+
187
+
"@radix-ui/react-use-previous": ["@radix-ui/react-use-previous@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ=="],
188
+
189
+
"@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ=="],
190
+
132
191
"@sinclair/typebox": ["@sinclair/typebox@0.34.41", "", {}, "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g=="],
133
192
134
193
"@tanstack/query-core": ["@tanstack/query-core@5.90.2", "", {}, "sha512-k/TcR3YalnzibscALLwxeiLUub6jN5EDLwKDiO7q5f4ICEoptJ+n9+7vcEFy5/x/i6Q+Lb/tXrsKCggf5uQJXQ=="],
···
153
212
154
213
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
155
214
215
+
"aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="],
216
+
156
217
"array-flatten": ["array-flatten@1.1.1", "", {}, "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="],
157
218
158
219
"atomic-sleep": ["atomic-sleep@1.0.0", "", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="],
···
189
250
190
251
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
191
252
253
+
"class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="],
254
+
192
255
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
193
256
194
257
"code-block-writer": ["code-block-writer@13.0.3", "", {}, "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg=="],
···
219
282
220
283
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
221
284
285
+
"detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="],
286
+
222
287
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
223
288
224
289
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
···
264
329
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
265
330
266
331
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
332
+
333
+
"get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="],
267
334
268
335
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
269
336
···
297
364
298
365
"lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
299
366
367
+
"lucide-react": ["lucide-react@0.546.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Z94u6fKT43lKeYHiVyvyR8fT7pwCzDu7RyMPpTvh054+xahSgj4HFQ+NmflvzdXsoAjYGdCguGaFKYuvq0ThCQ=="],
368
+
300
369
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
301
370
302
371
"media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="],
···
364
433
"react": ["react@19.2.0", "", {}, "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ=="],
365
434
366
435
"react-dom": ["react-dom@19.2.0", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ=="],
436
+
437
+
"react-remove-scroll": ["react-remove-scroll@2.7.1", "", { "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-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA=="],
438
+
439
+
"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=="],
440
+
441
+
"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=="],
367
442
368
443
"readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="],
369
444
···
403
478
404
479
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
405
480
481
+
"tailwind-merge": ["tailwind-merge@3.3.1", "", {}, "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g=="],
482
+
406
483
"tailwindcss": ["tailwindcss@4.1.14", "", {}, "sha512-b7pCxjGO98LnxVkKjaZSDeNuljC4ueKUddjENJOADtubtdo8llTaJy7HwBMeLNSSo2N5QIAgklslK1+Ir8r6CA=="],
407
484
408
485
"thread-stream": ["thread-stream@2.7.0", "", { "dependencies": { "real-require": "^0.2.0" } }, "sha512-qQiRWsU/wvNolI6tbbCKd9iKaTnCXsTwVxhhKM6nctPdujTyztjlbUkUTUymidWcMnZ5pWR0ej4a0tjsW021vw=="],
···
417
494
418
495
"ts-morph": ["ts-morph@24.0.0", "", { "dependencies": { "@ts-morph/common": "~0.25.0", "code-block-writer": "^13.0.3" } }, "sha512-2OAOg/Ob5yx9Et7ZX4CvTCc0UFoZHwLEJ+dpDPSUi5TgwwlTlX47w+iFRrEwzUZwYACjq83cgjS/Da50Ga37uw=="],
419
496
497
+
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
498
+
499
+
"tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="],
500
+
420
501
"type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="],
421
502
422
503
"uint8array-extras": ["uint8array-extras@1.5.0", "", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="],
···
430
511
"undici-types": ["undici-types@7.14.0", "", {}, "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA=="],
431
512
432
513
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
514
+
515
+
"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=="],
516
+
517
+
"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=="],
433
518
434
519
"utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="],
435
520
+21
components.json
+21
components.json
···
1
+
{
2
+
"$schema": "https://ui.shadcn.com/schema.json",
3
+
"style": "new-york",
4
+
"rsc": false,
5
+
"tsx": true,
6
+
"tailwind": {
7
+
"config": "",
8
+
"css": "src/styles/globals.css",
9
+
"baseColor": "neutral",
10
+
"cssVariables": true,
11
+
"prefix": ""
12
+
},
13
+
"aliases": {
14
+
"components": "@public/components",
15
+
"utils": "@public/lib/utils",
16
+
"ui": "@public/components/ui",
17
+
"lib": "@public/libs",
18
+
"hooks": "@public/hooks"
19
+
},
20
+
"iconLibrary": "lucide"
21
+
}
+10
-1
package.json
+10
-1
package.json
···
15
15
"@elysiajs/eden": "^1.4.3",
16
16
"@elysiajs/openapi": "^1.4.11",
17
17
"@elysiajs/static": "^1.4.2",
18
+
"@radix-ui/react-dialog": "^1.1.15",
19
+
"@radix-ui/react-label": "^2.1.7",
20
+
"@radix-ui/react-radio-group": "^1.3.8",
21
+
"@radix-ui/react-slot": "^1.2.3",
22
+
"@radix-ui/react-tabs": "^1.1.13",
18
23
"@tanstack/react-query": "^5.90.2",
24
+
"class-variance-authority": "^0.7.1",
19
25
"clsx": "^2.1.1",
20
26
"elysia": "latest",
21
27
"iron-session": "^8.0.4",
28
+
"lucide-react": "^0.546.0",
22
29
"react": "^19.2.0",
23
30
"react-dom": "^19.2.0",
24
-
"tailwindcss": "4"
31
+
"tailwind-merge": "^3.3.1",
32
+
"tailwindcss": "4",
33
+
"tw-animate-css": "^1.4.0"
25
34
},
26
35
"devDependencies": {
27
36
"@types/react": "^19.2.2",
+46
public/components/ui/badge.tsx
+46
public/components/ui/badge.tsx
···
1
+
import * as React from "react"
2
+
import { Slot } from "@radix-ui/react-slot"
3
+
import { cva, type VariantProps } from "class-variance-authority"
4
+
5
+
import { cn } from "@public/lib/utils"
6
+
7
+
const badgeVariants = cva(
8
+
"inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
9
+
{
10
+
variants: {
11
+
variant: {
12
+
default:
13
+
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
14
+
secondary:
15
+
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
16
+
destructive:
17
+
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
18
+
outline:
19
+
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
20
+
},
21
+
},
22
+
defaultVariants: {
23
+
variant: "default",
24
+
},
25
+
}
26
+
)
27
+
28
+
function Badge({
29
+
className,
30
+
variant,
31
+
asChild = false,
32
+
...props
33
+
}: React.ComponentProps<"span"> &
34
+
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
35
+
const Comp = asChild ? Slot : "span"
36
+
37
+
return (
38
+
<Comp
39
+
data-slot="badge"
40
+
className={cn(badgeVariants({ variant }), className)}
41
+
{...props}
42
+
/>
43
+
)
44
+
}
45
+
46
+
export { Badge, badgeVariants }
+92
public/components/ui/card.tsx
+92
public/components/ui/card.tsx
···
1
+
import * as React from "react"
2
+
3
+
import { cn } from "@public/lib/utils"
4
+
5
+
function Card({ className, ...props }: React.ComponentProps<"div">) {
6
+
return (
7
+
<div
8
+
data-slot="card"
9
+
className={cn(
10
+
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
11
+
className
12
+
)}
13
+
{...props}
14
+
/>
15
+
)
16
+
}
17
+
18
+
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
19
+
return (
20
+
<div
21
+
data-slot="card-header"
22
+
className={cn(
23
+
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
24
+
className
25
+
)}
26
+
{...props}
27
+
/>
28
+
)
29
+
}
30
+
31
+
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
32
+
return (
33
+
<div
34
+
data-slot="card-title"
35
+
className={cn("leading-none font-semibold", className)}
36
+
{...props}
37
+
/>
38
+
)
39
+
}
40
+
41
+
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
42
+
return (
43
+
<div
44
+
data-slot="card-description"
45
+
className={cn("text-muted-foreground text-sm", className)}
46
+
{...props}
47
+
/>
48
+
)
49
+
}
50
+
51
+
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
52
+
return (
53
+
<div
54
+
data-slot="card-action"
55
+
className={cn(
56
+
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
57
+
className
58
+
)}
59
+
{...props}
60
+
/>
61
+
)
62
+
}
63
+
64
+
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
65
+
return (
66
+
<div
67
+
data-slot="card-content"
68
+
className={cn("px-6", className)}
69
+
{...props}
70
+
/>
71
+
)
72
+
}
73
+
74
+
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
75
+
return (
76
+
<div
77
+
data-slot="card-footer"
78
+
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
79
+
{...props}
80
+
/>
81
+
)
82
+
}
83
+
84
+
export {
85
+
Card,
86
+
CardHeader,
87
+
CardFooter,
88
+
CardTitle,
89
+
CardAction,
90
+
CardDescription,
91
+
CardContent,
92
+
}
+141
public/components/ui/dialog.tsx
+141
public/components/ui/dialog.tsx
···
1
+
import * as React from "react"
2
+
import * as DialogPrimitive from "@radix-ui/react-dialog"
3
+
import { XIcon } from "lucide-react"
4
+
5
+
import { cn } from "@public/lib/utils"
6
+
7
+
function Dialog({
8
+
...props
9
+
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
10
+
return <DialogPrimitive.Root data-slot="dialog" {...props} />
11
+
}
12
+
13
+
function DialogTrigger({
14
+
...props
15
+
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
16
+
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
17
+
}
18
+
19
+
function DialogPortal({
20
+
...props
21
+
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
22
+
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
23
+
}
24
+
25
+
function DialogClose({
26
+
...props
27
+
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
28
+
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
29
+
}
30
+
31
+
function DialogOverlay({
32
+
className,
33
+
...props
34
+
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
35
+
return (
36
+
<DialogPrimitive.Overlay
37
+
data-slot="dialog-overlay"
38
+
className={cn(
39
+
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
40
+
className
41
+
)}
42
+
{...props}
43
+
/>
44
+
)
45
+
}
46
+
47
+
function DialogContent({
48
+
className,
49
+
children,
50
+
showCloseButton = true,
51
+
...props
52
+
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
53
+
showCloseButton?: boolean
54
+
}) {
55
+
return (
56
+
<DialogPortal data-slot="dialog-portal">
57
+
<DialogOverlay />
58
+
<DialogPrimitive.Content
59
+
data-slot="dialog-content"
60
+
className={cn(
61
+
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
62
+
className
63
+
)}
64
+
{...props}
65
+
>
66
+
{children}
67
+
{showCloseButton && (
68
+
<DialogPrimitive.Close
69
+
data-slot="dialog-close"
70
+
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
71
+
>
72
+
<XIcon />
73
+
<span className="sr-only">Close</span>
74
+
</DialogPrimitive.Close>
75
+
)}
76
+
</DialogPrimitive.Content>
77
+
</DialogPortal>
78
+
)
79
+
}
80
+
81
+
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
82
+
return (
83
+
<div
84
+
data-slot="dialog-header"
85
+
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
86
+
{...props}
87
+
/>
88
+
)
89
+
}
90
+
91
+
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
92
+
return (
93
+
<div
94
+
data-slot="dialog-footer"
95
+
className={cn(
96
+
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
97
+
className
98
+
)}
99
+
{...props}
100
+
/>
101
+
)
102
+
}
103
+
104
+
function DialogTitle({
105
+
className,
106
+
...props
107
+
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
108
+
return (
109
+
<DialogPrimitive.Title
110
+
data-slot="dialog-title"
111
+
className={cn("text-lg leading-none font-semibold", className)}
112
+
{...props}
113
+
/>
114
+
)
115
+
}
116
+
117
+
function DialogDescription({
118
+
className,
119
+
...props
120
+
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
121
+
return (
122
+
<DialogPrimitive.Description
123
+
data-slot="dialog-description"
124
+
className={cn("text-muted-foreground text-sm", className)}
125
+
{...props}
126
+
/>
127
+
)
128
+
}
129
+
130
+
export {
131
+
Dialog,
132
+
DialogClose,
133
+
DialogContent,
134
+
DialogDescription,
135
+
DialogFooter,
136
+
DialogHeader,
137
+
DialogOverlay,
138
+
DialogPortal,
139
+
DialogTitle,
140
+
DialogTrigger,
141
+
}
+21
public/components/ui/input.tsx
+21
public/components/ui/input.tsx
···
1
+
import * as React from "react"
2
+
3
+
import { cn } from "@public/lib/utils"
4
+
5
+
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
6
+
return (
7
+
<input
8
+
type={type}
9
+
data-slot="input"
10
+
className={cn(
11
+
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
12
+
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
13
+
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
14
+
className
15
+
)}
16
+
{...props}
17
+
/>
18
+
)
19
+
}
20
+
21
+
export { Input }
+22
public/components/ui/label.tsx
+22
public/components/ui/label.tsx
···
1
+
import * as React from "react"
2
+
import * as LabelPrimitive from "@radix-ui/react-label"
3
+
4
+
import { cn } from "@public/lib/utils"
5
+
6
+
function Label({
7
+
className,
8
+
...props
9
+
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
10
+
return (
11
+
<LabelPrimitive.Root
12
+
data-slot="label"
13
+
className={cn(
14
+
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
15
+
className
16
+
)}
17
+
{...props}
18
+
/>
19
+
)
20
+
}
21
+
22
+
export { Label }
+45
public/components/ui/radio-group.tsx
+45
public/components/ui/radio-group.tsx
···
1
+
"use client"
2
+
3
+
import * as React from "react"
4
+
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
5
+
import { CircleIcon } from "lucide-react"
6
+
7
+
import { cn } from "@public/lib/utils"
8
+
9
+
function RadioGroup({
10
+
className,
11
+
...props
12
+
}: React.ComponentProps<typeof RadioGroupPrimitive.Root>) {
13
+
return (
14
+
<RadioGroupPrimitive.Root
15
+
data-slot="radio-group"
16
+
className={cn("grid gap-3", className)}
17
+
{...props}
18
+
/>
19
+
)
20
+
}
21
+
22
+
function RadioGroupItem({
23
+
className,
24
+
...props
25
+
}: React.ComponentProps<typeof RadioGroupPrimitive.Item>) {
26
+
return (
27
+
<RadioGroupPrimitive.Item
28
+
data-slot="radio-group-item"
29
+
className={cn(
30
+
"border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
31
+
className
32
+
)}
33
+
{...props}
34
+
>
35
+
<RadioGroupPrimitive.Indicator
36
+
data-slot="radio-group-indicator"
37
+
className="relative flex items-center justify-center"
38
+
>
39
+
<CircleIcon className="fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2" />
40
+
</RadioGroupPrimitive.Indicator>
41
+
</RadioGroupPrimitive.Item>
42
+
)
43
+
}
44
+
45
+
export { RadioGroup, RadioGroupItem }
+64
public/components/ui/tabs.tsx
+64
public/components/ui/tabs.tsx
···
1
+
import * as React from "react"
2
+
import * as TabsPrimitive from "@radix-ui/react-tabs"
3
+
4
+
import { cn } from "@public/lib/utils"
5
+
6
+
function Tabs({
7
+
className,
8
+
...props
9
+
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
10
+
return (
11
+
<TabsPrimitive.Root
12
+
data-slot="tabs"
13
+
className={cn("flex flex-col gap-2", className)}
14
+
{...props}
15
+
/>
16
+
)
17
+
}
18
+
19
+
function TabsList({
20
+
className,
21
+
...props
22
+
}: React.ComponentProps<typeof TabsPrimitive.List>) {
23
+
return (
24
+
<TabsPrimitive.List
25
+
data-slot="tabs-list"
26
+
className={cn(
27
+
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]",
28
+
className
29
+
)}
30
+
{...props}
31
+
/>
32
+
)
33
+
}
34
+
35
+
function TabsTrigger({
36
+
className,
37
+
...props
38
+
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
39
+
return (
40
+
<TabsPrimitive.Trigger
41
+
data-slot="tabs-trigger"
42
+
className={cn(
43
+
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
44
+
className
45
+
)}
46
+
{...props}
47
+
/>
48
+
)
49
+
}
50
+
51
+
function TabsContent({
52
+
className,
53
+
...props
54
+
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
55
+
return (
56
+
<TabsPrimitive.Content
57
+
data-slot="tabs-content"
58
+
className={cn("outline-none", className)}
59
+
{...props}
60
+
/>
61
+
)
62
+
}
63
+
64
+
export { Tabs, TabsList, TabsTrigger, TabsContent }
+503
-92
public/editor/editor.tsx
+503
-92
public/editor/editor.tsx
···
1
-
import { useState, useRef } from 'react'
1
+
import { useState } from 'react'
2
2
import { createRoot } from 'react-dom/client'
3
+
import { Button } from '@public/components/ui/button'
4
+
import {
5
+
Card,
6
+
CardContent,
7
+
CardDescription,
8
+
CardHeader,
9
+
CardTitle
10
+
} from '@public/components/ui/card'
11
+
import { Input } from '@public/components/ui/input'
12
+
import { Label } from '@public/components/ui/label'
13
+
import {
14
+
Tabs,
15
+
TabsContent,
16
+
TabsList,
17
+
TabsTrigger
18
+
} from '@public/components/ui/tabs'
19
+
import { Badge } from '@public/components/ui/badge'
20
+
import {
21
+
Dialog,
22
+
DialogContent,
23
+
DialogDescription,
24
+
DialogHeader,
25
+
DialogTitle,
26
+
DialogFooter
27
+
} from '@public/components/ui/dialog'
28
+
import { RadioGroup, RadioGroupItem } from '@public/components/ui/radio-group'
29
+
import {
30
+
Globe,
31
+
Upload,
32
+
Settings,
33
+
ExternalLink,
34
+
CheckCircle2,
35
+
XCircle,
36
+
AlertCircle
37
+
} from 'lucide-react'
3
38
4
39
import Layout from '@public/layouts'
5
40
6
-
function Editor() {
7
-
const [uploading, setUploading] = useState(false)
8
-
const [result, setResult] = useState<any>(null)
9
-
const [error, setError] = useState<string | null>(null)
10
-
const folderInputRef = useRef<HTMLInputElement>(null)
11
-
const siteNameRef = useRef<HTMLInputElement>(null)
41
+
// Mock user data - replace with actual auth
42
+
const mockUser = {
43
+
did: 'did:plc:abc123xyz',
44
+
handle: 'alice.bsky.social',
45
+
wispSubdomain: 'alice'
46
+
}
12
47
13
-
const handleFileUpload = async (e: React.FormEvent) => {
14
-
e.preventDefault()
15
-
setError(null)
16
-
setResult(null)
48
+
function Dashboard() {
49
+
const [customDomain, setCustomDomain] = useState('')
50
+
const [verificationStatus, setVerificationStatus] = useState<
51
+
'idle' | 'verifying' | 'success' | 'error'
52
+
>('idle')
53
+
const [selectedSite, setSelectedSite] = useState('')
17
54
18
-
const files = folderInputRef.current?.files
19
-
const siteName = siteNameRef.current?.value
55
+
const [configureModalOpen, setConfigureModalOpen] = useState(false)
56
+
const [addDomainModalOpen, setAddDomainModalOpen] = useState(false)
57
+
const [currentSite, setCurrentSite] = useState<{
58
+
id: string
59
+
name: string
60
+
domain: string | null
61
+
} | null>(null)
62
+
const [selectedDomain, setSelectedDomain] = useState<string>('')
20
63
21
-
if (!files || files.length === 0) {
22
-
setError('Please select a folder to upload')
23
-
return
64
+
// Mock sites data
65
+
const [sites] = useState([
66
+
{
67
+
id: '1',
68
+
name: 'my-blog',
69
+
domain: 'alice.wisp.place',
70
+
status: 'active'
71
+
},
72
+
{ id: '2', name: 'portfolio', domain: null, status: 'active' },
73
+
{
74
+
id: '3',
75
+
name: 'docs-site',
76
+
domain: 'docs.example.com',
77
+
status: 'active'
24
78
}
79
+
])
25
80
26
-
if (!siteName) {
27
-
setError('Please enter a site name')
28
-
return
29
-
}
81
+
const availableDomains = [
82
+
{ value: 'alice.wisp.place', label: 'alice.wisp.place', type: 'wisp' },
83
+
{
84
+
value: 'docs.example.com',
85
+
label: 'docs.example.com',
86
+
type: 'custom'
87
+
},
88
+
{ value: 'none', label: 'No domain (use default URL)', type: 'none' }
89
+
]
30
90
31
-
setUploading(true)
32
-
33
-
try {
34
-
const formData = new FormData()
35
-
formData.append('siteName', siteName)
36
-
37
-
for (let i = 0; i < files.length; i++) {
38
-
formData.append('files', files[i])
39
-
}
91
+
const handleVerifyDNS = async () => {
92
+
setVerificationStatus('verifying')
93
+
// Simulate DNS verification
94
+
setTimeout(() => {
95
+
setVerificationStatus('success')
96
+
}, 2000)
97
+
}
40
98
41
-
const response = await fetch('/wisp/upload-files', {
42
-
method: 'POST',
43
-
body: formData
44
-
})
99
+
const handleConfigureSite = (site: {
100
+
id: string
101
+
name: string
102
+
domain: string | null
103
+
}) => {
104
+
setCurrentSite(site)
105
+
setSelectedDomain(site.domain || 'none')
106
+
setConfigureModalOpen(true)
107
+
}
45
108
46
-
if (!response.ok) {
47
-
throw new Error(`Upload failed: ${response.statusText}`)
48
-
}
109
+
const handleSaveConfiguration = () => {
110
+
console.log(
111
+
'[v0] Saving configuration for site:',
112
+
currentSite?.name,
113
+
'with domain:',
114
+
selectedDomain
115
+
)
116
+
// TODO: Implement actual save logic
117
+
setConfigureModalOpen(false)
118
+
}
49
119
50
-
const data = await response.json()
51
-
setResult(data)
52
-
} catch (err) {
53
-
setError(err instanceof Error ? err.message : 'Upload failed')
54
-
} finally {
55
-
setUploading(false)
120
+
const getSiteUrl = (site: { name: string; domain: string | null }) => {
121
+
if (site.domain) {
122
+
return `https://${site.domain}`
56
123
}
124
+
return `https://sites.wisp.place/${mockUser.did}/${site.name}`
57
125
}
58
126
59
127
return (
60
-
<div className="w-full max-w-2xl mx-auto p-6">
61
-
<h1 className="text-3xl font-bold mb-6 text-center">Upload Folder</h1>
62
-
63
-
<form onSubmit={handleFileUpload} className="space-y-4">
64
-
<div>
65
-
<label htmlFor="siteName" className="block text-sm font-medium mb-2">
66
-
Site Name
67
-
</label>
68
-
<input
69
-
ref={siteNameRef}
70
-
type="text"
71
-
id="siteName"
72
-
placeholder="Enter site name"
73
-
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
74
-
/>
128
+
<div className="w-full min-h-screen bg-background">
129
+
{/* Header */}
130
+
<header className="border-b border-border/40 bg-background/80 backdrop-blur-sm sticky top-0 z-50">
131
+
<div className="container mx-auto px-4 py-4 flex items-center justify-between">
132
+
<div className="flex items-center gap-2">
133
+
<div className="w-8 h-8 bg-primary rounded-lg flex items-center justify-center">
134
+
<Globe className="w-5 h-5 text-primary-foreground" />
135
+
</div>
136
+
<span className="text-xl font-semibold text-foreground">
137
+
wisp.place
138
+
</span>
139
+
</div>
140
+
<div className="flex items-center gap-3">
141
+
<span className="text-sm text-muted-foreground">
142
+
{mockUser.handle}
143
+
</span>
144
+
</div>
75
145
</div>
146
+
</header>
76
147
77
-
<div>
78
-
<label htmlFor="folder" className="block text-sm font-medium mb-2">
79
-
Select Folder
80
-
</label>
81
-
<input
82
-
ref={folderInputRef}
83
-
type="file"
84
-
id="folder"
85
-
{...({ webkitdirectory: '', directory: '' } as any)}
86
-
multiple
87
-
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
88
-
/>
148
+
<div className="container mx-auto px-4 py-8 max-w-6xl w-full">
149
+
<div className="mb-8">
150
+
<h1 className="text-3xl font-bold mb-2">Dashboard</h1>
151
+
<p className="text-muted-foreground">
152
+
Manage your sites and domains
153
+
</p>
89
154
</div>
90
155
91
-
<button
92
-
type="submit"
93
-
disabled={uploading}
94
-
className="w-full bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 text-white font-semibold py-2 px-4 rounded-md transition-colors"
95
-
>
96
-
{uploading ? 'Uploading...' : 'Upload Folder'}
97
-
</button>
98
-
</form>
156
+
<Tabs defaultValue="sites" className="space-y-6 w-full">
157
+
<TabsList className="grid w-full grid-cols-3 max-w-md">
158
+
<TabsTrigger value="sites">Sites</TabsTrigger>
159
+
<TabsTrigger value="domains">Domains</TabsTrigger>
160
+
<TabsTrigger value="upload">Upload</TabsTrigger>
161
+
</TabsList>
162
+
163
+
{/* Sites Tab */}
164
+
<TabsContent value="sites" className="space-y-4 min-h-[400px]">
165
+
<Card>
166
+
<CardHeader>
167
+
<CardTitle>Your Sites</CardTitle>
168
+
<CardDescription>
169
+
View and manage all your deployed sites
170
+
</CardDescription>
171
+
</CardHeader>
172
+
<CardContent className="space-y-4">
173
+
{sites.map((site) => (
174
+
<div
175
+
key={site.id}
176
+
className="flex items-center justify-between p-4 border border-border rounded-lg hover:bg-muted/50 transition-colors"
177
+
>
178
+
<div className="flex-1">
179
+
<div className="flex items-center gap-3 mb-2">
180
+
<h3 className="font-semibold text-lg">
181
+
{site.name}
182
+
</h3>
183
+
<Badge
184
+
variant="secondary"
185
+
className="text-xs"
186
+
>
187
+
{site.status}
188
+
</Badge>
189
+
</div>
190
+
<a
191
+
href={getSiteUrl(site)}
192
+
target="_blank"
193
+
rel="noopener noreferrer"
194
+
className="text-sm text-accent hover:text-accent/80 flex items-center gap-1"
195
+
>
196
+
{site.domain ||
197
+
`sites.wisp.place/${mockUser.did}/${site.name}`}
198
+
<ExternalLink className="w-3 h-3" />
199
+
</a>
200
+
</div>
201
+
<Button
202
+
variant="outline"
203
+
size="sm"
204
+
onClick={() =>
205
+
handleConfigureSite(site)
206
+
}
207
+
>
208
+
<Settings className="w-4 h-4 mr-2" />
209
+
Configure
210
+
</Button>
211
+
</div>
212
+
))}
213
+
</CardContent>
214
+
</Card>
215
+
</TabsContent>
216
+
217
+
{/* Domains Tab */}
218
+
<TabsContent value="domains" className="space-y-4 min-h-[400px]">
219
+
<Card>
220
+
<CardHeader>
221
+
<CardTitle>wisp.place Subdomain</CardTitle>
222
+
<CardDescription>
223
+
Your free subdomain on the wisp.place
224
+
network
225
+
</CardDescription>
226
+
</CardHeader>
227
+
<CardContent>
228
+
<div className="flex items-center gap-2 p-4 bg-muted/50 rounded-lg">
229
+
<CheckCircle2 className="w-5 h-5 text-green-500" />
230
+
<span className="font-mono text-lg">
231
+
{mockUser.wispSubdomain}.wisp.place
232
+
</span>
233
+
</div>
234
+
<p className="text-sm text-muted-foreground mt-3">
235
+
Configure which site uses this domain in the
236
+
Sites tab
237
+
</p>
238
+
</CardContent>
239
+
</Card>
240
+
241
+
<Card>
242
+
<CardHeader>
243
+
<CardTitle>Custom Domains</CardTitle>
244
+
<CardDescription>
245
+
Bring your own domain with DNS verification
246
+
</CardDescription>
247
+
</CardHeader>
248
+
<CardContent className="space-y-4">
249
+
<Button
250
+
onClick={() => setAddDomainModalOpen(true)}
251
+
className="w-full"
252
+
>
253
+
Add Custom Domain
254
+
</Button>
255
+
256
+
<div className="space-y-2">
257
+
<div className="flex items-center justify-between p-3 border border-border rounded-lg">
258
+
<div className="flex items-center gap-2">
259
+
<CheckCircle2 className="w-4 h-4 text-green-500" />
260
+
<span className="font-mono">
261
+
docs.example.com
262
+
</span>
263
+
</div>
264
+
<Badge variant="secondary">
265
+
Verified
266
+
</Badge>
267
+
</div>
268
+
</div>
269
+
</CardContent>
270
+
</Card>
271
+
</TabsContent>
272
+
273
+
{/* Upload Tab */}
274
+
<TabsContent value="upload" className="space-y-4 min-h-[400px]">
275
+
<Card>
276
+
<CardHeader>
277
+
<CardTitle>Upload Site</CardTitle>
278
+
<CardDescription>
279
+
Deploy a new site from a folder or Git
280
+
repository
281
+
</CardDescription>
282
+
</CardHeader>
283
+
<CardContent className="space-y-6">
284
+
<div className="space-y-2">
285
+
<Label htmlFor="site-name">Site Name</Label>
286
+
<Input
287
+
id="site-name"
288
+
placeholder="my-awesome-site"
289
+
/>
290
+
</div>
291
+
292
+
<div className="grid md:grid-cols-2 gap-4">
293
+
<Card className="border-2 border-dashed hover:border-accent transition-colors cursor-pointer">
294
+
<CardContent className="flex flex-col items-center justify-center p-8 text-center">
295
+
<Upload className="w-12 h-12 text-muted-foreground mb-4" />
296
+
<h3 className="font-semibold mb-2">
297
+
Upload Folder
298
+
</h3>
299
+
<p className="text-sm text-muted-foreground mb-4">
300
+
Drag and drop or click to upload
301
+
your static site files
302
+
</p>
303
+
<Button variant="outline">
304
+
Choose Folder
305
+
</Button>
306
+
</CardContent>
307
+
</Card>
308
+
309
+
<Card className="border-2 border-dashed hover:border-accent transition-colors">
310
+
<CardContent className="flex flex-col items-center justify-center p-8 text-center">
311
+
<Globe className="w-12 h-12 text-muted-foreground mb-4" />
312
+
<h3 className="font-semibold mb-2">
313
+
Connect Git Repository
314
+
</h3>
315
+
<p className="text-sm text-muted-foreground mb-4">
316
+
Link your GitHub, GitLab, or any
317
+
Git repository
318
+
</p>
319
+
<Button variant="outline">
320
+
Connect Git
321
+
</Button>
322
+
</CardContent>
323
+
</Card>
324
+
</div>
325
+
</CardContent>
326
+
</Card>
327
+
</TabsContent>
328
+
</Tabs>
329
+
</div>
330
+
331
+
<Dialog
332
+
open={configureModalOpen}
333
+
onOpenChange={setConfigureModalOpen}
334
+
>
335
+
<DialogContent className="sm:max-w-md">
336
+
<DialogHeader>
337
+
<DialogTitle>Configure Site Domain</DialogTitle>
338
+
<DialogDescription>
339
+
Choose which domain {currentSite?.name} should use
340
+
</DialogDescription>
341
+
</DialogHeader>
342
+
<div className="space-y-4 py-4">
343
+
<RadioGroup
344
+
value={selectedDomain}
345
+
onValueChange={setSelectedDomain}
346
+
>
347
+
{availableDomains.map((domain) => (
348
+
<div
349
+
key={domain.value}
350
+
className="flex items-center space-x-2"
351
+
>
352
+
<RadioGroupItem
353
+
value={domain.value}
354
+
id={domain.value}
355
+
/>
356
+
<Label
357
+
htmlFor={domain.value}
358
+
className="flex-1 cursor-pointer"
359
+
>
360
+
<div className="flex items-center justify-between">
361
+
<span className="font-mono text-sm">
362
+
{domain.label}
363
+
</span>
364
+
{domain.type === 'wisp' && (
365
+
<Badge
366
+
variant="secondary"
367
+
className="text-xs"
368
+
>
369
+
Free
370
+
</Badge>
371
+
)}
372
+
{domain.type === 'custom' && (
373
+
<Badge
374
+
variant="outline"
375
+
className="text-xs"
376
+
>
377
+
Custom
378
+
</Badge>
379
+
)}
380
+
</div>
381
+
</Label>
382
+
</div>
383
+
))}
384
+
</RadioGroup>
385
+
</div>
386
+
<DialogFooter>
387
+
<Button
388
+
variant="outline"
389
+
onClick={() => setConfigureModalOpen(false)}
390
+
>
391
+
Cancel
392
+
</Button>
393
+
<Button onClick={handleSaveConfiguration}>
394
+
Save Configuration
395
+
</Button>
396
+
</DialogFooter>
397
+
</DialogContent>
398
+
</Dialog>
399
+
400
+
<Dialog
401
+
open={addDomainModalOpen}
402
+
onOpenChange={setAddDomainModalOpen}
403
+
>
404
+
<DialogContent className="sm:max-w-lg">
405
+
<DialogHeader>
406
+
<DialogTitle>Add Custom Domain</DialogTitle>
407
+
<DialogDescription>
408
+
Configure DNS records to verify your domain
409
+
ownership
410
+
</DialogDescription>
411
+
</DialogHeader>
412
+
<div className="space-y-4 py-4">
413
+
<div className="space-y-2">
414
+
<Label htmlFor="new-domain">Domain Name</Label>
415
+
<Input
416
+
id="new-domain"
417
+
placeholder="example.com"
418
+
value={customDomain}
419
+
onChange={(e) =>
420
+
setCustomDomain(e.target.value)
421
+
}
422
+
/>
423
+
</div>
99
424
100
-
{error && (
101
-
<div className="mt-4 p-3 bg-red-100 border border-red-400 text-red-700 rounded-md">
102
-
{error}
103
-
</div>
104
-
)}
425
+
{customDomain && (
426
+
<div className="space-y-4 p-4 bg-muted/30 rounded-lg border border-border">
427
+
<div>
428
+
<h4 className="font-semibold mb-2 flex items-center gap-2">
429
+
<AlertCircle className="w-4 h-4 text-accent" />
430
+
DNS Configuration Required
431
+
</h4>
432
+
<p className="text-sm text-muted-foreground mb-4">
433
+
Add these DNS records to your domain
434
+
provider:
435
+
</p>
436
+
</div>
105
437
106
-
{result && (
107
-
<div className="mt-4 p-3 bg-green-100 border border-green-400 text-green-700 rounded-md">
108
-
<h3 className="font-semibold mb-2">Upload Successful!</h3>
109
-
<p>Files uploaded: {result.fileCount}</p>
110
-
<p>Site name: {result.siteName}</p>
111
-
<p>URI: {result.uri}</p>
112
-
</div>
113
-
)}
438
+
<div className="space-y-3">
439
+
<div className="p-3 bg-background rounded border border-border">
440
+
<div className="flex justify-between items-start mb-1">
441
+
<span className="text-xs font-semibold text-muted-foreground">
442
+
TXT Record
443
+
</span>
444
+
</div>
445
+
<div className="font-mono text-sm space-y-1">
446
+
<div>
447
+
<span className="text-muted-foreground">
448
+
Name:
449
+
</span>{' '}
450
+
_wisp
451
+
</div>
452
+
<div>
453
+
<span className="text-muted-foreground">
454
+
Value:
455
+
</span>{' '}
456
+
{mockUser.did}
457
+
</div>
458
+
</div>
459
+
</div>
460
+
461
+
<div className="p-3 bg-background rounded border border-border">
462
+
<div className="flex justify-between items-start mb-1">
463
+
<span className="text-xs font-semibold text-muted-foreground">
464
+
CNAME Record
465
+
</span>
466
+
</div>
467
+
<div className="font-mono text-sm space-y-1">
468
+
<div>
469
+
<span className="text-muted-foreground">
470
+
Name:
471
+
</span>{' '}
472
+
@ or {customDomain}
473
+
</div>
474
+
<div>
475
+
<span className="text-muted-foreground">
476
+
Value:
477
+
</span>{' '}
478
+
abc123.dns.wisp.place
479
+
</div>
480
+
</div>
481
+
</div>
482
+
</div>
483
+
</div>
484
+
)}
485
+
</div>
486
+
<DialogFooter className="flex-col sm:flex-row gap-2">
487
+
<Button
488
+
variant="outline"
489
+
onClick={() => {
490
+
setAddDomainModalOpen(false)
491
+
setCustomDomain('')
492
+
setVerificationStatus('idle')
493
+
}}
494
+
className="w-full sm:w-auto"
495
+
>
496
+
Cancel
497
+
</Button>
498
+
<Button
499
+
onClick={handleVerifyDNS}
500
+
disabled={
501
+
!customDomain ||
502
+
verificationStatus === 'verifying'
503
+
}
504
+
className="w-full sm:w-auto"
505
+
>
506
+
{verificationStatus === 'verifying' ? (
507
+
<>Verifying DNS...</>
508
+
) : verificationStatus === 'success' ? (
509
+
<>
510
+
<CheckCircle2 className="w-4 h-4 mr-2" />
511
+
Verified
512
+
</>
513
+
) : verificationStatus === 'error' ? (
514
+
<>
515
+
<XCircle className="w-4 h-4 mr-2" />
516
+
Verification Failed
517
+
</>
518
+
) : (
519
+
<>Verify DNS Records</>
520
+
)}
521
+
</Button>
522
+
</DialogFooter>
523
+
</DialogContent>
524
+
</Dialog>
114
525
</div>
115
526
)
116
527
}
···
118
529
const root = createRoot(document.getElementById('elysia')!)
119
530
root.render(
120
531
<Layout className="gap-6">
121
-
<Editor />
532
+
<Dashboard />
122
533
</Layout>
123
-
)
534
+
)
public/images/maddelena-1.webp
public/images/maddelena-1.webp
This is a binary file and will not be displayed.
public/images/maddelena-2.webp
public/images/maddelena-2.webp
This is a binary file and will not be displayed.
+281
-127
public/index.tsx
+281
-127
public/index.tsx
···
1
1
import { useState, useRef, useEffect } from 'react'
2
2
import { createRoot } from 'react-dom/client'
3
+
import {
4
+
ArrowRight,
5
+
Shield,
6
+
Zap,
7
+
Globe,
8
+
Lock,
9
+
Code,
10
+
Server
11
+
} from 'lucide-react'
3
12
4
13
import Layout from '@public/layouts'
14
+
import { Button } from '@public/components/ui/button'
15
+
import { Card } from '@public/components/ui/card'
5
16
6
17
function App() {
7
18
const [showForm, setShowForm] = useState(false)
···
14
25
}, [showForm])
15
26
16
27
return (
17
-
<>
18
-
<section id="header" className="py-24 px-6">
19
-
<div className="text-center space-y-8">
20
-
<div className="space-y-4">
21
-
<h1 className="text-6xl md:text-8xl font-bold text-balance leading-tight">
22
-
The complete platform to{' '}
23
-
<span className="gradient-text">
24
-
publish the web.
25
-
</span>
26
-
</h1>
27
-
<p className="text-xl md:text-2xl text-muted-foreground max-w-3xl mx-auto text-balance">
28
-
Your decentralized toolkit to stop configuring and
29
-
start publishing. Securely build, deploy, and own
30
-
your web presence with AT Protocol.
31
-
</p>
28
+
<div className="min-h-screen">
29
+
{/* Header */}
30
+
<header className="border-b border-border/40 bg-background/80 backdrop-blur-sm sticky top-0 z-50">
31
+
<div className="container mx-auto px-4 py-4 flex items-center justify-between">
32
+
<div className="flex items-center gap-2">
33
+
<div className="w-8 h-8 bg-primary rounded-lg flex items-center justify-center">
34
+
<Globe className="w-5 h-5 text-primary-foreground" />
35
+
</div>
36
+
<span className="text-xl font-semibold text-foreground">
37
+
wisp.place
38
+
</span>
32
39
</div>
33
-
34
-
<div className="inline-flex items-center gap-2 bg-accent/10 border border-accent/20 rounded-full px-4 py-2">
35
-
<svg
36
-
xmlns="http://www.w3.org/2000/svg"
37
-
className="h-5 w-5 text-accent"
38
-
viewBox="0 0 20 20"
39
-
fill="currentColor"
40
+
<div className="flex items-center gap-3">
41
+
<Button
42
+
variant="ghost"
43
+
size="sm"
44
+
onClick={() => setShowForm(true)}
40
45
>
41
-
<path
42
-
fillRule="evenodd"
43
-
d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.414-1.414L11 9.586V6z"
44
-
clipRule="evenodd"
45
-
/>
46
-
</svg>
47
-
<span className="text-sm font-medium text-accent">
48
-
Publish once, own forever
46
+
Sign In
47
+
</Button>
48
+
<Button
49
+
size="sm"
50
+
className="bg-accent text-accent-foreground hover:bg-accent/90"
51
+
>
52
+
Get Started
53
+
</Button>
54
+
</div>
55
+
</div>
56
+
</header>
57
+
58
+
{/* Hero Section */}
59
+
<section className="container mx-auto px-4 py-20 md:py-32">
60
+
<div className="max-w-4xl mx-auto text-center">
61
+
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-accent/10 border border-accent/20 mb-8">
62
+
<span className="w-2 h-2 bg-accent rounded-full animate-pulse"></span>
63
+
<span className="text-sm text-accent-foreground">
64
+
Built on AT Protocol
49
65
</span>
50
66
</div>
51
67
52
-
<div className="max-w-md mx-auto space-y-4 mt-8">
53
-
<div className="relative h-16">
54
-
<div
55
-
className={`transition-all duration-500 ease-in-out absolute inset-0 ${
56
-
showForm
57
-
? 'opacity-0 -translate-y-5 pointer-events-none'
58
-
: 'opacity-100 translate-y-0'
59
-
}`}
60
-
>
61
-
<button
62
-
onClick={() => setShowForm(true)}
63
-
className="w-full bg-primary hover:bg-primary/90 text-primary-foreground font-semibold py-4 px-6 text-lg rounded-lg inline-flex items-center justify-center transition-colors"
64
-
>
65
-
Log in with AT Proto
66
-
<svg
67
-
xmlns="http://www.w3.org/2000/svg"
68
-
className="ml-2 w-5 h-5"
69
-
viewBox="0 0 24 24"
70
-
fill="none"
71
-
stroke="currentColor"
72
-
strokeWidth="2"
73
-
strokeLinecap="round"
74
-
strokeLinejoin="round"
75
-
>
76
-
<path d="M5 12h14M12 5l7 7-7 7" />
77
-
</svg>
78
-
</button>
79
-
</div>
68
+
<h1 className="text-5xl md:text-7xl font-bold text-balance mb-6 leading-tight">
69
+
Host your sites on the{' '}
70
+
<span className="text-primary">decentralized</span> web
71
+
</h1>
80
72
81
-
<div
82
-
className={`transition-all duration-500 ease-in-out absolute inset-0 ${
83
-
showForm
84
-
? 'opacity-100 translate-y-0'
85
-
: 'opacity-0 translate-y-5 pointer-events-none'
86
-
}`}
73
+
<p className="text-xl md:text-2xl text-muted-foreground text-balance mb-10 leading-relaxed max-w-3xl mx-auto">
74
+
Deploy static sites to a truly open network. Your
75
+
content, your control, your identity. No platform
76
+
lock-in, ever.
77
+
</p>
78
+
79
+
<div className="max-w-md mx-auto relative">
80
+
<div
81
+
className={`transition-all duration-500 ease-in-out ${
82
+
showForm
83
+
? 'opacity-0 -translate-y-5 pointer-events-none'
84
+
: 'opacity-100 translate-y-0'
85
+
}`}
86
+
>
87
+
<Button
88
+
size="lg"
89
+
className="bg-primary text-primary-foreground hover:bg-primary/90 text-lg px-8 py-6 w-full"
90
+
onClick={() => setShowForm(true)}
87
91
>
88
-
<form
89
-
onSubmit={async (e) => {
90
-
e.preventDefault()
91
-
try {
92
-
const handle =
93
-
inputRef.current?.value
94
-
const res = await fetch(
95
-
'/api/auth/signin',
96
-
{
97
-
method: 'POST',
98
-
headers: {
99
-
'Content-Type':
100
-
'application/json'
101
-
},
102
-
body: JSON.stringify({
103
-
handle
104
-
})
105
-
}
106
-
)
107
-
if (!res.ok)
108
-
throw new Error(
109
-
'Request failed'
110
-
)
111
-
const data = await res.json()
112
-
if (data.url) {
113
-
window.location.href = data.url
114
-
} else {
115
-
alert('Unexpected response')
92
+
Log in with AT Proto
93
+
<ArrowRight className="ml-2 w-5 h-5" />
94
+
</Button>
95
+
</div>
96
+
97
+
<div
98
+
className={`transition-all duration-500 ease-in-out absolute inset-0 ${
99
+
showForm
100
+
? 'opacity-100 translate-y-0'
101
+
: 'opacity-0 translate-y-5 pointer-events-none'
102
+
}`}
103
+
>
104
+
<form
105
+
onSubmit={async (e) => {
106
+
e.preventDefault()
107
+
try {
108
+
const handle = inputRef.current?.value
109
+
const res = await fetch(
110
+
'/api/auth/signin',
111
+
{
112
+
method: 'POST',
113
+
headers: {
114
+
'Content-Type':
115
+
'application/json'
116
+
},
117
+
body: JSON.stringify({ handle })
116
118
}
117
-
} catch (error) {
118
-
console.error(
119
-
'Login failed:',
120
-
error
121
-
)
122
-
alert('Authentication failed')
119
+
)
120
+
if (!res.ok)
121
+
throw new Error('Request failed')
122
+
const data = await res.json()
123
+
if (data.url) {
124
+
window.location.href = data.url
125
+
} else {
126
+
alert('Unexpected response')
123
127
}
124
-
}}
125
-
className="space-y-3"
128
+
} catch (error) {
129
+
console.error('Login failed:', error)
130
+
alert('Authentication failed')
131
+
}
132
+
}}
133
+
className="space-y-3"
134
+
>
135
+
<input
136
+
ref={inputRef}
137
+
type="text"
138
+
name="handle"
139
+
placeholder="Enter your handle (e.g., alice.bsky.social)"
140
+
className="w-full py-4 px-4 text-lg bg-input border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-accent"
141
+
/>
142
+
<button
143
+
type="submit"
144
+
className="w-full bg-accent hover:bg-accent/90 text-accent-foreground font-semibold py-4 px-6 text-lg rounded-lg inline-flex items-center justify-center transition-colors"
126
145
>
127
-
<input
128
-
ref={inputRef}
129
-
type="text"
130
-
name="handle"
131
-
placeholder="Enter your handle (e.g., alice.bsky.social)"
132
-
className="w-full py-4 px-4 text-lg bg-input border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-accent"
133
-
/>
134
-
<button
135
-
type="submit"
136
-
className="w-full bg-accent hover:bg-accent/90 text-accent-foreground font-semibold py-4 px-6 text-lg rounded-lg inline-flex items-center justify-center transition-colors"
137
-
>
138
-
Continue
139
-
<svg
140
-
xmlns="http://www.w3.org/2000/svg"
141
-
className="ml-2 w-5 h-5"
142
-
viewBox="0 0 24 24"
143
-
fill="none"
144
-
stroke="currentColor"
145
-
strokeWidth="2"
146
-
strokeLinecap="round"
147
-
strokeLinejoin="round"
148
-
>
149
-
<path d="M5 12h14M12 5l7 7-7 7" />
150
-
</svg>
151
-
</button>
152
-
</form>
146
+
Continue
147
+
<ArrowRight className="ml-2 w-5 h-5" />
148
+
</button>
149
+
</form>
150
+
</div>
151
+
</div>
152
+
</div>
153
+
</section>
154
+
155
+
{/* Stats Section */}
156
+
<section className="container mx-auto px-4 py-16">
157
+
<div className="grid grid-cols-2 md:grid-cols-4 gap-8 max-w-5xl mx-auto">
158
+
{[
159
+
{ value: '100%', label: 'Decentralized' },
160
+
{ value: '0ms', label: 'Cold Start' },
161
+
{ value: '∞', label: 'Scalability' },
162
+
{ value: 'You', label: 'Own Your Data' }
163
+
].map((stat, i) => (
164
+
<div key={i} className="text-center">
165
+
<div className="text-4xl md:text-5xl font-bold text-primary mb-2">
166
+
{stat.value}
167
+
</div>
168
+
<div className="text-sm text-muted-foreground">
169
+
{stat.label}
153
170
</div>
154
171
</div>
172
+
))}
173
+
</div>
174
+
</section>
175
+
176
+
{/* Features Grid */}
177
+
<section id="features" className="container mx-auto px-4 py-20">
178
+
<div className="text-center mb-16">
179
+
<h2 className="text-4xl md:text-5xl font-bold mb-4 text-balance">
180
+
Built for the open web
181
+
</h2>
182
+
<p className="text-xl text-muted-foreground text-balance max-w-2xl mx-auto">
183
+
Everything you need to deploy and manage static sites on
184
+
a decentralized network
185
+
</p>
186
+
</div>
187
+
188
+
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6 max-w-6xl mx-auto">
189
+
{[
190
+
{
191
+
icon: Shield,
192
+
title: 'True Ownership',
193
+
description:
194
+
'Your content lives on the AT Protocol network. No single company can take it down or lock you out.'
195
+
},
196
+
{
197
+
icon: Zap,
198
+
title: 'Lightning Fast',
199
+
description:
200
+
'Distributed edge network ensures your sites load instantly from anywhere in the world.'
201
+
},
202
+
{
203
+
icon: Lock,
204
+
title: 'Cryptographic Security',
205
+
description:
206
+
'Content-addressed storage and cryptographic verification ensure integrity and authenticity.'
207
+
},
208
+
{
209
+
icon: Code,
210
+
title: 'Developer Friendly',
211
+
description:
212
+
'Simple CLI, Git integration, and familiar workflows. Deploy with a single command.'
213
+
},
214
+
{
215
+
icon: Server,
216
+
title: 'Zero Vendor Lock-in',
217
+
description:
218
+
'Built on open protocols. Migrate your sites anywhere, anytime. Your data is portable.'
219
+
},
220
+
{
221
+
icon: Globe,
222
+
title: 'Global Network',
223
+
description:
224
+
'Leverage the power of decentralized infrastructure for unmatched reliability and uptime.'
225
+
}
226
+
].map((feature, i) => (
227
+
<Card
228
+
key={i}
229
+
className="p-6 hover:shadow-lg transition-shadow border-2 bg-card"
230
+
>
231
+
<div className="w-12 h-12 rounded-lg bg-accent/10 flex items-center justify-center mb-4">
232
+
<feature.icon className="w-6 h-6 text-accent" />
233
+
</div>
234
+
<h3 className="text-xl font-semibold mb-2 text-card-foreground">
235
+
{feature.title}
236
+
</h3>
237
+
<p className="text-muted-foreground leading-relaxed">
238
+
{feature.description}
239
+
</p>
240
+
</Card>
241
+
))}
242
+
</div>
243
+
</section>
244
+
245
+
{/* How It Works */}
246
+
<section
247
+
id="how-it-works"
248
+
className="container mx-auto px-4 py-20 bg-muted/30"
249
+
>
250
+
<div className="max-w-4xl mx-auto">
251
+
<h2 className="text-4xl md:text-5xl font-bold text-center mb-16 text-balance">
252
+
Deploy in three steps
253
+
</h2>
254
+
255
+
<div className="space-y-12">
256
+
{[
257
+
{
258
+
step: '01',
259
+
title: 'Upload your site',
260
+
description:
261
+
'Link your Git repository or upload a folder containing your static site directly.'
262
+
},
263
+
{
264
+
step: '02',
265
+
title: 'Name and set domain',
266
+
description:
267
+
'Name your site and set domain routing to it. You can bring your own domain too.'
268
+
},
269
+
{
270
+
step: '03',
271
+
title: 'Deploy to AT Protocol',
272
+
description:
273
+
'Your site is published to the decentralized network with a permanent, verifiable identity.'
274
+
}
275
+
].map((step, i) => (
276
+
<div key={i} className="flex gap-6 items-start">
277
+
<div className="text-6xl font-bold text-accent/20 min-w-[80px]">
278
+
{step.step}
279
+
</div>
280
+
<div className="flex-1 pt-2">
281
+
<h3 className="text-2xl font-semibold mb-3">
282
+
{step.title}
283
+
</h3>
284
+
<p className="text-lg text-muted-foreground leading-relaxed">
285
+
{step.description}
286
+
</p>
287
+
</div>
288
+
</div>
289
+
))}
155
290
</div>
156
291
</div>
157
292
</section>
158
-
</>
293
+
294
+
{/* Footer */}
295
+
<footer className="border-t border-border/40 bg-muted/20">
296
+
<div className="container mx-auto px-4 py-8">
297
+
<div className="text-center text-sm text-muted-foreground">
298
+
<p>
299
+
Built by{' '}
300
+
<a
301
+
href="https://bsky.app/profile/nekomimi.pet"
302
+
target="_blank"
303
+
rel="noopener noreferrer"
304
+
className="text-accent hover:text-accent/80 transition-colors font-medium"
305
+
>
306
+
@nekomimi.pet
307
+
</a>
308
+
</p>
309
+
</div>
310
+
</div>
311
+
</footer>
312
+
</div>
159
313
)
160
314
}
161
315
+1
-1
public/layouts/index.tsx
+1
-1
public/layouts/index.tsx
+6
public/lib/utils.ts
+6
public/lib/utils.ts
public/libs/api.ts
public/lib/api.ts
public/libs/api.ts
public/lib/api.ts
-13
public/other/index.html
-13
public/other/index.html
···
1
-
<!doctype html>
2
-
<html lang="en">
3
-
<head>
4
-
<meta charset="UTF-8" />
5
-
6
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
-
<title>Elysia Static</title>
8
-
</head>
9
-
<body>
10
-
<div id="elysia"></div>
11
-
<script type="module" src="./index.tsx"></script>
12
-
</body>
13
-
</html>
-27
public/other/index.tsx
-27
public/other/index.tsx
···
1
-
import { createRoot } from 'react-dom/client'
2
-
import { useQuery } from '@tanstack/react-query'
3
-
4
-
import Layout from '../layouts'
5
-
import { api } from '../libs/api'
6
-
7
-
function App() {
8
-
const { data: response, isLoading } = useQuery({
9
-
queryKey: ['version'],
10
-
queryFn: () => api.message.get()
11
-
})
12
-
13
-
return (
14
-
<>
15
-
<img src="/images/maddelena-2.webp" className="max-w-40" />
16
-
<h1 className="text-3xl">API call!</h1>
17
-
<h2 className="text-6xl">{response?.data?.message}</h2>
18
-
</>
19
-
)
20
-
}
21
-
22
-
const root = createRoot(document.getElementById('elysia')!)
23
-
root.render(
24
-
<Layout className="gap-6">
25
-
<App />
26
-
</Layout>
27
-
)
+90
-145
public/styles/global.css
+90
-145
public/styles/global.css
···
1
1
@import "tailwindcss";
2
+
@import "tw-animate-css";
2
3
3
-
.gradient-text {
4
-
background: linear-gradient(135deg,
5
-
#FFAAD2 0%, /* lavender pink */
6
-
#348AA7 25%, /* blue munsell */
7
-
#413C58 50%, /* english violet */
8
-
#CCD7C5 75%, /* ash gray */
9
-
#F2E7C9 100% /* parchment */
10
-
);
11
-
background-size: 200% 200%;
12
-
-webkit-background-clip: text;
13
-
-webkit-text-fill-color: transparent;
14
-
background-clip: text;
15
-
animation: gradient-shift 4s ease-in-out infinite;
16
-
}
4
+
@custom-variant dark (&:is(.dark *));
5
+
6
+
:root {
7
+
/* #F2E7C9 - parchment background */
8
+
--background: oklch(0.93 0.03 85);
9
+
/* #413C58 - violet for text */
10
+
--foreground: oklch(0.32 0.04 285);
11
+
12
+
--card: oklch(0.98 0.01 85);
13
+
--card-foreground: oklch(0.32 0.04 285);
14
+
15
+
--popover: oklch(0.98 0.01 85);
16
+
--popover-foreground: oklch(0.32 0.04 285);
17
+
18
+
/* #413C58 - violet primary */
19
+
--primary: oklch(0.32 0.04 285);
20
+
--primary-foreground: oklch(0.98 0.01 85);
21
+
22
+
/* #FFAAD2 - pink accent */
23
+
--accent: oklch(0.78 0.15 345);
24
+
--accent-foreground: oklch(0.32 0.04 285);
25
+
26
+
/* #348AA7 - blue secondary */
27
+
--secondary: oklch(0.56 0.08 220);
28
+
--secondary-foreground: oklch(0.98 0.01 85);
29
+
30
+
/* #CCD7C5 - ash muted */
31
+
--muted: oklch(0.85 0.02 130);
32
+
--muted-foreground: oklch(0.45 0.03 285);
33
+
34
+
--border: oklch(0.75 0.02 285);
35
+
--input: oklch(0.75 0.02 285);
36
+
--ring: oklch(0.78 0.15 345);
37
+
38
+
--destructive: oklch(0.577 0.245 27.325);
39
+
--destructive-foreground: oklch(0.985 0 0);
17
40
18
-
@keyframes gradient-shift {
19
-
0%,
20
-
100% {
21
-
background-position: 0% 50%;
22
-
}
23
-
50% {
24
-
background-position: 100% 50%;
25
-
}
41
+
--chart-1: oklch(0.78 0.15 345);
42
+
--chart-2: oklch(0.32 0.04 285);
43
+
--chart-3: oklch(0.56 0.08 220);
44
+
--chart-4: oklch(0.85 0.02 130);
45
+
--chart-5: oklch(0.93 0.03 85);
46
+
47
+
--radius: 0.75rem;
48
+
--sidebar: oklch(0.985 0 0);
49
+
--sidebar-foreground: oklch(0.145 0 0);
50
+
--sidebar-primary: oklch(0.205 0 0);
51
+
--sidebar-primary-foreground: oklch(0.985 0 0);
52
+
--sidebar-accent: oklch(0.97 0 0);
53
+
--sidebar-accent-foreground: oklch(0.205 0 0);
54
+
--sidebar-border: oklch(0.922 0 0);
55
+
--sidebar-ring: oklch(0.708 0 0);
26
56
}
27
57
28
-
/*
29
-
WISPY / GHOSTY THEME (Dark baseline)
30
-
Philosophy: elegant violet depths with pink accents
31
-
Palette: #FFAAD2 (pink), #413C58 (violet), #348AA7 (blue), #CCD7C5 (ash), #F2E7C9 (parchment)
32
-
*/
33
-
:root {
34
-
/* Core surfaces - english violet base */
35
-
--background: #413C58; /* english violet */
36
-
--foreground: #F2E7C9; /* parchment */
37
-
--card: #4d4763; /* slightly lighter violet */
38
-
--card-foreground: var(--foreground);
39
-
--popover: #48445f;
40
-
--popover-foreground: var(--foreground);
58
+
.dark {
59
+
/* #413C58 - violet background for dark mode */
60
+
--background: oklch(0.28 0.04 285);
61
+
/* #F2E7C9 - parchment text */
62
+
--foreground: oklch(0.93 0.03 85);
63
+
64
+
--card: oklch(0.32 0.04 285);
65
+
--card-foreground: oklch(0.93 0.03 85);
41
66
42
-
/* Brand spectral axis (pink accent!) */
43
-
--primary: #FFAAD2; /* lavender pink - main accent */
44
-
--primary-foreground: #413C58; /* violet */
45
-
--secondary: #348AA7; /* blue munsell */
46
-
--secondary-foreground: #F2E7C9; /* parchment */
47
-
--accent: #FFAAD2; /* lavender pink - keeping the pink! */
48
-
--accent-foreground: #413C58; /* violet */
49
-
--muted: #5a5570; /* muted violet */
50
-
--muted-foreground: #CCD7C5;
67
+
--popover: oklch(0.32 0.04 285);
68
+
--popover-foreground: oklch(0.93 0.03 85);
51
69
52
-
/* Feedback / semantic */
53
-
--destructive: #ff5588; /* brighter pink for warnings */
54
-
--destructive-foreground: #fff;
70
+
/* #FFAAD2 - pink primary in dark mode */
71
+
--primary: oklch(0.78 0.15 345);
72
+
--primary-foreground: oklch(0.32 0.04 285);
55
73
56
-
/* Interaction frame tokens */
57
-
--border: #5a5570; /* muted violet */
58
-
--input: #4d4763;
59
-
--ring: #FFAAD2; /* pink focus ring */
74
+
--accent: oklch(0.78 0.15 345);
75
+
--accent-foreground: oklch(0.32 0.04 285);
60
76
61
-
/* Data viz (ordered spectral gentle ramp) */
62
-
--chart-1: #FFAAD2; /* lavender pink */
63
-
--chart-2: #348AA7; /* blue munsell */
64
-
--chart-3: #CCD7C5; /* ash gray */
65
-
--chart-4: #F2E7C9; /* parchment */
66
-
--chart-5: #413C58; /* english violet */
77
+
--secondary: oklch(0.56 0.08 220);
78
+
--secondary-foreground: oklch(0.93 0.03 85);
67
79
68
-
--radius: 0.75rem;
80
+
--muted: oklch(0.38 0.03 285);
81
+
--muted-foreground: oklch(0.75 0.02 85);
69
82
70
-
/* Sidebar palette reuses base tokens for cohesion */
71
-
--sidebar: #38344a; /* darker violet */
72
-
--sidebar-foreground: var(--foreground);
73
-
--sidebar-primary: var(--primary);
74
-
--sidebar-primary-foreground: var(--primary-foreground);
75
-
--sidebar-accent: var(--accent);
76
-
--sidebar-accent-foreground: var(--accent-foreground);
77
-
--sidebar-border: var(--border);
78
-
--sidebar-ring: var(--ring);
79
-
}
83
+
--border: oklch(0.42 0.03 285);
84
+
--input: oklch(0.42 0.03 285);
85
+
--ring: oklch(0.78 0.15 345);
80
86
81
-
/* Light (parchment) variant */
82
-
[data-theme="light"] {
83
-
--background: #F2E7C9; /* parchment */
84
-
--foreground: #413C58; /* english violet */
85
-
--card: #faf5e6; /* lighter parchment */
86
-
--card-foreground: var(--foreground);
87
-
--popover: #fff;
88
-
--popover-foreground: var(--foreground);
89
-
--primary: #FFAAD2; /* lavender pink - keep the pink! */
90
-
--primary-foreground: #413C58;
91
-
--secondary: #348AA7; /* blue munsell */
92
-
--secondary-foreground: #fff;
93
-
--accent: #FFAAD2; /* lavender pink accent */
94
-
--accent-foreground: #413C58;
95
-
--muted: #e8dfc0;
96
-
--muted-foreground: #5a5570;
97
-
--destructive: #d8006d;
98
-
--destructive-foreground: #fff;
99
-
--border: #CCD7C5; /* ash gray */
100
-
--input: #faf5e6;
101
-
--ring: #FFAAD2; /* pink ring */
102
-
--chart-1: #FFAAD2;
103
-
--chart-2: #348AA7;
104
-
--chart-3: #413C58;
105
-
--chart-4: #CCD7C5;
106
-
--chart-5: #5a5570;
107
-
--sidebar: #f7f0da;
108
-
--sidebar-foreground: var(--foreground);
109
-
--sidebar-primary: var(--primary);
110
-
--sidebar-primary-foreground: var(--primary-foreground);
111
-
--sidebar-accent: var(--accent);
112
-
--sidebar-accent-foreground: var(--accent-foreground);
113
-
--sidebar-border: var(--border);
114
-
--sidebar-ring: var(--ring);
115
-
}
87
+
--destructive: oklch(0.577 0.245 27.325);
88
+
--destructive-foreground: oklch(0.985 0 0);
116
89
117
-
@media (prefers-color-scheme: light) {
118
-
:root:not([data-theme="dark"]) {
119
-
--background: #F2E7C9; /* parchment */
120
-
--foreground: #413C58; /* english violet */
121
-
--card: #faf5e6; /* lighter parchment */
122
-
--card-foreground: var(--foreground);
123
-
--popover: #fff;
124
-
--popover-foreground: var(--foreground);
125
-
--primary: #FFAAD2; /* lavender pink */
126
-
--primary-foreground: #413C58;
127
-
--secondary: #348AA7; /* blue munsell */
128
-
--secondary-foreground: #fff;
129
-
--accent: #FFAAD2; /* lavender pink */
130
-
--accent-foreground: #413C58;
131
-
--muted: #e8dfc0;
132
-
--muted-foreground: #5a5570;
133
-
--destructive: #d8006d;
134
-
--destructive-foreground: #fff;
135
-
--border: #CCD7C5; /* ash gray */
136
-
--input: #faf5e6;
137
-
--ring: #FFAAD2;
138
-
--chart-1: #FFAAD2;
139
-
--chart-2: #348AA7;
140
-
--chart-3: #413C58;
141
-
--chart-4: #CCD7C5;
142
-
--chart-5: #5a5570;
143
-
--sidebar: #f7f0da;
144
-
--sidebar-foreground: var(--foreground);
145
-
--sidebar-primary: var(--primary);
146
-
--sidebar-primary-foreground: var(--primary-foreground);
147
-
--sidebar-accent: var(--accent);
148
-
--sidebar-accent-foreground: var(--accent-foreground);
149
-
--sidebar-border: var(--border);
150
-
--sidebar-ring: var(--ring);
151
-
}
90
+
--chart-1: oklch(0.78 0.15 345);
91
+
--chart-2: oklch(0.93 0.03 85);
92
+
--chart-3: oklch(0.56 0.08 220);
93
+
--chart-4: oklch(0.85 0.02 130);
94
+
--chart-5: oklch(0.32 0.04 285);
95
+
--sidebar: oklch(0.205 0 0);
96
+
--sidebar-foreground: oklch(0.985 0 0);
97
+
--sidebar-primary: oklch(0.488 0.243 264.376);
98
+
--sidebar-primary-foreground: oklch(0.985 0 0);
99
+
--sidebar-accent: oklch(0.269 0 0);
100
+
--sidebar-accent-foreground: oklch(0.985 0 0);
101
+
--sidebar-border: oklch(0.269 0 0);
102
+
--sidebar-ring: oklch(0.439 0 0);
152
103
}
153
104
154
105
@theme inline {
155
-
--font-sans: var(--font-geist-sans);
156
-
--font-mono: var(--font-geist-mono);
106
+
/* optional: --font-sans, --font-serif, --font-mono if they are applied in the layout.tsx */
157
107
--color-background: var(--background);
158
108
--color-foreground: var(--foreground);
159
109
--color-card: var(--card);
···
200
150
@apply bg-background text-foreground;
201
151
}
202
152
}
203
-
204
-
/* Reduced motion respect: disable animated shimmer if user prefers */
205
-
@media (prefers-reduced-motion: reduce) {
206
-
.gradient-text { animation: none; }
207
-
}
+2
src/index.ts
+2
src/index.ts
···
12
12
} from './lib/oauth-client'
13
13
import { authRoutes } from './routes/auth'
14
14
import { wispRoutes } from './routes/wisp'
15
+
import { domainRoutes } from './routes/domain'
15
16
16
17
const config: Config = {
17
18
domain: (Bun.env.DOMAIN ?? `https://${BASE_HOST}`) as `https://${string}`,
···
33
34
)
34
35
.use(authRoutes(client))
35
36
.use(wispRoutes(client))
37
+
.use(domainRoutes(client))
36
38
.get('/client-metadata.json', (c) => {
37
39
return createClientMetadata(config)
38
40
})
+129
src/routes/domain.ts
+129
src/routes/domain.ts
···
1
+
import { Elysia } from 'elysia'
2
+
import { requireAuth, type AuthenticatedContext } from '../lib/wisp-auth'
3
+
import { NodeOAuthClient } from '@atproto/oauth-client-node'
4
+
import { Agent } from '@atproto/api'
5
+
import {
6
+
claimDomain,
7
+
getDomainByDid,
8
+
isDomainAvailable,
9
+
isValidHandle,
10
+
toDomain,
11
+
updateDomain,
12
+
} from '../lib/db'
13
+
14
+
export const domainRoutes = (client: NodeOAuthClient) =>
15
+
new Elysia({ prefix: '/api/domain' })
16
+
.derive(async ({ cookie }) => {
17
+
const auth = await requireAuth(client, cookie)
18
+
return { auth }
19
+
})
20
+
.get('/check', async ({ query }) => {
21
+
try {
22
+
const handle = (query.handle || "")
23
+
.trim()
24
+
.toLowerCase();
25
+
26
+
if (!isValidHandle(handle)) {
27
+
return {
28
+
available: false,
29
+
reason: "invalid"
30
+
};
31
+
}
32
+
33
+
const available = await isDomainAvailable(handle);
34
+
return {
35
+
available,
36
+
domain: toDomain(handle)
37
+
};
38
+
} catch (err) {
39
+
console.error("domain/check error", err);
40
+
return {
41
+
available: false
42
+
};
43
+
}
44
+
})
45
+
.post('/claim', async ({ body, auth }) => {
46
+
try {
47
+
const { handle } = body as { handle?: string };
48
+
const normalizedHandle = (handle || "").trim().toLowerCase();
49
+
50
+
if (!isValidHandle(normalizedHandle)) {
51
+
throw new Error("Invalid handle");
52
+
}
53
+
54
+
// ensure user hasn't already claimed
55
+
const existing = await getDomainByDid(auth.did);
56
+
if (existing) {
57
+
throw new Error("Already claimed");
58
+
}
59
+
60
+
// claim in DB
61
+
let domain: string;
62
+
try {
63
+
domain = await claimDomain(auth.did, normalizedHandle);
64
+
} catch (err) {
65
+
throw new Error("Handle taken");
66
+
}
67
+
68
+
// write place.wisp.domain record rkey = self
69
+
const agent = new Agent((url, init) => auth.session.fetchHandler(url, init));
70
+
await agent.com.atproto.repo.putRecord({
71
+
repo: auth.did,
72
+
collection: "place.wisp.domain",
73
+
rkey: "self",
74
+
record: {
75
+
$type: "place.wisp.domain",
76
+
domain,
77
+
createdAt: new Date().toISOString(),
78
+
} as any,
79
+
validate: false,
80
+
});
81
+
82
+
return { success: true, domain };
83
+
} catch (err) {
84
+
console.error("domain/claim error", err);
85
+
throw new Error(`Failed to claim: ${err instanceof Error ? err.message : 'Unknown error'}`);
86
+
}
87
+
})
88
+
.post('/update', async ({ body, auth }) => {
89
+
try {
90
+
const { handle } = body as { handle?: string };
91
+
const normalizedHandle = (handle || "").trim().toLowerCase();
92
+
93
+
if (!isValidHandle(normalizedHandle)) {
94
+
throw new Error("Invalid handle");
95
+
}
96
+
97
+
const desiredDomain = toDomain(normalizedHandle);
98
+
const current = await getDomainByDid(auth.did);
99
+
100
+
if (current === desiredDomain) {
101
+
return { success: true, domain: current };
102
+
}
103
+
104
+
let domain: string;
105
+
try {
106
+
domain = await updateDomain(auth.did, normalizedHandle);
107
+
} catch (err) {
108
+
throw new Error("Handle taken");
109
+
}
110
+
111
+
const agent = new Agent((url, init) => auth.session.fetchHandler(url, init));
112
+
await agent.com.atproto.repo.putRecord({
113
+
repo: auth.did,
114
+
collection: "place.wisp.domain",
115
+
rkey: "self",
116
+
record: {
117
+
$type: "place.wisp.domain",
118
+
domain,
119
+
createdAt: new Date().toISOString(),
120
+
} as any,
121
+
validate: false,
122
+
});
123
+
124
+
return { success: true, domain };
125
+
} catch (err) {
126
+
console.error("domain/update error", err);
127
+
throw new Error(`Failed to update: ${err instanceof Error ? err.message : 'Unknown error'}`);
128
+
}
129
+
});