Files
ContentGen_FE/src/app/[locale]/(dashboard)/dashboard/x-to-video/page.tsx
Harun CAN dd8878d403
Some checks failed
UI Deploy (Next-Auth Support) 🎨 / build-and-deploy (push) Has been cancelled
main
2026-04-05 20:37:03 +03:00

429 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { motion, AnimatePresence } from "framer-motion";
import {
AtSign,
Link2,
Loader2,
ArrowRight,
Clock,
Palette,
Monitor,
Smartphone,
Square,
Sparkles,
Wand2,
MessageSquare,
Heart,
Repeat2,
Eye,
Image as ImageIcon,
} from "lucide-react";
import { cn } from "@/lib/utils";
import { useTweetPreview, useCreateFromTweet } from "@/hooks/use-api";
import { useToast } from "@/components/ui/toast";
const videoStyles = [
{ id: "CINEMATIC", label: "Sinematik", emoji: "🎬" },
{ id: "DOCUMENTARY", label: "Belgesel", emoji: "📹" },
{ id: "EDUCATIONAL", label: "Eğitim", emoji: "📚" },
{ id: "STORYTELLING", label: "Hikâye", emoji: "📖" },
{ id: "NEWS", label: "Haber", emoji: "📰" },
];
const aspectRatios = [
{ id: "PORTRAIT_9_16", label: "9:16", icon: Smartphone, desc: "Shorts / Reels" },
{ id: "SQUARE_1_1", label: "1:1", icon: Square, desc: "Instagram" },
{ id: "LANDSCAPE_16_9", label: "16:9", icon: Monitor, desc: "YouTube" },
];
const languages = [
{ code: "tr", label: "Türkçe", flag: "🇹🇷" },
{ code: "en", label: "English", flag: "🇺🇸" },
{ code: "de", label: "Deutsch", flag: "🇩🇪" },
{ code: "es", label: "Español", flag: "🇪🇸" },
];
export default function XToVideoPage() {
const router = useRouter();
const toast = useToast();
const tweetPreview = useTweetPreview();
const createFromTweet = useCreateFromTweet();
const [tweetUrl, setTweetUrl] = useState("");
const [style, setStyle] = useState("CINEMATIC");
const [duration, setDuration] = useState(60);
const [aspectRatio, setAspectRatio] = useState("PORTRAIT_9_16");
const [language, setLanguage] = useState("tr");
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const [previewData, setPreviewData] = useState<any>(null);
const isValidUrl = /https?:\/\/(x\.com|twitter\.com)\/\w+\/status\/\d+/.test(tweetUrl);
const handlePreview = async () => {
if (!isValidUrl) {
toast.error("Geçerli bir X/Twitter URL'si girin (https://x.com/...)");
return;
}
try {
const result = await tweetPreview.mutateAsync(tweetUrl);
setPreviewData(result);
toast.success("Tweet başarıyla yüklendi!");
} catch {
toast.error("Tweet yüklenemedi. URL'yi kontrol edin.");
}
};
const handleGenerate = async () => {
try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const result: any = await createFromTweet.mutateAsync({
tweetUrl,
language,
aspectRatio,
videoStyle: style,
targetDuration: duration,
});
toast.success("Tweet → Video projesi oluşturuldu!");
const projectId = result?.id;
if (projectId) {
router.push(`/dashboard/projects/${projectId}`);
} else {
router.push("/dashboard/projects");
}
} catch {
toast.error("Proje oluşturulurken bir hata oluştu.");
}
};
return (
<div className="max-w-3xl mx-auto space-y-6">
{/* Header */}
<div>
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-violet-500/10 border border-violet-500/20 text-violet-300 text-xs font-medium mb-3">
<AtSign size={12} />
X Video
</div>
<h1 className="font-[family-name:var(--font-display)] text-2xl md:text-3xl font-bold">
Tweet'ten Video Oluştur
</h1>
<p className="text-sm text-[var(--color-text-muted)] mt-1">
X/Twitter yazılarını AI ile kısa videolara dönüştürün
</p>
</div>
{/* URL Input */}
<div className="card p-5 space-y-4">
<label className="text-sm font-medium text-[var(--color-text-secondary)] block">
<Link2 size={14} className="inline mr-1.5 text-cyan-400" />
Tweet URL
</label>
<div className="flex gap-2">
<input
type="url"
value={tweetUrl}
onChange={(e) => {
setTweetUrl(e.target.value);
setPreviewData(null);
}}
placeholder="https://x.com/username/status/123456..."
className="flex-1 px-4 py-2.5 rounded-xl bg-[var(--color-bg-surface)] border border-[var(--color-border-faint)] text-sm text-[var(--color-text-primary)] placeholder:text-[var(--color-text-ghost)] focus:outline-none focus:border-violet-500/40 focus:ring-1 focus:ring-violet-500/20 transition-all"
/>
<button
onClick={handlePreview}
disabled={!isValidUrl || tweetPreview.isPending}
className={cn(
"px-4 py-2.5 rounded-xl text-sm font-semibold flex items-center gap-2 transition-all shrink-0",
isValidUrl
? "btn-primary"
: "bg-[var(--color-bg-elevated)] text-[var(--color-text-ghost)] cursor-not-allowed",
)}
>
{tweetPreview.isPending ? (
<Loader2 size={16} className="animate-spin" />
) : (
<>
<Eye size={16} />
Önizle
</>
)}
</button>
</div>
<p className="text-[11px] text-[var(--color-text-ghost)]">
Thread desteği: Çoklu tweet zincirleri de otomatik olarak algılanır
</p>
</div>
{/* Tweet Preview */}
<AnimatePresence>
{previewData && (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
className="card p-5 space-y-3"
>
<h3 className="text-sm font-semibold text-[var(--color-text-secondary)]">
<MessageSquare
size={14}
className="inline mr-1.5 text-violet-400"
/>
Tweet Önizleme
</h3>
<div className="p-4 rounded-xl bg-[var(--color-bg-surface)] border border-[var(--color-border-faint)] space-y-3">
{/* Author */}
<div className="flex items-center gap-3">
<div className="w-9 h-9 rounded-full bg-gradient-to-br from-violet-500/20 to-cyan-500/20 flex items-center justify-center text-sm font-bold text-violet-300">
{(previewData.tweet?.author?.name ?? "X")?.[0]}
</div>
<div>
<p className="text-sm font-bold text-[var(--color-text-primary)]">
{previewData.tweet?.author?.name ?? "Kullanıcı"}
</p>
<p className="text-[11px] text-[var(--color-text-ghost)]">
@{previewData.tweet?.author?.username ?? "handle"}
</p>
</div>
</div>
{/* Content */}
<p className="text-sm text-[var(--color-text-secondary)] whitespace-pre-line">
{previewData.tweet?.text ?? ""}
</p>
{/* Images */}
{(previewData.tweet?.media?.length > 0) && (
<div className="flex gap-2 overflow-x-auto">
{(previewData.tweet.media ?? [])
.filter((m: any) => m.type === 'photo')
.map(
(m: any, i: number) => (
<div
key={i}
className="w-20 h-20 rounded-lg bg-[var(--color-bg-elevated)] overflow-hidden shrink-0"
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={m.url}
alt={`Media ${i + 1}`}
className="w-full h-full object-cover"
/>
</div>
),
)}
</div>
)}
{/* Stats */}
<div className="flex items-center gap-4 text-[11px] text-[var(--color-text-ghost)]">
<span className="flex items-center gap-1">
<Heart size={12} />
{previewData.tweet?.metrics?.likes ?? 0}
</span>
<span className="flex items-center gap-1">
<Repeat2 size={12} />
{previewData.tweet?.metrics?.retweets ?? 0}
</span>
<span className="flex items-center gap-1">
<Eye size={12} />
{previewData.tweet?.metrics?.views ?? 0}
</span>
{previewData.tweet?.isThread && (
<span className="flex items-center gap-1 text-violet-400">
<MessageSquare size={12} />
{previewData.tweet?.threadTweets?.length ?? 0} tweet thread
</span>
)}
</div>
</div>
{/* Suggested info */}
{previewData.suggestedTitle && (
<div className="flex items-center gap-1.5 text-[11px] text-cyan-400">
<Sparkles size={12} />
Önerilen başlık: {previewData.suggestedTitle} · Tahmini süre: {previewData.estimatedDuration}sn · Viral skoru: {previewData.viralScore}/100
</div>
)}
{/* Images tag */}
{(previewData.tweet?.media?.filter((m: any) => m.type === 'photo')?.length > 0) && (
<div className="flex items-center gap-1.5 text-[11px] text-cyan-400">
<ImageIcon size={12} />
{previewData.tweet.media.filter((m: any) => m.type === 'photo').length} görsel referans olarak kullanılacak + AI görsel üretilecek
</div>
)}
</motion.div>
)}
</AnimatePresence>
{/* Video Settings */}
<AnimatePresence>
{previewData && (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
className="space-y-6"
>
{/* Language */}
<div className="card p-5 space-y-3">
<label className="text-sm font-medium text-[var(--color-text-secondary)]">
Video Dili
</label>
<div className="flex gap-2">
{languages.map((l) => (
<button
key={l.code}
onClick={() => setLanguage(l.code)}
className={cn(
"flex items-center gap-1.5 px-3 py-2 rounded-xl text-xs transition-all",
language === l.code
? "bg-violet-500/12 border border-violet-500/30 text-violet-300"
: "bg-[var(--color-bg-surface)] border border-[var(--color-border-faint)] text-[var(--color-text-muted)]",
)}
>
<span>{l.flag}</span>
{l.label}
</button>
))}
</div>
</div>
{/* Style */}
<div className="card p-5 space-y-3">
<label className="text-sm font-medium text-[var(--color-text-secondary)]">
<Palette size={14} className="inline mr-1.5 text-violet-400" />
Video Stili
</label>
<div className="flex flex-wrap gap-2">
{videoStyles.map((s) => (
<button
key={s.id}
onClick={() => setStyle(s.id)}
className={cn(
"flex items-center gap-1.5 px-3 py-2 rounded-xl text-xs transition-all",
style === s.id
? "bg-violet-500/12 border border-violet-500/30 text-violet-300"
: "bg-[var(--color-bg-surface)] border border-[var(--color-border-faint)] text-[var(--color-text-muted)]",
)}
>
<span>{s.emoji}</span>
{s.label}
</button>
))}
</div>
</div>
{/* Duration + Aspect Ratio */}
<div className="card p-5 space-y-4">
<div>
<label className="text-sm font-medium text-[var(--color-text-secondary)] mb-3 block">
<Clock size={14} className="inline mr-1.5 text-cyan-400" />
Hedef Süre: <span className="text-violet-400">{duration}s</span>
</label>
<input
type="range"
min={15}
max={120}
step={5}
value={duration}
onChange={(e) => setDuration(Number(e.target.value))}
className="w-full h-1.5 rounded-full bg-[var(--color-bg-elevated)] appearance-none cursor-pointer
[&::-webkit-slider-thumb]:appearance-none
[&::-webkit-slider-thumb]:w-5 [&::-webkit-slider-thumb]:h-5
[&::-webkit-slider-thumb]:rounded-full
[&::-webkit-slider-thumb]:bg-violet-500
[&::-webkit-slider-thumb]:shadow-[0_0_12px_rgba(139,92,246,0.4)]
[&::-webkit-slider-thumb]:cursor-grab"
/>
</div>
<div>
<label className="text-sm font-medium text-[var(--color-text-secondary)] mb-3 block">
En-Boy Oranı
</label>
<div className="flex gap-2">
{aspectRatios.map((ar) => {
const Icon = ar.icon;
return (
<button
key={ar.id}
onClick={() => setAspectRatio(ar.id)}
className={cn(
"flex-1 flex flex-col items-center gap-1.5 py-3 rounded-xl text-xs transition-all",
aspectRatio === ar.id
? "bg-violet-500/12 border border-violet-500/30 text-violet-300"
: "bg-[var(--color-bg-surface)] border border-[var(--color-border-faint)] text-[var(--color-text-muted)]",
)}
>
<Icon size={20} />
<span className="font-semibold">{ar.label}</span>
<span className="text-[10px] text-[var(--color-text-ghost)]">
{ar.desc}
</span>
</button>
);
})}
</div>
</div>
</div>
{/* Generate Button */}
<button
onClick={handleGenerate}
disabled={createFromTweet.isPending}
className={cn(
"w-full py-4 rounded-xl font-semibold text-base flex items-center justify-center gap-2 transition-all",
createFromTweet.isPending
? "bg-violet-500/20 text-violet-300 cursor-wait"
: "btn-primary text-lg",
)}
>
{createFromTweet.isPending ? (
<>
<Loader2 size={20} className="animate-spin" />
<span>Video Projesi Oluşturuluyor...</span>
</>
) : (
<>
<Wand2 size={20} />
<span>Tweet → Video Oluştur</span>
<ArrowRight size={16} />
</>
)}
</button>
<p className="text-center text-[11px] text-[var(--color-text-ghost)]">
Bu işlem 1 kredi kullanır • AI senaryo + görsel üretim dahil
</p>
</motion.div>
)}
</AnimatePresence>
{/* Info Box */}
{!previewData && (
<div className="card p-5 bg-gradient-to-br from-violet-500/5 to-cyan-500/5">
<div className="flex items-start gap-3">
<Sparkles size={20} className="text-violet-400 shrink-0 mt-0.5" />
<div className="space-y-2">
<h3 className="text-sm font-semibold text-[var(--color-text-primary)]">
Nasıl Çalışır?
</h3>
<ol className="text-xs text-[var(--color-text-muted)] space-y-1.5 list-decimal list-inside">
<li>X/Twitter URL'sini yapıştırın ve "Önizle" butonuna tıklayın</li>
<li>Tweet içeriği otomatik olarak çekilir (thread desteği dahil)</li>
<li>Video stilini, süresini ve dilini seçin</li>
<li>AI otomatik olarak senaryo yazar ve görseller üretir</li>
<li>Video render edilir ve indirilmeye hazır hale gelir</li>
</ol>
</div>
</div>
</div>
)}
</div>
);
}