summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--bun.lock10
-rw-r--r--message.txt3
-rw-r--r--package.json2
-rw-r--r--src/actions/login.ts57
-rw-r--r--src/components/Flashcard/Card.tsx121
-rw-r--r--src/components/Flashcard/Deck.tsx151
-rw-r--r--src/components/Flashcard/cards.css86
-rw-r--r--src/components/Login2.tsx22
-rw-r--r--src/components/ParseForm.tsx222
-rw-r--r--src/components/ui/progress.tsx29
-rw-r--r--src/lib/db/index.ts92
-rw-r--r--src/lib/db/schema.sql11
-rw-r--r--src/lib/db/seed.ts494
-rw-r--r--src/lib/hooks/useCookie.ts35
-rw-r--r--src/lib/resources/lang.ts8
-rw-r--r--src/lib/server/cookie.ts52
-rw-r--r--src/lib/server/cookiebridge.ts76
-rw-r--r--src/lib/server/setcookie.ts16
-rw-r--r--src/lib/types/cards.ts210
-rw-r--r--src/lib/types/index.ts2
l---------src/lib/useful_thai.json1
-rw-r--r--src/pages.gen.ts14
-rw-r--r--src/pages/index.tsx106
-rw-r--r--src/pages/lang/[slug].tsx74
-rw-r--r--src/pages/lesson/[slug].tsx66
-rw-r--r--src/pages/login.tsx29
-rw-r--r--src/pages/logintest/Form.tsx53
-rw-r--r--src/pages/logintest/ServerForm.tsx67
-rw-r--r--src/pages/logintest/funcs.ts24
-rw-r--r--src/pages/logintest/index.tsx24
-rw-r--r--src/pages/parse.tsx26
-rw-r--r--src/picker/App.tsx2
-rw-r--r--src/zoom/ServerWord.tsx12
-rw-r--r--waku.config.ts4
34 files changed, 2013 insertions, 188 deletions
diff --git a/bun.lock b/bun.lock
index c436aff..32c90fc 100644
--- a/bun.lock
+++ b/bun.lock
@@ -4,10 +4,10 @@
"": {
"name": "waku",
"dependencies": {
- "@edge-runtime/cookies": "^6.0.0",
"@hookform/resolvers": "^5.0.1",
"@radix-ui/react-dialog": "^1.1.13",
"@radix-ui/react-label": "^2.1.6",
+ "@radix-ui/react-progress": "^1.1.7",
"@radix-ui/react-select": "^2.2.4",
"@radix-ui/react-slot": "^1.2.2",
"class-variance-authority": "^0.7.1",
@@ -85,8 +85,6 @@
"@babel/types": ["@babel/types@7.27.1", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q=="],
- "@edge-runtime/cookies": ["@edge-runtime/cookies@6.0.0", "", {}, "sha512-VVO/8AwC2qVbygLb2IOkX1zWFx2yWIHzFv4D602CTnoRffd/+cdcXqpSydKaedFrk7a1dRYXbWwjzfV/gwZ2Gw=="],
-
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.4", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q=="],
"@esbuild/android-arm": ["@esbuild/android-arm@0.25.4", "", { "os": "android", "cpu": "arm" }, "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ=="],
@@ -225,6 +223,8 @@
"@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.2", "", { "dependencies": { "@radix-ui/react-slot": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-uHa+l/lKfxuDD2zjN/0peM/RhhSmRjr5YWdk/37EnSv1nJ88uvG85DPexSm8HdFQROd2VdERJ6ynXbkCFi+APw=="],
+ "@radix-ui/react-progress": ["@radix-ui/react-progress@1.1.7", "", { "dependencies": { "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg=="],
+
"@radix-ui/react-select": ["@radix-ui/react-select@2.2.4", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.2", "@radix-ui/react-collection": "1.1.6", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.9", "@radix-ui/react-focus-guards": "1.1.2", "@radix-ui/react-focus-scope": "1.1.6", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.6", "@radix-ui/react-portal": "1.1.8", "@radix-ui/react-primitive": "2.1.2", "@radix-ui/react-slot": "1.2.2", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/OOm58Gil4Ev5zT8LyVzqfBcij4dTHYdeyuF5lMHZ2bIp0Lk9oETocYiJ5QC0dHekEQnK6L/FNJCceeb4AkZ6Q=="],
"@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.2", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-y7TBO4xN4Y94FvcWIOIh18fM4R1A8S4q1jhoz4PNzOoHsFcN8pogcFmZrTYAm4F9VRUrWP/Mw7xSKybIeRI+CQ=="],
@@ -855,6 +855,8 @@
"@anthropic-ai/sdk/@types/node": ["@types/node@18.19.100", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-ojmMP8SZBKprc3qGrGk8Ujpo80AXkrP7G2tOT4VWr5jlr5DHjsJF+emXJz+Wm0glmy4Js62oKMdZZ6B9Y+tEcA=="],
+ "@radix-ui/react-progress/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
+
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.4.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.4.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ=="],
@@ -877,6 +879,8 @@
"@anthropic-ai/sdk/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
+ "@radix-ui/react-progress/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
+
"groq-sdk/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
"openai/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
diff --git a/message.txt b/message.txt
new file mode 100644
index 0000000..402d946
--- /dev/null
+++ b/message.txt
@@ -0,0 +1,3 @@
+WHY ISNT THIS SHIT WORKING FOR FUCKS SAKE
+
+pepe from server! \ No newline at end of file
diff --git a/package.json b/package.json
index 9fec0ca..8b39a03 100644
--- a/package.json
+++ b/package.json
@@ -9,10 +9,10 @@
"start": "bunx --bun waku start"
},
"dependencies": {
- "@edge-runtime/cookies": "^6.0.0",
"@hookform/resolvers": "^5.0.1",
"@radix-ui/react-dialog": "^1.1.13",
"@radix-ui/react-label": "^2.1.6",
+ "@radix-ui/react-progress": "^1.1.7",
"@radix-ui/react-select": "^2.2.4",
"@radix-ui/react-slot": "^1.2.2",
"class-variance-authority": "^0.7.1",
diff --git a/src/actions/login.ts b/src/actions/login.ts
index 4ceb13f..ee15fe6 100644
--- a/src/actions/login.ts
+++ b/src/actions/login.ts
@@ -1,9 +1,7 @@
"use server";
-import { AsyncRes } from "@/lib/types";
+import type { AsyncRes } from "@/lib/types";
import db from "../lib/db";
-import cookie from "cookie";
-import { cookies } from "../lib/server/cookiebridge";
-import { unstable_redirect, unstable_rerenderRoute } from "waku/router/server";
+import { useCookies } from "../lib/server/cookiebridge";
export type FormState = {
name?: string;
@@ -11,21 +9,30 @@ export type FormState = {
error?: string;
success?: boolean;
};
-async function call(formData: FormData, register: boolean) {
+async function call(
+ formData: FormData,
+ register: boolean,
+): AsyncRes<number | bigint> {
const nam = formData.get("username");
const creds = formData.get("password");
const password = await Bun.password.hash(creds!.toString());
const name = nam!.toString();
- const res = register
- ? db.addUser(name, password)
- : await db.loginUser(name, creds!.toString());
- return res;
+ try {
+ const res = register
+ ? db.addUser(name, password)
+ : await db.loginUser(name, creds!.toString());
+ return res;
+ } catch (e) {
+ console.error(e);
+ return { error: `${e}` };
+ }
}
export async function postRegister(
prevState: FormState,
formData: FormData,
): Promise<FormState> {
+ // "use server";
const res = await call(formData, true);
console.log("reg res", res);
if ("error" in res) return { error: "Something went wrong" };
@@ -37,15 +44,15 @@ export async function postRegister(
export async function postLogin(
prevState: FormState,
formData: FormData,
-): Promise<FormState> {
- return prevState;
- // const res = await call(formData, false);
- // if ("error" in res) return { error: res.error };
- // else {
- // setCookie(res.ok as number);
- // return prevState;
- // // return { success: true };
- // }
+): Promise<any> {
+ console.log(formData);
+ const res = await call(formData, false);
+ console.log({ res });
+ if ("error" in res) return { error: res.error };
+ else {
+ setCookie(res.ok as number);
+ return { success: true };
+ }
}
async function setCookie(userId: number) {
const COOKIE_EXPIRY = Date.now() + 1000 * 60 * 60 * 24 * 30;
@@ -54,13 +61,13 @@ async function setCookie(userId: number) {
const { randomBytes } = await import("node:crypto");
const cokistring = randomBytes(32).toBase64();
const res = db.setCookie(cokistring, userId, COOKIE_EXPIRY);
- const { setCookie } = cookies();
- setCookie("sorlang", cokistring, COOKIE_OPTS);
+ const { setCookie } = useCookies();
+ setCookie(cokistring);
// unstable_redirect("/");
}
-export async function postLogout(prev: number) {
- const { delCookie } = cookies();
- const rest = delCookie("sorlang");
- return prev + 9;
-}
+// export async function postLogout(prev: number) {
+// const { delCookie } = cookies();
+// const rest = delCookie("sorlang");
+// return prev + 9;
+// }
diff --git a/src/components/Flashcard/Card.tsx b/src/components/Flashcard/Card.tsx
new file mode 100644
index 0000000..7cada24
--- /dev/null
+++ b/src/components/Flashcard/Card.tsx
@@ -0,0 +1,121 @@
+"use client";
+
+import { CardResponse } from "@/lib/types/cards";
+
+// export default function ({ user }: { user: { name: string; id: number } }) {
+// const [state, formAction, isPending] = useActionState(postLogout, 0);
+// return (
+// <form action={formAction}>
+// <Card>
+// <CardHeader>
+// <CardTitle>Profile</CardTitle>
+// {state}
+// </CardHeader>
+// <CardContent>
+// <p>Username: {user.name}</p>
+// <p>User ID: {user.id}</p>
+// </CardContent>
+// <CardFooter>
+// <Button type="submit">Log out</Button>
+// </CardFooter>
+// </Card>
+// </form>
+// );
+// }
+// "use client";
+
+// --- Flashcard Component ---
+interface FlashcardProps {
+ data: CardResponse;
+ isFlipped: boolean;
+ onFlip: () => void;
+ animationDirection:
+ | "enter-left"
+ | "enter-right"
+ | "exit-left"
+ | "exit-right"
+ | "none";
+}
+
+const Flashcard: React.FC<FlashcardProps> = ({
+ data,
+ isFlipped,
+ onFlip,
+ animationDirection,
+}) => {
+ const getAnimationClass = () => {
+ switch (animationDirection) {
+ case "enter-right":
+ return "animate-slide-in-right";
+ case "enter-left":
+ return "animate-slide-in-left";
+ case "exit-right":
+ return "animate-slide-out-right";
+ case "exit-left":
+ return "animate-slide-out-left";
+ default:
+ return "";
+ }
+ };
+
+ return (
+ <div
+ className={`w-full max-w-md h-80 perspective group ${getAnimationClass()}`}
+ onClick={onFlip}
+ >
+ <div
+ className={`relative w-full h-full rounded-xl shadow-xl transition-transform duration-700 ease-in-out transform-style-preserve-3d cursor-pointer ${
+ isFlipped ? "rotate-y-180" : ""
+ }`}
+ >
+ {/* Front of the card */}
+ <div className="absolute w-full h-full bg-white dark:bg-slate-800 rounded-xl backface-hidden flex flex-col justify-between items-center p-6">
+ <span className="text-xs text-slate-500 dark:text-slate-400 self-start">
+ {data.expression.ipa.map((ip, i) => (
+ <span key={ip.ipa + i} className="ipa">
+ {ip.ipa}
+ </span>
+ ))}
+ </span>
+ <p className="text-3xl md:text-4xl font-semibold text-slate-800 dark:text-slate-100 text-center">
+ {data.expression.spelling}
+ </p>
+ <div className="w-full h-6">
+ {" "}
+ {/* Placeholder for spacing, mimics bottom controls */}
+ <span className="text-xs text-slate-400 dark:text-slate-500 self-end invisible">
+ Flip
+ </span>
+ </div>
+ </div>
+
+ {/* Back of the card */}
+ <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">
+ <span className="text-lg text-slate-500 dark:text-slate-400 self-start">
+ {data.expression.senses.map((ss, i) => (
+ <div key={`ss${i}`}>
+ {ss.senses.map((sss, i) => (
+ <div key={`sss${i}`}>
+ {sss.glosses.map((ssss, i) => (
+ <p key={ssss + i}>{ssss}</p>
+ ))}
+ </div>
+ ))}
+ </div>
+ ))}
+ </span>
+ <p className="text-3xl md:text-4xl font-semibold text-slate-800 dark:text-slate-100 text-center">
+ {data.note}
+ </p>
+ <div className="w-full h-6">
+ <span className="text-xs text-slate-400 dark:text-slate-500 self-end invisible">
+ Flip
+ </span>
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+};
+
+export default Flashcard;
diff --git a/src/components/Flashcard/Deck.tsx b/src/components/Flashcard/Deck.tsx
new file mode 100644
index 0000000..d3c736f
--- /dev/null
+++ b/src/components/Flashcard/Deck.tsx
@@ -0,0 +1,151 @@
+"use client";
+
+import { CardResponse, DeckResponse } from "@/lib/types/cards";
+import React, { useCallback, useEffect, useState } from "react";
+import { Button } from "../ui/button";
+import { ChevronLeftIcon, ChevronRightIcon, RotateCcwIcon } from "lucide-react";
+import "./cards.css";
+import Flashcard from "./Card";
+
+// --- Main App Component ---
+function Deck({ data }: { data: DeckResponse }) {
+ const [currentPage, setCurrentPage] = useState<number>(0);
+ const [currentIndex, setCurrentIndex] = useState<number>(0);
+ const [isFlipped, setIsFlipped] = useState<boolean>(false);
+ const [animationDirection, setAnimationDirection] = useState<
+ "enter-left" | "enter-right" | "exit-left" | "exit-right" | "none"
+ >("none");
+ const flashcards = data.cards;
+ const [isAnimating, setIsAnimating] = useState<boolean>(false);
+
+ const handleFlip = () => {
+ if (isAnimating) return;
+ setIsFlipped(!isFlipped);
+ };
+
+ const handleNext = useCallback(() => {
+ if (isAnimating || currentIndex >= flashcards.length - 1) return;
+ setIsAnimating(true);
+ setIsFlipped(false); // Flip back to front before changing card
+ setAnimationDirection("exit-left");
+
+ setTimeout(() => {
+ setCurrentIndex((prevIndex) =>
+ Math.min(prevIndex + 1, flashcards.length - 1),
+ );
+ setAnimationDirection("enter-right");
+ setTimeout(() => {
+ setAnimationDirection("none");
+ setIsAnimating(false);
+ }, 500); // Duration of enter animation
+ }, 500); // Duration of exit animation
+ }, [currentIndex, flashcards.length, isAnimating]);
+
+ const handlePrev = useCallback(() => {
+ if (isAnimating || currentIndex <= 0) return;
+ setIsAnimating(true);
+ setIsFlipped(false); // Flip back to front
+ setAnimationDirection("exit-right");
+
+ setTimeout(() => {
+ setCurrentIndex((prevIndex) => Math.max(prevIndex - 1, 0));
+ setAnimationDirection("enter-left");
+ setTimeout(() => {
+ setAnimationDirection("none");
+ setIsAnimating(false);
+ }, 500); // Duration of enter animation
+ }, 500); // Duration of exit animation
+ }, [currentIndex, isAnimating]);
+
+ // Keyboard navigation
+ useEffect(() => {
+ const handleKeyDown = (event: KeyboardEvent) => {
+ if (isAnimating) return;
+ if (event.key === "ArrowRight") {
+ handleNext();
+ } else if (event.key === "ArrowLeft") {
+ handlePrev();
+ } else if (event.key === " " || event.key === "Enter") {
+ // Space or Enter to flip
+ event.preventDefault(); // Prevent scrolling if space is pressed
+ handleFlip();
+ }
+ };
+
+ window.addEventListener("keydown", handleKeyDown);
+ return () => {
+ window.removeEventListener("keydown", handleKeyDown);
+ };
+ }, [handleNext, handlePrev, isAnimating]);
+
+ if (flashcards.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">
+ <p>No flashcards available.</p>
+ </div>
+ );
+ }
+
+ const currentCard = flashcards[currentIndex];
+ if (!currentCard) return <p>wtf</p>;
+
+ 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">
+ <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">
+ {" "}
+ {/* Ensure this matches card height */}
+ <Flashcard
+ key={currentCard.id} // Important for re-rendering on card change with animation
+ data={currentCard}
+ isFlipped={isFlipped}
+ onFlip={handleFlip}
+ animationDirection={animationDirection}
+ />
+ </div>
+ </div>
+
+ <div className="flex items-center justify-between w-full max-w-md mb-6">
+ <Button
+ onClick={handlePrev}
+ disabled={currentIndex === 0 || isAnimating}
+ variant="outline"
+ size="icon"
+ aria-label="Previous card"
+ >
+ <ChevronLeftIcon />
+ </Button>
+ <div className="text-center">
+ <p className="text-sm text-slate-600 dark:text-slate-400">
+ Card {currentIndex + 1} of {flashcards.length}
+ </p>
+ <Button
+ onClick={handleFlip}
+ variant="ghost"
+ size="sm"
+ className="mt-1 text-slate-600 dark:text-slate-400"
+ disabled={isAnimating}
+ >
+ <RotateCcwIcon className="w-4 h-4 mr-2" /> Flip Card
+ </Button>
+ </div>
+ <Button
+ onClick={handleNext}
+ disabled={currentIndex === flashcards.length - 1 || isAnimating}
+ variant="outline"
+ size="icon"
+ aria-label="Next card"
+ >
+ <ChevronRightIcon />
+ </Button>
+ </div>
+
+ <div className="text-xs text-slate-500 dark:text-slate-400 mt-8">
+ Use Arrow Keys (← →) to navigate, Space/Enter to flip.
+ </div>
+ </div>
+ );
+}
+
+export default Deck;
diff --git a/src/components/Flashcard/cards.css b/src/components/Flashcard/cards.css
new file mode 100644
index 0000000..2f75ad6
--- /dev/null
+++ b/src/components/Flashcard/cards.css
@@ -0,0 +1,86 @@
+body {
+ font-family: "Inter", sans-serif;
+}
+
+.perspective {
+ perspective: 1000px;
+}
+
+.transform-style-preserve-3d {
+ transform-style: preserve-3d;
+}
+
+.backface-hidden {
+ backface-visibility: hidden;
+ -webkit-backface-visibility: hidden;
+ /* Safari */
+}
+
+.rotate-y-180 {
+ transform: rotateY(180deg);
+}
+
+/* Slide animations */
+@keyframes slide-in-right {
+ from {
+ transform: translateX(100%);
+ opacity: 0;
+ }
+
+ to {
+ transform: translateX(0);
+ opacity: 1;
+ }
+}
+
+.animate-slide-in-right {
+ animation: slide-in-right 0.5s ease-out forwards;
+}
+
+@keyframes slide-in-left {
+ from {
+ transform: translateX(-100%);
+ opacity: 0;
+ }
+
+ to {
+ transform: translateX(0);
+ opacity: 1;
+ }
+}
+
+.animate-slide-in-left {
+ animation: slide-in-left 0.5s ease-out forwards;
+}
+
+@keyframes slide-out-left {
+ from {
+ transform: translateX(0);
+ opacity: 1;
+ }
+
+ to {
+ transform: translateX(-100%);
+ opacity: 0;
+ }
+}
+
+.animate-slide-out-left {
+ animation: slide-out-left 0.5s ease-in forwards;
+}
+
+@keyframes slide-out-right {
+ from {
+ transform: translateX(0);
+ opacity: 1;
+ }
+
+ to {
+ transform: translateX(100%);
+ opacity: 0;
+ }
+}
+
+.animate-slide-out-right {
+ animation: slide-out-right 0.5s ease-in forwards;
+} \ No newline at end of file
diff --git a/src/components/Login2.tsx b/src/components/Login2.tsx
index ef3a603..6c26efc 100644
--- a/src/components/Login2.tsx
+++ b/src/components/Login2.tsx
@@ -11,19 +11,9 @@ import {
CardFooter,
CardTitle,
} from "@/components/ui/card";
-import {
- Form,
- FormControl,
- FormDescription,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from "@/components/ui/form";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
-import { useRouter } from "waku";
export default function AuthScreen() {
const [isReg, setReg] = useState(false);
@@ -52,15 +42,17 @@ function OOldform({ isReg, toggle }: { isReg: boolean; toggle: () => void }) {
else setStrings(logstrings);
}, [isReg]);
+ // const [state, formAction, isPending] = useActionState<FormState, FormData>(
+ // isReg ? postRegister : postLogin,
+ // { error: "" },
+ // "/login",
+ // );
const [state, formAction, isPending] = useActionState<FormState, FormData>(
- isReg ? postRegister : postLogin,
+ postLogin,
{ error: "" },
"/login",
);
- // const nav = useRouter();
- // useEffect(() => {
- // if (state.success) nav.replace("/");
- // }, [state]);
+ console.log({ state });
return (
<form action={formAction}>
<div className="flex flex-col gap-6">
diff --git a/src/components/ParseForm.tsx b/src/components/ParseForm.tsx
new file mode 100644
index 0000000..3e6f3e7
--- /dev/null
+++ b/src/components/ParseForm.tsx
@@ -0,0 +1,222 @@
+// src/components/SorlangPage.tsx
+"use client"; // For Next.js App Router, if applicable
+
+import React, {
+ useState,
+ useRef,
+ useTransition,
+ useEffect,
+ useCallback,
+ startTransition,
+} from "react";
+import { Button } from "@/components/ui/button";
+import { Textarea } from "@/components/ui/textarea";
+import {
+ Card,
+ CardContent,
+ CardFooter,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+import { Loader2 } from "lucide-react"; // Loading spinner
+import { useRouter } from "waku";
+
+const SorlangPage: React.FC = () => {
+ const [textValue, setTextValue] = useState<string>("");
+ const [pastedImageUrl, setPastedImageUrl] = useState<string | null>(null);
+ const [pastedImageFile, setPastedImageFile] = useState<File | null>(null); // Store the file for extraction
+ const [isAnalyzing, setIsAnalyzing] = useState<boolean>(false);
+ const [isExtracting, setIsExtracting] = useState<boolean>(false);
+
+ const textareaRef = useRef<HTMLTextAreaElement>(null);
+
+ // Cleanup object URL when component unmounts or image changes
+ useEffect(() => {
+ return () => {
+ if (pastedImageUrl) {
+ URL.revokeObjectURL(pastedImageUrl);
+ }
+ };
+ }, [pastedImageUrl]);
+
+ const handlePaste = useCallback(
+ (event: ClipboardEvent) => {
+ const items = event.clipboardData?.items;
+ console.log({ items });
+ if (!items) return;
+
+ let imageFound = false;
+ for (const item of items) {
+ if (item.kind === "file" && item.type.startsWith("image/")) {
+ event.preventDefault(); // Prevent pasting image data as text
+ const file = item.getAsFile();
+ if (file) {
+ if (pastedImageUrl) {
+ URL.revokeObjectURL(pastedImageUrl); // Revoke previous if any
+ }
+ const newImageUrl = URL.createObjectURL(file);
+ setPastedImageUrl(newImageUrl);
+ setPastedImageFile(file);
+ imageFound = true;
+ }
+ break; // Handle first image found
+ }
+ }
+
+ // // If no image was found, let the default text paste happen
+ // // Or, if you want to explicitly handle text paste:
+ // if (!imageFound) {
+ // // Let the default textarea paste handle it, or:
+ // // event.preventDefault();
+ // // const text = event.clipboardData.getData('text/plain');
+ // // setTextValue(prev => prev + text); // Or replace, depending on desired behavior
+ // // setPastedImageUrl(null); // Clear image if text is pasted
+ // // setPastedImageFile(null);
+ // }
+ },
+ [pastedImageUrl],
+ );
+ useEffect(() => {
+ window.addEventListener("paste", handlePaste);
+ return () => {
+ window.removeEventListener("paste", handlePaste);
+ };
+ }, [handlePaste]);
+
+ const router = useRouter();
+ async function fetchNLP(text: string, app: "spacy" | "stanza") {
+ const opts = {
+ method: "POST",
+ headers: { "Content-type": "application/json" },
+ body: JSON.stringify({ text, app }),
+ };
+ const res = await fetch("/api/nlp", opts);
+ const j = await res.json();
+ console.log("j", j);
+ if ("ok" in j) {
+ sessionStorage.setItem(`${app}res`, JSON.stringify(j.ok));
+ }
+ }
+
+ const handleProcessText = async () => {
+ setIsAnalyzing(true);
+ const text = textValue.trim();
+ if (!text) {
+ alert("Text area is empty!");
+ return;
+ }
+ await Promise.all([fetchNLP(text, "spacy"), fetchNLP(text, "stanza")]);
+ router.push("/zoom");
+ setIsAnalyzing(false);
+ };
+
+ // const [isPending, startTransition] = useTransition();
+ const handleExtractTextFromImage = async () => {
+ if (!pastedImageFile) {
+ alert("No image to extract text from!");
+ return;
+ }
+ setIsExtracting(true);
+ const formData = new FormData();
+ formData.append("file", pastedImageFile, pastedImageFile.name);
+ console.log("Extracting text from image:", pastedImageFile.name);
+ const res = await fetch("/api/formdata/ocr", {
+ method: "POST",
+ body: formData,
+ });
+ const j = await res.json();
+ console.log("ocr res", j);
+ if ("ok" in j) {
+ setTextValue((t) => t + j.ok.join("\n"));
+ }
+ setIsExtracting(false);
+ // handleClearImage();
+ };
+
+ // setPastedImageUrl(null);
+ // setPastedImageFile(null);
+
+ const handleClearImage = () => {
+ if (pastedImageUrl) {
+ URL.revokeObjectURL(pastedImageUrl);
+ }
+ setPastedImageUrl(null);
+ setPastedImageFile(null);
+ };
+
+ return (
+ <div className="flex min-h-screen flex-col items-center justify-center bg-background p-4">
+ <Card className="w-full max-w-2xl">
+ <CardHeader>
+ <CardTitle className="text-center text-3xl font-bold">
+ Sorlang
+ </CardTitle>
+ </CardHeader>
+ <CardContent className="space-y-6">
+ <Textarea
+ ref={textareaRef}
+ value={textValue}
+ onChange={(e) => setTextValue(e.target.value)}
+ placeholder="Paste text here, or paste an image..."
+ className="min-h-[200px] w-full text-base"
+ aria-label="Input text area"
+ />
+
+ {pastedImageUrl && (
+ <div className="mt-4 p-4 border rounded-md bg-muted/40">
+ <div className="flex justify-between items-start mb-2">
+ <h3 className="text-lg font-semibold">Pasted Image:</h3>
+ <Button
+ variant="ghost"
+ size="sm"
+ onClick={handleClearImage}
+ className="text-xs"
+ >
+ Clear Image
+ </Button>
+ </div>
+ <img
+ src={pastedImageUrl}
+ alt="Pasted content"
+ className="max-w-full max-h-60 mx-auto rounded-md border"
+ />
+ </div>
+ )}
+ </CardContent>
+ <CardFooter className="flex flex-col sm:flex-row justify-center gap-4">
+ {pastedImageUrl ? (
+ <Button
+ onClick={handleExtractTextFromImage}
+ disabled={isExtracting}
+ className="w-full sm:w-auto"
+ >
+ {isExtracting ? (
+ <>
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
+ Extracting...
+ </>
+ ) : (
+ "Extract Text from Image"
+ )}
+ </Button>
+ ) : (
+ <Button onClick={handleProcessText} className="w-full sm:w-auto">
+ {isAnalyzing ? (
+ <>
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" /> Analyzing...
+ </>
+ ) : (
+ "Process Text"
+ )}
+ </Button>
+ )}
+ </CardFooter>
+ </Card>
+ <footer className="mt-8 text-center text-sm text-muted-foreground">
+ <p>© {new Date().getFullYear()} Sorlang App. All rights reserved.</p>
+ </footer>
+ </div>
+ );
+};
+
+export default SorlangPage;
diff --git a/src/components/ui/progress.tsx b/src/components/ui/progress.tsx
new file mode 100644
index 0000000..10af7e6
--- /dev/null
+++ b/src/components/ui/progress.tsx
@@ -0,0 +1,29 @@
+import * as React from "react"
+import * as ProgressPrimitive from "@radix-ui/react-progress"
+
+import { cn } from "@/lib/utils"
+
+function Progress({
+ className,
+ value,
+ ...props
+}: React.ComponentProps<typeof ProgressPrimitive.Root>) {
+ return (
+ <ProgressPrimitive.Root
+ data-slot="progress"
+ className={cn(
+ "bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",
+ className
+ )}
+ {...props}
+ >
+ <ProgressPrimitive.Indicator
+ data-slot="progress-indicator"
+ className="bg-primary h-full w-full flex-1 transition-all"
+ style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
+ />
+ </ProgressPrimitive.Root>
+ )
+}
+
+export { Progress }
diff --git a/src/lib/db/index.ts b/src/lib/db/index.ts
index a710a1e..b43edc3 100644
--- a/src/lib/db/index.ts
+++ b/src/lib/db/index.ts
@@ -1,8 +1,9 @@
import Database from "bun:sqlite";
import { getDBOffset, wordFactorial } from "../utils";
-import type { AddSense, AddWord, State } from "../types";
+import type { AddSense, AddWord, Result, State } from "../types";
import { DEFAULT_SRS } from "../services/srs";
import { DBWord, WordData } from "@/zoom/logic/types";
+import { DeckResponse } from "../types/cards";
const PAGE_SIZE = 100;
@@ -57,16 +58,35 @@ class DatabaseHandler {
// read
//
fetchLanguage(lang: string, page?: number) {
- const query = this.db.query(
- `
- SELECT * FROM lessons
- JOIN languages l ON l.code = lessons.lang
- WHERE lessons.lang= ?
- `,
- );
+ const query = this.db.query(`
+ SELECT
+ l.id,
+ l.name,
+ l.position,
+ l.description,
+ COUNT(cl.card_id) AS count
+ FROM
+ lessons l
+ INNER JOIN
+ lang_lessons ll ON l.id = ll.lesson_id
+ LEFT JOIN
+ cards_lessons cl ON l.id = cl.lesson_id
+ WHERE
+ ll.lang = ?
+ GROUP BY
+ l.id, l.name, l.position, l.description
+ ORDER BY
+ l.position, l.name;
+ `);
const res = query.all(lang);
console.log(res, "results");
- return res;
+ return res as Array<{
+ id: number;
+ count: number;
+ name: string;
+ description: string;
+ position: number;
+ }>;
}
fetchSyllables(lang: string, page?: number) {
const query = this.db.query(
@@ -135,7 +155,7 @@ class DatabaseHandler {
GROUP_CONCAT(c.name, ',') AS category,
FROM expressions
JOIN word_categories wc ON wc.word_id = words.id
- JOIN categories c ON c.id = wc.category_id
+ JOIN categories c ON c.name = wc.category
WHERE spelling = $spelling
GROUP BY words.id
`);
@@ -151,7 +171,7 @@ class DatabaseHandler {
GROUP_CONCAT(c.name, ',') AS category
FROM expressions e
JOIN word_categories wc ON wc.word_id = e.id
- JOIN categories c ON c.id = wc.category_id
+ JOIN categories c ON c.name= wc.category
ORDER BY e.frequency DESC
LIMIT $count
OFFSET $offset
@@ -297,7 +317,7 @@ class DatabaseHandler {
LEFT JOIN
word_categories wc ON wc.word_id = e.id
LEFT JOIN
- categories cat ON cat.id = wc.category_id
+ categories cat ON cat.name = wc.category
GROUP BY
l.id, c.id, e.id
ORDER BY
@@ -309,7 +329,12 @@ 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) {
+ fetchLesson(
+ userId: number,
+ lessonId: number,
+ count?: number,
+ page?: number,
+ ): Result<DeckResponse> {
const p = page ? page : 1;
const size = count ? count : PAGE_SIZE;
const offset = getDBOffset(p, size);
@@ -318,7 +343,7 @@ class DatabaseHandler {
console.log(tomorrow.getTime());
const queryString = `
SELECT
- l.name, l.description, l.lang as llang, cards.text, cards.note, cards.id as cid,
+ l.name, l.description, ll.lang as llang, cards.text, cards.note, cards.id as cid,
up.id as upid,
up.repetition_count,
up.ease_factor,
@@ -342,6 +367,7 @@ class DatabaseHandler {
WHERE cl_inner.lesson_id = l.id) AS total_card_count
FROM cards_lessons cl
JOIN lessons l ON l.id = cl.lesson_id
+ JOIN lang_lessons ll ON l.id = ll.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
@@ -367,7 +393,7 @@ class DatabaseHandler {
const query = this.db.query(queryString);
const res = query.all(userId, lessonId, tomorrow.getTime(), size, offset);
console.log(res.length);
- if (res.length === 0) return null;
+ if (res.length === 0) return { error: "Lesson not found" };
const row: any = res[0];
// console.log({ row });
const lesson = {
@@ -377,8 +403,10 @@ class DatabaseHandler {
language: row.llang,
cardCount: row.total_card_count,
};
+ // TODO IPA, prosody, senses... should we unify the format on the wikisource standard?
const cards = res.map((row: any) => {
// TODO parse here...?
+ // console.log({ row });
const sense_array = JSON.parse(row.senses_array);
const senses = sense_array.map((s: any) => {
const senses = JSON.parse(s.senses);
@@ -417,7 +445,7 @@ class DatabaseHandler {
};
return card;
});
- return { lesson, cards };
+ return { ok: { lesson, cards } };
}
fetchCard(cid: number, userid: number) {
const query = this.db.query(`
@@ -448,7 +476,7 @@ class DatabaseHandler {
VALUES (${columns.map((c) => "?").join(",")})
`;
const query = this.db.query(queryString).run(...Object.values(params));
- return query;
+ return query.lastInsertRowid;
}
addSense(params: AddSense) {
const columns = Object.keys(params);
@@ -521,25 +549,28 @@ class DatabaseHandler {
}) {
const { text, mnote, eid, lesson_id } = params;
const note = mnote ? mnote : null;
- const query = this.db.query(`
+ const tx = this.db.transaction(() => {
+ const query = this.db.query(`
INSERT
INTO cards(text, note)
VALUES(?, ?)
`);
- const res = query.run(text, note);
- const cid = res.lastInsertRowid;
- const query2 = this.db.query(`
+ const res = query.run(text, note);
+ const cid = res.lastInsertRowid;
+ const query2 = this.db.query(`
INSERT OR IGNORE INTO cards_expressions(card_id, expression_id)
VALUES(?, ?)
`);
- query2.run(cid, eid);
- if (lesson_id) {
- const query = this.db.query(`
+ query2.run(cid, eid);
+ if (lesson_id) {
+ const query = this.db.query(`
INSERT INTO cards_lessons(card_id, lesson_id)
VALUES(?, ?)
`);
- query.run(cid, lesson_id);
- }
+ query.run(cid, lesson_id);
+ }
+ });
+ return tx();
}
addCardO(lesson_id: number | bigint | null, text: string, mnote?: string) {
// wtf is this fucntion when did I write this
@@ -634,12 +665,9 @@ class DatabaseHandler {
}
addWCat(wordId: number | bigint, category: string) {
const queryString = `
- INSERT
- INTO word_categories(word_id, category_id)
- VALUES($wordId, (
- SELECT id FROM categories
- WHERE name = $category
- ))
+ INSERT OR IGNORE
+ INTO word_categories(word_id, category)
+ VALUES($wordId, $category)
`;
const query = this.db.query(queryString);
const res = query.run({ wordId, category });
diff --git a/src/lib/db/schema.sql b/src/lib/db/schema.sql
index 1b678c5..8d1b288 100644
--- a/src/lib/db/schema.sql
+++ b/src/lib/db/schema.sql
@@ -114,9 +114,14 @@ CREATE TABLE IF NOT EXISTS lessons(
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
position INTEGER NOT NULL DEFAULT 0,
- description TEXT,
- lang TEXT,
- FOREIGN KEY (lang) REFERENCES languages(code)
+ description TEXT
+);
+CREATE TABLE IF NOT EXISTS lang_lessons(
+ lesson_id INTEGER NOT NULL,
+ lang TEXT NOT NULL,
+ PRIMARY KEY (lang, lesson_id),
+ FOREIGN KEY (lang) REFERENCES languages(code),
+ FOREIGN KEY (lesson_id) REFERENCES lessons(id)
);
CREATE TABLE IF NOT EXISTS cards(
id INTEGER PRIMARY KEY AUTOINCREMENT,
diff --git a/src/lib/db/seed.ts b/src/lib/db/seed.ts
new file mode 100644
index 0000000..c4094de
--- /dev/null
+++ b/src/lib/db/seed.ts
@@ -0,0 +1,494 @@
+import { readWiktionaryDump } from "../services/wiki";
+import { getStressedSyllable, getSyllableCount } from "../utils";
+import useful from "@/lib/useful_thai.json";
+import db from ".";
+
+const SYMBOL_REGEX = new RegExp(/[\W\d]/);
+
+async function handleFile(
+ filename: string,
+ func: (line: string, idx: number) => void,
+) {
+ const file = Bun.file(filename);
+ const s = file.stream();
+ const reader = s.getReader();
+ const decoder = new TextDecoder();
+ let leftover = "";
+ let lineCount = 0;
+ while (true) {
+ const { value, done } = await reader.read();
+ if (done) break;
+ const chunk = decoder.decode(value, { stream: true });
+ const lines = (leftover + chunk).split("\n");
+
+ // Process each line except the last (which might be incomplete)
+ for (const line of lines.slice(0, -1)) {
+ lineCount++;
+ func(line, lineCount);
+ }
+
+ // Save the last incomplete line to process in the next iteration
+ leftover = lines[lines.length - 1];
+ }
+
+ // Handle any remaining content after reading all chunks
+ if (leftover) func(leftover, lineCount + 1);
+}
+
+function goodPos(pos: string): boolean {
+ const list = [
+ "CC",
+ "DT",
+ "EX",
+ "IN",
+ "LS",
+ "MD",
+ "PDT",
+ "POS",
+ "PRP",
+ "PRP$",
+ "RP",
+ "TO",
+ "WDT",
+ "WP",
+ "WP$",
+ ];
+ return list.includes(pos);
+}
+// function englishKaggle() {
+// handleFile("../datasets/words_pos.csv", (line, idx) => {
+// const [_, spelling, pos] = line.split(",");
+// if (!goodPos(pos)) return;
+// const rowid = addWord(db, spelling, "", "en-us", "word", null);
+// const category = poss[pos] || "unknown;";
+// addCat(db, rowid, category);
+// });
+// }
+// async function englishIPA() {
+// handleFile("ipa/en-us/ipadict.txt", (line, idx) => {
+// const [spelling, ipa] = line.split(/\s+/);
+// if (!spelling || !ipa) return;
+// const hasSymbols = spelling.match(SYMBOL_REGEX);
+// if (hasSymbols) return;
+// const split = spelling.split(" ");
+// const type = split.length > 1 ? "expression" : "word";
+// const subtype = null;
+// addWord(db, spelling, ipa, "en-us", type, subtype);
+// });
+// }
+
+async function englishFreq() {
+ handleFile(
+ "/home/y/code/prosody/hanchu/datasets/unigram_freq.csv",
+ (line, idx) => {
+ const [spelling, _frequency] = line.split(",");
+ db.addFrequency(spelling, idx);
+ },
+ );
+}
+async function thaiFreq() {
+ const files = [
+ "/home/y/code/prosody/prosody/langdata/thai/data/1yin_freq.csv",
+ "/home/y/code/prosody/prosody/langdata/thai/data/2yin_freq.csv",
+ "/home/y/code/prosody/prosody/langdata/thai/data/3yin_freq.csv",
+ "/home/y/code/prosody/prosody/langdata/thai/data/4yin_freq.csv",
+ "/home/y/code/prosody/prosody/langdata/thai/data/5yin_freq.csv",
+ "/home/y/code/prosody/prosody/langdata/thai/data/6yin_freq.csv",
+ ];
+ for (let f of files) {
+ handleFile(f, (line, idx) => {
+ const [spelling, IPA, tone, length, frequency, ...rest] = line.split(",");
+ db.addFrequency(spelling, Number(frequency));
+ });
+ }
+}
+
+const thaiTones: Record<string, number> = {
+ M: 1,
+ L: 2,
+ F: 3,
+ H: 4,
+ R: 5,
+};
+const thaiTones2: Record<string, number> = {
+ "˧": 1,
+ "˨˩": 2,
+ "˥˩": 3,
+ "˦˥": 4,
+ "˩˩˦": 5,
+};
+async function thaiSyllables() {
+ handleFile(
+ "/home/y/code/prosody/prosody/langdata/thai/data/1yin_freq.csv",
+ (line, idx) => {
+ const [spelling, IPA, toneS, length, frequency, ...rest] =
+ line.split(",");
+ const isLong = length === "長";
+ const tone = thaiTones[toneS];
+ const prosody = JSON.stringify({ isLong, tone, lang: "th" });
+ db.upsertWord({
+ spelling,
+ lang: "th",
+ ipa: JSON.stringify([{ ipa: IPA, tags: ["sortug"] }]),
+ prosody,
+ syllables: 1,
+ type: "syllable",
+ frequency: Number(frequency),
+ confidence: 10,
+ });
+ },
+ );
+ handleFile(
+ "/home/y/code/prosody/prosody/langdata/thai/data/1yinjie.csv",
+ (line, idx) => {
+ const [spelling, IPA] = line.split(",");
+ const isLong = IPA.includes("ː");
+ let tone = 0;
+ const toneMarks = Object.keys(thaiTones2);
+ for (let tm of toneMarks) {
+ if (IPA.includes(tm)) tone = thaiTones2[tm];
+ }
+ const prosody = JSON.stringify({ isLong, tone, lang: "th" });
+ db.upsertWord({
+ spelling,
+ lang: "th",
+ ipa: JSON.stringify([{ ipa: IPA, tags: ["sortug"] }]),
+ prosody,
+ syllables: 1,
+ type: "syllable",
+ confidence: 10,
+ });
+ },
+ );
+}
+
+// // Save the last incomplete line to process in the next iteration
+// }
+// TODO no conjunctions or adpositions in Wordnet!!
+// function englishWordnet() {
+// // LEFT JOIN lexes_pronunciations ukpr ON ukpr.wordid = words.wordid AND uspr.variety = 'GB'
+// // LEFT JOIN pronunciations ukp ON ukp.pronunciationid = ukpr.pronunciationid
+// const queryString = `
+// WITH ranked_ipa AS (
+// SELECT
+// lp.wordid,
+// pr.pronunciation,
+// lp.variety,
+// ROW_NUMBER() OVER (
+// PARTITION BY lp.wordid
+// ORDER BY
+// CASE
+// WHEN lp.variety = 'US' THEN 1
+// WHEN lp.variety IS NULL THEN 2
+// WHEN lp.variety IS 'GB' THEN 3
+// ELSE 4
+// END
+// ) AS rank
+// FROM lexes_pronunciations lp
+// JOIN pronunciations pr ON pr.pronunciationid = lp.pronunciationid
+// )
+// SELECT words.wordid, word, rp.pronunciation as ipa, domainname
+// FROM words
+// LEFT JOIN ranked_ipa rp ON rp.wordid = words.wordid AND rp.rank = 1
+// LEFT JOIN senses ON senses.wordid = words.wordid
+// LEFT JOIN synsets ON synsets.synsetid = senses.synsetid
+// LEFT JOIN domains ON domains.domainid = synsets.domainid
+// GROUP BY words.wordid
+// `;
+// const query = wndb.query(queryString);
+// const res: Array<{
+// word: string;
+// ipa: string;
+// domainname: string;
+// }> = query.all() as any;
+// console.log("res", res.length);
+// for (const r of res) {
+// console.log(r, "r");
+// // if (r.word === 'abrasive') throw new Error('stop right here');
+// const ok = filterWord(r.word);
+// if (!ok) continue;
+// const split = r.word.split(" ");
+// const type = split.length > 1 ? "expression" : "word";
+// const subtype = null;
+// const wordid = addWord(db, r.word, r.ipa, "en-us", type, subtype);
+// const category = domains[r.domainname] || "unknown;";
+// addCat(db, wordid, category);
+// }
+// }
+function filterWord(s: string) {
+ const hasSymbols = s.match(SYMBOL_REGEX);
+ if (hasSymbols) return false;
+ else return true;
+}
+
+// function checkWordNet(word: string) {
+// const query = wndb.query(`SELECT * FROM words WHERE word = $word`);
+// const res = query.get({ $word: word });
+// return !!res;
+// }
+
+// function englishCards() {
+// const lesson_id = addLesson(db, "First Lesson, some easy stuff");
+// const texts = [
+// "I",
+// "friend",
+// "my friend",
+// "you",
+// "your friend",
+// "my friends' friend",
+// "you are my friend",
+// "I am your friend",
+// "your friend is my friend",
+// "my friend is your friend",
+// "he is my friend",
+// "this is mine",
+// "this is yours",
+// "this is my friends'",
+// "no",
+// "you are not my friend",
+// "this is not yours",
+// "your friend is not my friend",
+// "that is mine",
+// "this is mine, that is yours",
+// "he is not your friend",
+// "no, I am not",
+// "that is not me",
+// "that is not mine, that is my friends'",
+// ];
+// for (const text of texts) {
+// addCard(db, lesson_id, text);
+// }
+// }
+// englishWordnet();
+// englishFreq();
+// englishCards();
+// englishKaggle();
+
+async function fillFromDump() {
+ await db.init();
+ // const log = Bun.file("./stuff.log");
+ // const logWriter = log.writer();
+ let count = 0;
+ const fields = new Set<string>();
+ // let biggest = 0;
+ for await (const line of readWiktionaryDump()) {
+ try {
+ count++;
+ // if (count > 80) break;
+ // if (line.length > biggest) {
+ // biggest = line.length;
+ // Bun.write("./biggest.log", line, { createPath: true });
+ // }
+ const j = JSON.parse(line);
+ db.addLanguage(j.lang_code, j.lang);
+ db.addCat(j.pos);
+ // for (let key of Object.keys(j)) {
+ // if (!fields.has(key)) {
+ // fields.add(key);
+ // logWriter.write(`${line}\n`);
+ // }
+ // }
+ if (j.lang_code === "en" || j.lang_code === "th") {
+ console.log("saving", j.word);
+ // console.log(j.sounds);
+ const related = {
+ derived: j.derived,
+ antonyms: j.antonyms,
+ synonyms: j.synonyms,
+ related: j.related,
+ };
+ let rhyme = "";
+ let ipaExample = "";
+ let ipa: any[] = [];
+ for (let snd of j.sounds || []) {
+ if ("ipa" in snd) {
+ ipa.push(snd);
+ if (!ipaExample) ipaExample = snd.ipa;
+ }
+ if ("rhymes" in snd) rhyme = snd.rhymes;
+ }
+ const isWord = j.word.trim().split(" ").length === 1;
+ const type: any = isWord ? "word" : "expression";
+ const syllables = ipaExample ? getSyllableCount(ipaExample) : 0;
+ console.log({ ipaExample, syllables });
+ let prosody: any = {};
+ if (ipaExample) {
+ const stressedSyllable = getStressedSyllable(ipaExample);
+ if ("ok" in stressedSyllable)
+ prosody.stressedSyllable = stressedSyllable.ok;
+ }
+ if (rhyme) prosody.rhyme = rhyme;
+ try {
+ const row = db.addWord({
+ spelling: j.word,
+ lang: j.lang_code,
+ ipa: JSON.stringify(ipa),
+ prosody: JSON.stringify(prosody),
+ syllables,
+ type,
+ });
+ let parent_id: number | bigint;
+ if (row.changes === 1) parent_id = row.lastInsertRowid;
+ else {
+ const data: any = db.fetchExpressionBySpelling(j.word, j.lang_code);
+ parent_id = data.id;
+ }
+ const senseRow = db.addSense({
+ id: count - 1,
+ parent_id,
+ spelling: j.word,
+ etymology: j.etymology_text || "",
+ pos: j.pos,
+ ipa: JSON.stringify(ipa),
+ prosody: JSON.stringify(prosody),
+ senses: JSON.stringify(j.senses),
+ forms: JSON.stringify(j.forms || []),
+ related: JSON.stringify(related),
+ });
+ } catch (e) {
+ console.log("error inserting", e);
+ }
+ }
+ // langset.add(j.lang_code);
+ // if (j.lang === "Translingual") continue;
+ // if (j.lang_code === "en") en++;
+ // if (j.lang_code === "th") thai++;
+ // if (j.lang_code === "zh") zh++;
+
+ // if (j.word === "cat") {
+ // console.log(j.word);
+ // console.log(Object.keys(j));
+ // console.log(j);
+ // console.log("senses", j.senses);
+ // console.log("forms", j.forms);
+ // // console.log("ett", j.etymology_templates);
+ // // console.log("derived", j.derived);
+ // // const meaning: Meaning = {etymology: j.etymology_text}
+ // // const wd = { lang: j.lang_code, spelling: j.word, ipa, {} };
+ // break;
+ // }
+ } catch (e) {
+ console.log("error parsing", e);
+ }
+ }
+ console.log("fields", fields);
+}
+
+function addDecks() {
+ // const lesson_id = db.addLesson({
+ // name: "Thai Syllables",
+ // description: "All the syllables in the Thai language ordered by frequency",
+ // lang: "th",
+ // });
+ const syllables: any[] = db.fetchExpressionRaw({
+ confidence: "10",
+ syllables: "1",
+ lang: "th",
+ });
+ for (let expression of syllables) {
+ db.addCard({
+ lesson_id: 5,
+ eid: expression.id,
+ text: "Syllable",
+ mnote: "from Sortug Development",
+ });
+ }
+}
+function adjustFrequency(lang: string) {
+ const frequencies: Set<number> = new Set();
+ const all: any[] = db.fetchExpressionRaw({ lang });
+ for (let row of all) {
+ if (row.frequency) frequencies.add(row.frequency);
+ }
+ const freqArray = Array.from(frequencies).sort((a, b) => b - a);
+ console.log(freqArray);
+ for (let row of all) {
+ if (row.frequency) {
+ const f = freqArray.indexOf(row.frequency);
+ if (f === -1) throw new Error("wtf" + row.frequency);
+ db.updateWord(row.id, { frequency: f + 1 });
+ }
+ }
+}
+
+// -- INSERT INTO lessons(name, description) values('8000 Super Useful Expressions', 'David Martins Facebook list of coloquial Thai expressions');
+// -- INSERT INTO lang_lessons(lesson_id, lang) VALUES(1, 'th');
+// -- INSERT INTO lessons(name, description) values('Thai Syllables', 'All syllables in Thai phonology');
+// -- INSERT INTO lang_lessons(lesson_id, lang) VALUES(2, 'th');
+function addThaiUseful() {
+ let idx = 0;
+ for (const level in useful) {
+ db.addCat(level);
+ const exps = (useful as any)[level];
+ console.log(level, exps.length);
+ for (const exp of exps) {
+ const split = exp.ipa.split("/").filter((s) => s.trim());
+ const ipa = split.map((ip: any) => ({ ipa: ip, tags: [] }));
+ try {
+ idx++;
+ const tx = db.db.transaction(() => {
+ const wid = db.addWord({
+ spelling: exp.spelling,
+ lang: "th",
+ type: "expression",
+ ipa: JSON.stringify(ipa),
+ });
+ console.log({ wid });
+ db.addWCat(wid, level);
+ if (exp.register) {
+ db.addCat(exp.register);
+ db.addWCat(wid, exp.register);
+ }
+ const glosses = [exp.english];
+ if (exp.note) glosses.push(exp.note);
+ db.addSense({
+ parent_id: wid,
+ spelling: exp.spelling,
+ senses: JSON.stringify([{ glosses }]),
+ });
+ db.addCard({
+ text: `Super Useful ${idx}`,
+ eid: wid as any,
+ lesson_id: 1,
+ });
+ });
+ tx();
+ } catch (e) {
+ console.log({ exp });
+ console.error(`${e}`);
+ // break;
+ }
+ }
+ }
+}
+
+function addThaiSyllablesLesson() {
+ const res = db.db
+ .query(
+ "SELECT id FROM expressions e WHERE e.type = 'syllable' and e.lang = 'th'",
+ )
+ .all() as any[];
+ for (const row of res) {
+ db.addCard({ text: "Syllable", eid: row.id, lesson_id: 2 });
+ }
+}
+// function fixIpa() {
+// const res = db.db.query(`SELECT id, ipa FROM expressions`).all() as any[];
+// for (const row of res) {
+// try {
+// const jon = JSON.parse(row.ipa);
+// } catch (_) {
+// const clean: string = row.ipa.replace("...", "").trim();
+// db.db.query(`UPDATE expressions SET ipa = ? WHERE `).run(JSON.stringify(ipa));
+// }
+// }
+// }
+addThaiUseful();
+// addThaiSyllablesLesson();
+
+// adjustFrequency("th");
+
+// addDecks();
+// fillFromDump();
+// thaiSyllables();
+// thaiFreq();
diff --git a/src/lib/hooks/useCookie.ts b/src/lib/hooks/useCookie.ts
new file mode 100644
index 0000000..904738a
--- /dev/null
+++ b/src/lib/hooks/useCookie.ts
@@ -0,0 +1,35 @@
+import { getContext } from "waku/middleware/context";
+import { mergeSetCookies } from "./setcookie";
+
+const useCookies = () => {
+ const ctx = getContext();
+ console.log(ctx.req, "cookie bridge");
+ const headers = ctx.req.headers;
+ console.log({ headers });
+ return "hi";
+
+ // const headerObj = ctx.headers || {};
+ // headerObj["set-cookie"] = mergeSetCookies(
+ // headerObj["set-cookie"] || [],
+ // (ctx.cookies || []) as ResponseCookie[],
+ // );
+ // const headers = new Headers(headerObj as Record<string, string>);
+ // const reqCookies = new RequestCookies(headers);
+ // const resCookies = new ResponseCookies(headers);
+
+ // const getCookie: ResponseCookies["get"] = (...args) =>
+ // resCookies.get(...args) || reqCookies.get(...args);
+ // const setCookie: ResponseCookies["set"] = (...args) => {
+ // const updated = resCookies.set(...args);
+ // ctx.cookies = updated.getAll();
+ // return updated;
+ // };
+ // const delCookie: ResponseCookies["delete"] = (...args) => {
+ // const updated = resCookies.delete(...args);
+ // ctx.cookies = updated.getAll();
+ // return updated;
+ // };
+ // return { getCookie, setCookie, delCookie };
+};
+
+export { useCookies };
diff --git a/src/lib/resources/lang.ts b/src/lib/resources/lang.ts
new file mode 100644
index 0000000..caa4c03
--- /dev/null
+++ b/src/lib/resources/lang.ts
@@ -0,0 +1,8 @@
+export const flags: Record<string, string> = {
+ th: "🇹🇭",
+ en: "🇬🇧",
+ zh: "🇨🇳",
+ ja: "🇯🇵",
+ es: "🇪🇸",
+ fr: "🇫🇷",
+};
diff --git a/src/lib/server/cookie.ts b/src/lib/server/cookie.ts
index 30f215e..bbabd63 100644
--- a/src/lib/server/cookie.ts
+++ b/src/lib/server/cookie.ts
@@ -1,47 +1,41 @@
-import {
- getCookie,
- getSignedCookie,
- setCookie,
- setSignedCookie,
- deleteCookie,
-} from "hono/cookie";
+import { getHonoContext } from "waku/unstable_hono";
import cookie from "cookie";
-// console.log("db module path:", "@/lib/db");
-// console.log(
-// "globalThis.__WAKU_MIDDLEWARE_CONTEXT_STORAGE__:",
-// globalThis.__WAKU_MIDDLEWARE_CONTEXT_STORAGE__,
-// );
import db from "../db";
import type { Middleware } from "waku/config";
-// XXX we would probably like to extend config.
-
const cookieMiddleware: Middleware = () => {
console.log("cookieMiddleware executed");
return async (ctx, next) => {
const cookies = cookie.parse(ctx.req.headers.cookie || "");
- console.log({ cookies });
const coki = cookies.sorlang;
- if (!coki) {
- if (ctx.req.url.pathname === "/login") return await next();
- ctx.res.status = 301;
- ctx.res.headers = {
- Location: "/login",
- };
- }
+ // if (!coki) {
+ // if (ctx.req.url.pathname === "/login") return await next();
+ // ctx.res.status = 301;
+ // ctx.res.headers = {
+ // Location: "/login",
+ // };
+ // }
if (coki) {
const userRow = db.fetchCookie(coki);
+ console.log({ userRow });
if (userRow) ctx.data.user = { id: userRow.id, name: userRow.name };
- else {
- if (ctx.req.url.pathname === "/login") return await next();
- ctx.res.status = 301;
- ctx.res.headers = {
- Location: "/login",
- };
- }
+ // else {
+ // if (ctx.req.url.pathname === "/login") return await next();
+ // ctx.res.status = 301;
+ // ctx.res.headers = {
+ // Location: "/login",
+ // };
+ // }
}
await next();
+ const hctx: any = getHonoContext();
+ 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;
+ ctx.res.headers["set-lmao"] = "wtf man";
};
};
diff --git a/src/lib/server/cookiebridge.ts b/src/lib/server/cookiebridge.ts
index 4dce095..778fc2c 100644
--- a/src/lib/server/cookiebridge.ts
+++ b/src/lib/server/cookiebridge.ts
@@ -1,38 +1,52 @@
-import { getContextData } from "waku/middleware/context";
-import {
- RequestCookies,
- ResponseCookies,
- type ResponseCookie,
-} from "@edge-runtime/cookies";
-import { mergeSetCookies } from "./setcookie";
+import { getContext, getContextData } from "waku/middleware/context";
-const cookies = () => {
- const ctx = getContextData() as {
- headers: Record<string, string | string[]>;
- cookies?: ResponseCookie[];
- };
- const headerObj = ctx.headers || {};
- headerObj["set-cookie"] = mergeSetCookies(
- headerObj["set-cookie"] || [],
- (ctx.cookies || []) as ResponseCookie[],
- );
- const headers = new Headers(headerObj as Record<string, string>);
- const reqCookies = new RequestCookies(headers);
- const resCookies = new ResponseCookies(headers);
+const useCookies = () => {
+ const ctx = getContext();
+ const headers = ctx.req.headers;
+ console.log(headers.cookie);
- const getCookie: ResponseCookies["get"] = (...args) =>
- resCookies.get(...args) || reqCookies.get(...args);
- const setCookie: ResponseCookies["set"] = (...args) => {
- const updated = resCookies.set(...args);
- ctx.cookies = updated.getAll();
- return updated;
+ const getCookie = (s: string) => {
+ const coki = headers.cookie;
+ if (!coki) return {};
+ const cokiMap = parseCoki(coki);
+ return cokiMap;
+ };
+ const setCookie = (s: string) => {
+ const ctxdata = getContextData();
+ // (ctxdata.cokimap as Record<string, string>).sorlang = s;
+ // ctxdata.cookie = `sorlang=${s}; Secure`
+ ctxdata.cookie = `sorlang=${s};`;
};
- const delCookie: ResponseCookies["delete"] = (...args) => {
- const updated = resCookies.delete(...args);
- ctx.cookies = updated.getAll();
- return updated;
+ const delCookie = (s: string) => {
+ const ctxdata = getContextData();
+ delete (ctxdata.cokimap as Record<string, string>).sorlang;
};
return { getCookie, setCookie, delCookie };
};
-export { cookies };
+export { useCookies };
+
+function parseCoki(s: string) {
+ return s
+ .split(";")
+ .map((v) => v.split("="))
+ .reduce((acc: Record<string, string>, v: any) => {
+ acc[decodeURIComponent(v[0].trim())] = decodeURIComponent(v[1].trim());
+ return acc;
+ }, {});
+}
+function parseSetCoki(s: string) {
+ return s
+ .split(";")
+ .map((v) => v.split("="))
+ .reduce((acc: Record<string, string>, v: any) => {
+ acc[decodeURIComponent(v[0].trim())] = decodeURIComponent(v[1].trim());
+ return acc;
+ }, {});
+}
+function cokiToString(m: Record<string, string>): string {
+ return Object.entries(m).reduce((acc: string, item: [string, string]) => {
+ const [key, val] = item;
+ return `${acc} ${key}=${val};`;
+ }, "");
+}
diff --git a/src/lib/server/setcookie.ts b/src/lib/server/setcookie.ts
index f64b380..61da128 100644
--- a/src/lib/server/setcookie.ts
+++ b/src/lib/server/setcookie.ts
@@ -1,24 +1,10 @@
import type { Middleware } from "waku/config";
-import { type ResponseCookie, stringifyCookie } from "@edge-runtime/cookies";
-
-export const mergeSetCookies = (
- resSetCookies: string | string[],
- cookiesInContext: ResponseCookie[],
-) => {
- if (typeof resSetCookies === "string") {
- resSetCookies = [resSetCookies];
- }
- return [...resSetCookies, ...cookiesInContext.map(stringifyCookie)];
-};
const setCookieMiddleware: Middleware = () => {
return async (ctx, next) => {
await next();
ctx.res.headers ||= {};
- ctx.res.headers["set-cookie"] = mergeSetCookies(
- ctx.res.headers["set-cookie"] || [],
- (ctx.data.cookies || []) as ResponseCookie[],
- );
+ ctx.res.headers["set-cookie"] = ctx.data.cookie as string;
};
};
diff --git a/src/lib/types/cards.ts b/src/lib/types/cards.ts
new file mode 100644
index 0000000..0592a34
--- /dev/null
+++ b/src/lib/types/cards.ts
@@ -0,0 +1,210 @@
+// src/types.ts
+
+import { ReactNode } from "react";
+
+// Language definition
+export interface Language {
+ code: string;
+ name: string;
+ nativeName?: string;
+ supportsSource?: boolean;
+ supportsTarget?: boolean;
+ progress: number;
+ cardCount: number;
+}
+
+// Translation service provider
+export interface Provider {
+ id: string;
+ name: string;
+}
+
+// Translation history item
+export interface TranslationHistoryItem {
+ id: number;
+ text: string;
+ translation: string;
+ from: string;
+ to: string;
+ provider: string;
+ timestamp: number;
+}
+
+// Response from translation API
+export interface TranslationResponse {
+ translation: string;
+ source: string;
+ target: string;
+ provider: string;
+ characters: number;
+}
+
+// Error response from API
+export interface ErrorResponse {
+ error: string;
+ details?: any;
+}
+
+// Props for components
+export interface LanguageSelectorProps {
+ value: string;
+ onChange: (value: string) => void;
+ languages: Language[];
+ showCharCount?: boolean;
+ charCount?: number;
+ showCopyButton?: boolean;
+ onCopy?: () => void;
+ setMore?: (t: { text: string; lang: string }) => void;
+ text?: string;
+ disabled?: boolean;
+}
+
+export interface TextAreaProps {
+ value: string;
+ onChange: (value: string) => void;
+ lang?: string;
+ transliteration?: TransliterationOptions;
+ placeholder: string;
+ readOnly?: boolean;
+}
+
+export interface ProviderSelectorProps {
+ value: string;
+ onChange: (value: string) => void;
+ providers: Provider[];
+}
+
+export interface TranslationHistoryProps {
+ items: TranslationHistoryItem[];
+ languages: Language[];
+ providers: Provider[];
+}
+
+export interface TransliterationOptions extends Language {
+ scripts: TransliterationLanguage[];
+}
+export interface TransliterationLanguage extends Language {
+ toScripts: Language[];
+}
+
+export type Meaning = {
+ pos: string; // part of speech;
+ meaning: string[];
+ etymology: string;
+ references?: any;
+};
+
+export type Prompts = {
+ translate: string;
+};
+export type AnalyzeRes = {
+ word: string;
+ syllables: string[];
+ ipa: string;
+ pos: POS;
+};
+type POS = string;
+
+export type WordData = {
+ spelling: string;
+ lang: string;
+ ipa: string;
+ meanings: Meaning[];
+ references?: any;
+};
+
+// app proper
+// Mock data for the app
+export type UserStats = {
+ streakDays: number;
+ cardsLearned: number;
+ minutesStudied: number;
+ dueToday: number;
+};
+export type UserData = {
+ id: number;
+ name: string;
+ stats: UserStats;
+};
+export type DeckResponse = {
+ lesson: {
+ name: string;
+ description: string;
+ language: string;
+ id: number;
+ cardCount: number;
+ };
+ cards: CardResponse[];
+};
+
+export interface SRSProgress {
+ repetitionCount: number;
+ easeFactor: number;
+ interval: number;
+ nextReviewDate: number;
+ lastReviewed: number;
+ isMastered: boolean;
+}
+
+export interface ReviewResult {
+ cardId: number;
+ accuracy: number;
+ reviewTime: number;
+}
+export type CardResponse = {
+ id: number;
+ text: string;
+ note: string;
+ progress: SRSProgress;
+ expression: {
+ ipa: Array<{ ipa: string; tags: string[] }>;
+ spelling: string;
+ type: ExpressionType;
+ syllables: number;
+ confidence: number;
+ lang: string;
+ frequency: number;
+ prosody: any;
+ senses: Sense[];
+ };
+};
+export type Sense = {
+ etymology: string;
+ pos: string;
+ forms: Array<{ form: string; tags: string[] }>;
+ related: any;
+ senses: Array<{ glosses: string[]; links: Array<[string, string]> }>;
+};
+
+export type SyllableProsody = { isLong: boolean; tone: number; lang: string };
+
+export interface Deck {
+ id: number;
+ name: string;
+ description: string;
+ cardCount: number;
+ dueCards: number;
+ progress: number;
+ language: string;
+}
+
+export interface Card {
+ id: number;
+ front: ReactNode;
+ back: ReactNode;
+ language: string;
+ difficulty: number;
+ nextReview: Date;
+ interval: number;
+ easeFactor: number;
+}
+
+export type ExpressionType = "word" | "expression" | "syllable";
+export type ExpressionSearchParams = {
+ lang?: string;
+ spelling?: string;
+ pos?: string;
+ syllables?: { num: number; sign: string }; // ">" | "<" | "="
+ frequency?: { num: number; above: boolean };
+ type?: ExpressionType;
+};
diff --git a/src/lib/types/index.ts b/src/lib/types/index.ts
index 0a46643..ce3f5fb 100644
--- a/src/lib/types/index.ts
+++ b/src/lib/types/index.ts
@@ -98,7 +98,7 @@ export type AddSense = {
parent_id: number | bigint;
spelling: string;
etymology?: string;
- pos: string;
+ pos?: string;
ipa?: string;
prosody?: string;
senses?: string;
diff --git a/src/lib/useful_thai.json b/src/lib/useful_thai.json
new file mode 120000
index 0000000..a24a80f
--- /dev/null
+++ b/src/lib/useful_thai.json
@@ -0,0 +1 @@
+/home/y/code/bun/scripts/useful_thai.json \ No newline at end of file
diff --git a/src/pages.gen.ts b/src/pages.gen.ts
index 2cb3b4a..7d23584 100644
--- a/src/pages.gen.ts
+++ b/src/pages.gen.ts
@@ -6,8 +6,14 @@ import type { PathsForPages, GetConfigResponse } from 'waku/router';
// prettier-ignore
import type { getConfig as Zoom_getConfig } from './pages/zoom';
// prettier-ignore
+import type { getConfig as LangSlug_getConfig } from './pages/lang/[slug]';
+// prettier-ignore
+import type { getConfig as LessonSlug_getConfig } from './pages/lesson/[slug]';
+// prettier-ignore
import type { getConfig as Login_getConfig } from './pages/login';
// prettier-ignore
+import type { getConfig as Parse_getConfig } from './pages/parse';
+// prettier-ignore
import type { getConfig as Db_getConfig } from './pages/db';
// prettier-ignore
import type { getConfig as Form_getConfig } from './pages/form';
@@ -16,12 +22,17 @@ import type { getConfig as Picker_getConfig } from './pages/picker';
// prettier-ignore
import type { getConfig as About_getConfig } from './pages/about';
// prettier-ignore
+import type { getConfig as LogintestIndex_getConfig } from './pages/logintest/index';
+// prettier-ignore
import type { getConfig as Index_getConfig } from './pages/index';
// prettier-ignore
type Page =
| ({ path: '/zoom' } & GetConfigResponse<typeof Zoom_getConfig>)
+| ({ path: '/lang/[slug]' } & GetConfigResponse<typeof LangSlug_getConfig>)
+| ({ path: '/lesson/[slug]' } & GetConfigResponse<typeof LessonSlug_getConfig>)
| ({ path: '/login' } & GetConfigResponse<typeof Login_getConfig>)
+| ({ path: '/parse' } & GetConfigResponse<typeof Parse_getConfig>)
| ({ path: '/db' } & GetConfigResponse<typeof Db_getConfig>)
| { path: '/test/client-modal'; render: 'dynamic' }
| { path: '/test/trigger-modal-button'; render: 'dynamic' }
@@ -29,6 +40,9 @@ type Page =
| ({ path: '/form' } & GetConfigResponse<typeof Form_getConfig>)
| ({ path: '/picker' } & GetConfigResponse<typeof Picker_getConfig>)
| ({ path: '/about' } & GetConfigResponse<typeof About_getConfig>)
+| { path: '/logintest/Form'; render: 'dynamic' }
+| ({ path: '/logintest' } & GetConfigResponse<typeof LogintestIndex_getConfig>)
+| { path: '/logintest/ServerForm'; render: 'dynamic' }
| ({ path: '/' } & GetConfigResponse<typeof Index_getConfig>);
// prettier-ignore
diff --git a/src/pages/index.tsx b/src/pages/index.tsx
index 82ffd99..48fa46e 100644
--- a/src/pages/index.tsx
+++ b/src/pages/index.tsx
@@ -1,13 +1,62 @@
import { Link } from "waku";
-import { Counter } from "../components/counter";
+import { Progress } from "@/components/ui/progress";
import { getContextData } from "waku/middleware/context";
-import Main from "../components/Main";
+
+type LanguageChoice = "th" | "en" | "zh" | "ja" | "es" | "fr";
+type LangMeta = { flag: string; name: string };
+const langs: Record<LanguageChoice, LangMeta> = {
+ th: { flag: "🇹🇭", name: "Thai" },
+ en: { flag: "🇬🇧", name: "English" },
+ zh: { flag: "🇨🇳", name: "Chinese" },
+ ja: { flag: "🇯🇵", name: "Japanese" },
+ es: { flag: "🇪🇸", name: "Spanish" },
+ fr: { flag: "🇫🇷", name: "French" },
+};
export default async function HomePage() {
const { user } = getContextData();
- return <Main />;
+ return (
+ <div className="min-h-screen bg-gray-50">
+ <header className="bg-white shadow-sm 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 items-center h-16">
+ <div className="flex items-center">
+ <h1 className="text-2xl font-bold text-indigo-600">Prosody</h1>
+ </div>
+
+ {/* Desktop Navigation */}
+ <nav className="hidden md:flex space-x-8">
+ <Link to="/">
+ <button
+ className={`py-2 font-medium text-indigo-600 border-b-2 border-indigo-600`}
+ >
+ Home
+ </button>
+ </Link>
+ <Link to="/parse">
+ <button
+ className={`py-2 font-medium text-gray-600 hover:text-indigo-600`}
+ >
+ Analyze Text
+ </button>
+ </Link>
+ </nav>
+ </div>
+ </div>
+
+ {/* Mobile Navigation */}
+ </header>
+ <section className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
+ <h2 className="text-lg"> Your Languages</h2>
+ <LanguageItem lang="en" />
+ <LanguageItem lang="th" />
+ <LanguageItem lang="zh" />
+ <LanguageItem lang="ja" />
+ </section>
+ </div>
+ );
}
const getData = async () => {
@@ -25,3 +74,54 @@ export const getConfig = async () => {
render: "dynamic",
} as const;
};
+
+async function LanguageItem({ lang }: { lang: LanguageChoice }) {
+ return (
+ <Link to={`/lang/${lang}`}>
+ <div className="bg-white rounded-xl h-32 shadow-sm overflow-hidden hover:shadow-md transition-shadow duration-300">
+ <div className="p-6">
+ <div className="flex">
+ <div className="text-lg">{langs[lang].flag}</div>
+ <div className="text-lg">{langs[lang].name}</div>
+ </div>
+ <Progress value={50} className="w-[60%]" />
+ </div>
+ </div>
+ </Link>
+ );
+}
+{
+ /* Language Cards */
+}
+// <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
+// {languages.slice(0, 3).map((lang) => (
+// <div
+// key={lang.id}
+// className="bg-white rounded-xl shadow-sm overflow-hidden hover:shadow-md transition-shadow duration-300"
+// >
+// <div className="p-6">
+// <div className="flex items-center justify-between mb-4">
+// <div className="flex items-center">
+// <span className="text-3xl mr-3">{lang.flag}</span>
+// <h3 className="text-xl font-semibold">{lang.name}</h3>
+// </div>
+// <span className="text-xs font-semibold bg-indigo-100 text-indigo-800 px-2 py-1 rounded-full">
+// Beginner
+// </span>
+// </div>
+// <div className="mb-4">
+// <div className="w-full bg-gray-200 rounded-full h-2.5 mb-1">
+// <div
+// className="bg-indigo-600 h-2.5 rounded-full"
+// style={{ width: "40%" }}
+// ></div>
+// </div>
+// <div className="text-right text-sm text-gray-500">40% Complete</div>
+// </div>
+// <button className="w-full py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors duration-300">
+// Continue Learning
+// </button>
+// </div>
+// </div>
+// ))}
+// </div>;
diff --git a/src/pages/lang/[slug].tsx b/src/pages/lang/[slug].tsx
new file mode 100644
index 0000000..11962a5
--- /dev/null
+++ b/src/pages/lang/[slug].tsx
@@ -0,0 +1,74 @@
+import { Button } from "@/components/ui/button";
+import { Link } from "waku";
+
+import { getContextData } from "waku/middleware/context";
+import type { PageProps } from "waku/router";
+import db from "@/lib/db";
+import { Progress } from "@/components/ui/progress";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+
+const flags: Record<string, string> = {
+ th: "🇹🇭",
+ en: "🇬🇧",
+ zh: "🇨🇳",
+ ja: "🇯🇵",
+ es: "🇪🇸",
+ fr: "🇫🇷",
+};
+
+export default async function HomePage(props: PageProps<"/lang/[slug]">) {
+ const lessons = await getData(props.slug);
+ const { user } = getContextData();
+ function commit() {}
+
+ return (
+ <>
+ <section>
+ <h2 className="text-lg">Thai!</h2>
+ {lessons.map((l) => (
+ <Link key={l.id} to={`/lesson/${l.id}`}>
+ <Card>
+ <CardHeader>
+ <CardTitle>
+ <h3>{l.name}</h3>
+ </CardTitle>
+ <CardDescription>
+ <p>{l.description}</p>
+ </CardDescription>
+ </CardHeader>
+ <CardContent>
+ <Progress value={(l.position / l.count) * 100} />
+ </CardContent>
+ </Card>
+ </Link>
+ ))}
+ </section>
+ </>
+ );
+}
+
+const getData = async (lang: string) => {
+ const lessons = db.fetchLanguage(lang);
+
+ return lessons;
+};
+
+export const getConfig = async () => {
+ return {
+ render: "dynamic",
+ } as const;
+};
+
+async function LanguageItem({ lang }: { lang: string }) {
+ return (
+ <div className="flex">
+ <div className="text-lg">{flags[lang] || ""}</div>
+ </div>
+ );
+}
diff --git a/src/pages/lesson/[slug].tsx b/src/pages/lesson/[slug].tsx
new file mode 100644
index 0000000..6632838
--- /dev/null
+++ b/src/pages/lesson/[slug].tsx
@@ -0,0 +1,66 @@
+import { getHonoContext } from "waku/unstable_hono";
+import { Button } from "@/components/ui/button";
+import { Link } from "waku";
+
+import { getContext, getContextData } from "waku/middleware/context";
+import * as WServer from "waku/server";
+import type { PageProps } from "waku/router";
+import db from "@/lib/db";
+import { useCookies } from "@/lib/server/cookiebridge";
+import Deck from "@/components/Flashcard/Deck";
+
+const flags: Record<string, string> = {
+ th: "🇹🇭",
+ en: "🇬🇧",
+ zh: "🇨🇳",
+ ja: "🇯🇵",
+ es: "🇪🇸",
+ fr: "🇫🇷",
+};
+
+export default async function HomePage(props: PageProps<"/lesson/[slug]">) {
+ const hctx: any = getHonoContext();
+ console.log({ hctx });
+ const ctx = getContext();
+ console.log(ctx.req.headers, "heders");
+ hctx.set("lol", "lmao");
+ const cokis = useCookies();
+ const coki = cokis.getCookie("sorlang");
+ console.log({ coki });
+ console.log({ props });
+ // const { user } = getContextData() as any;
+ // console.log({ user });
+ const user = { id: 2 };
+ const data = await getData(Number(props.slug), user.id);
+ if ("error" in data) return <p>Error</p>;
+ // console.log({ data });
+
+ return (
+ <>
+ <section>
+ <h2 className="text-lg">Thai!</h2>
+ <Deck data={data.ok} />
+ </section>
+ </>
+ );
+}
+
+const getData = async (lesson: number, userId: number) => {
+ const lessons = db.fetchLesson(userId, lesson);
+
+ return lessons;
+};
+
+export const getConfig = async () => {
+ return {
+ render: "dynamic",
+ } as const;
+};
+
+async function LanguageItem({ lang }: { lang: string }) {
+ return (
+ <div className="flex">
+ <div className="text-lg">{flags[lang] || ""}</div>
+ </div>
+ );
+}
diff --git a/src/pages/login.tsx b/src/pages/login.tsx
index 13d3bd4..d70d365 100644
--- a/src/pages/login.tsx
+++ b/src/pages/login.tsx
@@ -1,31 +1,18 @@
import AuthScreen from "@/components/Login2";
import ProfileScreen from "@/components/Profile";
-import { Link } from "waku";
-import db from "@/lib/db";
import { getContextData } from "waku/middleware/context";
export default async function AuthPage() {
const ctx = getContextData();
- console.log({ ctx });
- const data = await getData();
- if (ctx.user) return <ProfileScreen user={ctx.user as any} />;
- else
- return (
- <div>
- <AuthScreen />
- </div>
- );
+ // console.log({ ctx });
+ // if (ctx.user) return <ProfileScreen user={ctx.user as any} />;
+ // else
+ return (
+ <div>
+ <AuthScreen />
+ </div>
+ );
}
-
-const getData = async () => {
- // const data = {
- // title: "Waku",
- // headline: "Waku",
- // body: "Hello world!",
- // };
- // return data;
-};
-
export const getConfig = async () => {
return {
render: "static",
diff --git a/src/pages/logintest/Form.tsx b/src/pages/logintest/Form.tsx
new file mode 100644
index 0000000..a593acb
--- /dev/null
+++ b/src/pages/logintest/Form.tsx
@@ -0,0 +1,53 @@
+"use client";
+
+import { useFormStatus } from "react-dom";
+
+const SubmitButton = () => {
+ const { pending } = useFormStatus();
+ return (
+ <>
+ <button
+ disabled={pending}
+ type="submit"
+ className="hover:bg-slate-50 w-fit rounded-lg bg-white p-2"
+ >
+ {pending ? "Pending..." : "Submit"}
+ </button>
+ </>
+ );
+};
+
+export const Form = ({
+ message,
+ greet,
+}: {
+ message: Promise<string>;
+ greet: (formData: FormData) => Promise<void>;
+}) => (
+ <div style={{ border: "3px blue dashed", margin: "1em", padding: "1em" }}>
+ <p>{message}</p>
+ <form action={greet}>
+ <div className="flex flex-col gap-1 text-left">
+ <div>
+ Name:{" "}
+ <input
+ name="name"
+ required
+ className="invalid:border-red-500 rounded-sm border px-2 py-1"
+ />
+ </div>
+ <div>
+ Email:{" "}
+ <input
+ type="email"
+ name="email"
+ required
+ className="invalid:border-red-500 rounded-sm border px-2 py-1"
+ />
+ </div>
+ <SubmitButton />
+ </div>
+ </form>
+ <h3>This is a client component.</h3>
+ </div>
+);
diff --git a/src/pages/logintest/ServerForm.tsx b/src/pages/logintest/ServerForm.tsx
new file mode 100644
index 0000000..8e629b8
--- /dev/null
+++ b/src/pages/logintest/ServerForm.tsx
@@ -0,0 +1,67 @@
+async function submitUserProfile(formData: FormData) {
+ "use server";
+ const name = formData.get("name");
+ const age = formData.get("age");
+ const favoriteColor = formData.get("favoriteColor");
+ const hobby = formData.get("hobby");
+ const isSubscribed = formData.get("newsletter") === "on";
+
+ console.log({
+ name,
+ age,
+ favoriteColor,
+ hobby,
+ isSubscribed,
+ });
+}
+
+export const ServerForm = () => {
+ return (
+ <form action={submitUserProfile} className="space-y-4">
+ <div style={{ display: "flex", gap: 4, marginBottom: 4 }}>
+ <label htmlFor="name">Full Name</label>
+ <input type="text" name="name" id="name" required />
+ </div>
+
+ <div style={{ display: "flex", gap: 4, marginBottom: 4 }}>
+ <label htmlFor="age">Age</label>
+ <input type="number" name="age" id="age" min="13" max="120" />
+ </div>
+
+ <div style={{ display: "flex", gap: 4, marginBottom: 4 }}>
+ <label htmlFor="favoriteColor">Favorite Color</label>
+ <select name="favoriteColor" id="favoriteColor">
+ <option value="red">Red</option>
+ <option value="blue">Blue</option>
+ <option value="green">Green</option>
+ <option value="purple">Purple</option>
+ <option value="yellow">Yellow</option>
+ </select>
+ </div>
+
+ <div style={{ display: "flex", gap: 4, marginBottom: 4 }}>
+ <label htmlFor="hobby">Favorite Hobby</label>
+ <input
+ type="text"
+ name="hobby"
+ id="hobby"
+ placeholder="e.g. Reading, Gaming, Cooking"
+ />
+ </div>
+
+ <div style={{ display: "flex", gap: 4, marginBottom: 4 }}>
+ <label>
+ <input type="checkbox" name="newsletter" />
+ Subscribe to newsletter
+ </label>
+ </div>
+
+ <button
+ type="submit"
+ className="hover:bg-slate-50 w-fit rounded-lg bg-white p-2"
+ >
+ Save Profile
+ </button>
+ </form>
+ );
+};
diff --git a/src/pages/logintest/funcs.ts b/src/pages/logintest/funcs.ts
new file mode 100644
index 0000000..4ffd5ef
--- /dev/null
+++ b/src/pages/logintest/funcs.ts
@@ -0,0 +1,24 @@
+import { readFile, writeFile } from "node:fs/promises";
+import { unstable_rerenderRoute } from "waku/router/server";
+
+export const getMessage = async () => {
+ const data = await readFile("./message.txt", "utf8");
+ return data;
+};
+
+export const greet = async (formData: FormData) => {
+ "use server";
+ // simulate a slow server response
+ await new Promise((resolve) => setTimeout(resolve, 1000));
+ const currentData = await getMessage();
+ await writeFile(
+ "./message.txt",
+ currentData + "\n" + formData.get("name") + " from server!",
+ );
+ unstable_rerenderRoute("/");
+};
+
+export const increment = async (count: number) => {
+ "use server";
+ return count + 1;
+};
diff --git a/src/pages/logintest/index.tsx b/src/pages/logintest/index.tsx
new file mode 100644
index 0000000..df8bc08
--- /dev/null
+++ b/src/pages/logintest/index.tsx
@@ -0,0 +1,24 @@
+import { Form } from "./Form";
+import { getMessage, greet } from "./funcs";
+import { ServerForm } from "./ServerForm";
+
+export default function HomePage() {
+ return (
+ <div className="flex h-full w-full flex-col items-center justify-center gap-8 p-6">
+ <div className="bg-slate-100 rounded-md p-4">
+ <h2 className="text-2xl">Server Form</h2>
+ <ServerForm />
+ </div>
+ <div className="bg-slate-100 rounded-md p-4">
+ <h2 className="text-2xl">Client Form</h2>
+ <Form message={getMessage()} greet={greet} />
+ </div>
+ </div>
+ );
+}
+
+export const getConfig = async () => {
+ return {
+ render: "dynamic",
+ } as const;
+};
diff --git a/src/pages/parse.tsx b/src/pages/parse.tsx
new file mode 100644
index 0000000..8c0f793
--- /dev/null
+++ b/src/pages/parse.tsx
@@ -0,0 +1,26 @@
+import { Link } from "waku";
+
+import { getContextData } from "waku/middleware/context";
+import ParseForm from "@/components/ParseForm";
+
+export default async function HomePage() {
+ const { user } = getContextData();
+
+ return <ParseForm />;
+}
+
+const getData = async () => {
+ const data = {
+ title: "Waku",
+ headline: "Waku",
+ body: "Hello world!",
+ };
+
+ return data;
+};
+
+export const getConfig = async () => {
+ return {
+ render: "dynamic",
+ } as const;
+};
diff --git a/src/picker/App.tsx b/src/picker/App.tsx
index a3e4f43..0b4d46f 100644
--- a/src/picker/App.tsx
+++ b/src/picker/App.tsx
@@ -54,7 +54,7 @@ export default function NlpTextAnalysisScreen({
(elementType: GranularityId, elementData: any, elementText: string) => {
if (elementType === "word") {
startTransition(async () => {
- const modal = await wordAction(elementData.text, "en");
+ const modal = await wordAction(elementData.lemma, "en");
setModalContent(modal);
});
}
diff --git a/src/zoom/ServerWord.tsx b/src/zoom/ServerWord.tsx
index d98e54b..75b631d 100644
--- a/src/zoom/ServerWord.tsx
+++ b/src/zoom/ServerWord.tsx
@@ -39,9 +39,9 @@ export default async function Wordd({
lang: string;
}) {
const data = db.fetchWordBySpelling(word, "en");
- console.log({ data, word });
if (!data) return <p>oh...</p>;
+ console.log(data.senses[0]);
return (
<Card className="overflow-y-scroll max-h-[80vh]">
<CardHeader>
@@ -125,10 +125,10 @@ const ExampleDisplay = ({ examples }: { examples: Example[] }) => {
<li key={idx} className="text-xs text-gray-600">
<span className="italic">"{ex.text}"</span>
{ex.ref && (
- <span className="text-gray-400 text-xxs"> ({ex.ref})</span>
+ <span className="text-gray-400 text-xs"> ({ex.ref})</span>
)}
{ex.type !== "quote" && (
- <span className="ml-1 text-xxs bg-sky-100 text-sky-700 px-1 rounded-sm">
+ <span className="ml-1 text-xs bg-sky-100 text-sky-700 px-1 rounded-sm">
{ex.type}
</span>
)}
@@ -162,7 +162,7 @@ const RelatedTermsDisplay = ({
{term.word}
</a>
{/*term.source && (
- <span className="text-xxs text-gray-400"> ({term.source})</span>
+ <span className="text-xs text-gray-400"> ({term.source})</span>
)*/}
{idx < terms.length - 1 && ", "}
</React.Fragment>
@@ -207,7 +207,7 @@ const SubSenseDisplay = ({
{subSense.categories.map((cat, idx) => (
<span
key={idx}
- className="text-xxs bg-gray-100 text-gray-700 px-1.5 py-0.5 rounded-full"
+ className="text-xs bg-gray-100 text-gray-700 px-1.5 py-0.5 rounded-full"
>
{cat}
</span>
@@ -229,7 +229,7 @@ const SubSenseDisplay = ({
{subSense.tags.map((tag, idx) => (
<span
key={idx}
- className="text-xxs bg-purple-100 text-purple-700 px-1.5 py-0.5 rounded-full"
+ className="text-xs bg-purple-100 text-purple-700 px-1.5 py-0.5 rounded-full"
>
{tag}
</span>
diff --git a/waku.config.ts b/waku.config.ts
index 7b4aa09..c1243c4 100644
--- a/waku.config.ts
+++ b/waku.config.ts
@@ -5,8 +5,8 @@ import tsconfigPaths from "vite-tsconfig-paths";
export default defineConfig({
middleware: [
"waku/middleware/context",
- "./src/lib/server/header",
- "./src/lib/server/setcookie",
+ // "./src/lib/server/header",
+ // "./src/lib/server/setcookie",
"./src/lib/server/cookie",
"waku/middleware/dev-server",
"waku/middleware/handler",