summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorpolwex <polwex@sortug.com>2025-05-21 17:13:11 +0700
committerpolwex <polwex@sortug.com>2025-05-21 17:13:11 +0700
commit7abf2227438362ad30820ee236405ec1b57a40b6 (patch)
tree41c2eb60befef7d665c8cd6feebb91cdf4536934
parent9192e6c7747fd2d3f6a6c5c07d886a0982b53f11 (diff)
m
-rw-r--r--src/actions/deck.ts32
-rw-r--r--src/components/Flashcard/BookmarkButton.tsx41
-rw-r--r--src/components/Flashcard/Deck2.tsx29
-rw-r--r--src/components/Flashcard/ServerCard.tsx13
-rw-r--r--src/lib/db/index.ts82
-rw-r--r--src/lib/db/schema.sql7
-rw-r--r--src/lib/server/cookie.ts6
-rw-r--r--src/lib/types/cards.ts6
-rw-r--r--src/pages/lesson/[slug].tsx6
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;
};