diff options
author | polwex <polwex@sortug.com> | 2025-05-15 12:17:54 +0700 |
---|---|---|
committer | polwex <polwex@sortug.com> | 2025-05-15 12:17:54 +0700 |
commit | 1ae274a658d0a705b698a8873c286ec73403b1a6 (patch) | |
tree | 12d4d77404a3b3862fbc949a581fe598a0d8c152 | |
parent | ee2352b5268a1f33c4db72237a7c5171f0c1efbc (diff) |
m
-rw-r--r-- | components.json | 21 | ||||
-rw-r--r-- | src/actions/lang.ts | 12 | ||||
-rw-r--r-- | src/actions/login.ts | 19 | ||||
-rw-r--r-- | src/components/Main.tsx | 227 | ||||
-rw-r--r-- | src/components/Profile.tsx | 36 | ||||
-rw-r--r-- | src/components/ui/textarea.tsx | 18 | ||||
-rw-r--r-- | src/pages/index.tsx | 18 | ||||
-rw-r--r-- | src/pages/login.tsx | 14 | ||||
-rw-r--r-- | src/styles/globals.css | 124 |
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 |