From 7abf2227438362ad30820ee236405ec1b57a40b6 Mon Sep 17 00:00:00 2001 From: polwex Date: Wed, 21 May 2025 17:13:11 +0700 Subject: m --- src/actions/deck.ts | 32 +++++++++++ src/components/Flashcard/BookmarkButton.tsx | 41 +++++++++++++++ src/components/Flashcard/Deck2.tsx | 29 ++++++++-- src/components/Flashcard/ServerCard.tsx | 13 ++--- src/lib/db/index.ts | 82 +++++++++++++++++++---------- src/lib/db/schema.sql | 7 ++- src/lib/server/cookie.ts | 6 +-- src/lib/types/cards.ts | 6 ++- src/pages/lesson/[slug].tsx | 6 ++- 9 files changed, 174 insertions(+), 48 deletions(-) create mode 100644 src/actions/deck.ts create mode 100644 src/components/Flashcard/BookmarkButton.tsx diff --git a/src/actions/deck.ts b/src/actions/deck.ts new file mode 100644 index 0000000..e501f23 --- /dev/null +++ b/src/actions/deck.ts @@ -0,0 +1,32 @@ +"use server"; +import ServerWord from "@/zoom/ServerWord"; +import { analyzeTHWord, segmentateThai } from "@/pages/api/nlp"; +import db from "../lib/db"; + +export function shuffleDeck(userId: number, lessonId: number) { + const res = db.fetchLesson({ userId, lessonId, random: true }); + return res; +} + +export async function thaiAnalysis(text: string) { + const res = await segmentateThai(text); + const res2 = await analyzeTHWord(text); + console.log({ res, res2 }); +} +export async function toggleBookmark( + userId: number, + wordId: number, + is: boolean, + notes?: string, +) { + console.log("toggling on server, ostensibly"); + const r = !is + ? db.addBookmark(userId, wordId, notes) + : db.delBookmark(userId, wordId); + return { ok: "ack" }; +} + +// export async function ocrAction(file: File): AsyncRes { +// const res = await NLP.ocr(file); +// return res; +// } diff --git a/src/components/Flashcard/BookmarkButton.tsx b/src/components/Flashcard/BookmarkButton.tsx new file mode 100644 index 0000000..5128b91 --- /dev/null +++ b/src/components/Flashcard/BookmarkButton.tsx @@ -0,0 +1,41 @@ +"use client"; + +import { toggleBookmark } from "@/actions/deck"; +import { CardResponse } from "@/lib/types/cards"; +import { BookMarkedIcon, BookmarkIcon } from "lucide-react"; +import { useEffect, useState, useTransition } from "react"; + +export const BookmarkIconito: React.FC<{ card: CardResponse }> = ({ card }) => { + const [notes, setNotes] = useState(); + const [isBookmarked, setBookmarked] = useState(false); + useEffect(() => { + setBookmarked(card.expression.isBookmarked); + }, [card]); + + const [isPending, startTransition] = useTransition(); + const toggle = (e: React.MouseEvent) => { + console.log("toggling on fe"); + e.stopPropagation(); + startTransition(async () => { + const res = await toggleBookmark( + 2, + card.expression.id, + isBookmarked, + notes, + ); + if ("ok" in res) setBookmarked(true); + }); + }; + + return isBookmarked ? ( + + ) : ( + + ); +}; diff --git a/src/components/Flashcard/Deck2.tsx b/src/components/Flashcard/Deck2.tsx index 4fd8740..3194037 100644 --- a/src/components/Flashcard/Deck2.tsx +++ b/src/components/Flashcard/Deck2.tsx @@ -1,7 +1,13 @@ "use client"; import { CardResponse, DeckResponse } from "@/lib/types/cards"; -import React, { ReactNode, useCallback, useEffect, useState } from "react"; +import React, { + ReactNode, + useCallback, + useEffect, + useState, + useTransition, +} from "react"; import { Button } from "../ui/button"; import { ChevronLeftIcon, ChevronRightIcon, RotateCcwIcon } from "lucide-react"; import "./cards.css"; @@ -39,8 +45,8 @@ function Deck({ data, cards }: { data: DeckResponse; cards: CardData[] }) { setTimeout(() => { setAnimationDirection("none"); setIsAnimating(false); - }, 500); // Duration of enter animation - }, 500); // Duration of exit animation + }, 200); // Duration of enter animation + }, 200); // Duration of exit animation }, [currentIndex, cards.length, isAnimating]); const handlePrev = useCallback(() => { @@ -55,8 +61,8 @@ function Deck({ data, cards }: { data: DeckResponse; cards: CardData[] }) { setTimeout(() => { setAnimationDirection("none"); setIsAnimating(false); - }, 500); // Duration of enter animation - }, 500); // Duration of exit animation + }, 200); // Duration of enter animation + }, 200); // Duration of exit animation }, [currentIndex, isAnimating]); // Keyboard navigation @@ -80,6 +86,14 @@ function Deck({ data, cards }: { data: DeckResponse; cards: CardData[] }) { }; }, [handleNext, handlePrev, isAnimating]); + const [isPending, startTransition] = useTransition(); + const shuffle = () => { + startTransition(async () => { + "use server"; + console.log("shuffling deck..."); + }); + }; + if (cards.length === 0) { return (
@@ -93,6 +107,10 @@ function Deck({ data, cards }: { data: DeckResponse; cards: CardData[] }) { return (
+
+

Deck: {data.lesson.name}

+

{data.lesson.description}

+
{/* This div is for positioning the card and managing overflow during animations */}
@@ -145,6 +163,7 @@ function Deck({ data, cards }: { data: DeckResponse; cards: CardData[] }) {
Use Arrow Keys (← →) to navigate, Space/Enter to flip.
+
); } diff --git a/src/components/Flashcard/ServerCard.tsx b/src/components/Flashcard/ServerCard.tsx index 75442b4..d377dce 100644 --- a/src/components/Flashcard/ServerCard.tsx +++ b/src/components/Flashcard/ServerCard.tsx @@ -25,11 +25,12 @@ import { import { CardResponse } from "@/lib/types/cards"; import { thaiData } from "@/pages/api/nlp"; import { getRandomHexColor } from "@/lib/utils"; +import { BookmarkIconito } from "./BookmarkButton"; export async function CardFront({ data }: { data: CardResponse }) { // const extraData = data.expression.lang const extraData = await thaiData(data.expression.spelling); - console.log({ extraData }); + // console.log({ extraData }); return (
@@ -41,10 +42,11 @@ export async function CardFront({ data }: { data: CardResponse }) { } >

- {extraData[0]?.syllables.map((syl) => ( + {extraData[0]?.syllables.map((syl, i) => ( {syl} @@ -92,10 +94,9 @@ export const IpaDisplay = ({ }; export async function CardBack({ data }: { data: CardResponse }) { - // return ( -

- +
+ {data.expression.senses.map((ss, i) => (
diff --git a/src/lib/db/index.ts b/src/lib/db/index.ts index b43edc3..fcfab57 100644 --- a/src/lib/db/index.ts +++ b/src/lib/db/index.ts @@ -44,7 +44,7 @@ class DatabaseHandler { name: string; expiry: number; }; - console.log("cokifetch", { coki, res }); + // console.log("cokifetch", { coki, res }); return res; } setCookie(coki: string, user: number, expiry: number) { @@ -329,18 +329,24 @@ class DatabaseHandler { } // SELECT l.id, l.text, cards.text, cards.note FROM cards_lessons cl LEFT JOIN lessons l ON l.id = cl.lesson_id LEFT JOIN cards ON cards.id = cl.card_id ORDER BY l.id ASC LIMIT 20 OFFSET 0; - fetchLesson( - userId: number, - lessonId: number, - count?: number, - page?: number, - ): Result { + fetchLesson({ + userId, + lessonId, + count, + page, + random, + }: { + userId: number; + lessonId: number; + count?: number; + page?: number; + random?: boolean; + }): Result { const p = page ? page : 1; const size = count ? count : PAGE_SIZE; const offset = getDBOffset(p, size); const tomorrow = new Date(); tomorrow.setDate(tomorrow.getDate() + 1); - console.log(tomorrow.getTime()); const queryString = ` SELECT l.name, l.description, ll.lang as llang, cards.text, cards.note, cards.id as cid, @@ -351,7 +357,9 @@ class DatabaseHandler { up.next_review_date, up.last_reviewed, up.is_mastered, + e.id as eid, e.*, + (CASE WHEN bm.word_id IS NULL THEN 0 ELSE 1 END) as is_bookmarked, (SELECT json_group_array(json_object( 'pos', pos, @@ -371,31 +379,28 @@ class DatabaseHandler { JOIN cards ON cards.id = cl.card_id JOIN cards_expressions ce ON cards.id = ce.card_id JOIN expressions e ON e.id = ce.expression_id - LEFT JOIN user_progress up ON up.card_id = cards.id AND up.user_id = ? - WHERE l.id = ? AND (up.next_review_date IS NULL OR up.next_review_date < ?) - ORDER BY cards.id, e.id - LIMIT ? OFFSET ?; + LEFT JOIN user_progress up ON up.card_id = cards.id AND up.user_id = ?1 + LEFT JOIN bookmarks bm ON bm.word_id = e.id AND bm.user_id = ?1 + WHERE l.id = ?2 AND (up.next_review_date IS NULL OR up.next_review_date < ?3) + ${ + random + ? // ? "AND e.id IN (SELECT id FROM expressions ORDER BY RANDOM() LIMIT ?4 OFFSET ?5)" + // "AND e.rowid > (ABS(RANDOM()) % (SELECT max(rowid) FROM expressions)) LIMIT ?4 OFFSET ?5" + "ORDER BY RANDOM() LIMIT ?4 OFFSET ?5" + : "ORDER BY cards.id, e.id LIMIT ?4 OFFSET ?5" + }; `; - // const queryString = ` - // SELECT - // l.id, l.name, l.description, l.lang, cards.text, cards.note, cards.id as cid, - // spelling, ipa, frequency, e.id as eid, - // GROUP_CONCAT(wc.category, ',') AS category - // FROM cards_lessons cl - // JOIN lessons l ON l.id = cl.lesson_id - // JOIN cards ON cards.id = cl.card_id - // JOIN cards_expressions ce ON cards.id = ce.card_id - // JOIN expressions e ON e.id = ce.expression_id - // JOIN word_categories wc ON wc.word_id = e.id - // WHERE l.id = ? - // LIMIT ? OFFSET ?; - // `; + // SELECT * FROM expressions e + // WHERE e.rowid > ( + // ABS(RANDOM()) % (SELECT max(rowid) FROM expressions) + // ) + // LIMIT 10; const query = this.db.query(queryString); const res = query.all(userId, lessonId, tomorrow.getTime(), size, offset); - console.log(res.length); + // console.log("cards", res.length); if (res.length === 0) return { error: "Lesson not found" }; const row: any = res[0]; - // console.log({ row }); + console.log({ row }); const lesson = { id: lessonId, name: row.name, @@ -415,6 +420,7 @@ class DatabaseHandler { return { ...s, senses, related, forms }; }); const expression = { + isBookmarked: row.is_bookmarked > 0, ipa: JSON.parse(row.ipa), prosody: JSON.parse(row.prosody), syllables: row.syllables, @@ -673,6 +679,26 @@ class DatabaseHandler { const res = query.run({ wordId, category }); return res.lastInsertRowid; } + addBookmark(userId: number, wordId: number | bigint, notes?: string) { + const queryString = ` + INSERT OR IGNORE + INTO bookmarks(user_id, word_id, created, notes) + VALUES(?, ?, ?, ?) + `; + const query = this.db.query(queryString); + const res = query.run(userId, wordId, Date.now(), notes || null); + return res.lastInsertRowid; + } + delBookmark(userId: number, wordId: number | bigint) { + const queryString = ` + DELETE + FROM bookmarks + WHERE word_id = ? AND user_id = ? + `; + const query = this.db.query(queryString); + const res = query.run(wordId, userId); + return res; + } addThaiSyl(params: { spelling: string; tone: number; diff --git a/src/lib/db/schema.sql b/src/lib/db/schema.sql index 4506619..129400a 100644 --- a/src/lib/db/schema.sql +++ b/src/lib/db/schema.sql @@ -58,10 +58,13 @@ CREATE INDEX IF NOT EXISTS idx_senses_parent ON senses(parent_id); CREATE TABLE IF NOT EXISTS bookmarks( - word_id INTEGER PRIMARY KEY, + word_id INTEGER NOT NULL, + user_id INTEGER NOT NULL, notes TEXT, created INTEGER NOT NULL, - FOREIGN KEY (word_id) REFERENCES expressions(id) + PRIMARY KEY (word_id, user_id), + FOREIGN KEY (word_id) REFERENCES expressions(id), + FOREIGN KEY (user_id) REFERENCES users(id) ); CREATE INDEX IF NOT EXISTS idx_bookmarks ON bookmarks(word_id); diff --git a/src/lib/server/cookie.ts b/src/lib/server/cookie.ts index bbabd63..9a7e632 100644 --- a/src/lib/server/cookie.ts +++ b/src/lib/server/cookie.ts @@ -18,7 +18,7 @@ const cookieMiddleware: Middleware = () => { // } if (coki) { const userRow = db.fetchCookie(coki); - console.log({ userRow }); + // console.log({ userRow }); if (userRow) ctx.data.user = { id: userRow.id, name: userRow.name }; // else { // if (ctx.req.url.pathname === "/login") return await next(); @@ -30,8 +30,8 @@ const cookieMiddleware: Middleware = () => { } await next(); const hctx: any = getHonoContext(); - console.log("hono", hctx.lol); - console.log("ctx coki", ctx.data.cookie); + // console.log("hono", hctx.lol); + // console.log("ctx coki", ctx.data.cookie); ctx.res.headers ||= {}; if (ctx.data.cookie) ctx.res.headers["set-cookie"] = ctx.data.cookie as string; diff --git a/src/lib/types/cards.ts b/src/lib/types/cards.ts index 0592a34..cef02d2 100644 --- a/src/lib/types/cards.ts +++ b/src/lib/types/cards.ts @@ -154,18 +154,20 @@ export interface ReviewResult { export type CardResponse = { id: number; text: string; - note: string; + note: string | null; progress: SRSProgress; expression: { + id: number; ipa: Array<{ ipa: string; tags: string[] }>; spelling: string; type: ExpressionType; - syllables: number; + syllables: number | null; confidence: number; lang: string; frequency: number; prosody: any; senses: Sense[]; + isBookmarked: boolean; }; }; export type Sense = { diff --git a/src/pages/lesson/[slug].tsx b/src/pages/lesson/[slug].tsx index 9e6e6cc..9078958 100644 --- a/src/pages/lesson/[slug].tsx +++ b/src/pages/lesson/[slug].tsx @@ -52,8 +52,10 @@ export default async function HomePage(props: PageProps<"/lesson/[slug]">) { ); } -const getData = async (lesson: number, userId: number) => { - const lessons = db.fetchLesson(userId, lesson); +const getData = async (lessonId: number, userId: number) => { + const lessons = db.fetchLesson({ userId, lessonId, random: true }); + // const lessons = db.fetchLesson({ userId, lessonId, random: false, page: 3 }); + console.log({ lessons }); return lessons; }; -- cgit v1.2.3