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 /src/components | |
parent | ee2352b5268a1f33c4db72237a7c5171f0c1efbc (diff) |
m
Diffstat (limited to 'src/components')
-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 |
3 files changed, 281 insertions, 0 deletions
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 } |