summaryrefslogtreecommitdiff
path: root/src/components
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 /src/components
parentee2352b5268a1f33c4db72237a7c5171f0c1efbc (diff)
m
Diffstat (limited to 'src/components')
-rw-r--r--src/components/Main.tsx227
-rw-r--r--src/components/Profile.tsx36
-rw-r--r--src/components/ui/textarea.tsx18
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 }