summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--bun.lock9
-rw-r--r--package.json3
-rw-r--r--src/actions/login.ts58
-rw-r--r--src/actions/test.ts24
-rw-r--r--src/components/Login.tsx305
-rw-r--r--src/components/Login2.tsx148
-rw-r--r--src/components/actiontest.tsx27
-rw-r--r--src/components/counter.tsx12
-rw-r--r--src/components/ui/button.tsx58
-rw-r--r--src/components/ui/card.tsx68
-rw-r--r--src/components/ui/form.tsx165
-rw-r--r--src/components/ui/input.tsx19
-rw-r--r--src/components/ui/label.tsx24
-rw-r--r--src/components/ui/select.tsx179
-rw-r--r--src/components/ui/sonner.tsx23
-rw-r--r--src/components/ui/spinner.tsx8
-rw-r--r--src/lib/db/index.ts36
-rw-r--r--src/lib/db/schema.sql172
-rw-r--r--src/lib/server/cookie.ts47
-rw-r--r--src/lib/server/cookiebridge.ts33
-rw-r--r--src/lib/server/header.ts12
-rw-r--r--src/lib/server/setcookie.ts25
-rw-r--r--src/lib/utils.ts2
-rw-r--r--src/pages.gen.ts3
-rw-r--r--src/pages/api/auth.ts17
-rw-r--r--src/pages/login.tsx28
-rw-r--r--waku.config.ts23
27 files changed, 1508 insertions, 20 deletions
diff --git a/bun.lock b/bun.lock
index 6c36957..756e04c 100644
--- a/bun.lock
+++ b/bun.lock
@@ -4,12 +4,14 @@
"": {
"name": "waku",
"dependencies": {
+ "@edge-runtime/cookies": "^6.0.0",
"@hookform/resolvers": "^5.0.1",
"@radix-ui/react-label": "^2.1.6",
"@radix-ui/react-select": "^2.2.4",
"@radix-ui/react-slot": "^1.2.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
+ "cookie": "^1.0.2",
"franc-all": "^7.2.0",
"lucide-react": "^0.510.0",
"next-themes": "^0.4.6",
@@ -27,6 +29,7 @@
"devDependencies": {
"@tailwindcss/postcss": "4.1.4",
"@types/bun": "latest",
+ "@types/cookie": "^1.0.0",
"@types/react": "19.1.2",
"@types/react-dom": "19.1.2",
"postcss": "8.5.3",
@@ -77,6 +80,8 @@
"@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=="],
@@ -313,6 +318,8 @@
"@types/bun": ["@types/bun@1.2.13", "", { "dependencies": { "bun-types": "1.2.13" } }, "sha512-u6vXep/i9VBxoJl3GjZsl/BFIsvML8DfVDO0RYLEwtSZSp981kEO1V5NwRcO1CPJ7AmvpbnDCiMKo3JvbDEjAg=="],
+ "@types/cookie": ["@types/cookie@1.0.0", "", { "dependencies": { "cookie": "*" } }, "sha512-mGFXbkDQJ6kAXByHS7QAggRXgols0mAdP4MuXgloGY1tXokvzaFFM4SMqWvf7AH0oafI7zlFJwoGWzmhDqTZ9w=="],
+
"@types/eslint": ["@types/eslint@9.6.1", "", { "dependencies": { "@types/estree": "*", "@types/json-schema": "*" } }, "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag=="],
"@types/eslint-scope": ["@types/eslint-scope@3.7.7", "", { "dependencies": { "@types/eslint": "*", "@types/estree": "*" } }, "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg=="],
@@ -405,6 +412,8 @@
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
+ "cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="],
+
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
"debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
diff --git a/package.json b/package.json
index 31f7965..8ee4fbb 100644
--- a/package.json
+++ b/package.json
@@ -9,12 +9,14 @@
"start": "bunx --bun waku start"
},
"dependencies": {
+ "@edge-runtime/cookies": "^6.0.0",
"@hookform/resolvers": "^5.0.1",
"@radix-ui/react-label": "^2.1.6",
"@radix-ui/react-select": "^2.2.4",
"@radix-ui/react-slot": "^1.2.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
+ "cookie": "^1.0.2",
"franc-all": "^7.2.0",
"lucide-react": "^0.510.0",
"next-themes": "^0.4.6",
@@ -32,6 +34,7 @@
"devDependencies": {
"@tailwindcss/postcss": "4.1.4",
"@types/bun": "latest",
+ "@types/cookie": "^1.0.0",
"@types/react": "19.1.2",
"@types/react-dom": "19.1.2",
"postcss": "8.5.3",
diff --git a/src/actions/login.ts b/src/actions/login.ts
new file mode 100644
index 0000000..3d83b55
--- /dev/null
+++ b/src/actions/login.ts
@@ -0,0 +1,58 @@
+"use server";
+import { 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";
+
+export type FormState = {
+ name?: string;
+ password?: string;
+ error?: string;
+ success?: boolean;
+};
+async function call(formData: FormData, register: boolean) {
+ 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;
+}
+
+export async function postRegister(
+ prevState: FormState,
+ formData: FormData,
+): Promise<FormState> {
+ const res = await call(formData, true);
+ console.log("reg res", res);
+ if ("error" in res) return { error: "Something went wrong" };
+ else {
+ return { success: true };
+ }
+}
+
+export async function postLogin(
+ prevState: FormState,
+ formData: FormData,
+): Promise<FormState> {
+ const res = await call(formData, false);
+ 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;
+ const COOKIE_OPTS = { expires: new Date(COOKIE_EXPIRY) };
+
+ 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);
+ // unstable_redirect("/");
+}
diff --git a/src/actions/test.ts b/src/actions/test.ts
new file mode 100644
index 0000000..99d42a0
--- /dev/null
+++ b/src/actions/test.ts
@@ -0,0 +1,24 @@
+"use server";
+import db from "../lib/db";
+
+export async function testFn(lol: any) {
+ console.log({ lol });
+ return "lmao";
+}
+export async function testLogin(state: number, formdata: FormData) {
+ return state + 9;
+}
+// export async function testLogin({
+// name,
+// creds,
+// }: {
+// name: string;
+// creds: string;
+// }) {
+// const res1 = db.loginUser(name, creds);
+// console.log({ res1 });
+// return res1;
+// // const res = db.addUser(name, creds);
+// // console.log({ res });
+// // return res;
+// }
diff --git a/src/components/Login.tsx b/src/components/Login.tsx
new file mode 100644
index 0000000..5747d06
--- /dev/null
+++ b/src/components/Login.tsx
@@ -0,0 +1,305 @@
+"use client";
+
+import { postLogin, postRegister } from "@/actions/login";
+import { cn } from "@/lib/utils";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { useForm } from "react-hook-form";
+import { z } from "zod";
+
+import { toast } from "sonner";
+import { Button } from "@/components/ui/button";
+import {
+ Card,
+ CardHeader,
+ CardDescription,
+ CardContent,
+ 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 { useActionState } from "react";
+
+const FormSchema = z.object({
+ username: z.string().min(2, {
+ message: "Username must be at least 2 characters.",
+ }),
+ password: z.string().min(2, {
+ message: "Password must be at least 2 characters.",
+ }),
+});
+export default function AuthScreen() {
+ return <OOldform />;
+}
+
+function OOldform() {
+ const [state, formAction, isPending] = useActionState<
+ { msg: string },
+ FormData
+ >(postLogin, { msg: "init" });
+ return (
+ <form action={formAction}>
+ {state.msg}
+ <label>
+ Username
+ <input type="text" placeholder="shadcn" name="username" />
+ </label>
+ <label className="flex justify-between">
+ <span>Password</span>
+ <a
+ href="#"
+ className="ml-auto text-sm underline-offset-4 hover:underline"
+ >
+ Forgot your password?
+ </a>
+ <input type="password" placeholder="..." name="password" />
+ </label>
+ <button type="submit" className="w-full">
+ Login
+ </button>
+ <div className="text-center text-sm">
+ Don&apos;t have an account?{" "}
+ <a href="#" className="underline underline-offset-4">
+ Sign up
+ </a>
+ </div>
+ </form>
+ );
+}
+function Oldform() {
+ const [state, formAction, isPending] = useActionState<
+ { msg: string },
+ FormData
+ >(postLogin, { msg: "init" });
+ return (
+ <form action={formAction}>
+ {state.msg}
+ <div className="flex flex-col gap-6">
+ <Card>
+ <CardHeader className="text-center">
+ <CardTitle className="text-xl">Welcome back</CardTitle>
+ <CardDescription>Login to Sorlang</CardDescription>
+ <p>{state.msg}</p>
+ </CardHeader>
+ <CardContent>
+ <div className="grid gap-6">
+ <div className="grid gap-6">
+ <Label>
+ Username
+ <Input placeholder="shadcn" name="username" />
+ </Label>
+ <Label className="flex justify-between">
+ <span>Password</span>
+ <a
+ href="#"
+ className="ml-auto text-sm underline-offset-4 hover:underline"
+ >
+ Forgot your password?
+ </a>
+ <Input type="password" placeholder="..." name="password" />
+ </Label>
+ </div>
+ <Button type="submit" className="w-full">
+ Login
+ </Button>
+ <div className="text-center text-sm">
+ Don&apos;t have an account?{" "}
+ <a href="#" className="underline underline-offset-4">
+ Sign up
+ </a>
+ </div>
+ </div>
+ </CardContent>
+ </Card>
+ <div className="text-balance text-center text-xs text-muted-foreground [&_a]:underline [&_a]:underline-offset-4 [&_a]:hover:text-primary ">
+ By clicking continue, you agree to our{" "}
+ <a href="#">Terms of Service</a> and <a href="#">Privacy Policy</a>.
+ </div>
+ </div>
+ </form>
+ );
+}
+
+function Register({ setRegister }: { setRegister: (b: boolean) => void }) {
+ const form = useForm<z.infer<typeof FormSchema>>({
+ resolver: zodResolver(FormSchema),
+ defaultValues: {
+ username: "",
+ password: "",
+ },
+ });
+
+ async function onSubmit(data: z.infer<typeof FormSchema>) {
+ const body = JSON.stringify({
+ name: data.username,
+ creds: data.password,
+ });
+ const opts = {
+ method: "POST",
+ headers: { "Content-type": "application/json" },
+ body,
+ };
+ const res = await fetch("/api/db/user/new", opts);
+ const j = await res.json();
+ console.log(j);
+ if ("error" in j) toast(`Error: ${j.error}`);
+ else {
+ toast("Register successful");
+ }
+ }
+
+ return (
+ <Card>
+ <CardTitle>Register</CardTitle>
+ <Form {...form}>
+ <form
+ onSubmit={form.handleSubmit(onSubmit)}
+ className="w-2/3 space-y-6"
+ >
+ <CardContent>
+ <FormField
+ control={form.control}
+ name="username"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>Username</FormLabel>
+ <FormControl>
+ <Input placeholder="shadcn" {...field} />
+ </FormControl>
+ <FormDescription>
+ This is your public display name.
+ </FormDescription>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ <FormField
+ control={form.control}
+ name="password"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>Password</FormLabel>
+ <FormControl>
+ <Input type="password" placeholder="..." {...field} />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ </CardContent>
+ <CardFooter>
+ <Button type="submit">Submit</Button>
+ </CardFooter>
+ </form>
+ </Form>
+ </Card>
+ );
+}
+
+function LoginForm({
+ className,
+ ...props
+}: React.ComponentPropsWithoutRef<"div">) {
+ const form = useForm<z.infer<typeof FormSchema>>({
+ resolver: zodResolver(FormSchema),
+ defaultValues: {
+ username: "",
+ password: "",
+ },
+ });
+ async function onSubmit(data: z.infer<typeof FormSchema>) {
+ console.log("oh hai");
+ const body = JSON.stringify({
+ name: data.username,
+ creds: data.password,
+ });
+ const opts = {
+ method: "POST",
+ headers: { "Content-type": "application/json" },
+ body,
+ };
+ const res = await fetch("/api/login", opts);
+ const j = await res.json();
+ console.log({ j });
+ if ("error" in j) toast(`Error! ${j.error}`);
+ else {
+ toast("Login successful");
+ }
+ }
+ return (
+ <div className={cn("flex flex-col gap-6", className)} {...props}>
+ <Card>
+ <CardHeader className="text-center">
+ <CardTitle className="text-xl">Welcome back</CardTitle>
+ <CardDescription>Login to Sorlang</CardDescription>
+ </CardHeader>
+ <CardContent>
+ <Form {...form}>
+ <form onSubmit={form.handleSubmit(onSubmit)}>
+ <div className="grid gap-6">
+ <div className="grid gap-6">
+ <FormField
+ control={form.control}
+ name="username"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>Username</FormLabel>
+ <FormControl>
+ <Input placeholder="shadcn" {...field} />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ <FormField
+ control={form.control}
+ name="password"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel className="flex justify-between">
+ <span>Password</span>
+ <a
+ href="#"
+ className="ml-auto text-sm underline-offset-4 hover:underline"
+ >
+ Forgot your password?
+ </a>
+ </FormLabel>
+ <FormControl>
+ <Input type="password" placeholder="..." {...field} />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ </div>
+ <Button type="submit" className="w-full">
+ Login
+ </Button>
+ <div className="text-center text-sm">
+ Don&apos;t have an account?{" "}
+ <a href="#" className="underline underline-offset-4">
+ Sign up
+ </a>
+ </div>
+ </div>
+ </form>
+ </Form>
+ </CardContent>
+ </Card>
+ <div className="text-balance text-center text-xs text-muted-foreground [&_a]:underline [&_a]:underline-offset-4 [&_a]:hover:text-primary ">
+ By clicking continue, you agree to our <a href="#">Terms of Service</a>{" "}
+ and <a href="#">Privacy Policy</a>.
+ </div>
+ </div>
+ );
+}
diff --git a/src/components/Login2.tsx b/src/components/Login2.tsx
new file mode 100644
index 0000000..2164b1a
--- /dev/null
+++ b/src/components/Login2.tsx
@@ -0,0 +1,148 @@
+"use client";
+
+import { FormState, postLogin, postRegister } from "@/actions/login";
+
+import { useActionState, useEffect, useState } from "react";
+import {
+ Card,
+ CardHeader,
+ CardDescription,
+ CardContent,
+ 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);
+ return <OOldform isReg={isReg} toggle={() => setReg((b) => !b)} />;
+}
+
+function OOldform({ isReg, toggle }: { isReg: boolean; toggle: () => void }) {
+ const regstrings = {
+ title: "Welcome to Sorlang",
+ desc: "Sign up",
+ button: "Sign up",
+ toggle: "Have an account?",
+ toggle2: "Login",
+ };
+ const logstrings = {
+ title: "Welcome back",
+ desc: "Login to Sorlang",
+ button: "Login",
+ toggle: "Don't have an account?",
+ toggle2: "Sign up",
+ };
+ const [strings, setStrings] = useState(logstrings);
+
+ useEffect(() => {
+ if (isReg) setStrings(regstrings);
+ else setStrings(logstrings);
+ }, [isReg]);
+
+ const [state, formAction, isPending] = useActionState<FormState, FormData>(
+ isReg ? postRegister : postLogin,
+ {},
+ "/login",
+ );
+ // const nav = useRouter();
+ // useEffect(() => {
+ // if (state.success) nav.replace("/");
+ // }, [state]);
+ return (
+ <form action={formAction}>
+ <div className="flex flex-col gap-6">
+ <Card>
+ <CardHeader className="text-center">
+ <CardTitle className="text-xl">{strings.title}</CardTitle>
+ <CardDescription>{strings.desc}</CardDescription>
+ </CardHeader>
+ <CardContent>
+ <div className="grid gap-6">
+ <div className="grid gap-6">
+ <Label>
+ Username
+ <Input placeholder="shadcn" name="username" />
+ {state.name && <p>{state.name}</p>}
+ </Label>
+ <Label>
+ {isReg ? (
+ "Password"
+ ) : (
+ <Label className="flex justify-between">
+ <span>Password</span>
+ <a
+ href="#"
+ className="ml-auto text-sm underline-offset-4 hover:underline"
+ >
+ Forgot your password?
+ </a>
+ </Label>
+ )}
+ <Input type="password" placeholder="..." name="password" />
+ {state.password && <p>{state.password}</p>}
+ </Label>
+ </div>
+ <Button type="submit" className="w-full">
+ {strings.button}
+ </Button>
+ <div className="text-center text-sm">
+ {strings.toggle}
+ <a
+ onClick={toggle}
+ href="#"
+ className="underline underline-offset-4"
+ >
+ {strings.toggle2}
+ </a>
+ </div>
+ </div>
+ {state.error && <p className="text-red">{state.error}</p>}
+ </CardContent>
+ </Card>
+ <div className="text-balance text-center text-xs text-muted-foreground [&_a]:underline [&_a]:underline-offset-4 [&_a]:hover:text-primary ">
+ By clicking continue, you agree to our{" "}
+ <a href="#">Terms of Service</a> and <a href="#">Privacy Policy</a>.
+ </div>
+ </div>
+ </form>
+ // <form action={formAction} aria-disabled={isPending}>
+ // {state.msg}
+ // <label>
+ // Username
+ // <input type="text" placeholder="shadcn" name="username" />
+ // </label>
+ // <label className="flex justify-between">
+ // <span>Password</span>
+ // <a
+ // href="#"
+ // className="ml-auto text-sm underline-offset-4 hover:underline"
+ // >
+ // Forgot your password?
+ // </a>
+ // <input type="password" placeholder="..." name="password" />
+ // </label>
+ // <button type="submit" className="w-full">
+ // Login
+ // </button>
+ // <div className="text-center text-sm">
+ // Don&apos;t have an account?{" "}
+ // <a href="#" className="underline underline-offset-4">
+ // Sign up
+ // </a>
+ // </div>
+ // </form>
+ );
+}
diff --git a/src/components/actiontest.tsx b/src/components/actiontest.tsx
new file mode 100644
index 0000000..863f289
--- /dev/null
+++ b/src/components/actiontest.tsx
@@ -0,0 +1,27 @@
+"use client";
+
+import { testFn, testLogin } from "@/actions/test";
+import { useActionState, useState } from "react";
+
+export default function TestForm() {
+ const [state, formAction, isPending] = useActionState<number, FormData>(
+ testLogin,
+ 0,
+ );
+ return (
+ <form action={formAction}>
+ <p>State: {state}</p>
+ <label>
+ Username
+ <input type="text" placeholder="shadcn" name="username" />
+ </label>
+ <label className="flex justify-between">
+ <span>Password</span>
+ <input type="password" placeholder="..." name="password" />
+ </label>
+ <button type="submit" className="w-full">
+ Login
+ </button>
+ </form>
+ );
+}
diff --git a/src/components/counter.tsx b/src/components/counter.tsx
index 0e540b8..2122b75 100644
--- a/src/components/counter.tsx
+++ b/src/components/counter.tsx
@@ -1,11 +1,17 @@
-'use client';
+"use client";
-import { useState } from 'react';
+import { testFn, testLogin } from "@/actions/test";
+import { useState } from "react";
export const Counter = () => {
const [count, setCount] = useState(0);
- const handleIncrement = () => setCount((c) => c + 1);
+ const handleIncrement = async () => {
+ setCount((c) => c + 1);
+ const res = await testFn("rofl");
+ console.log({ res });
+ const oof = testLogin({ name: "yago", creds: "xd" });
+ };
return (
<section className="border-blue-400 -mx-4 mt-4 rounded-sm border border-dashed p-4">
diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx
new file mode 100644
index 0000000..61875fc
--- /dev/null
+++ b/src/components/ui/button.tsx
@@ -0,0 +1,58 @@
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const buttonVariants = cva(
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-[color,box-shadow] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 ring-ring/10 dark:ring-ring/20 dark:outline-ring/40 outline-ring/50 focus-visible:ring-4 focus-visible:outline-1 aria-invalid:focus-visible:ring-0",
+ {
+ variants: {
+ variant: {
+ default:
+ "bg-primary text-primary-foreground shadow-sm hover:bg-primary/90",
+ destructive:
+ "bg-destructive text-destructive-foreground shadow-xs hover:bg-destructive/90",
+ outline:
+ "border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground",
+ secondary:
+ "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
+ ghost: "hover:bg-accent hover:text-accent-foreground",
+ link: "text-primary underline-offset-4 hover:underline",
+ },
+ size: {
+ default: "h-9 px-4 py-2 has-[>svg]:px-3",
+ sm: "h-8 rounded-md px-3 has-[>svg]:px-2.5",
+ lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
+ icon: "size-9",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ }
+)
+
+function Button({
+ className,
+ variant,
+ size,
+ asChild = false,
+ ...props
+}: React.ComponentProps<"button"> &
+ VariantProps<typeof buttonVariants> & {
+ asChild?: boolean
+ }) {
+ const Comp = asChild ? Slot : "button"
+
+ return (
+ <Comp
+ data-slot="button"
+ className={cn(buttonVariants({ variant, size, className }))}
+ {...props}
+ />
+ )
+}
+
+export { Button, buttonVariants }
diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx
new file mode 100644
index 0000000..3ff199b
--- /dev/null
+++ b/src/components/ui/card.tsx
@@ -0,0 +1,68 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+function Card({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+ <div
+ data-slot="card"
+ className={cn(
+ "bg-card text-card-foreground rounded-xl border shadow-sm",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+ <div
+ data-slot="card-header"
+ className={cn("flex flex-col gap-1.5 p-6", className)}
+ {...props}
+ />
+ )
+}
+
+function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+ <div
+ data-slot="card-title"
+ className={cn("leading-none font-semibold tracking-tight", className)}
+ {...props}
+ />
+ )
+}
+
+function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+ <div
+ data-slot="card-description"
+ className={cn("text-muted-foreground text-sm", className)}
+ {...props}
+ />
+ )
+}
+
+function CardContent({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+ <div
+ data-slot="card-content"
+ className={cn("p-6 pt-0", className)}
+ {...props}
+ />
+ )
+}
+
+function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+ <div
+ data-slot="card-footer"
+ className={cn("flex items-center p-6 pt-0", className)}
+ {...props}
+ />
+ )
+}
+
+export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
diff --git a/src/components/ui/form.tsx b/src/components/ui/form.tsx
new file mode 100644
index 0000000..8a83b32
--- /dev/null
+++ b/src/components/ui/form.tsx
@@ -0,0 +1,165 @@
+import * as React from "react"
+import * as LabelPrimitive from "@radix-ui/react-label"
+import { Slot } from "@radix-ui/react-slot"
+import {
+ Controller,
+ ControllerProps,
+ FieldPath,
+ FieldValues,
+ FormProvider,
+ useFormContext,
+ useFormState,
+} from "react-hook-form"
+
+import { cn } from "@/lib/utils"
+import { Label } from "@/components/ui/label"
+
+const Form = FormProvider
+
+type FormFieldContextValue<
+ TFieldValues extends FieldValues = FieldValues,
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
+> = {
+ name: TName
+}
+
+const FormFieldContext = React.createContext<FormFieldContextValue>(
+ {} as FormFieldContextValue
+)
+
+const FormField = <
+ TFieldValues extends FieldValues = FieldValues,
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
+>({
+ ...props
+}: ControllerProps<TFieldValues, TName>) => {
+ return (
+ <FormFieldContext.Provider value={{ name: props.name }}>
+ <Controller {...props} />
+ </FormFieldContext.Provider>
+ )
+}
+
+const useFormField = () => {
+ const fieldContext = React.useContext(FormFieldContext)
+ const itemContext = React.useContext(FormItemContext)
+ const { getFieldState } = useFormContext()
+ const formState = useFormState({ name: fieldContext.name })
+ const fieldState = getFieldState(fieldContext.name, formState)
+
+ if (!fieldContext) {
+ throw new Error("useFormField should be used within <FormField>")
+ }
+
+ const { id } = itemContext
+
+ return {
+ id,
+ name: fieldContext.name,
+ formItemId: `${id}-form-item`,
+ formDescriptionId: `${id}-form-item-description`,
+ formMessageId: `${id}-form-item-message`,
+ ...fieldState,
+ }
+}
+
+type FormItemContextValue = {
+ id: string
+}
+
+const FormItemContext = React.createContext<FormItemContextValue>(
+ {} as FormItemContextValue
+)
+
+function FormItem({ className, ...props }: React.ComponentProps<"div">) {
+ const id = React.useId()
+
+ return (
+ <FormItemContext.Provider value={{ id }}>
+ <div
+ data-slot="form-item"
+ className={cn("grid gap-2", className)}
+ {...props}
+ />
+ </FormItemContext.Provider>
+ )
+}
+
+function FormLabel({
+ className,
+ ...props
+}: React.ComponentProps<typeof LabelPrimitive.Root>) {
+ const { error, formItemId } = useFormField()
+
+ return (
+ <Label
+ data-slot="form-label"
+ data-error={!!error}
+ className={cn("data-[error=true]:text-destructive", className)}
+ htmlFor={formItemId}
+ {...props}
+ />
+ )
+}
+
+function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
+ const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
+
+ return (
+ <Slot
+ data-slot="form-control"
+ id={formItemId}
+ aria-describedby={
+ !error
+ ? `${formDescriptionId}`
+ : `${formDescriptionId} ${formMessageId}`
+ }
+ aria-invalid={!!error}
+ {...props}
+ />
+ )
+}
+
+function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
+ const { formDescriptionId } = useFormField()
+
+ return (
+ <p
+ data-slot="form-description"
+ id={formDescriptionId}
+ className={cn("text-muted-foreground text-sm", className)}
+ {...props}
+ />
+ )
+}
+
+function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
+ const { error, formMessageId } = useFormField()
+ const body = error ? String(error?.message) : props.children
+
+ if (!body) {
+ return null
+ }
+
+ return (
+ <p
+ data-slot="form-message"
+ id={formMessageId}
+ className={cn("text-destructive text-sm font-medium", className)}
+ {...props}
+ >
+ {body}
+ </p>
+ )
+}
+
+export {
+ useFormField,
+ Form,
+ FormItem,
+ FormLabel,
+ FormControl,
+ FormDescription,
+ FormMessage,
+ FormField,
+}
diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx
new file mode 100644
index 0000000..596f806
--- /dev/null
+++ b/src/components/ui/input.tsx
@@ -0,0 +1,19 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+function Input({ className, type, ...props }: React.ComponentProps<"input">) {
+ return (
+ <input
+ type={type}
+ data-slot="input"
+ className={cn(
+ "border-input file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground aria-invalid:outline-destructive/60 aria-invalid:ring-destructive/20 dark:aria-invalid:outline-destructive dark:aria-invalid:ring-destructive/50 ring-ring/10 dark:ring-ring/20 dark:outline-ring/40 outline-ring/50 aria-invalid:outline-destructive/60 dark:aria-invalid:outline-destructive dark:aria-invalid:ring-destructive/40 aria-invalid:ring-destructive/20 aria-invalid:border-destructive/60 dark:aria-invalid:border-destructive flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-4 focus-visible:outline-1 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:focus-visible:ring-[3px] aria-invalid:focus-visible:outline-none md:text-sm dark:aria-invalid:focus-visible:ring-4",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+export { Input }
diff --git a/src/components/ui/label.tsx b/src/components/ui/label.tsx
new file mode 100644
index 0000000..f948bc3
--- /dev/null
+++ b/src/components/ui/label.tsx
@@ -0,0 +1,24 @@
+"use client"
+
+import * as React from "react"
+import * as LabelPrimitive from "@radix-ui/react-label"
+
+import { cn } from "@/lib/utils"
+
+function Label({
+ className,
+ ...props
+}: React.ComponentProps<typeof LabelPrimitive.Root>) {
+ return (
+ <LabelPrimitive.Root
+ data-slot="label"
+ className={cn(
+ "text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+export { Label }
diff --git a/src/components/ui/select.tsx b/src/components/ui/select.tsx
new file mode 100644
index 0000000..b624a5b
--- /dev/null
+++ b/src/components/ui/select.tsx
@@ -0,0 +1,179 @@
+import * as React from "react"
+import * as SelectPrimitive from "@radix-ui/react-select"
+import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+function Select({
+ ...props
+}: React.ComponentProps<typeof SelectPrimitive.Root>) {
+ return <SelectPrimitive.Root data-slot="select" {...props} />
+}
+
+function SelectGroup({
+ ...props
+}: React.ComponentProps<typeof SelectPrimitive.Group>) {
+ return <SelectPrimitive.Group data-slot="select-group" {...props} />
+}
+
+function SelectValue({
+ ...props
+}: React.ComponentProps<typeof SelectPrimitive.Value>) {
+ return <SelectPrimitive.Value data-slot="select-value" {...props} />
+}
+
+function SelectTrigger({
+ className,
+ children,
+ ...props
+}: React.ComponentProps<typeof SelectPrimitive.Trigger>) {
+ return (
+ <SelectPrimitive.Trigger
+ data-slot="select-trigger"
+ className={cn(
+ "border-input data-[placeholder]:text-muted-foreground aria-invalid:border-destructive ring-ring/10 dark:ring-ring/20 dark:outline-ring/40 outline-ring/50 [&_svg:not([class*='text-'])]:text-muted-foreground flex h-9 w-full items-center justify-between rounded-md border bg-transparent px-3 py-2 text-sm shadow-xs transition-[color,box-shadow] focus-visible:ring-4 focus-visible:outline-1 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:focus-visible:ring-0 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&>span]:line-clamp-1",
+ className
+ )}
+ {...props}
+ >
+ {children}
+ <SelectPrimitive.Icon asChild>
+ <ChevronDownIcon className="size-4 opacity-50" />
+ </SelectPrimitive.Icon>
+ </SelectPrimitive.Trigger>
+ )
+}
+
+function SelectContent({
+ className,
+ children,
+ position = "popper",
+ ...props
+}: React.ComponentProps<typeof SelectPrimitive.Content>) {
+ return (
+ <SelectPrimitive.Portal>
+ <SelectPrimitive.Content
+ data-slot="select-content"
+ className={cn(
+ "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border shadow-md",
+ position === "popper" &&
+ "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
+ className
+ )}
+ position={position}
+ {...props}
+ >
+ <SelectScrollUpButton />
+ <SelectPrimitive.Viewport
+ className={cn(
+ "p-1",
+ position === "popper" &&
+ "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
+ )}
+ >
+ {children}
+ </SelectPrimitive.Viewport>
+ <SelectScrollDownButton />
+ </SelectPrimitive.Content>
+ </SelectPrimitive.Portal>
+ )
+}
+
+function SelectLabel({
+ className,
+ ...props
+}: React.ComponentProps<typeof SelectPrimitive.Label>) {
+ return (
+ <SelectPrimitive.Label
+ data-slot="select-label"
+ className={cn("px-2 py-1.5 text-sm font-semibold", className)}
+ {...props}
+ />
+ )
+}
+
+function SelectItem({
+ className,
+ children,
+ ...props
+}: React.ComponentProps<typeof SelectPrimitive.Item>) {
+ return (
+ <SelectPrimitive.Item
+ data-slot="select-item"
+ className={cn(
+ "focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
+ className
+ )}
+ {...props}
+ >
+ <span className="absolute right-2 flex size-3.5 items-center justify-center">
+ <SelectPrimitive.ItemIndicator>
+ <CheckIcon className="size-4" />
+ </SelectPrimitive.ItemIndicator>
+ </span>
+ <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
+ </SelectPrimitive.Item>
+ )
+}
+
+function SelectSeparator({
+ className,
+ ...props
+}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
+ return (
+ <SelectPrimitive.Separator
+ data-slot="select-separator"
+ className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
+ {...props}
+ />
+ )
+}
+
+function SelectScrollUpButton({
+ className,
+ ...props
+}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
+ return (
+ <SelectPrimitive.ScrollUpButton
+ data-slot="select-scroll-up-button"
+ className={cn(
+ "flex cursor-default items-center justify-center py-1",
+ className
+ )}
+ {...props}
+ >
+ <ChevronUpIcon className="size-4" />
+ </SelectPrimitive.ScrollUpButton>
+ )
+}
+
+function SelectScrollDownButton({
+ className,
+ ...props
+}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
+ return (
+ <SelectPrimitive.ScrollDownButton
+ data-slot="select-scroll-down-button"
+ className={cn(
+ "flex cursor-default items-center justify-center py-1",
+ className
+ )}
+ {...props}
+ >
+ <ChevronDownIcon className="size-4" />
+ </SelectPrimitive.ScrollDownButton>
+ )
+}
+
+export {
+ Select,
+ SelectContent,
+ SelectGroup,
+ SelectItem,
+ SelectLabel,
+ SelectScrollDownButton,
+ SelectScrollUpButton,
+ SelectSeparator,
+ SelectTrigger,
+ SelectValue,
+}
diff --git a/src/components/ui/sonner.tsx b/src/components/ui/sonner.tsx
new file mode 100644
index 0000000..cd62aff
--- /dev/null
+++ b/src/components/ui/sonner.tsx
@@ -0,0 +1,23 @@
+import { useTheme } from "next-themes"
+import { Toaster as Sonner, ToasterProps } from "sonner"
+
+const Toaster = ({ ...props }: ToasterProps) => {
+ const { theme = "system" } = useTheme()
+
+ return (
+ <Sonner
+ theme={theme as ToasterProps["theme"]}
+ className="toaster group"
+ style={
+ {
+ "--normal-bg": "var(--popover)",
+ "--normal-text": "var(--popover-foreground)",
+ "--normal-border": "var(--border)",
+ } as React.CSSProperties
+ }
+ {...props}
+ />
+ )
+}
+
+export { Toaster }
diff --git a/src/components/ui/spinner.tsx b/src/components/ui/spinner.tsx
new file mode 100644
index 0000000..d1511ad
--- /dev/null
+++ b/src/components/ui/spinner.tsx
@@ -0,0 +1,8 @@
+export const Spinner = ({ className }: { className?: string }) => (
+ <div
+ className={
+ "w-7 h-7 border-[3px] border-transparent border-t-primary rounded-full animate-spin" +
+ ` ${className}`
+ }
+ />
+);
diff --git a/src/lib/db/index.ts b/src/lib/db/index.ts
index 9897af8..3d46fd9 100644
--- a/src/lib/db/index.ts
+++ b/src/lib/db/index.ts
@@ -1,7 +1,7 @@
import Database from "bun:sqlite";
-import { getDBOffset, wordFactorial } from "@/lib/utils";
-import type { AddSense, AddWord, State } from "@/lib/types";
-import { DEFAULT_SRS } from "@/lib/services/srs";
+import { getDBOffset, wordFactorial } from "../utils";
+import type { AddSense, AddWord, State } from "../types";
+import { DEFAULT_SRS } from "../services/srs";
const PAGE_SIZE = 100;
@@ -32,11 +32,17 @@ class DatabaseHandler {
fetchCookie(coki: string) {
const query = this.db.query(
`
- SELECT * FROM cookies
- WHERE cookie = ?
+ SELECT u.id, u.name, c.expiry FROM cookies as c
+ JOIN users as u ON u.id = c.user
+ WHERE c.cookie = ?
`,
);
- const res = query.get(coki);
+ const res = query.get(coki) as {
+ id: number;
+ name: string;
+ expiry: number;
+ };
+ console.log("cokifetch", { coki, res });
return res;
}
setCookie(coki: string, user: number, expiry: number) {
@@ -577,14 +583,22 @@ class DatabaseHandler {
return { error: `${e}` };
}
}
- loginUser(name: string, creds: string) {
+ async loginUser(name: string, creds: string) {
const query = this.db.query(`
- SELECT id FROM users
- WHERE name = ? AND creds = ?
+ SELECT * FROM users
+ WHERE name = ?
`);
- const row = query.get(name, creds) as { id: number } | null;
+ const row = query.get(name) as {
+ id: number;
+ name: string;
+ creds: string;
+ } | null;
if (!row) return { error: "not found" };
- else return { ok: row.id };
+ else {
+ const ok = await Bun.password.verify(creds, row.creds);
+ if (!ok) return { error: "Wrong password" };
+ else return { ok: row.id };
+ }
}
addCat(category: string) {
const queryString = `
diff --git a/src/lib/db/schema.sql b/src/lib/db/schema.sql
new file mode 100644
index 0000000..1b678c5
--- /dev/null
+++ b/src/lib/db/schema.sql
@@ -0,0 +1,172 @@
+-- Enable foreign key support
+PRAGMA foreign_keys = ON;
+PRAGMA journal_mode = WAL;
+PRAGMA cache_size = -2000;
+PRAGMA mmap_size = 30000000000;
+
+
+CREATE TABLE IF NOT EXISTS cookies(
+ user INTEGER NOT NULL,
+ cookie TEXT NOT NULL,
+ expiry INTEGER NOT NULL,
+ PRIMARY KEY(user, cookie),
+ FOREIGN KEY (user) REFERENCES users(id) ON DELETE CASCADE
+);
+CREATE TABLE IF NOT EXISTS languages (
+ code TEXT PRIMARY KEY,
+ name TEXT NOT NULL,
+ native_name TEXT
+);
+-- type is "word" or other
+--
+
+-- an orthographic (and likely phonetic too entity)
+-- lang is 2char code ISO 6393-1 I gues
+CREATE TABLE IF NOT EXISTS expressions(
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ spelling TEXT NOT NULL,
+ lang TEXT NOT NULL,
+ frequency INTEGER,
+ type TEXT NOT NULL,
+ syllables INTEGER,
+ ipa JSONB,
+ prosody JSONB,
+ confidence INTEGER NOT NULL DEFAULT 0,
+ FOREIGN KEY (lang) REFERENCES languages(code),
+ CONSTRAINT spell_unique UNIQUE (spelling, lang)
+);
+CREATE INDEX IF NOT EXISTS idx_words_spelling ON expressions(spelling);
+CREATE INDEX IF NOT EXISTS idx_words_type ON expressions(type);
+CREATE INDEX IF NOT EXISTS idx_words_lang ON expressions(lang);
+-- a semantic entity
+CREATE TABLE IF NOT EXISTS senses(
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ parent_id INTEGER NOT NULL,
+ spelling TEXT NOT NULL,
+ pos TEXT,
+ etymology TEXT,
+ ipa JSONB,
+ prosody JSONB,
+ senses JSONB,
+ forms JSONB,
+ related JSONB,
+ confidence INTEGER NOT NULL DEFAULT 0,
+ FOREIGN KEY (parent_id) REFERENCES expressions(id)
+);
+CREATE INDEX IF NOT EXISTS idx_words_pos ON senses(pos);
+CREATE INDEX IF NOT EXISTS idx_senses_parent ON senses(parent_id);
+
+
+
+-- Categories table (for noun and verb categories)
+CREATE TABLE IF NOT EXISTS categories (
+ name TEXT PRIMARY KEY
+);
+
+-- Word Categories junction table
+CREATE TABLE IF NOT EXISTS word_categories (
+ word_id INTEGER NOT NULL,
+ category INTEGER NOT NULL,
+ PRIMARY KEY (word_id, category),
+ FOREIGN KEY (word_id) REFERENCES expressions(id),
+ FOREIGN KEY (category) REFERENCES categories(name)
+);
+CREATE INDEX IF NOT EXISTS idx_word_categories_category_id ON word_categories(category);
+
+
+-- Progress
+CREATE TABLE IF NOT EXISTS users(
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ name TEXT NOT NULL,
+ creds TEXT NOT NULL,
+ CONSTRAINT name_unique UNIQUE (name)
+);
+CREATE TABLE IF NOT EXISTS user_progress (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ user_id INTEGER NOT NULL,
+ card_id INTEGER NOT NULL,
+ repetition_count INTEGER DEFAULT 0,
+ ease_factor REAL DEFAULT 2.5,
+ interval INTEGER DEFAULT 1,
+ next_review_date INTEGER,
+ last_reviewed INTEGER,
+ is_mastered BOOLEAN DEFAULT FALSE,
+ FOREIGN KEY (user_id) REFERENCES users(id),
+ FOREIGN KEY (card_id) REFERENCES cards(id),
+ CONSTRAINT progress_unique UNIQUE (user_id, card_id)
+);
+-- CREATE TABLE IF NOT EXISTS user_progress (
+-- id INTEGER PRIMARY KEY AUTOINCREMENT,
+-- user_id INTEGER NOT NULL,
+-- card_id INTEGER NOT NULL,
+-- repetition_count INTEGER DEFAULT 0,
+-- ease_factor REAL DEFAULT 2.5,
+-- interval INTEGER DEFAULT 1,
+-- next_review_date DATETIME,
+-- last_reviewed DATETIME,
+-- is_mastered BOOLEAN DEFAULT FALSE,
+-- CONSTRAINT progress_unique UNIQUE (user_id, card_id)
+-- FOREIGN KEY (user_id) REFERENCES users(id),
+-- FOREIGN KEY (card_id) REFERENCES cards(id)
+-- );
+-- Lessons
+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)
+);
+CREATE TABLE IF NOT EXISTS cards(
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ text TEXT NOT NULL,
+ note TEXT
+);
+CREATE TABLE IF NOT EXISTS cards_expressions(
+ expression_id INTEGER NOT NULL,
+ card_id INTEGER NOT NULL,
+ PRIMARY KEY (card_id, expression_id),
+ FOREIGN KEY (card_id) REFERENCES cards(id),
+ FOREIGN KEY (expression_id) REFERENCES expressions(id)
+);
+CREATE TABLE IF NOT EXISTS cards_lessons(
+ lesson_id INTEGER,
+ card_id INTEGER NOT NULL,
+ PRIMARY KEY (card_id, lesson_id),
+ FOREIGN KEY (card_id) REFERENCES cards(id),
+ FOREIGN KEY (lesson_id) REFERENCES lessons(id)
+);
+
+CREATE TABLE IF NOT EXISTS attempts(
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ user_id INTEGER NOT NULL,
+ timestamp INTEGER NOT NULL,
+ card_id INTEGER NOT NULL,
+ good INTEGER NOT NULL, -- 0 or 1
+ FOREIGN KEY (user_id) REFERENCES users(id)
+ FOREIGN KEY (card_id) REFERENCES cards(id)
+);
+
+-- Index to query attempts on a specific card
+CREATE INDEX IF NOT EXISTS idx_attempts_card ON attempts(card_id);
+
+-- Index to query attempts for a specific user
+CREATE INDEX IF NOT EXISTS idx_attempts_user ON attempts(user_id);
+
+-- (Optional) Index to query attempts by user and resource (useful if you often query by both)
+CREATE INDEX IF NOT EXISTS idx_attempts_user_resource ON attempts(user_id, card_id);
+CREATE INDEX IF NOT EXISTS idx_cards_resources
+ON cards_expressions(expression_id, card_id);
+
+-- CREATE TRIGGER IF NOT EXISTS populate_cards_resources
+-- AFTER INSERT ON cards
+-- FOR EACH ROW
+-- BEGIN
+-- -- Insert matching words into cards_resources
+-- INSERT INTO cards_expressions(card_id, expression_id)
+-- SELECT NEW.id, w.id
+-- FROM expressions w
+-- WHERE NEW.text LIKE '%' || w.spelling || '%';
+-- END;
+--
diff --git a/src/lib/server/cookie.ts b/src/lib/server/cookie.ts
new file mode 100644
index 0000000..fadac9d
--- /dev/null
+++ b/src/lib/server/cookie.ts
@@ -0,0 +1,47 @@
+import {
+ getCookie,
+ getSignedCookie,
+ setCookie,
+ setSignedCookie,
+ deleteCookie,
+} from "hono/cookie";
+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) => {
+ if (ctx.req.url.pathname === "/login") return await next();
+ const cookies = cookie.parse(ctx.req.headers.cookie || "");
+ console.log({ cookies });
+ const coki = cookies.sorlang;
+ if (!coki) {
+ ctx.res.status = 301;
+ ctx.res.headers = {
+ Location: "/login",
+ };
+ }
+ if (coki) {
+ const userRow = db.fetchCookie(coki);
+ if (userRow) ctx.data.user = { id: userRow.id, name: userRow.name };
+ else {
+ ctx.res.status = 301;
+ ctx.res.headers = {
+ Location: "/login",
+ };
+ }
+ }
+ await next();
+ };
+};
+
+export default cookieMiddleware;
diff --git a/src/lib/server/cookiebridge.ts b/src/lib/server/cookiebridge.ts
new file mode 100644
index 0000000..ca4bd44
--- /dev/null
+++ b/src/lib/server/cookiebridge.ts
@@ -0,0 +1,33 @@
+import { getContextData } from "waku/middleware/context";
+import {
+ RequestCookies,
+ ResponseCookies,
+ type ResponseCookie,
+} from "@edge-runtime/cookies";
+import { mergeSetCookies } from "./setcookie";
+
+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 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;
+ };
+ return { getCookie, setCookie };
+};
+
+export { cookies };
diff --git a/src/lib/server/header.ts b/src/lib/server/header.ts
new file mode 100644
index 0000000..33f8792
--- /dev/null
+++ b/src/lib/server/header.ts
@@ -0,0 +1,12 @@
+// https://github.com/t6adev/waku-auth-middleware-demo/blob/a476ecb3d5caf0c7731a34314450400d4dcc2dac/src/middleware/validateRouting.ts
+
+import type { Middleware } from "waku/config";
+
+const headersMiddleware: Middleware = () => {
+ return async (ctx, next) => {
+ ctx.data.headers = ctx.req.headers;
+ await next();
+ };
+};
+
+export default headersMiddleware;
diff --git a/src/lib/server/setcookie.ts b/src/lib/server/setcookie.ts
new file mode 100644
index 0000000..f64b380
--- /dev/null
+++ b/src/lib/server/setcookie.ts
@@ -0,0 +1,25 @@
+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[],
+ );
+ };
+};
+
+export default setCookieMiddleware;
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
index 113c874..d3fdf9c 100644
--- a/src/lib/utils.ts
+++ b/src/lib/utils.ts
@@ -1,6 +1,6 @@
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
-import type { Result } from "@/lib/types";
+import type { Result } from "./types";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
diff --git a/src/pages.gen.ts b/src/pages.gen.ts
index 6f44dd4..94e0a4a 100644
--- a/src/pages.gen.ts
+++ b/src/pages.gen.ts
@@ -4,6 +4,8 @@
import type { PathsForPages, GetConfigResponse } from 'waku/router';
// prettier-ignore
+import type { getConfig as Login_getConfig } from './pages/login';
+// prettier-ignore
import type { getConfig as Db_getConfig } from './pages/db';
// prettier-ignore
import type { getConfig as About_getConfig } from './pages/about';
@@ -12,6 +14,7 @@ import type { getConfig as Index_getConfig } from './pages/index';
// prettier-ignore
type Page =
+| ({ path: '/login' } & GetConfigResponse<typeof Login_getConfig>)
| ({ path: '/db' } & GetConfigResponse<typeof Db_getConfig>)
| ({ path: '/about' } & GetConfigResponse<typeof About_getConfig>)
| ({ path: '/' } & GetConfigResponse<typeof Index_getConfig>);
diff --git a/src/pages/api/auth.ts b/src/pages/api/auth.ts
new file mode 100644
index 0000000..3ed9b76
--- /dev/null
+++ b/src/pages/api/auth.ts
@@ -0,0 +1,17 @@
+import db from "../../lib/db";
+export const POST = async (request: Request): Promise<Response> => {
+ const body = await request.json();
+
+ if (!body.name || !body.creds) {
+ return Response.json({ message: "Invalid" }, { status: 400 });
+ }
+
+ try {
+ const res = db.loginUser(body.name, body.creds);
+ console.log({ res });
+
+ return Response.json(res, { status: 200 });
+ } catch (error) {
+ return Response.json({ message: "Failure" }, { status: 500 });
+ }
+};
diff --git a/src/pages/login.tsx b/src/pages/login.tsx
new file mode 100644
index 0000000..8ddc1a1
--- /dev/null
+++ b/src/pages/login.tsx
@@ -0,0 +1,28 @@
+import AuthScreen from "@/components/Login2";
+import { Link } from "waku";
+import db from "@/lib/db";
+
+export default async function AuthPage() {
+ const data = await getData();
+
+ 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",
+ } as const;
+};
diff --git a/waku.config.ts b/waku.config.ts
index 4c455f3..7b4aa09 100644
--- a/waku.config.ts
+++ b/waku.config.ts
@@ -5,14 +5,27 @@ import tsconfigPaths from "vite-tsconfig-paths";
export default defineConfig({
middleware: [
"waku/middleware/context",
+ "./src/lib/server/header",
+ "./src/lib/server/setcookie",
+ "./src/lib/server/cookie",
"waku/middleware/dev-server",
"waku/middleware/handler",
],
unstable_viteConfigs: {
- common: () => ({
- plugins: [
- tsconfigPaths({ root: fileURLToPath(new URL(".", import.meta.url)) }),
- ],
- }),
+ common: () => {
+ // console.log(
+ // "WAKU_DEBUG: Applying common Vite config from waku.config.ts",
+ // );
+ return {
+ plugins: [
+ tsconfigPaths({ root: fileURLToPath(new URL(".", import.meta.url)) }),
+ ],
+ resolve: {
+ alias: {
+ "@": fileURLToPath(new URL("./src", import.meta.url)),
+ },
+ },
+ };
+ },
},
});