main
UI Deploy (Next-Auth Support) 🎨 / build-and-deploy (push) Has been cancelled

This commit is contained in:
Harun CAN
2026-04-30 13:25:43 +02:00
parent 1b69eaf219
commit 5144ee4d9a
22 changed files with 989 additions and 1411 deletions
@@ -8,11 +8,6 @@ import {
Link2,
Loader2,
ArrowRight,
Clock,
Palette,
Monitor,
Smartphone,
Square,
Sparkles,
Wand2,
MessageSquare,
@@ -24,31 +19,16 @@ import {
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: "🇪🇸" },
];
import {
LanguageSelector,
StyleSelector,
DurationSelector,
AspectRatioSelector,
} from "@/components/projects/ProjectConfiguration";
export default function XToVideoPage() {
const router = useRouter();
const toast = useToast();
const { toast } = useToast();
const tweetPreview = useTweetPreview();
const createFromTweet = useCreateFromTweet();
@@ -66,15 +46,15 @@ export default function XToVideoPage() {
const handlePreview = async () => {
if (!isValidUrl) {
toast.error("Geçerli bir X/Twitter URL'si girin (https://x.com/...)");
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!");
toast("success", "Tweet başarıyla yüklendi!");
} catch {
toast.error("Tweet yüklenemedi. URL'yi kontrol edin.");
toast("error", "Tweet yüklenemedi. URL'yi kontrol edin.");
}
};
@@ -89,7 +69,7 @@ export default function XToVideoPage() {
cinematicReference: cinematicReference ? cinematicReference : undefined,
targetDuration: duration,
});
toast.success("Tweet → Video projesi oluşturuldu!");
toast("success", "Tweet → Video projesi oluşturuldu!");
const projectId = result?.id;
if (projectId) {
router.push(`/dashboard/projects/${projectId}`);
@@ -97,22 +77,21 @@ export default function XToVideoPage() {
router.push("/dashboard/projects");
}
} catch {
toast.error("Proje oluşturulurken bir hata oluştu.");
toast("error", "Proje oluşturulurken bir hata oluştu.");
}
};
return (
<div className="max-w-3xl mx-auto space-y-6">
<div className="max-w-3xl mx-auto space-y-6 pb-24">
{/* 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 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-2xl md:text-3xl font-bold">
<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-sm text-[var(--color-text-muted)] mt-1">
<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>
@@ -132,7 +111,7 @@ export default function XToVideoPage() {
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"
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}
@@ -235,7 +214,7 @@ export default function XToVideoPage() {
{previewData.tweet?.metrics?.views ?? 0}
</span>
{previewData.tweet?.isThread && (
<span className="flex items-center gap-1 text-violet-400">
<span className="flex items-center gap-1 text-[var(--color-bg-inverted)]">
<MessageSquare size={12} />
{previewData.tweet?.threadTweets?.length ?? 0} tweet thread
</span>
@@ -245,7 +224,7 @@ export default function XToVideoPage() {
{/* Suggested info */}
{previewData.suggestedTitle && (
<div className="flex items-center gap-1.5 text-[11px] text-cyan-400">
<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>
@@ -253,7 +232,7 @@ export default function XToVideoPage() {
{/* 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">
<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>
@@ -270,147 +249,46 @@ export default function XToVideoPage() {
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>
{style === "CINEMATIC" && (
<div className="pt-3 mt-3 border-t border-[var(--color-border-faint)]">
<label className="text-xs font-medium text-[var(--color-text-secondary)] mb-2 block">
Özel Sinematik Referans (Opsiyonel)
</label>
<input
type="text"
placeholder="Örn: Wes Anderson, Matrix, Neon Cyberpunk..."
value={cinematicReference}
onChange={(e) => setCinematicReference(e.target.value)}
className="w-full bg-[var(--color-bg-elevated)] border border-[var(--color-border-faint)] rounded-xl py-2 px-3 text-sm focus:border-violet-500/50 outline-none transition-colors"
/>
</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 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 */}
<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>
<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
@@ -421,9 +299,9 @@ export default function XToVideoPage() {
{/* Info Box */}
{!previewData && (
<div className="card p-5 bg-gradient-to-br from-violet-500/5 to-cyan-500/5">
<div className="card p-5 bg-[var(--color-bg-surface)]">
<div className="flex items-start gap-3">
<Sparkles size={20} className="text-violet-400 shrink-0 mt-0.5" />
<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?