refactor: move memorize feature to /decks/[deck_id]/learn route
- Delete (features)/memorize directory - Create /decks/[deck_id]/learn with Memorize component and page - Update InDeck.tsx to navigate to new learn route - Fix homepage memorize link to point to /decks
This commit is contained in:
@@ -1,114 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
import { useTranslations } from "next-intl";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { Layers } from "lucide-react";
|
|
||||||
import type { ActionOutputDeck } from "@/modules/deck/deck-action-dto";
|
|
||||||
import type { ActionOutputCardStats } from "@/modules/card/card-action-dto";
|
|
||||||
import { PageLayout } from "@/components/ui/PageLayout";
|
|
||||||
import { PrimaryButton } from "@/design-system/base/button";
|
|
||||||
|
|
||||||
interface DeckWithStats extends ActionOutputDeck {
|
|
||||||
stats?: ActionOutputCardStats;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DeckSelectorProps {
|
|
||||||
decks: ActionOutputDeck[];
|
|
||||||
deckStats: Map<number, ActionOutputCardStats | undefined>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const DeckSelector: React.FC<DeckSelectorProps> = ({ decks, deckStats }) => {
|
|
||||||
const t = useTranslations("memorize.deck_selector");
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const formatCardStats = (stats: ActionOutputCardStats | undefined): string => {
|
|
||||||
if (!stats) return t("noCards");
|
|
||||||
const parts: string[] = [];
|
|
||||||
if (stats.new > 0) parts.push(`${t("new")}: ${stats.new}`);
|
|
||||||
if (stats.learning > 0) parts.push(`${t("learning")}: ${stats.learning}`);
|
|
||||||
if (stats.review > 0) parts.push(`${t("review")}: ${stats.review}`);
|
|
||||||
if (stats.due > 0) parts.push(`${t("due")}: ${stats.due}`);
|
|
||||||
return parts.length > 0 ? parts.join(" • ") : t("noCards");
|
|
||||||
};
|
|
||||||
|
|
||||||
const getDueCount = (deckId: number): number => {
|
|
||||||
const stats = deckStats.get(deckId);
|
|
||||||
return stats?.due ?? 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PageLayout>
|
|
||||||
{decks.length === 0 ? (
|
|
||||||
<div className="text-center">
|
|
||||||
<h1 className="text-2xl md:text-3xl font-bold text-gray-800 mb-4">
|
|
||||||
{t("noDecks")}
|
|
||||||
</h1>
|
|
||||||
<Link href="/decks">
|
|
||||||
<PrimaryButton className="px-6 py-2">
|
|
||||||
{t("goToDecks")}
|
|
||||||
</PrimaryButton>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<h1 className="text-2xl md:text-3xl font-bold text-gray-800 mb-6">
|
|
||||||
{t("selectDeck")}
|
|
||||||
</h1>
|
|
||||||
<div className="border border-gray-200 rounded-lg max-h-96 overflow-y-auto">
|
|
||||||
{decks
|
|
||||||
.toSorted((a, b) => a.id - b.id)
|
|
||||||
.map((deck) => {
|
|
||||||
const stats = deckStats.get(deck.id);
|
|
||||||
const dueCount = getDueCount(deck.id);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={deck.id}
|
|
||||||
onClick={() =>
|
|
||||||
router.push(`/memorize?deck_id=${deck.id}`)
|
|
||||||
}
|
|
||||||
className="flex flex-row items-center p-4 gap-3 hover:cursor-pointer hover:bg-gray-50 transition-colors border-b border-gray-100 last:border-b-0"
|
|
||||||
>
|
|
||||||
<div className="shrink-0">
|
|
||||||
<Layers className="text-gray-600 w-5 h-5" />
|
|
||||||
</div>
|
|
||||||
<div className="flex-1">
|
|
||||||
<div className="font-medium text-gray-900">
|
|
||||||
{deck.name}
|
|
||||||
</div>
|
|
||||||
<div className="text-sm text-gray-500">
|
|
||||||
{formatCardStats(stats)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{dueCount > 0 && (
|
|
||||||
<div className="bg-blue-500 text-white text-xs font-bold px-2 py-1 rounded-full">
|
|
||||||
{dueCount}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="text-gray-400">
|
|
||||||
<svg
|
|
||||||
className="w-5 h-5"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={2}
|
|
||||||
d="M9 5l7 7-7 7"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</PageLayout>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export { DeckSelector };
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
import { redirect } from "next/navigation";
|
|
||||||
import { getTranslations } from "next-intl/server";
|
|
||||||
import { isNonNegativeInteger } from "@/utils/random";
|
|
||||||
import { DeckSelector } from "./DeckSelector";
|
|
||||||
import { Memorize } from "./Memorize";
|
|
||||||
import { auth } from "@/auth";
|
|
||||||
import { headers } from "next/headers";
|
|
||||||
import { actionGetDecksByUserId } from "@/modules/deck/deck-action";
|
|
||||||
import { actionGetCardStats } from "@/modules/card/card-action";
|
|
||||||
|
|
||||||
export default async function MemorizePage({
|
|
||||||
searchParams,
|
|
||||||
}: {
|
|
||||||
searchParams: Promise<{ deck_id?: string; }>;
|
|
||||||
}) {
|
|
||||||
const deckIdParam = (await searchParams).deck_id;
|
|
||||||
|
|
||||||
const t = await getTranslations("memorize.page");
|
|
||||||
|
|
||||||
const deckId = deckIdParam
|
|
||||||
? isNonNegativeInteger(deckIdParam)
|
|
||||||
? parseInt(deckIdParam)
|
|
||||||
: null
|
|
||||||
: null;
|
|
||||||
|
|
||||||
const session = await auth.api.getSession({ headers: await headers() });
|
|
||||||
if (!session) redirect("/login?redirect=/memorize");
|
|
||||||
|
|
||||||
if (!deckId) {
|
|
||||||
const decksResult = await actionGetDecksByUserId(session.user.id);
|
|
||||||
const decks = decksResult.data ?? [];
|
|
||||||
|
|
||||||
const deckStats = new Map<number, Awaited<ReturnType<typeof actionGetCardStats>>["data"]>();
|
|
||||||
for (const deck of decks) {
|
|
||||||
const statsResult = await actionGetCardStats({ deckId: deck.id });
|
|
||||||
if (statsResult.success && statsResult.data) {
|
|
||||||
deckStats.set(deck.id, statsResult.data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DeckSelector
|
|
||||||
decks={decks}
|
|
||||||
deckStats={deckStats}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const decksResult = await actionGetDecksByUserId(session.user.id);
|
|
||||||
const deck = decksResult.data?.find(d => d.id === deckId);
|
|
||||||
|
|
||||||
if (!deck) {
|
|
||||||
redirect("/memorize");
|
|
||||||
}
|
|
||||||
|
|
||||||
return <Memorize deckId={deckId} deckName={deck.name} />;
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { ArrowLeft, Plus, RotateCcw } from "lucide-react";
|
import { ArrowLeft, Plus, RotateCcw } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { redirect, useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { AddCardModal } from "./AddCardModal";
|
import { AddCardModal } from "./AddCardModal";
|
||||||
import { CardItem } from "./CardItem";
|
import { CardItem } from "./CardItem";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
@@ -99,7 +99,7 @@ export function InDeck({ deckId, isReadOnly }: { deckId: number; isReadOnly: boo
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
redirect(`/memorize?deck_id=${deckId}`);
|
router.push(`/decks/${deckId}/learn`);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t("memorize")}
|
{t("memorize")}
|
||||||
|
|||||||
@@ -9,11 +9,11 @@ import type { ActionOutputCardWithNote, ActionOutputScheduledCard } from "@/modu
|
|||||||
import { actionGetCardsForReview, actionAnswerCard } from "@/modules/card/card-action";
|
import { actionGetCardsForReview, actionAnswerCard } from "@/modules/card/card-action";
|
||||||
import { PageLayout } from "@/components/ui/PageLayout";
|
import { PageLayout } from "@/components/ui/PageLayout";
|
||||||
import { LightButton } from "@/design-system/base/button";
|
import { LightButton } from "@/design-system/base/button";
|
||||||
import { CardType } from "../../../../generated/prisma/enums";
|
import { CardType } from "../../../../../generated/prisma/enums";
|
||||||
import { calculatePreviewIntervals, formatPreviewInterval, type CardPreview } from "./interval-preview";
|
import { calculatePreviewIntervals, formatPreviewInterval, type CardPreview } from "./interval-preview";
|
||||||
|
|
||||||
const myFont = localFont({
|
const myFont = localFont({
|
||||||
src: "../../../../public/fonts/NotoNaskhArabic-VariableFont_wght.ttf",
|
src: "../../../../../public/fonts/NotoNaskhArabic-VariableFont_wght.ttf",
|
||||||
});
|
});
|
||||||
|
|
||||||
interface MemorizeProps {
|
interface MemorizeProps {
|
||||||
@@ -207,7 +207,7 @@ const Memorize: React.FC<MemorizeProps> = ({ deckId, deckName }) => {
|
|||||||
<PageLayout>
|
<PageLayout>
|
||||||
<div className="text-center py-12">
|
<div className="text-center py-12">
|
||||||
<p className="text-red-600 mb-4">{error}</p>
|
<p className="text-red-600 mb-4">{error}</p>
|
||||||
<LightButton onClick={() => router.push("/memorize")} className="px-4 py-2">
|
<LightButton onClick={() => router.push("/decks")} className="px-4 py-2">
|
||||||
{t("backToDecks")}
|
{t("backToDecks")}
|
||||||
</LightButton>
|
</LightButton>
|
||||||
</div>
|
</div>
|
||||||
@@ -224,7 +224,7 @@ const Memorize: React.FC<MemorizeProps> = ({ deckId, deckName }) => {
|
|||||||
</div>
|
</div>
|
||||||
<h2 className="text-2xl font-bold text-gray-800 mb-2">{t("allDone")}</h2>
|
<h2 className="text-2xl font-bold text-gray-800 mb-2">{t("allDone")}</h2>
|
||||||
<p className="text-gray-600 mb-6">{t("allDoneDesc")}</p>
|
<p className="text-gray-600 mb-6">{t("allDoneDesc")}</p>
|
||||||
<LightButton onClick={() => router.push("/memorize")} className="px-4 py-2">
|
<LightButton onClick={() => router.push("/decks")} className="px-4 py-2">
|
||||||
{t("backToDecks")}
|
{t("backToDecks")}
|
||||||
</LightButton>
|
</LightButton>
|
||||||
</div>
|
</div>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { CardType } from "../../../../generated/prisma/enums";
|
import { CardType } from "../../../../../generated/prisma/enums";
|
||||||
import { SM2_CONFIG } from "@/modules/card/card-service-dto";
|
import { SM2_CONFIG } from "@/modules/card/card-service-dto";
|
||||||
|
|
||||||
export interface CardPreview {
|
export interface CardPreview {
|
||||||
34
src/app/decks/[deck_id]/learn/page.tsx
Normal file
34
src/app/decks/[deck_id]/learn/page.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { redirect } from "next/navigation";
|
||||||
|
import { auth } from "@/auth";
|
||||||
|
import { headers } from "next/headers";
|
||||||
|
import { actionGetDeckById } from "@/modules/deck/deck-action";
|
||||||
|
import { Memorize } from "./Memorize";
|
||||||
|
|
||||||
|
export default async function LearnPage({
|
||||||
|
params,
|
||||||
|
}: {
|
||||||
|
params: Promise<{ deck_id: string }>;
|
||||||
|
}) {
|
||||||
|
const session = await auth.api.getSession({ headers: await headers() });
|
||||||
|
const { deck_id } = await params;
|
||||||
|
const deckId = Number(deck_id);
|
||||||
|
|
||||||
|
if (!deckId) {
|
||||||
|
redirect("/decks");
|
||||||
|
}
|
||||||
|
|
||||||
|
const deckInfo = (await actionGetDeckById({ deckId })).data;
|
||||||
|
|
||||||
|
if (!deckInfo) {
|
||||||
|
redirect("/decks");
|
||||||
|
}
|
||||||
|
|
||||||
|
const isOwner = session?.user?.id === deckInfo.userId;
|
||||||
|
const isPublic = deckInfo.visibility === "PUBLIC";
|
||||||
|
|
||||||
|
if (!isOwner && !isPublic) {
|
||||||
|
redirect("/decks");
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Memorize deckId={deckId} deckName={deckInfo.name} />;
|
||||||
|
}
|
||||||
@@ -73,7 +73,7 @@ export default async function HomePage() {
|
|||||||
color="#dd7486"
|
color="#dd7486"
|
||||||
></LinkArea>
|
></LinkArea>
|
||||||
<LinkArea
|
<LinkArea
|
||||||
href="/memorize"
|
href="/decks"
|
||||||
name={t("memorize.name")}
|
name={t("memorize.name")}
|
||||||
description={t("memorize.description")}
|
description={t("memorize.description")}
|
||||||
color="#cc9988"
|
color="#cc9988"
|
||||||
|
|||||||
Reference in New Issue
Block a user