summaryrefslogtreecommitdiff
path: root/src/components
diff options
context:
space:
mode:
authorpolwex <polwex@sortug.com>2025-05-29 15:49:50 +0700
committerpolwex <polwex@sortug.com>2025-05-29 15:49:50 +0700
commit84c5b778039102a77b7fda2ddcab2bbf70085bdc (patch)
treecd9458f2efe5c890dde2a9bb0b2ae6353dbad068 /src/components
parentf23f7d2f0106882183929c740e4862a1939900d0 (diff)
good progress good progrss
Diffstat (limited to 'src/components')
-rw-r--r--src/components/Flashcard/StudyCard.tsx94
-rw-r--r--src/components/Navbar.tsx202
-rw-r--r--src/components/ui/avatar.tsx50
3 files changed, 311 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