summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorpolwex <polwex@sortug.com>2025-05-29 15:54:51 +0700
committerpolwex <polwex@sortug.com>2025-05-29 15:54:51 +0700
commit7de09570c0d7907424c30f492207e80ff69e4061 (patch)
tree5f0971b9eeac9e1cc6506954843093b6b77ebd63
parent84c5b778039102a77b7fda2ddcab2bbf70085bdc (diff)
very pretty
-rw-r--r--bun.lock17
-rw-r--r--package.json3
-rw-r--r--src/components/Flashcard/ServerCard.tsx4
-rw-r--r--src/components/ui/dropdown-menu.tsx205
-rw-r--r--src/pages.gen.ts3
-rw-r--r--src/pages/_layout.tsx8
-rw-r--r--src/pages/index.tsx258
-rw-r--r--src/pages/logout.tsx49
8 files changed, 481 insertions, 66 deletions
diff --git a/bun.lock b/bun.lock
index f810cfe..92e40f2 100644
--- a/bun.lock
+++ b/bun.lock
@@ -5,7 +5,10 @@
"name": "waku",
"dependencies": {
"@hookform/resolvers": "^5.0.1",
+ "@radix-ui/react-avatar": "^1.1.10",
"@radix-ui/react-dialog": "^1.1.13",
+ "@radix-ui/react-dropdown-menu": "^2.1.15",
+ "@radix-ui/react-icons": "^1.3.2",
"@radix-ui/react-label": "^2.1.6",
"@radix-ui/react-progress": "^1.1.7",
"@radix-ui/react-select": "^2.2.4",
@@ -195,6 +198,8 @@
"@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.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-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="],
+ "@radix-ui/react-avatar": ["@radix-ui/react-avatar@1.1.10", "", { "dependencies": { "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-is-hydrated": "0.1.0", "@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-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog=="],
+
"@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=="],
"@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=="],
@@ -207,14 +212,20 @@
"@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.10", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@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-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ=="],
+ "@radix-ui/react-dropdown-menu": ["@radix-ui/react-dropdown-menu@2.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-menu": "2.1.15", "@radix-ui/react-primitive": "2.1.3", "@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-mIBnOjgwo9AH3FyKaSWoSu/dYj6VdhJ7frEPiGTeXCdUFHjl9h3mFh2wwhEtINOmYXWhdpf1rY2minFsmaNgVQ=="],
+
"@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA=="],
"@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=="],
+ "@radix-ui/react-icons": ["@radix-ui/react-icons@1.3.2", "", { "peerDependencies": { "react": "^16.x || ^17.x || ^18.x || ^19.0.0 || ^19.0.0-rc" } }, "sha512-fyQIhGDhzfc9pK2kH6Pl9c4BDJGfMkPqkyIgYDthyNYoNg3wVhoJMMh19WS4Up/1KMPFVpNsT2q3WmXn2N1m6g=="],
+
"@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=="],
"@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=="],
+ "@radix-ui/react-menu": ["@radix-ui/react-menu@2.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@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-dismissable-layer": "1.1.10", "@radix-ui/react-focus-guards": "1.1.2", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.7", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.4", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.10", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "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-tVlmA3Vb9n8SZSd+YSbuFR66l87Wiy4du+YE+0hzKQEANA+7cWKH1WgqcEX4pXqxUFQKrWQGHdvEfw00TjFiew=="],
+
"@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.7", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@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-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "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-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ=="],
"@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=="],
@@ -225,6 +236,8 @@
"@radix-ui/react-progress": ["@radix-ui/react-progress@1.1.7", "", { "dependencies": { "@radix-ui/react-context": "1.1.2", "@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-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg=="],
+ "@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.10", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@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-dT9aOXUen9JSsxnMPv/0VqySQf5eDQ6LCk5Sw28kamz8wSOW2bJdlX2Bg5VUIIcV+6XlHpWTIuTPCf/UNIyq8Q=="],
+
"@radix-ui/react-select": ["@radix-ui/react-select@2.2.5", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.2", "@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-dismissable-layer": "1.1.10", "@radix-ui/react-focus-guards": "1.1.2", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.7", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3", "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-HnMTdXEVuuyzx63ME0ut4+sEMYW6oouHWNGUZc7ddvUWIcfCva/AMoqEW/3wnEllriMWBa0RHspCYnfCWJQYmA=="],
"@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=="],
@@ -237,6 +250,8 @@
"@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=="],
+ "@radix-ui/react-use-is-hydrated": ["@radix-ui/react-use-is-hydrated@0.1.0", "", { "dependencies": { "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA=="],
+
"@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=="],
"@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=="],
@@ -815,6 +830,8 @@
"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=="],
+ "use-sync-external-store": ["use-sync-external-store@1.5.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A=="],
+
"uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="],
"vite": ["vite@6.3.5", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "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-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ=="],
diff --git a/package.json b/package.json
index 86e634f..7d9d795 100644
--- a/package.json
+++ b/package.json
@@ -10,7 +10,10 @@
},
"dependencies": {
"@hookform/resolvers": "^5.0.1",
+ "@radix-ui/react-avatar": "^1.1.10",
"@radix-ui/react-dialog": "^1.1.13",
+ "@radix-ui/react-dropdown-menu": "^2.1.15",
+ "@radix-ui/react-icons": "^1.3.2",
"@radix-ui/react-label": "^2.1.6",
"@radix-ui/react-progress": "^1.1.7",
"@radix-ui/react-select": "^2.2.4",
diff --git a/src/components/Flashcard/ServerCard.tsx b/src/components/Flashcard/ServerCard.tsx
index df37ba8..bb822af 100644
--- a/src/components/Flashcard/ServerCard.tsx
+++ b/src/components/Flashcard/ServerCard.tsx
@@ -66,9 +66,9 @@ export async function CardFront({
/>
))
) : (
- <p className="text-5xl cursor-pointer hover:text-blue-700 font-semibold text-slate-800 dark:text-slate-100 text-center">
+ <span className="text-5xl cursor-pointer hover:text-blue-700 font-semibold text-slate-800 dark:text-slate-100 text-center">
{data.expression.spelling}
- </p>
+ </span>
)}
</p>
</Suspense>
diff --git a/src/components/ui/dropdown-menu.tsx b/src/components/ui/dropdown-menu.tsx
new file mode 100644
index 0000000..c8116cb
--- /dev/null
+++ b/src/components/ui/dropdown-menu.tsx
@@ -0,0 +1,205 @@
+"use client"
+
+import * as React from "react"
+import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
+import {
+ CheckIcon,
+ ChevronRightIcon,
+ DotFilledIcon,
+} from "@radix-ui/react-icons"
+
+import { cn } from "@/lib/utils"
+
+const DropdownMenu = DropdownMenuPrimitive.Root
+
+const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
+
+const DropdownMenuGroup = DropdownMenuPrimitive.Group
+
+const DropdownMenuPortal = DropdownMenuPrimitive.Portal
+
+const DropdownMenuSub = DropdownMenuPrimitive.Sub
+
+const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
+
+const DropdownMenuSubTrigger = React.forwardRef<
+ React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
+ inset?: boolean
+ }
+>(({ className, inset, children, ...props }, ref) => (
+ <DropdownMenuPrimitive.SubTrigger
+ ref={ref}
+ className={cn(
+ "flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
+ inset && "pl-8",
+ className
+ )}
+ {...props}
+ >
+ {children}
+ <ChevronRightIcon className="ml-auto h-4 w-4" />
+ </DropdownMenuPrimitive.SubTrigger>
+))
+DropdownMenuSubTrigger.displayName =
+ DropdownMenuPrimitive.SubTrigger.displayName
+
+const DropdownMenuSubContent = React.forwardRef<
+ React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
+>(({ className, ...props }, ref) => (
+ <DropdownMenuPrimitive.SubContent
+ ref={ref}
+ className={cn(
+ "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg 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 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
+ className
+ )}
+ {...props}
+ />
+))
+DropdownMenuSubContent.displayName =
+ DropdownMenuPrimitive.SubContent.displayName
+
+const DropdownMenuContent = React.forwardRef<
+ React.ElementRef<typeof DropdownMenuPrimitive.Content>,
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
+>(({ className, sideOffset = 4, ...props }, ref) => (
+ <DropdownMenuPrimitive.Portal>
+ <DropdownMenuPrimitive.Content
+ ref={ref}
+ sideOffset={sideOffset}
+ className={cn(
+ "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
+ "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 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
+ className
+ )}
+ {...props}
+ />
+ </DropdownMenuPrimitive.Portal>
+))
+DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
+
+const DropdownMenuItem = React.forwardRef<
+ React.ElementRef<typeof DropdownMenuPrimitive.Item>,
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
+ inset?: boolean
+ }
+>(({ className, inset, ...props }, ref) => (
+ <DropdownMenuPrimitive.Item
+ ref={ref}
+ className={cn(
+ "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
+ inset && "pl-8",
+ className
+ )}
+ {...props}
+ />
+))
+DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
+
+const DropdownMenuCheckboxItem = React.forwardRef<
+ React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
+>(({ className, children, checked, ...props }, ref) => (
+ <DropdownMenuPrimitive.CheckboxItem
+ ref={ref}
+ className={cn(
+ "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
+ className
+ )}
+ checked={checked}
+ {...props}
+ >
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
+ <DropdownMenuPrimitive.ItemIndicator>
+ <CheckIcon className="h-4 w-4" />
+ </DropdownMenuPrimitive.ItemIndicator>
+ </span>
+ {children}
+ </DropdownMenuPrimitive.CheckboxItem>
+))
+DropdownMenuCheckboxItem.displayName =
+ DropdownMenuPrimitive.CheckboxItem.displayName
+
+const DropdownMenuRadioItem = React.forwardRef<
+ React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
+>(({ className, children, ...props }, ref) => (
+ <DropdownMenuPrimitive.RadioItem
+ ref={ref}
+ className={cn(
+ "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
+ className
+ )}
+ {...props}
+ >
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
+ <DropdownMenuPrimitive.ItemIndicator>
+ <DotFilledIcon className="h-4 w-4 fill-current" />
+ </DropdownMenuPrimitive.ItemIndicator>
+ </span>
+ {children}
+ </DropdownMenuPrimitive.RadioItem>
+))
+DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
+
+const DropdownMenuLabel = React.forwardRef<
+ React.ElementRef<typeof DropdownMenuPrimitive.Label>,
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
+ inset?: boolean
+ }
+>(({ className, inset, ...props }, ref) => (
+ <DropdownMenuPrimitive.Label
+ ref={ref}
+ className={cn(
+ "px-2 py-1.5 text-sm font-semibold",
+ inset && "pl-8",
+ className
+ )}
+ {...props}
+ />
+))
+DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
+
+const DropdownMenuSeparator = React.forwardRef<
+ React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
+>(({ className, ...props }, ref) => (
+ <DropdownMenuPrimitive.Separator
+ ref={ref}
+ className={cn("-mx-1 my-1 h-px bg-muted", className)}
+ {...props}
+ />
+))
+DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
+
+const DropdownMenuShortcut = ({
+ className,
+ ...props
+}: React.HTMLAttributes<HTMLSpanElement>) => {
+ return (
+ <span
+ className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
+ {...props}
+ />
+ )
+}
+DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
+
+export {
+ DropdownMenu,
+ DropdownMenuTrigger,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuCheckboxItem,
+ DropdownMenuRadioItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuShortcut,
+ DropdownMenuGroup,
+ DropdownMenuPortal,
+ DropdownMenuSub,
+ DropdownMenuSubContent,
+ DropdownMenuSubTrigger,
+ DropdownMenuRadioGroup,
+} \ No newline at end of file
diff --git a/src/pages.gen.ts b/src/pages.gen.ts
index fa7a6fa..6695ceb 100644
--- a/src/pages.gen.ts
+++ b/src/pages.gen.ts
@@ -14,6 +14,8 @@ import type { getConfig as File_Login_getConfig } from './pages/login';
// prettier-ignore
import type { getConfig as File_Parse_getConfig } from './pages/parse';
// prettier-ignore
+import type { getConfig as File_Logout_getConfig } from './pages/logout';
+// prettier-ignore
import type { getConfig as File_Db_getConfig } from './pages/db';
// prettier-ignore
import type { getConfig as File_Form_getConfig } from './pages/form';
@@ -36,6 +38,7 @@ type Page =
| ({ path: '/lesson/[slug]' } & GetConfigResponse<typeof File_LessonSlug_getConfig>)
| ({ path: '/login' } & GetConfigResponse<typeof File_Login_getConfig>)
| ({ path: '/parse' } & GetConfigResponse<typeof File_Parse_getConfig>)
+| ({ path: '/logout' } & GetConfigResponse<typeof File_Logout_getConfig>)
| ({ path: '/db' } & GetConfigResponse<typeof File_Db_getConfig>)
| { path: '/test/client-modal'; render: 'dynamic' }
| { path: '/test/trigger-modal-button'; render: 'dynamic' }
diff --git a/src/pages/_layout.tsx b/src/pages/_layout.tsx
index 7f6a434..c19c1fb 100644
--- a/src/pages/_layout.tsx
+++ b/src/pages/_layout.tsx
@@ -1,9 +1,7 @@
import "../styles.css";
import type { ReactNode } from "react";
-
-import { Header } from "../components/header";
-import { Footer } from "../components/footer";
+import { getContextData } from "waku/middleware/context";
type RootLayoutProps = { children: ReactNode };
@@ -11,10 +9,10 @@ export default async function RootLayout({ children }: RootLayoutProps) {
const data = await getData();
return (
- <div className="font-['Nunito']">
+ <div className="font-sans antialiased">
<meta name="description" content={data.description} />
<link rel="icon" type="image/png" href={data.icon} />
- <main className="m-6 items-center *:min-h-64 *:min-w-64 lg:m-0 lg:min-h-svh lg:justify-center">
+ <main className="min-h-screen">
{children}
</main>
</div>
diff --git a/src/pages/index.tsx b/src/pages/index.tsx
index 48fa46e..7b66f5d 100644
--- a/src/pages/index.tsx
+++ b/src/pages/index.tsx
@@ -1,92 +1,232 @@
import { Link } from "waku";
+import { getContextData } from "waku/middleware/context";
+import { ArrowRightIcon, BookOpenIcon, BrainIcon, LanguagesIcon, GraduationCapIcon } from "lucide-react";
import { Progress } from "@/components/ui/progress";
-import { getContextData } from "waku/middleware/context";
+import { Button } from "@/components/ui/button";
+import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
+import Navbar from "@/components/Navbar";
type LanguageChoice = "th" | "en" | "zh" | "ja" | "es" | "fr";
-type LangMeta = { flag: string; name: string };
+type LangMeta = { flag: string; name: string; level?: string; progress?: number; totalCards?: number; dueCards?: number };
+
const langs: Record<LanguageChoice, LangMeta> = {
- th: { flag: "🇹🇭", name: "Thai" },
- en: { flag: "🇬🇧", name: "English" },
- zh: { flag: "🇨🇳", name: "Chinese" },
- ja: { flag: "🇯🇵", name: "Japanese" },
- es: { flag: "🇪🇸", name: "Spanish" },
- fr: { flag: "🇫🇷", name: "French" },
+ th: { flag: "🇹🇭", name: "Thai", level: "Beginner", progress: 32, totalCards: 245, dueCards: 12 },
+ en: { flag: "🇬🇧", name: "English", level: "Advanced", progress: 87, totalCards: 542, dueCards: 5 },
+ zh: { flag: "🇨🇳", name: "Chinese", level: "Intermediate", progress: 59, totalCards: 378, dueCards: 8 },
+ ja: { flag: "🇯🇵", name: "Japanese", level: "Beginner", progress: 23, totalCards: 198, dueCards: 15 },
+ es: { flag: "🇪🇸", name: "Spanish", level: "Not Started", progress: 0, totalCards: 312, dueCards: 0 },
+ fr: { flag: "🇫🇷", name: "French", level: "Not Started", progress: 0, totalCards: 289, dueCards: 0 },
};
export default async function HomePage() {
- const { user } = getContextData();
+ const { user } = getContextData() as any;
+
+ // Get due cards count for the progress badge
+ const totalDueCards = Object.values(langs)
+ .reduce((sum, lang) => sum + (lang.dueCards || 0), 0);
return (
<div className="min-h-screen bg-gray-50">
- <header className="bg-white shadow-sm sticky top-0 z-50">
+ <Navbar user={user} />
+
+ {/* Hero section */}
+ <section className="bg-gradient-to-r from-indigo-600 to-indigo-700 text-white py-16">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
- <div className="flex justify-between items-center h-16">
- <div className="flex items-center">
- <h1 className="text-2xl font-bold text-indigo-600">Prosody</h1>
+ <div className="flex flex-col md:flex-row items-center">
+ <div className="md:w-1/2 mb-8 md:mb-0">
+ <h1 className="text-4xl md:text-5xl font-bold leading-tight mb-4">
+ Master Languages with Sorlang
+ </h1>
+ <p className="text-lg md:text-xl mb-6 text-indigo-100">
+ Boost your language learning with our scientifically proven spaced repetition system. Track progress, analyze text, and learn efficiently.
+ </p>
+ <div className="flex flex-wrap gap-4">
+ <Link to="/study">
+ <Button size="lg" className="bg-white text-indigo-700 hover:bg-indigo-50">
+ Start Learning
+ <ArrowRightIcon className="ml-2 h-4 w-4" />
+ </Button>
+ </Link>
+ <Link to="/parse">
+ <Button size="lg" variant="outline" className="border-white text-white hover:bg-indigo-600">
+ Analyze Text
+ </Button>
+ </Link>
+ </div>
+ </div>
+ <div className="md:w-1/2">
+ <div className="flex justify-center">
+ <div className="relative">
+ <div className="absolute -top-4 -left-4 bg-indigo-500 text-white p-3 rounded-lg shadow-lg">
+ 🇹🇭
+ </div>
+ <div className="absolute top-8 -right-4 bg-indigo-500 text-white p-3 rounded-lg shadow-lg">
+ 🇨🇳
+ </div>
+ <div className="absolute -bottom-4 -left-4 bg-indigo-500 text-white p-3 rounded-lg shadow-lg">
+ 🇯🇵
+ </div>
+ <div className="absolute -bottom-4 right-4 bg-indigo-500 text-white p-3 rounded-lg shadow-lg">
+ 🇬🇧
+ </div>
+ <div className="bg-white rounded-xl p-8 shadow-xl">
+ <div className="text-indigo-700 text-6xl font-bold mb-2">
+ SRS
+ </div>
+ <div className="text-gray-600">
+ Spaced Repetition System
+ </div>
+ </div>
+ </div>
+ </div>
</div>
-
- {/* Desktop Navigation */}
- <nav className="hidden md:flex space-x-8">
- <Link to="/">
- <button
- className={`py-2 font-medium text-indigo-600 border-b-2 border-indigo-600`}
- >
- Home
- </button>
- </Link>
- <Link to="/parse">
- <button
- className={`py-2 font-medium text-gray-600 hover:text-indigo-600`}
- >
- Analyze Text
- </button>
- </Link>
- </nav>
</div>
</div>
-
- {/* Mobile Navigation */}
- </header>
- <section className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
- <h2 className="text-lg"> Your Languages</h2>
- <LanguageItem lang="en" />
- <LanguageItem lang="th" />
- <LanguageItem lang="zh" />
- <LanguageItem lang="ja" />
+ </section>
+
+ {/* Feature highlights */}
+ <section className="py-12 bg-white">
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
+ <div className="text-center mb-12">
+ <h2 className="text-3xl font-bold text-gray-900 mb-4">Learning Features</h2>
+ <p className="text-lg text-gray-600 max-w-3xl mx-auto">
+ Tools designed to accelerate your language learning journey
+ </p>
+ </div>
+
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-8">
+ <Card>
+ <CardHeader>
+ <div className="bg-indigo-100 w-12 h-12 rounded-lg flex items-center justify-center mb-2">
+ <BrainIcon className="h-6 w-6 text-indigo-600" />
+ </div>
+ <CardTitle>Spaced Repetition</CardTitle>
+ <CardDescription>
+ Optimized review schedule based on your performance
+ </CardDescription>
+ </CardHeader>
+ <CardContent>
+ <p className="text-gray-600">
+ Our SRS algorithm determines the optimal time for you to review each card, helping you memorize efficiently.
+ </p>
+ </CardContent>
+ </Card>
+
+ <Card>
+ <CardHeader>
+ <div className="bg-indigo-100 w-12 h-12 rounded-lg flex items-center justify-center mb-2">
+ <LanguagesIcon className="h-6 w-6 text-indigo-600" />
+ </div>
+ <CardTitle>Text Analysis</CardTitle>
+ <CardDescription>
+ Break down complex text into manageable pieces
+ </CardDescription>
+ </CardHeader>
+ <CardContent>
+ <p className="text-gray-600">
+ Parse any text to analyze grammar, vocabulary, and patterns to enhance your understanding.
+ </p>
+ </CardContent>
+ </Card>
+
+ <Card>
+ <CardHeader>
+ <div className="bg-indigo-100 w-12 h-12 rounded-lg flex items-center justify-center mb-2">
+ <GraduationCapIcon className="h-6 w-6 text-indigo-600" />
+ </div>
+ <CardTitle>Progress Tracking</CardTitle>
+ <CardDescription>
+ Monitor your learning journey with detailed statistics
+ </CardDescription>
+ </CardHeader>
+ <CardContent>
+ <p className="text-gray-600">
+ Track your progress across different languages, see your mastery level, and identify areas for improvement.
+ </p>
+ </CardContent>
+ </Card>
+ </div>
+ </div>
+ </section>
+
+ {/* Languages section */}
+ <section className="py-12 bg-gray-50">
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
+ <div className="flex justify-between items-center mb-8">
+ <h2 className="text-2xl font-bold text-gray-900">Your Languages</h2>
+ {totalDueCards > 0 && (
+ <div className="bg-indigo-100 text-indigo-800 px-3 py-1 rounded-full text-sm font-medium">
+ {totalDueCards} cards due for review
+ </div>
+ )}
+ </div>
+
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
+ {Object.entries(langs).map(([langCode, langData]) => (
+ <LanguageItem
+ key={langCode}
+ lang={langCode as LanguageChoice}
+ langData={langData}
+ />
+ ))}
+ </div>
+
+ <div className="text-center mt-8">
+ <Link to="/study">
+ <Button size="lg" className="bg-indigo-600 hover:bg-indigo-700">
+ <BookOpenIcon className="mr-2 h-4 w-4" />
+ Go to Study Dashboard
+ </Button>
+ </Link>
+ </div>
+ </div>
</section>
</div>
);
}
-const getData = async () => {
- const data = {
- title: "Waku",
- headline: "Waku",
- body: "Hello world!",
- };
-
- return data;
-};
-
export const getConfig = async () => {
return {
render: "dynamic",
} as const;
};
-async function LanguageItem({ lang }: { lang: LanguageChoice }) {
+function LanguageItem({ lang, langData }: { lang: LanguageChoice, langData: LangMeta }) {
return (
<Link to={`/lang/${lang}`}>
- <div className="bg-white rounded-xl h-32 shadow-sm overflow-hidden hover:shadow-md transition-shadow duration-300">
- <div className="p-6">
- <div className="flex">
- <div className="text-lg">{langs[lang].flag}</div>
- <div className="text-lg">{langs[lang].name}</div>
+ <Card className="hover:shadow-md transition-shadow duration-300 h-full">
+ <CardHeader className="pb-2">
+ <div className="flex items-center justify-between">
+ <div className="flex items-center gap-3">
+ <div className="text-3xl">{langData.flag}</div>
+ <div>
+ <CardTitle>{langData.name}</CardTitle>
+ <CardDescription>{langData.level}</CardDescription>
+ </div>
+ </div>
+ {langData.dueCards > 0 && (
+ <div className="bg-red-100 text-red-800 px-2 py-1 rounded-full text-xs font-medium">
+ {langData.dueCards} due
+ </div>
+ )}
</div>
- <Progress value={50} className="w-[60%]" />
- </div>
- </div>
+ </CardHeader>
+ <CardContent>
+ <div className="mb-2">
+ <Progress value={langData.progress} className="h-2" />
+ <div className="flex justify-between mt-1 text-xs text-gray-500">
+ <span>{langData.progress}% complete</span>
+ <span>{langData.totalCards} cards</span>
+ </div>
+ </div>
+ </CardContent>
+ <CardFooter className="pt-0">
+ <Button variant="outline" className="w-full">
+ {langData.progress > 0 ? "Continue Learning" : "Start Learning"}
+ </Button>
+ </CardFooter>
+ </Card>
</Link>
);
}
diff --git a/src/pages/logout.tsx b/src/pages/logout.tsx
new file mode 100644
index 0000000..880d175
--- /dev/null
+++ b/src/pages/logout.tsx
@@ -0,0 +1,49 @@
+import { getContextData } from "waku/middleware/context";
+import { useCookies } from "@/lib/server/cookiebridge";
+import { Button } from "@/components/ui/button";
+import { Card } from "@/components/ui/card";
+import Navbar from "@/components/Navbar";
+
+export default async function LogoutPage() {
+ const { user } = getContextData() as any;
+ const loggedIn = !!user;
+
+ // If the user is logged in, delete the cookie
+ if (loggedIn) {
+ const { delCookie } = useCookies();
+ delCookie("sorlang");
+ }
+
+ return (
+ <div className="min-h-screen bg-gray-50">
+ <Navbar user={null} />
+
+ <div className="container mx-auto py-16 px-4">
+ <Card className="max-w-md mx-auto p-6 text-center">
+ <h1 className="text-2xl font-bold mb-4">
+ {loggedIn ? "You've been logged out" : "Already logged out"}
+ </h1>
+ <p className="text-gray-600 mb-6">
+ {loggedIn
+ ? "Your session has been ended successfully. Thank you for using Sorlang."
+ : "You were not logged in."}
+ </p>
+ <div className="flex flex-col space-y-4">
+ <Button asChild>
+ <a href="/login">Log back in</a>
+ </Button>
+ <Button variant="outline" asChild>
+ <a href="/">Return to home page</a>
+ </Button>
+ </div>
+ </Card>
+ </div>
+ </div>
+ );
+}
+
+export const getConfig = async () => {
+ return {
+ render: "dynamic",
+ } as const;
+}; \ No newline at end of file