generated from fahricansecer/boilerplate-fe
This commit is contained in:
Vendored
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
/// <reference types="next" />
|
/// <reference types="next" />
|
||||||
/// <reference types="next/image-types/global" />
|
/// <reference types="next/image-types/global" />
|
||||||
import "./.next/dev/types/routes.d.ts";
|
import "./.next/types/routes.d.ts";
|
||||||
|
|
||||||
// NOTE: This file should not be edited
|
// NOTE: This file should not be edited
|
||||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
import { Home, FolderOpen, LayoutGrid, Settings, Sparkles, AtSign, ShieldCheck, LogOut } from "lucide-react";
|
import { Home, FolderOpen, LayoutGrid, Settings, Sparkles, AtSign, ShieldCheck, LogOut, Link2, FileText } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { useCreditBalance, useCurrentUser } from "@/hooks/use-api";
|
import { useCreditBalance, useCurrentUser } from "@/hooks/use-api";
|
||||||
@@ -13,6 +13,8 @@ const navItems = [
|
|||||||
{ href: "/dashboard", icon: Home, label: "Ana Sayfa" },
|
{ href: "/dashboard", icon: Home, label: "Ana Sayfa" },
|
||||||
{ href: "/dashboard/projects", icon: FolderOpen, label: "Projeler" },
|
{ href: "/dashboard/projects", icon: FolderOpen, label: "Projeler" },
|
||||||
{ href: "/dashboard/x-to-video", icon: AtSign, label: "X → Video" },
|
{ href: "/dashboard/x-to-video", icon: AtSign, label: "X → Video" },
|
||||||
|
{ href: "/dashboard/youtube-to-video", icon: Link2, label: "YT → Video" },
|
||||||
|
{ href: "/dashboard/document-to-video", icon: FileText, label: "Belge → Video" },
|
||||||
{ href: "/dashboard/templates", icon: LayoutGrid, label: "Şablonlar" },
|
{ href: "/dashboard/templates", icon: LayoutGrid, label: "Şablonlar" },
|
||||||
{ href: "/dashboard/settings", icon: Settings, label: "Ayarlar" },
|
{ href: "/dashboard/settings", icon: Settings, label: "Ayarlar" },
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -242,7 +242,7 @@ export function SceneCard({
|
|||||||
<div className="flex items-center gap-2 mt-1">
|
<div className="flex items-center gap-2 mt-1">
|
||||||
<button
|
<button
|
||||||
onClick={() => onGenerateImage?.(scene.id, scene.visualPrompt)}
|
onClick={() => onGenerateImage?.(scene.id, scene.visualPrompt)}
|
||||||
disabled={isRendering || isGeneratingImage || isUpscalingImage}
|
disabled={isGeneratingImage || isUpscalingImage}
|
||||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-emerald-500/15 text-emerald-400 text-xs font-medium hover:bg-emerald-500/25 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-emerald-500/15 text-emerald-400 text-xs font-medium hover:bg-emerald-500/25 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
{isGeneratingImage ? <RefreshCw size={13} className="animate-spin" /> : <ImageIcon size={13} />}
|
{isGeneratingImage ? <RefreshCw size={13} className="animate-spin" /> : <ImageIcon size={13} />}
|
||||||
@@ -251,7 +251,7 @@ export function SceneCard({
|
|||||||
{thumbnailAsset?.url && (
|
{thumbnailAsset?.url && (
|
||||||
<button
|
<button
|
||||||
onClick={() => onUpscaleImage?.(scene.id)}
|
onClick={() => onUpscaleImage?.(scene.id)}
|
||||||
disabled={isRendering || isUpscalingImage || isGeneratingImage}
|
disabled={isUpscalingImage || isGeneratingImage}
|
||||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-orange-500/15 text-orange-400 text-xs font-medium hover:bg-orange-500/25 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-orange-500/15 text-orange-400 text-xs font-medium hover:bg-orange-500/25 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
{isUpscalingImage ? <RefreshCw size={13} className="animate-spin" /> : <Wand2 size={13} />}
|
{isUpscalingImage ? <RefreshCw size={13} className="animate-spin" /> : <Wand2 size={13} />}
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ import {
|
|||||||
type Project,
|
type Project,
|
||||||
type CreateProjectPayload,
|
type CreateProjectPayload,
|
||||||
type CreateFromTweetPayload,
|
type CreateFromTweetPayload,
|
||||||
|
type CreateFromYoutubePayload,
|
||||||
|
type CreateFromDocumentPayload,
|
||||||
|
type Template,
|
||||||
type PaginatedResponse,
|
type PaginatedResponse,
|
||||||
} from '@/lib/api/api-service';
|
} from '@/lib/api/api-service';
|
||||||
|
|
||||||
@@ -351,6 +354,30 @@ export function useCreateFromTweet() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** YouTube linkinden proje oluştur */
|
||||||
|
export function useCreateFromYoutube() {
|
||||||
|
const qc = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (data: CreateFromYoutubePayload) => projectsApi.createFromYoutube(data),
|
||||||
|
onSuccess: () => {
|
||||||
|
qc.invalidateQueries({ queryKey: queryKeys.projects.all });
|
||||||
|
qc.invalidateQueries({ queryKey: queryKeys.dashboard.stats });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Dokümandan proje oluştur */
|
||||||
|
export function useCreateFromDocument() {
|
||||||
|
const qc = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (data: CreateFromDocumentPayload) => projectsApi.createFromDocument(data),
|
||||||
|
onSuccess: () => {
|
||||||
|
qc.invalidateQueries({ queryKey: queryKeys.projects.all });
|
||||||
|
qc.invalidateQueries({ queryKey: queryKeys.dashboard.stats });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════
|
||||||
// NOTIFICATIONS — Bildirim hook'ları
|
// NOTIFICATIONS — Bildirim hook'ları
|
||||||
// ═══════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════
|
||||||
|
|||||||
@@ -248,6 +248,24 @@ export interface CreateFromTweetPayload {
|
|||||||
targetDuration?: number;
|
targetDuration?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CreateFromYoutubePayload {
|
||||||
|
youtubeUrl: string;
|
||||||
|
title?: string;
|
||||||
|
language?: string;
|
||||||
|
aspectRatio?: string;
|
||||||
|
videoStyle?: string;
|
||||||
|
targetDuration?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateFromDocumentPayload {
|
||||||
|
file: File;
|
||||||
|
title?: string;
|
||||||
|
language?: string;
|
||||||
|
aspectRatio?: string;
|
||||||
|
videoStyle?: string;
|
||||||
|
targetDuration?: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Notification {
|
export interface Notification {
|
||||||
id: string;
|
id: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
@@ -307,6 +325,25 @@ export const projectsApi = {
|
|||||||
createFromTweet: (data: CreateFromTweetPayload) =>
|
createFromTweet: (data: CreateFromTweetPayload) =>
|
||||||
apiClient.post<Project>('/projects/from-tweet', data).then((r) => r.data),
|
apiClient.post<Project>('/projects/from-tweet', data).then((r) => r.data),
|
||||||
|
|
||||||
|
createFromYoutube: (data: CreateFromYoutubePayload) =>
|
||||||
|
apiClient.post<Project>('/projects/from-youtube', data).then((r) => r.data),
|
||||||
|
|
||||||
|
createFromDocument: (data: CreateFromDocumentPayload) => {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', data.file);
|
||||||
|
if (data.title) formData.append('title', data.title);
|
||||||
|
if (data.language) formData.append('language', data.language);
|
||||||
|
if (data.aspectRatio) formData.append('aspectRatio', data.aspectRatio);
|
||||||
|
if (data.videoStyle) formData.append('videoStyle', data.videoStyle);
|
||||||
|
if (data.targetDuration) formData.append('targetDuration', data.targetDuration.toString());
|
||||||
|
|
||||||
|
return apiClient.post<Project>('/projects/from-document', formData, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data',
|
||||||
|
},
|
||||||
|
}).then((r) => r.data);
|
||||||
|
},
|
||||||
|
|
||||||
updateScene: (projectId: string, sceneId: string, data: Partial<Scene>) =>
|
updateScene: (projectId: string, sceneId: string, data: Partial<Scene>) =>
|
||||||
apiClient.patch<Scene>(`/projects/${projectId}/scenes/${sceneId}`, data).then((r) => r.data),
|
apiClient.patch<Scene>(`/projects/${projectId}/scenes/${sceneId}`, data).then((r) => r.data),
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user