Files
office_translator/office-translator-landing-page/components/translation-card.tsx
Sepehr Ramezani 26bd096a06 feat: production deployment - full update with providers, admin, glossaries, pricing, tests
Major changes across backend, frontend, infrastructure:
- Provider system with model selection (Google, DeepL, OpenAI, Ollama, Google Cloud)
- Admin panel: user management, pricing, settings
- Glossary system with CSV import/export
- Subscription and tier quota management
- Security hardening (rate limiting, API key auth, path traversal fixes)
- Docker compose for dev, prod, and IONOS deployment
- Alembic migrations for new tables
- Frontend: dashboard, pricing page, landing page, i18n (en/fr)
- Test suite and verification scripts

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-25 15:01:47 +02:00

331 lines
11 KiB
TypeScript

"use client"
import { useState, useCallback } from "react"
import {
Upload,
ArrowRight,
FileCheck,
X,
ShieldCheck,
Clock,
Loader2,
} from "lucide-react"
import { Button } from "@/components/ui/button"
import {
Card,
CardContent,
} from "@/components/ui/card"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { Progress } from "@/components/ui/progress"
import { cn } from "@/lib/utils"
const LANGUAGES = [
"English",
"Spanish",
"French",
"German",
"Italian",
"Portuguese",
"Chinese (Simplified)",
"Chinese (Traditional)",
"Japanese",
"Korean",
"Arabic",
"Russian",
"Dutch",
"Swedish",
"Turkish",
]
type Engine = "classic" | "pro"
export function TranslationCard() {
const [file, setFile] = useState<File | null>(null)
const [isDragOver, setIsDragOver] = useState(false)
const [sourceLang, setSourceLang] = useState("")
const [targetLang, setTargetLang] = useState("")
const [engine, setEngine] = useState<Engine>("classic")
const [isProcessing, setIsProcessing] = useState(false)
const [progress, setProgress] = useState(0)
const [currentStep, setCurrentStep] = useState("")
const acceptedTypes = [
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
]
const handleDragOver = useCallback((e: React.DragEvent) => {
e.preventDefault()
setIsDragOver(true)
}, [])
const handleDragLeave = useCallback((e: React.DragEvent) => {
e.preventDefault()
setIsDragOver(false)
}, [])
const handleDrop = useCallback((e: React.DragEvent) => {
e.preventDefault()
setIsDragOver(false)
const droppedFile = e.dataTransfer.files[0]
if (droppedFile) {
setFile(droppedFile)
}
}, [])
const handleFileSelect = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
const selected = e.target.files?.[0]
if (selected) {
setFile(selected)
}
}, [])
const removeFile = useCallback(() => {
setFile(null)
setIsProcessing(false)
setProgress(0)
setCurrentStep("")
}, [])
const handleTranslate = useCallback(() => {
if (!file || !sourceLang || !targetLang) return
setIsProcessing(true)
setProgress(0)
const steps = [
"Parsing document structure...",
"Extracting translatable content...",
"Processing... 3/15 slides",
"Processing... 7/15 slides",
"Processing... 11/15 slides",
"Processing... 14/15 slides",
"Rebuilding document formatting...",
"Finalizing output...",
]
let stepIndex = 0
setCurrentStep(steps[0])
const interval = setInterval(() => {
stepIndex++
if (stepIndex < steps.length) {
setCurrentStep(steps[stepIndex])
setProgress(Math.round(((stepIndex + 1) / steps.length) * 100))
} else {
clearInterval(interval)
setCurrentStep("Complete!")
setProgress(100)
setTimeout(() => {
setIsProcessing(false)
setProgress(0)
setCurrentStep("")
}, 2000)
}
}, 1200)
}, [file, sourceLang, targetLang])
const fileExtension = file?.name.split(".").pop()?.toLowerCase()
return (
<section className="flex flex-col items-center gap-5 px-6 pb-16">
<Card className="w-full max-w-xl border-border/70 shadow-lg">
<CardContent className="flex flex-col gap-5 pt-6">
{/* Drop Zone */}
<div
className={cn(
"relative flex flex-col items-center justify-center gap-3 rounded-lg border-2 border-dashed px-6 py-10 transition-colors",
isDragOver
? "border-accent bg-accent/5"
: file
? "border-success/40 bg-success/5"
: "border-border bg-muted/30 hover:border-muted-foreground/30"
)}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
>
{file ? (
<div className="flex items-center gap-3">
<div className="flex size-10 items-center justify-center rounded-lg bg-secondary">
<FileCheck className="size-5 text-foreground" />
</div>
<div className="flex flex-col">
<span className="text-sm font-medium text-foreground">
{file.name}
</span>
<span className="text-xs text-muted-foreground">
{(file.size / 1024).toFixed(1)} KB &middot; .{fileExtension}
</span>
</div>
<Button
variant="ghost"
size="icon-sm"
className="ml-2 text-muted-foreground hover:text-foreground"
onClick={removeFile}
aria-label="Remove file"
>
<X className="size-4" />
</Button>
</div>
) : (
<>
<div className="flex size-12 items-center justify-center rounded-xl bg-secondary">
<Upload className="size-5 text-muted-foreground" />
</div>
<div className="flex flex-col items-center gap-1">
<p className="text-sm font-medium text-foreground">
Drag & drop your .xlsx, .docx, or .pptx file here
</p>
<p className="text-xs text-muted-foreground">
or click to browse
</p>
</div>
<input
type="file"
accept={acceptedTypes.join(",")}
className="absolute inset-0 cursor-pointer opacity-0"
onChange={handleFileSelect}
aria-label="Upload file"
/>
</>
)}
</div>
{/* Language Selectors */}
<div className="flex items-center gap-3">
<div className="flex flex-1 flex-col gap-1.5">
<label className="text-xs font-medium text-muted-foreground">
Source Language
</label>
<Select value={sourceLang} onValueChange={setSourceLang}>
<SelectTrigger className="w-full">
<SelectValue placeholder="Detect / Select" />
</SelectTrigger>
<SelectContent>
{LANGUAGES.map((lang) => (
<SelectItem key={lang} value={lang}>
{lang}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<ArrowRight className="mt-5 size-4 shrink-0 text-muted-foreground" />
<div className="flex flex-1 flex-col gap-1.5">
<label className="text-xs font-medium text-muted-foreground">
Target Language
</label>
<Select value={targetLang} onValueChange={setTargetLang}>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select language" />
</SelectTrigger>
<SelectContent>
{LANGUAGES.map((lang) => (
<SelectItem key={lang} value={lang}>
{lang}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
{/* Engine Toggle */}
<div className="flex flex-col gap-1.5">
<label className="text-xs font-medium text-muted-foreground">
Translation Engine
</label>
<div className="flex rounded-lg border border-border bg-muted p-1">
<button
type="button"
className={cn(
"flex-1 rounded-md px-4 py-2 text-sm font-medium transition-all",
engine === "classic"
? "bg-card text-foreground shadow-sm"
: "text-muted-foreground hover:text-foreground"
)}
onClick={() => setEngine("classic")}
>
Classic
<span className="ml-1.5 text-xs text-muted-foreground">
Fast & Free
</span>
</button>
<button
type="button"
className={cn(
"flex-1 rounded-md px-4 py-2 text-sm font-medium transition-all",
engine === "pro"
? "bg-card text-foreground shadow-sm"
: "text-muted-foreground hover:text-foreground"
)}
onClick={() => setEngine("pro")}
>
Pro LLM
<span className="ml-1.5 text-xs text-muted-foreground">
Context-Aware
</span>
</button>
</div>
</div>
{/* Translate Button */}
<Button
size="lg"
className="w-full text-sm font-semibold"
disabled={!file || !sourceLang || !targetLang || isProcessing}
onClick={handleTranslate}
>
{isProcessing ? (
<>
<Loader2 className="size-4 animate-spin" />
Translating...
</>
) : (
"Translate Document"
)}
</Button>
{/* Progress Bar */}
{isProcessing && (
<div className="flex flex-col gap-2.5 rounded-lg border border-border bg-muted/50 px-4 py-3">
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-foreground">
{currentStep}
</span>
<span className="text-xs tabular-nums text-muted-foreground">
{progress}%
</span>
</div>
<Progress value={progress} className="h-1.5" />
</div>
)}
</CardContent>
</Card>
{/* Trust Badge */}
<div className="flex items-center gap-4 rounded-full border border-border bg-card px-5 py-2.5 shadow-sm">
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
<ShieldCheck className="size-3.5" />
<span className="font-medium">Privacy First:</span>
<span>Zero Data Retention</span>
</div>
<div className="h-3 w-px bg-border" />
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
<Clock className="size-3.5" />
<span>Files deleted after 60 min</span>
</div>
</div>
</section>
)
}