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

This commit is contained in:
Harun CAN
2026-04-12 11:44:20 +02:00
parent 804f5b395e
commit 75de91848e
7 changed files with 571 additions and 4 deletions
@@ -0,0 +1,252 @@
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { cn } from "@/lib/utils";
import { useCreateFromDocument } from "@/hooks/use-api";
import { useToast } from "@/components/ui/toast";
import {
FileText,
Loader2,
ArrowRight,
Clock,
Palette,
Monitor,
Smartphone,
Square,
Wand2,
} from "lucide-react";
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 DocumentToVideoPage() {
const router = useRouter();
const { toast } = useToast();
const createFromDocument = useCreateFromDocument();
const [file, setFile] = useState<File | null>(null);
const [style, setStyle] = useState("CINEMATIC");
const [duration, setDuration] = useState(60);
const [aspectRatio, setAspectRatio] = useState("PORTRAIT_9_16");
const [language, setLanguage] = useState("tr");
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files.length > 0) {
setFile(e.target.files[0]);
}
};
const handleGenerate = async () => {
if (!file) {
toast.error("Lütfen bir belge seçin.");
return;
}
try {
const result: any = await createFromDocument.mutateAsync({
file,
language,
aspectRatio,
videoStyle: style,
targetDuration: duration,
});
toast.success("Belge → Video projesi oluşturuldu!");
router.push(`/dashboard/projects/${result.id}`);
} catch (error) {
toast.error("Proje oluşturulurken hata oluştu.");
}
};
return (
<div className="max-w-3xl mx-auto space-y-8 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-blue-500/10 text-blue-500 mb-2 ring-1 ring-blue-500/20 shadow-[0_0_30px_rgba(59,130,246,0.15)]">
<FileText 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)]">
Belgeden Video Üret
</h1>
<p className="text-[var(--color-text-muted)] text-sm max-w-lg mx-auto">
PDF, Word, TXT vb. belgenizi yükleyin, yapay zeka içeriği tarayıp senaryolastirsin.
</p>
</div>
{/* Input */}
<div className="card p-6 md:p-8 space-y-4">
<div>
<label className="text-sm font-medium text-[var(--color-text-secondary)] mb-2 block">
Belge Yükle (PDF, DOCX, TXT)
</label>
<div className="relative">
<input
type="file"
onChange={handleFileChange}
accept=".pdf,.docx,.txt,.csv,.xlsx,.pptx"
className="block w-full text-sm text-[var(--color-text-muted)]
file:mr-4 file:py-3 file:px-4
file:rounded-xl file:border-0
file:text-sm file:font-semibold
file:bg-blue-500/10 file:text-blue-500
hover:file:bg-blue-500/20 cursor-pointer"
/>
</div>
</div>
</div>
{/* Video Settings */}
<div className="space-y-6">
<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-blue-500/12 border border-blue-500/30 text-blue-400"
: "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>
<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-blue-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-blue-500/12 border border-blue-500/30 text-blue-400"
: "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>
<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-blue-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-blue-500
[&::-webkit-slider-thumb]:shadow-[0_0_12px_rgba(59,130,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-blue-500/12 border border-blue-500/30 text-blue-400"
: "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>
<button
onClick={handleGenerate}
disabled={createFromDocument.isPending || !file}
className={cn(
"w-full py-4 rounded-xl font-semibold text-base flex items-center justify-center gap-2 transition-all",
createFromDocument.isPending
? "bg-blue-500/20 text-blue-400 cursor-wait"
: "bg-blue-500 hover:bg-blue-600 text-white shadow-lg shadow-blue-500/20",
!file && "opacity-50 cursor-not-allowed"
)}
>
{createFromDocument.isPending ? (
<>
<Loader2 size={20} className="animate-spin" />
<span>Video Projesi Oluşturuluyor... (Bu işlem uzun sürebilir)</span>
</>
) : (
<>
<Wand2 size={20} />
<span>Belge 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>
</div>
</div>
);
}
@@ -0,0 +1,249 @@
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { motion, AnimatePresence } from "framer-motion";
import {
Link2,
Loader2,
ArrowRight,
Clock,
Palette,
Monitor,
Smartphone,
Square,
Sparkles,
Wand2,
} from "lucide-react";
import { cn } from "@/lib/utils";
import { useCreateFromYoutube } 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 YoutubeToVideoPage() {
const router = useRouter();
const { toast } = useToast();
const createFromYoutube = useCreateFromYoutube();
const [youtubeUrl, setYoutubeUrl] = useState("");
const [style, setStyle] = useState("CINEMATIC");
const [duration, setDuration] = useState(60);
const [aspectRatio, setAspectRatio] = useState("PORTRAIT_9_16");
const [language, setLanguage] = useState("tr");
const handleGenerate = async () => {
if (!youtubeUrl.includes("youtube.com") && !youtubeUrl.includes("youtu.be")) {
toast.error("Lütfen geçerli bir YouTube URL'si girin.");
return;
}
try {
const result: any = await createFromYoutube.mutateAsync({
youtubeUrl,
language,
aspectRatio,
videoStyle: style,
targetDuration: duration,
});
toast.success("YouTube → Video projesi oluşturuldu!");
router.push(`/dashboard/projects/${result.id}`);
} catch (error) {
toast.error("Proje oluşturulurken hata oluştu.");
}
};
return (
<div className="max-w-3xl mx-auto space-y-8 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-red-500/10 text-red-500 mb-2 ring-1 ring-red-500/20 shadow-[0_0_30px_rgba(239,68,68,0.15)]">
<Link2 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)]">
YouTube'dan Video Üret
</h1>
<p className="text-[var(--color-text-muted)] text-sm max-w-lg mx-auto">
YouTube linkini yapıştırın, yapay zeka ana içeriği çıkartıp viral bir Reels/Shorts yaratsın.
</p>
</div>
{/* Input */}
<div className="card p-6 md:p-8 space-y-4">
<div>
<label className="text-sm font-medium text-[var(--color-text-secondary)] mb-2 block">
YouTube URL
</label>
<div className="relative">
<input
type="text"
placeholder="https://www.youtube.com/watch?v=..."
value={youtubeUrl}
onChange={(e) => setYoutubeUrl(e.target.value)}
className="w-full bg-[var(--color-bg-surface)] border-2 border-[var(--color-border-faint)] rounded-2xl py-4 pl-12 pr-4 text-sm
focus:border-red-500/50 focus:ring-4 focus:ring-red-500/10 transition-all outline-none"
/>
<Link2
size={18}
className="absolute left-4 top-1/2 -translate-y-1/2 text-[var(--color-text-ghost)]"
/>
</div>
</div>
</div>
{/* Video Settings */}
<div className="space-y-6">
<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-red-500/12 border border-red-500/30 text-red-400"
: "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>
<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-red-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-red-500/12 border border-red-500/30 text-red-400"
: "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>
<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-red-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-red-500
[&::-webkit-slider-thumb]:shadow-[0_0_12px_rgba(239,68,68,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-red-500/12 border border-red-500/30 text-red-400"
: "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>
<button
onClick={handleGenerate}
disabled={createFromYoutube.isPending || !youtubeUrl}
className={cn(
"w-full py-4 rounded-xl font-semibold text-base flex items-center justify-center gap-2 transition-all",
createFromYoutube.isPending
? "bg-red-500/20 text-red-400 cursor-wait"
: "bg-red-500 hover:bg-red-600 text-white shadow-lg shadow-red-500/20",
!youtubeUrl && "opacity-50 cursor-not-allowed"
)}
>
{createFromYoutube.isPending ? (
<>
<Loader2 size={20} className="animate-spin" />
<span>Video Projesi Oluşturuluyor... (Bu işlem uzun sürebilir)</span>
</>
) : (
<>
<Wand2 size={20} />
<span>YouTube 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>
</div>
</div>
);
}