generated from fahricansecer/boilerplate-fe
323 lines
13 KiB
TypeScript
323 lines
13 KiB
TypeScript
"use client";
|
||
|
||
import { useState } from "react";
|
||
import { useRouter } from "next/navigation";
|
||
import { motion, AnimatePresence } from "framer-motion";
|
||
import {
|
||
AtSign,
|
||
Link2,
|
||
Loader2,
|
||
ArrowRight,
|
||
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";
|
||
import {
|
||
LanguageSelector,
|
||
StyleSelector,
|
||
DurationSelector,
|
||
AspectRatioSelector,
|
||
} from "@/components/projects/ProjectConfiguration";
|
||
|
||
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 [cinematicReference, setCinematicReference] = useState("");
|
||
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,
|
||
cinematicReference: cinematicReference ? cinematicReference : undefined,
|
||
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 pb-24">
|
||
{/* Header */}
|
||
<div className="text-center space-y-3 pb-4">
|
||
<div className="inline-flex items-center justify-center w-16 h-16 rounded-3xl bg-[var(--color-bg-inverted)] text-[var(--color-text-inverted)] mb-2 ring-1 ring-[var(--color-bg-inverted)]/20 shadow-none">
|
||
<AtSign size={32} />
|
||
</div>
|
||
<h1 className="font-[family-name:var(--font-display)] text-3xl md:text-4xl font-bold tracking-tight text-[var(--color-text-primary)]">
|
||
Tweet'ten Video Oluştur
|
||
</h1>
|
||
<p className="text-[var(--color-text-muted)] text-sm max-w-lg mx-auto">
|
||
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-[var(--color-bg-inverted)]/40 focus:ring-1 focus:ring-[var(--color-bg-inverted)]/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-[var(--color-bg-inverted)]">
|
||
<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-[var(--color-text-secondary)]">
|
||
<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-[var(--color-text-secondary)]">
|
||
<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"
|
||
>
|
||
<div className="card p-6 space-y-8">
|
||
<LanguageSelector value={language} onChange={setLanguage} />
|
||
|
||
<StyleSelector
|
||
value={style}
|
||
onChange={setStyle}
|
||
cinematicReference={cinematicReference}
|
||
onCinematicReferenceChange={setCinematicReference}
|
||
/>
|
||
|
||
<DurationSelector value={duration} onChange={setDuration} />
|
||
|
||
<AspectRatioSelector value={aspectRatio} onChange={setAspectRatio} />
|
||
</div>
|
||
|
||
{/* Generate Button */}
|
||
<div className="flex justify-end">
|
||
<button
|
||
onClick={handleGenerate}
|
||
disabled={createFromTweet.isPending}
|
||
className={cn(
|
||
"group relative overflow-hidden flex items-center gap-3 px-8 py-4 rounded-2xl font-bold text-[var(--color-text-inverted)] shadow-none transition-all",
|
||
"bg-[var(--color-bg-inverted)] hover:scale-[1.02]",
|
||
"disabled:opacity-50 disabled:hover:scale-100 disabled:cursor-not-allowed"
|
||
)}
|
||
>
|
||
{createFromTweet.isPending ? (
|
||
<>
|
||
<Loader2 size={20} className="animate-spin" />
|
||
<span>Video Projesi Oluşturuluyor...</span>
|
||
</>
|
||
) : (
|
||
<>
|
||
<Wand2 size={20} className="group-hover:rotate-12 transition-transform" />
|
||
<span>Tweet → Video Oluştur</span>
|
||
<ArrowRight size={18} className="group-hover:translate-x-1 transition-transform" />
|
||
</>
|
||
)}
|
||
</button>
|
||
</div>
|
||
|
||
<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-[var(--color-bg-surface)]">
|
||
<div className="flex items-start gap-3">
|
||
<Sparkles size={20} className="text-[var(--color-text-secondary)] 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>
|
||
);
|
||
}
|