summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorpolwex <polwex@sortug.com>2025-05-15 12:17:54 +0700
committerpolwex <polwex@sortug.com>2025-05-15 12:17:54 +0700
commit1ae274a658d0a705b698a8873c286ec73403b1a6 (patch)
tree12d4d77404a3b3862fbc949a581fe598a0d8c152
parentee2352b5268a1f33c4db72237a7c5171f0c1efbc (diff)
m
-rw-r--r--components.json21
-rw-r--r--src/actions/lang.ts12
-rw-r--r--src/actions/login.ts19
-rw-r--r--src/components/Main.tsx227
-rw-r--r--src/components/Profile.tsx36
-rw-r--r--src/components/ui/textarea.tsx18
-rw-r--r--src/pages/index.tsx18
-rw-r--r--src/pages/login.tsx14
-rw-r--r--src/styles/globals.css124
9 files changed, 461 insertions, 28 deletions
diff --git a/components.json b/components.json
new file mode 100644
index 0000000..285033d
--- /dev/null
+++ b/components.json
@@ -0,0 +1,21 @@
+{
+ "$schema": "https://ui.shadcn.com/schema.json",
+ "style": "new-york",
+ "rsc": false,
+ "tsx": true,
+ "tailwind": {
+ "config": "",
+ "css": "src/styles/globals.css",
+ "baseColor": "neutral",
+ "cssVariables": true,
+ "prefix": ""
+ },
+ "aliases": {
+ "components": "@/components",
+ "utils": "@/lib/utils",
+ "ui": "@/components/ui",
+ "lib": "@/lib",
+ "hooks": "@/hooks"
+ },
+ "iconLibrary": "lucide"
+}
diff --git a/src/actions/lang.ts b/src/actions/lang.ts
new file mode 100644
index 0000000..d54a220
--- /dev/null
+++ b/src/actions/lang.ts
@@ -0,0 +1,12 @@
+"use server";
+import { AsyncRes } from "@/lib/types";
+import db from "../lib/db";
+
+export async function spacy(text: string, lang: string): AsyncRes<any> {
+ const res = await call(formData, true);
+ console.log("reg res", res);
+ if ("error" in res) return { error: "Something went wrong" };
+ else {
+ return { success: true };
+ }
+}
diff --git a/src/actions/login.ts b/src/actions/login.ts
index 7ca9d77..4ceb13f 100644
--- a/src/actions/login.ts
+++ b/src/actions/login.ts
@@ -38,13 +38,14 @@ 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 prevState;
- // return { success: true };
- }
+ 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 };
+ // }
}
async function setCookie(userId: number) {
const COOKIE_EXPIRY = Date.now() + 1000 * 60 * 60 * 24 * 30;
@@ -58,8 +59,8 @@ async function setCookie(userId: number) {
// unstable_redirect("/");
}
-async function postLogout(prev: any) {
+export async function postLogout(prev: number) {
const { delCookie } = cookies();
const rest = delCookie("sorlang");
- return prev;
+ return prev + 9;
}
diff --git a/src/components/Main.tsx b/src/components/Main.tsx
new file mode 100644
index 0000000..ee8dbab
--- /dev/null
+++ b/src/components/Main.tsx
@@ -0,0 +1,227 @@
+// 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
+
+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 [isExtracting, setIsExtracting] = useState<boolean>(false);
+ const [extractedTextResult, setExtractedTextResult] = useState<string | null>(
+ null,
+ );
+
+ 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: React.ClipboardEvent<HTMLTextAreaElement>) => {
+ const items = event.clipboardData?.items;
+ console.log({ items });
+ if (!items) return;
+
+ let imageFound = false;
+ for (let i = 0; i < items.length; i++) {
+ const item = items[i];
+ if (!item) return;
+ 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);
+ setTextValue(""); // Clear textarea when image is pasted, or decide on desired behavior
+ setExtractedTextResult(null); // Clear previous extraction results
+ 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],
+ );
+
+ const handleProcessText = () => {
+ if (!textValue.trim()) {
+ alert("Text area is empty!");
+ return;
+ }
+ console.log("Processing text:", textValue);
+ // Add your text processing logic here
+ alert(
+ `Text submitted: "${textValue.substring(0, 50)}${textValue.length > 50 ? "..." : ""}"`,
+ );
+ };
+
+ const [isPending, startTransition] = useTransition();
+ const onClick = () => {
+ startTransition(async() => {
+ const
+ })
+ }
+ const handleExtractTextFromImage = async () => {
+ if (!pastedImageFile) {
+ alert("No image to extract text from!");
+ return;
+ }
+ setIsExtracting(true);
+ setExtractedTextResult(null);
+ console.log("Extracting text from image:", pastedImageFile.name);
+
+ // --- SIMULATE OCR API CALL ---
+ // In a real app, you would send `pastedImageFile` to a backend
+ // or use a client-side OCR library like Tesseract.js
+ await new Promise((resolve) => setTimeout(resolve, 2000)); // Simulate network delay
+
+ // Example: Simulate successful extraction
+ const mockExtractedText = `This is simulated extracted text from "${pastedImageFile.name}".\nIt could be multiple lines.`;
+
+ // Example: Simulate an error
+ // const mockExtractedText = null;
+ // alert("Failed to extract text (simulated).");
+
+ if (mockExtractedText) {
+ setTextValue(mockExtractedText); // Put extracted text into the textarea
+ setExtractedTextResult(
+ `Successfully extracted text and placed it in the textarea.`,
+ );
+ } else {
+ setExtractedTextResult("Failed to extract text (simulated).");
+ }
+ // --- END SIMULATION ---
+
+ setIsExtracting(false);
+ // Optionally clear the image after attempting extraction
+ // setPastedImageUrl(null);
+ // setPastedImageFile(null);
+ };
+
+ const handleClearImage = () => {
+ if (pastedImageUrl) {
+ URL.revokeObjectURL(pastedImageUrl);
+ }
+ setPastedImageUrl(null);
+ setPastedImageFile(null);
+ setExtractedTextResult(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)}
+ onPaste={handlePaste}
+ placeholder="Paste text here, or paste an image..."
+ className="min-h-[200px] 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>
+ )}
+
+ {extractedTextResult && (
+ <p
+ className={`mt-2 text-sm ${extractedTextResult.startsWith("Failed") ? "text-destructive" : "text-green-600"}`}
+ >
+ {extractedTextResult}
+ </p>
+ )}
+ </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">
+ 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/Profile.tsx b/src/components/Profile.tsx
new file mode 100644
index 0000000..2ae73e9
--- /dev/null
+++ b/src/components/Profile.tsx
@@ -0,0 +1,36 @@
+"use client";
+
+import { postLogout } from "@/actions/login";
+import { Button } from "@/components/ui/button";
+import {
+ Card,
+ CardHeader,
+ CardDescription,
+ CardContent,
+ CardFooter,
+ CardTitle,
+} from "@/components/ui/card";
+import { Label } from "@/components/ui/label";
+import { Input } from "@/components/ui/input";
+import { useActionState } from "react";
+
+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>
+ );
+}
diff --git a/src/components/ui/textarea.tsx b/src/components/ui/textarea.tsx
new file mode 100644
index 0000000..7f21b5e
--- /dev/null
+++ b/src/components/ui/textarea.tsx
@@ -0,0 +1,18 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
+ return (
+ <textarea
+ data-slot="textarea"
+ className={cn(
+ "border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+export { Textarea }
diff --git a/src/pages/index.tsx b/src/pages/index.tsx
index c008c4d..82ffd99 100644
--- a/src/pages/index.tsx
+++ b/src/pages/index.tsx
@@ -1,21 +1,13 @@
import { Link } from "waku";
import { Counter } from "../components/counter";
+import { getContextData } from "waku/middleware/context";
+import Main from "../components/Main";
export default async function HomePage() {
- const data = await getData();
+ const { user } = getContextData();
- return (
- <div>
- <title>{data.title}</title>
- <h1 className="text-4xl font-bold tracking-tight">{data.headline}</h1>
- <p>{data.body}</p>
- <Counter />
- <Link to="/about" className="mt-4 inline-block underline">
- About page
- </Link>
- </div>
- );
+ return <Main />;
}
const getData = async () => {
@@ -30,6 +22,6 @@ const getData = async () => {
export const getConfig = async () => {
return {
- render: "static",
+ render: "dynamic",
} as const;
};
diff --git a/src/pages/login.tsx b/src/pages/login.tsx
index 2c9f643..13d3bd4 100644
--- a/src/pages/login.tsx
+++ b/src/pages/login.tsx
@@ -1,4 +1,5 @@
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";
@@ -7,12 +8,13 @@ export default async function AuthPage() {
const ctx = getContextData();
console.log({ ctx });
const data = await getData();
-
- return (
- <div>
- <AuthScreen />
- </div>
- );
+ if (ctx.user) return <ProfileScreen user={ctx.user as any} />;
+ else
+ return (
+ <div>
+ <AuthScreen />
+ </div>
+ );
}
const getData = async () => {
diff --git a/src/styles/globals.css b/src/styles/globals.css
new file mode 100644
index 0000000..4bc2c75
--- /dev/null
+++ b/src/styles/globals.css
@@ -0,0 +1,124 @@
+@import "tailwindcss";
+@import "tw-animate-css";
+
+@custom-variant dark (&:is(.dark *));
+
+:root {
+ --background: oklch(1 0 0);
+ --foreground: oklch(0.145 0 0);
+ --card: oklch(1 0 0);
+ --card-foreground: oklch(0.145 0 0);
+ --popover: oklch(1 0 0);
+ --popover-foreground: oklch(0.145 0 0);
+ --primary: oklch(0.205 0 0);
+ --primary-foreground: oklch(0.985 0 0);
+ --secondary: oklch(0.97 0 0);
+ --secondary-foreground: oklch(0.205 0 0);
+ --muted: oklch(0.97 0 0);
+ --muted-foreground: oklch(0.556 0 0);
+ --accent: oklch(0.97 0 0);
+ --accent-foreground: oklch(0.205 0 0);
+ --destructive: oklch(0.577 0.245 27.325);
+ --destructive-foreground: oklch(0.577 0.245 27.325);
+ --border: oklch(0.922 0 0);
+ --input: oklch(0.922 0 0);
+ --ring: oklch(0.708 0 0);
+ --chart-1: oklch(0.646 0.222 41.116);
+ --chart-2: oklch(0.6 0.118 184.704);
+ --chart-3: oklch(0.398 0.07 227.392);
+ --chart-4: oklch(0.828 0.189 84.429);
+ --chart-5: oklch(0.769 0.188 70.08);
+ --radius: 0.625rem;
+ --sidebar: oklch(0.985 0 0);
+ --sidebar-foreground: oklch(0.145 0 0);
+ --sidebar-primary: oklch(0.205 0 0);
+ --sidebar-primary-foreground: oklch(0.985 0 0);
+ --sidebar-accent: oklch(0.97 0 0);
+ --sidebar-accent-foreground: oklch(0.205 0 0);
+ --sidebar-border: oklch(0.922 0 0);
+ --sidebar-ring: oklch(0.708 0 0);
+}
+
+.dark {
+ --background: oklch(0.145 0 0);
+ --foreground: oklch(0.985 0 0);
+ --card: oklch(0.145 0 0);
+ --card-foreground: oklch(0.985 0 0);
+ --popover: oklch(0.145 0 0);
+ --popover-foreground: oklch(0.985 0 0);
+ --primary: oklch(0.985 0 0);
+ --primary-foreground: oklch(0.205 0 0);
+ --secondary: oklch(0.269 0 0);
+ --secondary-foreground: oklch(0.985 0 0);
+ --muted: oklch(0.269 0 0);
+ --muted-foreground: oklch(0.708 0 0);
+ --accent: oklch(0.269 0 0);
+ --accent-foreground: oklch(0.985 0 0);
+ --destructive: oklch(0.396 0.141 25.723);
+ --destructive-foreground: oklch(0.637 0.237 25.331);
+ --border: oklch(0.269 0 0);
+ --input: oklch(0.269 0 0);
+ --ring: oklch(0.439 0 0);
+ --chart-1: oklch(0.488 0.243 264.376);
+ --chart-2: oklch(0.696 0.17 162.48);
+ --chart-3: oklch(0.769 0.188 70.08);
+ --chart-4: oklch(0.627 0.265 303.9);
+ --chart-5: oklch(0.645 0.246 16.439);
+ --sidebar: oklch(0.205 0 0);
+ --sidebar-foreground: oklch(0.985 0 0);
+ --sidebar-primary: oklch(0.488 0.243 264.376);
+ --sidebar-primary-foreground: oklch(0.985 0 0);
+ --sidebar-accent: oklch(0.269 0 0);
+ --sidebar-accent-foreground: oklch(0.985 0 0);
+ --sidebar-border: oklch(0.269 0 0);
+ --sidebar-ring: oklch(0.439 0 0);
+}
+
+@theme inline {
+ --color-background: var(--background);
+ --color-foreground: var(--foreground);
+ --color-card: var(--card);
+ --color-card-foreground: var(--card-foreground);
+ --color-popover: var(--popover);
+ --color-popover-foreground: var(--popover-foreground);
+ --color-primary: var(--primary);
+ --color-primary-foreground: var(--primary-foreground);
+ --color-secondary: var(--secondary);
+ --color-secondary-foreground: var(--secondary-foreground);
+ --color-muted: var(--muted);
+ --color-muted-foreground: var(--muted-foreground);
+ --color-accent: var(--accent);
+ --color-accent-foreground: var(--accent-foreground);
+ --color-destructive: var(--destructive);
+ --color-destructive-foreground: var(--destructive-foreground);
+ --color-border: var(--border);
+ --color-input: var(--input);
+ --color-ring: var(--ring);
+ --color-chart-1: var(--chart-1);
+ --color-chart-2: var(--chart-2);
+ --color-chart-3: var(--chart-3);
+ --color-chart-4: var(--chart-4);
+ --color-chart-5: var(--chart-5);
+ --radius-sm: calc(var(--radius) - 4px);
+ --radius-md: calc(var(--radius) - 2px);
+ --radius-lg: var(--radius);
+ --radius-xl: calc(var(--radius) + 4px);
+ --color-sidebar: var(--sidebar);
+ --color-sidebar-foreground: var(--sidebar-foreground);
+ --color-sidebar-primary: var(--sidebar-primary);
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
+ --color-sidebar-accent: var(--sidebar-accent);
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
+ --color-sidebar-border: var(--sidebar-border);
+ --color-sidebar-ring: var(--sidebar-ring);
+}
+
+@layer base {
+ * {
+ @apply border-border outline-ring/50;
+ }
+
+ body {
+ @apply bg-background text-foreground;
+ }
+} \ No newline at end of file