diff options
-rw-r--r-- | src/actions/deck.ts | 32 | ||||
-rw-r--r-- | src/components/Flashcard/BookmarkButton.tsx | 41 | ||||
-rw-r--r-- | src/components/Flashcard/Deck2.tsx | 29 | ||||
-rw-r--r-- | src/components/Flashcard/ServerCard.tsx | 13 | ||||
-rw-r--r-- | src/lib/db/index.ts | 82 | ||||
-rw-r--r-- | src/lib/db/schema.sql | 7 | ||||
-rw-r--r-- | src/lib/server/cookie.ts | 6 | ||||
-rw-r--r-- | src/lib/types/cards.ts | 6 | ||||
-rw-r--r-- | src/pages/lesson/[slug].tsx | 6 |
9 files changed, 174 insertions, 48 deletions
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<string[]> { +// 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 ? ( + <BookMarkedIcon + onClick={toggle} + className="absolute top-5 right-3 hover:bg-red" + /> + ) : ( + <BookmarkIcon + onClick={toggle} + className="absolute top-5 right-3 hover:bg-red" + /> + ); +}; 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 ( <div className="min-h-screen bg-slate-50 dark:bg-slate-900 flex flex-col items-center justify-center p-4 font-inter text-slate-800 dark:text-slate-200"> @@ -93,6 +107,10 @@ function Deck({ data, cards }: { data: DeckResponse; cards: CardData[] }) { return ( <div className="min-h-screen bg-slate-100 dark:bg-slate-900 flex flex-col items-center justify-center p-4 font-inter transition-colors duration-300"> + <header> + <h1 className="text-2xl ">Deck: {data.lesson.name}</h1> + <p>{data.lesson.description}</p> + </header> <div className="w-full max-w-md mb-8 relative"> {/* This div is for positioning the card and managing overflow during animations */} <div className="relative h-80"> @@ -145,6 +163,7 @@ function Deck({ data, cards }: { data: DeckResponse; cards: CardData[] }) { <div className="text-xs text-slate-500 dark:text-slate-400 mt-8"> Use Arrow Keys (← →) to navigate, Space/Enter to flip. </div> + <Button onClick={shuffle}>Shuffle Deck</Button> </div> ); } 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 ( <div className="absolute w-full h-full bg-white dark:bg-slate-800 rounded-xl backface-hidden flex flex-col justify-center gap-8 items-center p-6"> @@ -41,10 +42,11 @@ export async function CardFront({ data }: { data: CardResponse }) { } > <p className="text-5xl cursor-pointer font-semibold text-slate-800 dark:text-slate-100 text-center"> - {extraData[0]?.syllables.map((syl) => ( + {extraData[0]?.syllables.map((syl, i) => ( <span + key={syl + i} style={{ color: getRandomHexColor() }} - className="m-1 hover:text-6l" + className="m-1 hover:text-6xl" > {syl} </span> @@ -92,10 +94,9 @@ export const IpaDisplay = ({ }; export async function CardBack({ data }: { data: CardResponse }) { - // <BookmarkIcon onClick={handleClick} className="absolute top-5 right-10" /> return ( - <div className="absolute w-full h-full bg-slate-50 dark:bg-slate-700 rounded-xl backface-hidden rotate-y-180 flex flex-col justify-between items-center p-6 relative"> - <BookmarkIcon className="absolute top-5 right-10" /> + <div className="w-full h-full bg-slate-50 dark:bg-slate-700 rounded-xl backface-hidden rotate-y-180 flex flex-col justify-between items-center p-6 relative"> + <BookmarkIconito card={data} /> <span className="text-lg text-slate-500 dark:text-slate-400 self-start"> {data.expression.senses.map((ss, i) => ( <div key={`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<DeckResponse> { + fetchLesson({ + userId, + lessonId, + count, + page, + random, + }: { + userId: number; + lessonId: number; + count?: number; + page?: number; + random?: boolean; + }): Result<DeckResponse> { 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; }; |