diff options
author | polwex <polwex@sortug.com> | 2025-05-29 15:49:50 +0700 |
---|---|---|
committer | polwex <polwex@sortug.com> | 2025-05-29 15:49:50 +0700 |
commit | 84c5b778039102a77b7fda2ddcab2bbf70085bdc (patch) | |
tree | cd9458f2efe5c890dde2a9bb0b2ae6353dbad068 | |
parent | f23f7d2f0106882183929c740e4862a1939900d0 (diff) |
good progress good progrss
-rw-r--r-- | src/components/Flashcard/StudyCard.tsx | 94 | ||||
-rw-r--r-- | src/components/Navbar.tsx | 202 | ||||
-rw-r--r-- | src/components/ui/avatar.tsx | 50 | ||||
-rw-r--r-- | src/lib/db/seed.ts | 1 |
4 files changed, 312 insertions, 35 deletions
diff --git a/src/components/Flashcard/StudyCard.tsx b/src/components/Flashcard/StudyCard.tsx index 4e554b4..1c52de7 100644 --- a/src/components/Flashcard/StudyCard.tsx +++ b/src/components/Flashcard/StudyCard.tsx @@ -16,11 +16,16 @@ interface StudyCardProps { onSkip?: () => void; } -export default function StudyCard({ card, userId, onComplete, onSkip }: StudyCardProps) { +export default function StudyCard({ + card, + userId, + onComplete, + onSkip, +}: StudyCardProps) { const [isFlipped, setIsFlipped] = useState(false); const [startTime, setStartTime] = useState(0); const [isSubmitting, setIsSubmitting] = useState(false); - + // Reset the timer when a new card is shown useEffect(() => { setIsFlipped(false); @@ -42,13 +47,13 @@ export default function StudyCard({ card, userId, onComplete, onSkip }: StudyCar // Handle card grading (Good/Again) const handleGrade = async (isCorrect: boolean) => { if (isSubmitting) return; - + setIsSubmitting(true); - + try { const result = await gradeCard(userId, card.id, isCorrect); - - if ('error' in result) { + + if ("error" in result) { console.error("Error grading card:", result.error); } else { onComplete(result as CardResponse); @@ -63,14 +68,14 @@ export default function StudyCard({ card, userId, onComplete, onSkip }: StudyCar // Handle detailed grading with accuracy level const handleDetailedGrade = async (accuracy: number) => { if (isSubmitting) return; - + setIsSubmitting(true); - + try { const reviewTime = getReviewTime(); const result = await processReview(userId, card.id, accuracy, reviewTime); - - if ('error' in result) { + + if ("error" in result) { console.error("Error processing review:", result.error); } else { onComplete(result as CardResponse); @@ -102,7 +107,7 @@ export default function StudyCard({ card, userId, onComplete, onSkip }: StudyCar if (card.expression.ipa && card.expression.ipa.length > 0) { return ( <div className="text-gray-500 text-sm mt-2"> - /{card.expression.ipa[0].ipa}/ + /{card.expression.ipa[0]!.ipa}/ </div> ); } @@ -116,14 +121,22 @@ export default function StudyCard({ card, userId, onComplete, onSkip }: StudyCar <div className="mt-4"> {card.expression.senses.map((sense, index) => ( <div key={index} className="mb-3"> - {sense.pos && <span className="text-xs font-medium text-blue-600 mr-2">{sense.pos}</span>} - {sense.senses && sense.senses.map((subsense, i) => ( - <div key={i} className="mt-1"> - {subsense.glosses && subsense.glosses.map((gloss, j) => ( - <div key={j} className="text-sm">{j+1}. {gloss}</div> - ))} - </div> - ))} + {sense.pos && ( + <span className="text-xs font-medium text-blue-600 mr-2"> + {sense.pos} + </span> + )} + {sense.senses && + sense.senses.map((subsense, i) => ( + <div key={i} className="mt-1"> + {subsense.glosses && + subsense.glosses.map((gloss, j) => ( + <div key={j} className="text-sm"> + {j + 1}. {gloss} + </div> + ))} + </div> + ))} </div> ))} </div> @@ -142,36 +155,47 @@ export default function StudyCard({ card, userId, onComplete, onSkip }: StudyCar return ( <div className="flex flex-col items-center"> - <div className={cn("flashcard-container", { flipped: isFlipped })} onClick={flipCard}> + <div + className={cn("flashcard-container", { flipped: isFlipped })} + onClick={flipCard} + > <div className="flashcard"> {/* Front of card */} <div className="flashcard-front"> <Card className="w-full h-full flex flex-col justify-center items-center p-6 relative"> {renderBookmarked()} - <div className="text-2xl font-bold">{card.expression.spelling}</div> + <div className="text-2xl font-bold"> + {card.expression.spelling} + </div> {!isFlipped && renderIPA()} <div className="mt-4 text-lg">{formatCardContent(card.text)}</div> - {card.note && <div className="mt-2 text-sm text-gray-500">{card.note}</div>} + {card.note && ( + <div className="mt-2 text-sm text-gray-500">{card.note}</div> + )} {!isFlipped && ( - <div className="mt-6 text-sm text-gray-400"> - Click to flip - </div> + <div className="mt-6 text-sm text-gray-400">Click to flip</div> )} </Card> </div> - + {/* Back of card */} <div className="flashcard-back"> <Card className="w-full h-full flex flex-col justify-between p-6 relative"> {renderBookmarked()} <div> - <div className="text-2xl font-bold">{card.expression.spelling}</div> + <div className="text-2xl font-bold"> + {card.expression.spelling} + </div> {renderIPA()} - <div className="mt-4 text-lg">{formatCardContent(card.text, true)}</div> - {card.note && <div className="mt-2 text-sm text-gray-500">{card.note}</div>} + <div className="mt-4 text-lg"> + {formatCardContent(card.text, true)} + </div> + {card.note && ( + <div className="mt-2 text-sm text-gray-500">{card.note}</div> + )} {renderSenses()} </div> - + <div className="flex flex-col mt-6"> <div className="text-sm text-gray-500 mb-2"> How well did you remember this? @@ -188,13 +212,13 @@ export default function StudyCard({ card, userId, onComplete, onSkip }: StudyCar <Button variant="default" onClick={() => handleGrade(true)} - disabled={isSubmitting} + disabled={isSubmitting} className="flex-1" > Good </Button> </div> - + {/* Optional: Detailed grading */} <div className="grid grid-cols-4 gap-2 mt-3"> <Button @@ -239,7 +263,7 @@ export default function StudyCard({ card, userId, onComplete, onSkip }: StudyCar </div> </div> </div> - + {/* Progress bar */} <div className="w-full mt-4"> <Progress value={getProgressPercentage()} className="h-2" /> @@ -248,7 +272,7 @@ export default function StudyCard({ card, userId, onComplete, onSkip }: StudyCar <span>Ease: {card.progress.easeFactor.toFixed(1)}</span> </div> </div> - + {/* Skip button */} {onSkip && ( <Button @@ -262,4 +286,4 @@ export default function StudyCard({ card, userId, onComplete, onSkip }: StudyCar )} </div> ); -}
\ No newline at end of file +} diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx new file mode 100644 index 0000000..68375ae --- /dev/null +++ b/src/components/Navbar.tsx @@ -0,0 +1,202 @@ +"use client"; + +import { useState } from "react"; +import { Link } from "waku"; +import { Button } from "@/components/ui/button"; +import { Avatar, AvatarFallback } from "@/components/ui/avatar"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { Menu, X, BookOpen, BarChart2, Settings } from "lucide-react"; + +interface NavbarProps { + user?: { id: number; name: string } | null; +} + +export default function Navbar({ user }: NavbarProps) { + const [isMenuOpen, setIsMenuOpen] = useState(false); + + const toggleMenu = () => setIsMenuOpen(!isMenuOpen); + + // Get initials for avatar + const getInitials = (name: string) => { + return name + .split(" ") + .map(n => n[0]) + .join("") + .toUpperCase(); + }; + + return ( + <nav className="bg-white border-b border-gray-200 sticky top-0 z-50"> + <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> + <div className="flex justify-between h-16"> + <div className="flex"> + <div className="flex-shrink-0 flex items-center"> + <Link to="/"> + <span className="text-2xl font-bold text-indigo-600">Sorlang</span> + </Link> + </div> + <div className="hidden sm:ml-6 sm:flex sm:space-x-8"> + <Link to="/"> + <span className="inline-flex items-center px-1 pt-1 border-b-2 border-indigo-500 text-sm font-medium text-gray-900"> + Home + </span> + </Link> + <Link to="/study"> + <span className="inline-flex items-center px-1 pt-1 border-b-2 border-transparent text-sm font-medium text-gray-500 hover:text-gray-700 hover:border-gray-300"> + Study + </span> + </Link> + <Link to="/parse"> + <span className="inline-flex items-center px-1 pt-1 border-b-2 border-transparent text-sm font-medium text-gray-500 hover:text-gray-700 hover:border-gray-300"> + Parse Text + </span> + </Link> + <Link to="/zoom"> + <span className="inline-flex items-center px-1 pt-1 border-b-2 border-transparent text-sm font-medium text-gray-500 hover:text-gray-700 hover:border-gray-300"> + Text Explorer + </span> + </Link> + </div> + </div> + + <div className="hidden sm:ml-6 sm:flex sm:items-center"> + {user ? ( + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button variant="ghost" className="relative h-10 w-10 rounded-full"> + <Avatar className="h-10 w-10 bg-indigo-100 text-indigo-800"> + <AvatarFallback>{getInitials(user.name)}</AvatarFallback> + </Avatar> + </Button> + </DropdownMenuTrigger> + <DropdownMenuContent align="end"> + <DropdownMenuLabel>My Account</DropdownMenuLabel> + <DropdownMenuLabel className="text-xs text-gray-500"> + Signed in as {user.name} + </DropdownMenuLabel> + <DropdownMenuSeparator /> + <Link to="/profile"> + <DropdownMenuItem>Profile</DropdownMenuItem> + </Link> + <Link to="/study"> + <DropdownMenuItem>My Cards</DropdownMenuItem> + </Link> + <Link to="/settings"> + <DropdownMenuItem>Settings</DropdownMenuItem> + </Link> + <DropdownMenuSeparator /> + <DropdownMenuItem> + <Link to="/logout">Sign out</Link> + </DropdownMenuItem> + </DropdownMenuContent> + </DropdownMenu> + ) : ( + <div className="flex items-center space-x-2"> + <Link to="/login"> + <Button variant="ghost">Login</Button> + </Link> + <Link to="/login?register=true"> + <Button>Sign Up</Button> + </Link> + </div> + )} + </div> + + <div className="-mr-2 flex items-center sm:hidden"> + <button + onClick={toggleMenu} + className="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500" + > + <span className="sr-only">Open main menu</span> + {isMenuOpen ? ( + <X className="block h-6 w-6" aria-hidden="true" /> + ) : ( + <Menu className="block h-6 w-6" aria-hidden="true" /> + )} + </button> + </div> + </div> + </div> + + {/* Mobile menu */} + <div className={`${isMenuOpen ? 'block' : 'hidden'} sm:hidden`}> + <div className="pt-2 pb-3 space-y-1"> + <Link to="/"> + <span className="bg-indigo-50 border-indigo-500 text-indigo-700 block pl-3 pr-4 py-2 border-l-4 text-base font-medium"> + Home + </span> + </Link> + <Link to="/study"> + <span className="border-transparent text-gray-500 hover:bg-gray-50 hover:border-gray-300 hover:text-gray-700 block pl-3 pr-4 py-2 border-l-4 text-base font-medium"> + Study + </span> + </Link> + <Link to="/parse"> + <span className="border-transparent text-gray-500 hover:bg-gray-50 hover:border-gray-300 hover:text-gray-700 block pl-3 pr-4 py-2 border-l-4 text-base font-medium"> + Parse Text + </span> + </Link> + <Link to="/zoom"> + <span className="border-transparent text-gray-500 hover:bg-gray-50 hover:border-gray-300 hover:text-gray-700 block pl-3 pr-4 py-2 border-l-4 text-base font-medium"> + Text Explorer + </span> + </Link> + </div> + <div className="pt-4 pb-3 border-t border-gray-200"> + {user ? ( + <> + <div className="flex items-center px-4"> + <div className="flex-shrink-0"> + <Avatar className="h-10 w-10 bg-indigo-100 text-indigo-800"> + <AvatarFallback>{getInitials(user.name)}</AvatarFallback> + </Avatar> + </div> + <div className="ml-3"> + <div className="text-base font-medium text-gray-800">{user.name}</div> + </div> + </div> + <div className="mt-3 space-y-1"> + <Link to="/profile"> + <span className="block px-4 py-2 text-base font-medium text-gray-500 hover:text-gray-800 hover:bg-gray-100"> + Profile + </span> + </Link> + <Link to="/study"> + <span className="block px-4 py-2 text-base font-medium text-gray-500 hover:text-gray-800 hover:bg-gray-100"> + My Cards + </span> + </Link> + <Link to="/settings"> + <span className="block px-4 py-2 text-base font-medium text-gray-500 hover:text-gray-800 hover:bg-gray-100"> + Settings + </span> + </Link> + <Link to="/logout"> + <span className="block px-4 py-2 text-base font-medium text-gray-500 hover:text-gray-800 hover:bg-gray-100"> + Sign out + </span> + </Link> + </div> + </> + ) : ( + <div className="mt-3 space-y-1 px-4"> + <Link to="/login"> + <Button className="w-full mb-2">Login</Button> + </Link> + <Link to="/login?register=true"> + <Button variant="outline" className="w-full">Sign Up</Button> + </Link> + </div> + )} + </div> + </div> + </nav> + ); +}
\ No newline at end of file diff --git a/src/components/ui/avatar.tsx b/src/components/ui/avatar.tsx new file mode 100644 index 0000000..98f925f --- /dev/null +++ b/src/components/ui/avatar.tsx @@ -0,0 +1,50 @@ +"use client" + +import * as React from "react" +import * as AvatarPrimitive from "@radix-ui/react-avatar" + +import { cn } from "@/lib/utils" + +const Avatar = React.forwardRef< + React.ElementRef<typeof AvatarPrimitive.Root>, + React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root> +>(({ className, ...props }, ref) => ( + <AvatarPrimitive.Root + ref={ref} + className={cn( + "relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full", + className + )} + {...props} + /> +)) +Avatar.displayName = AvatarPrimitive.Root.displayName + +const AvatarImage = React.forwardRef< + React.ElementRef<typeof AvatarPrimitive.Image>, + React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image> +>(({ className, ...props }, ref) => ( + <AvatarPrimitive.Image + ref={ref} + className={cn("aspect-square h-full w-full", className)} + {...props} + /> +)) +AvatarImage.displayName = AvatarPrimitive.Image.displayName + +const AvatarFallback = React.forwardRef< + React.ElementRef<typeof AvatarPrimitive.Fallback>, + React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback> +>(({ className, ...props }, ref) => ( + <AvatarPrimitive.Fallback + ref={ref} + className={cn( + "flex h-full w-full items-center justify-center rounded-full bg-muted", + className + )} + {...props} + /> +)) +AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName + +export { Avatar, AvatarImage, AvatarFallback }
\ No newline at end of file diff --git a/src/lib/db/seed.ts b/src/lib/db/seed.ts index b776782..4780dc3 100644 --- a/src/lib/db/seed.ts +++ b/src/lib/db/seed.ts @@ -521,6 +521,7 @@ async function redump() { for await (const line of readWiktionaryDump()) { try { count++; + console.log({ count }); // if (count > 50) break; const j = JSON.parse(line); // console.log(Object.keys(j), j.word); |