From 7de09570c0d7907424c30f492207e80ff69e4061 Mon Sep 17 00:00:00 2001 From: polwex Date: Thu, 29 May 2025 15:54:51 +0700 Subject: very pretty --- src/components/Flashcard/ServerCard.tsx | 4 +- src/components/ui/dropdown-menu.tsx | 205 +++++++++++++++++++++++++ src/pages.gen.ts | 3 + src/pages/_layout.tsx | 8 +- src/pages/index.tsx | 258 ++++++++++++++++++++++++-------- src/pages/logout.tsx | 49 ++++++ 6 files changed, 461 insertions(+), 66 deletions(-) create mode 100644 src/components/ui/dropdown-menu.tsx create mode 100644 src/pages/logout.tsx (limited to 'src') 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({ /> )) ) : ( -

+ {data.expression.spelling} -

+ )}

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, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, children, ...props }, ref) => ( + + {children} + + +)) +DropdownMenuSubTrigger.displayName = + DropdownMenuPrimitive.SubTrigger.displayName + +const DropdownMenuSubContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSubContent.displayName = + DropdownMenuPrimitive.SubContent.displayName + +const DropdownMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + + + +)) +DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName + +const DropdownMenuItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName + +const DropdownMenuCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, checked, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuCheckboxItem.displayName = + DropdownMenuPrimitive.CheckboxItem.displayName + +const DropdownMenuRadioItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName + +const DropdownMenuLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName + +const DropdownMenuSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName + +const DropdownMenuShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ) +} +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) | ({ path: '/login' } & GetConfigResponse) | ({ path: '/parse' } & GetConfigResponse) +| ({ path: '/logout' } & GetConfigResponse) | ({ path: '/db' } & GetConfigResponse) | { 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 ( -
+
-
+
{children}
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 = { - 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 (
-
+ + + {/* Hero section */} +
-
-
-

Prosody

+
+
+

+ Master Languages with Sorlang +

+

+ Boost your language learning with our scientifically proven spaced repetition system. Track progress, analyze text, and learn efficiently. +

+
+ + + + + + +
+
+
+
+
+
+ 🇹🇭 +
+
+ 🇨🇳 +
+
+ 🇯🇵 +
+
+ 🇬🇧 +
+
+
+ SRS +
+
+ Spaced Repetition System +
+
+
+
- - {/* Desktop Navigation */} -
- - {/* Mobile Navigation */} -
-
-

Your Languages

- - - - +
+ + {/* Feature highlights */} +
+
+
+

Learning Features

+

+ Tools designed to accelerate your language learning journey +

+
+ +
+ + +
+ +
+ Spaced Repetition + + Optimized review schedule based on your performance + +
+ +

+ Our SRS algorithm determines the optimal time for you to review each card, helping you memorize efficiently. +

+
+
+ + + +
+ +
+ Text Analysis + + Break down complex text into manageable pieces + +
+ +

+ Parse any text to analyze grammar, vocabulary, and patterns to enhance your understanding. +

+
+
+ + + +
+ +
+ Progress Tracking + + Monitor your learning journey with detailed statistics + +
+ +

+ Track your progress across different languages, see your mastery level, and identify areas for improvement. +

+
+
+
+
+
+ + {/* Languages section */} +
+
+
+

Your Languages

+ {totalDueCards > 0 && ( +
+ {totalDueCards} cards due for review +
+ )} +
+ +
+ {Object.entries(langs).map(([langCode, langData]) => ( + + ))} +
+ +
+ + + +
+
); } -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 ( -
-
-
-
{langs[lang].flag}
-
{langs[lang].name}
+ + +
+
+
{langData.flag}
+
+ {langData.name} + {langData.level} +
+
+ {langData.dueCards > 0 && ( +
+ {langData.dueCards} due +
+ )}
- -
-
+ + +
+ +
+ {langData.progress}% complete + {langData.totalCards} cards +
+
+
+ + + + ); } 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 ( +
+ + +
+ +

+ {loggedIn ? "You've been logged out" : "Already logged out"} +

+

+ {loggedIn + ? "Your session has been ended successfully. Thank you for using Sorlang." + : "You were not logged in."} +

+ +
+
+
+ ); +} + +export const getConfig = async () => { + return { + render: "dynamic", + } as const; +}; \ No newline at end of file -- cgit v1.2.3