generated from fahricansecer/boilerplate-fe
feat: Implement text-to-video and fix hydration UI issues
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import {
|
import {
|
||||||
FolderOpen,
|
FolderOpen,
|
||||||
@@ -95,6 +96,12 @@ function getStatCards(data?: typeof MOCK_STATS, creditBalance?: { balance: numbe
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function DashboardPage() {
|
export default function DashboardPage() {
|
||||||
|
const [mounted, setMounted] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setMounted(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Real API hook'ları — mock modunda çağrılmaz
|
// Real API hook'ları — mock modunda çağrılmaz
|
||||||
const statsQuery = useDashboardStats();
|
const statsQuery = useDashboardStats();
|
||||||
const creditQuery = useCreditBalance();
|
const creditQuery = useCreditBalance();
|
||||||
@@ -106,6 +113,14 @@ export default function DashboardPage() {
|
|||||||
|
|
||||||
const statCards = getStatCards(statsData, creditData);
|
const statCards = getStatCards(statsData, creditData);
|
||||||
|
|
||||||
|
if (!mounted) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center min-h-[60vh]">
|
||||||
|
<Loader2 size={32} className="animate-spin text-violet-500" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
variants={stagger}
|
variants={stagger}
|
||||||
|
|||||||
@@ -534,7 +534,7 @@ export default function ProjectDetailPage() {
|
|||||||
{/* ── Render Progress (WebSocket) ── */}
|
{/* ── Render Progress (WebSocket) ── */}
|
||||||
{isRendering && (
|
{isRendering && (
|
||||||
<motion.div variants={fadeUp}>
|
<motion.div variants={fadeUp}>
|
||||||
<RenderProgress renderState={renderState} />
|
<RenderProgress renderState={renderState} projectStatus={project.status} />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
'use client'
|
||||||
|
import { useEffect } from 'react'
|
||||||
|
|
||||||
|
export default function Error({
|
||||||
|
error,
|
||||||
|
reset,
|
||||||
|
}: {
|
||||||
|
error: Error & { digest?: string }
|
||||||
|
reset: () => void
|
||||||
|
}) {
|
||||||
|
useEffect(() => {
|
||||||
|
console.error('TEXT TO VIDEO ERROR:', error)
|
||||||
|
}, [error])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h2>Something went wrong!</h2>
|
||||||
|
<pre>{error.message}</pre>
|
||||||
|
<pre>{error.stack}</pre>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,252 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { useCreateFromText } from "@/hooks/use-api";
|
||||||
|
import { useToast } from "@/components/ui/toast";
|
||||||
|
import {
|
||||||
|
FileText,
|
||||||
|
Loader2,
|
||||||
|
ArrowRight,
|
||||||
|
Clock,
|
||||||
|
Palette,
|
||||||
|
Monitor,
|
||||||
|
Smartphone,
|
||||||
|
Square,
|
||||||
|
Wand2,
|
||||||
|
Type,
|
||||||
|
} 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 TextToVideoPage() {
|
||||||
|
const router = useRouter();
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
const createFromText = useCreateFromText();
|
||||||
|
|
||||||
|
const [textInput, setTextInput] = 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");
|
||||||
|
|
||||||
|
const handleGenerate = async () => {
|
||||||
|
if (!textInput.trim()) {
|
||||||
|
toast("error", "Lütfen bir konu, hikaye veya metin girin.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result: any = await createFromText.mutateAsync({
|
||||||
|
text: textInput,
|
||||||
|
language,
|
||||||
|
aspectRatio,
|
||||||
|
videoStyle: style,
|
||||||
|
cinematicReference: cinematicReference ? cinematicReference : undefined,
|
||||||
|
targetDuration: duration,
|
||||||
|
});
|
||||||
|
|
||||||
|
toast("success", "Video projesi başarıyla oluşturuldu!");
|
||||||
|
router.push(`/dashboard/projects/${result.id}`);
|
||||||
|
} catch (error) {
|
||||||
|
toast("error", "Proje oluşturulurken bir 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)]">
|
||||||
|
<Type 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)]">
|
||||||
|
Metinden Video Üret
|
||||||
|
</h1>
|
||||||
|
<p className="text-[var(--color-text-muted)] text-sm max-w-lg mx-auto">
|
||||||
|
İstediğiniz konuyu, bir hikayeyi veya makaleyi kopyalayıp yapıştırın; yapay zeka sizin için detaylı bir video senaryosu üretsin.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Input */}
|
||||||
|
<div className="card p-6 md:p-8 space-y-6">
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-[var(--color-text-secondary)] mb-2 block">
|
||||||
|
Fikriniz veya Metniniz
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
value={textInput}
|
||||||
|
onChange={(e) => setTextInput(e.target.value)}
|
||||||
|
placeholder="Örn: Evrenin başlangıcı ve kara deliklerin sırları hakkında detaylı ve sürükleyici bir anlatım..."
|
||||||
|
rows={6}
|
||||||
|
className="w-full bg-[var(--color-bg-subtle)] border border-[var(--color-border-faint)] rounded-xl px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/50 resize-none transition-all placeholder:text-[var(--color-text-ghost)]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Configurations */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 pt-4 border-t border-[var(--color-border-faint)]">
|
||||||
|
|
||||||
|
{/* Style */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<label className="text-xs font-semibold uppercase tracking-wider text-[var(--color-text-ghost)] flex items-center gap-2">
|
||||||
|
<Palette size={14} /> Video Stili
|
||||||
|
</label>
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
{videoStyles.slice(0, 4).map((s) => (
|
||||||
|
<button
|
||||||
|
key={s.id}
|
||||||
|
onClick={() => setStyle(s.id)}
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-2 p-3 rounded-xl border text-sm font-medium transition-all",
|
||||||
|
style === s.id
|
||||||
|
? "bg-blue-500/10 border-blue-500/30 text-blue-400"
|
||||||
|
: "bg-[var(--color-bg-subtle)] border-transparent text-[var(--color-text-muted)] hover:bg-[var(--color-bg-elevated)]"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span>{s.emoji}</span>
|
||||||
|
<span className="truncate">{s.label}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{/* Cinematic Reference */}
|
||||||
|
{style === "CINEMATIC" && (
|
||||||
|
<div className="mt-3">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={cinematicReference}
|
||||||
|
onChange={(e) => setCinematicReference(e.target.value)}
|
||||||
|
placeholder="Yönetmen/Film stili (Opsiyonel)"
|
||||||
|
className="w-full bg-black/20 border border-[var(--color-border-faint)] rounded-lg px-3 py-2 text-xs focus:outline-none focus:border-blue-500/50"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Aspect Ratio */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<label className="text-xs font-semibold uppercase tracking-wider text-[var(--color-text-ghost)] flex items-center gap-2">
|
||||||
|
<Monitor size={14} /> Format
|
||||||
|
</label>
|
||||||
|
<div className="grid grid-cols-3 gap-2">
|
||||||
|
{aspectRatios.map((ar) => {
|
||||||
|
const Icon = ar.icon;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={ar.id}
|
||||||
|
onClick={() => setAspectRatio(ar.id)}
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col items-center gap-2 p-3 rounded-xl border transition-all",
|
||||||
|
aspectRatio === ar.id
|
||||||
|
? "bg-blue-500/10 border-blue-500/30 text-blue-400"
|
||||||
|
: "bg-[var(--color-bg-subtle)] border-transparent text-[var(--color-text-muted)] hover:bg-[var(--color-bg-elevated)]"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Icon size={18} />
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-xs font-bold">{ar.label}</div>
|
||||||
|
<div className="text-[9px] opacity-70 truncate">{ar.desc}</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Duration */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<label className="text-xs font-semibold uppercase tracking-wider text-[var(--color-text-ghost)] flex items-center gap-2">
|
||||||
|
<Clock size={14} /> Süre
|
||||||
|
</label>
|
||||||
|
<div className="flex items-center gap-4 bg-[var(--color-bg-subtle)] p-3 rounded-xl">
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="15"
|
||||||
|
max="180"
|
||||||
|
step="15"
|
||||||
|
value={duration}
|
||||||
|
onChange={(e) => setDuration(Number(e.target.value))}
|
||||||
|
className="flex-1 accent-blue-500"
|
||||||
|
/>
|
||||||
|
<span className="text-sm font-mono font-medium text-blue-400 w-12 text-right">
|
||||||
|
{duration}s
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Language */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<label className="text-xs font-semibold uppercase tracking-wider text-[var(--color-text-ghost)] flex items-center gap-2">
|
||||||
|
<span className="text-[14px]">🗣</span> Seslendirme Dili
|
||||||
|
</label>
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
{languages.map((lang) => (
|
||||||
|
<button
|
||||||
|
key={lang.code}
|
||||||
|
onClick={() => setLanguage(lang.code)}
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-2 p-2.5 rounded-xl border text-sm font-medium transition-all",
|
||||||
|
language === lang.code
|
||||||
|
? "bg-blue-500/10 border-blue-500/30 text-blue-400"
|
||||||
|
: "bg-[var(--color-bg-subtle)] border-transparent text-[var(--color-text-muted)] hover:bg-[var(--color-bg-elevated)]"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span className="text-lg leading-none">{lang.flag}</span>
|
||||||
|
<span>{lang.label}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Action Button */}
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<button
|
||||||
|
onClick={handleGenerate}
|
||||||
|
disabled={createFromText.isPending || !textInput.trim()}
|
||||||
|
className={cn(
|
||||||
|
"group relative overflow-hidden flex items-center gap-3 px-8 py-4 rounded-2xl font-bold text-white shadow-[0_0_40px_rgba(59,130,246,0.3)] transition-all",
|
||||||
|
"bg-gradient-to-r from-blue-600 to-violet-600 hover:from-blue-500 hover:to-violet-500 hover:scale-[1.02]",
|
||||||
|
"disabled:opacity-50 disabled:hover:scale-100 disabled:cursor-not-allowed"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{createFromText.isPending ? (
|
||||||
|
<>
|
||||||
|
<Loader2 size={20} className="animate-spin" />
|
||||||
|
<span>Yapay Zeka Senaryoyu Yazıyor...</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Wand2 size={20} className="group-hover:rotate-12 transition-transform" />
|
||||||
|
<span>Video Projesi Oluştur</span>
|
||||||
|
<ArrowRight size={18} className="group-hover:translate-x-1 transition-transform" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
import {
|
import {
|
||||||
AreaChart,
|
AreaChart,
|
||||||
Area,
|
Area,
|
||||||
@@ -59,13 +60,19 @@ function formatPieData(stats: Record<string, unknown> | undefined) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function DashboardCharts() {
|
export function DashboardCharts() {
|
||||||
|
const [mounted, setMounted] = useState(false);
|
||||||
const { data, isLoading } = useDashboardStats();
|
const { data, isLoading } = useDashboardStats();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setMounted(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const stats = (data as any)?.data ?? data;
|
const stats = (data as any)?.data ?? data;
|
||||||
const weekData = formatWeekData(stats);
|
const weekData = formatWeekData(stats);
|
||||||
const pieData = formatPieData(stats);
|
const pieData = formatPieData(stats);
|
||||||
|
|
||||||
if (isLoading) {
|
if (!mounted || isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 md:gap-6">
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 md:gap-6">
|
||||||
{[1, 2].map((i) => (
|
{[1, 2].map((i) => (
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ 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/render-queue", icon: Activity, label: "Render Monitör" },
|
{ href: "/dashboard/render-queue", icon: Activity, label: "Render Monitör" },
|
||||||
|
{ href: "/dashboard/text-to-video", icon: FileText, label: "Metin → Video" },
|
||||||
{ 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/youtube-to-video", icon: Link2, label: "YT → Video" },
|
||||||
{ href: "/dashboard/document-to-video", icon: FileText, label: "Belge → Video" },
|
{ href: "/dashboard/document-to-video", icon: FileText, label: "Belge → Video" },
|
||||||
|
|||||||
@@ -16,12 +16,41 @@ const STAGE_DETAILS: Record<string, { label: string; icon: string; color: string
|
|||||||
|
|
||||||
interface RenderProgressProps {
|
interface RenderProgressProps {
|
||||||
renderState: RenderProgressState;
|
renderState: RenderProgressState;
|
||||||
|
projectStatus?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RenderProgress({ renderState }: RenderProgressProps) {
|
export function RenderProgress({ renderState, projectStatus }: RenderProgressProps) {
|
||||||
const { progress, stage, stageLabel, currentScene, totalScenes, eta, status, isConnected } = renderState;
|
const { progress, stage, stageLabel, currentScene, totalScenes, eta, status, isConnected } = renderState;
|
||||||
|
|
||||||
if (status === 'idle') return null;
|
if (status === 'idle') {
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 12 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
className="card-surface p-5 md:p-6"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2.5">
|
||||||
|
<Loader2 size={18} className="animate-spin text-amber-400" />
|
||||||
|
<h3 className="text-sm font-semibold text-[var(--color-text-primary)]">
|
||||||
|
{projectStatus === 'GENERATING_SCRIPT' ? 'AI Senaryo Üretiyor...' : 'İşlem kuyrukta veya başlatılıyor...'}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
{isConnected ? (
|
||||||
|
<Wifi size={13} className="text-emerald-400" />
|
||||||
|
) : (
|
||||||
|
<WifiOff size={13} className="text-red-400" />
|
||||||
|
)}
|
||||||
|
<span className="text-[10px] text-[var(--color-text-ghost)]">
|
||||||
|
{isConnected ? 'Canlı' : 'Bağlantı koptu'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { ChakraProvider } from "@chakra-ui/react";
|
||||||
|
import { system } from "@/theme/theme";
|
||||||
import { SessionProvider } from "next-auth/react";
|
import { SessionProvider } from "next-auth/react";
|
||||||
import { ThemeProvider } from "next-themes";
|
import { ThemeProvider } from "next-themes";
|
||||||
import ReactQueryProvider from "@/provider/react-query-provider";
|
import ReactQueryProvider from "@/provider/react-query-provider";
|
||||||
@@ -7,17 +9,19 @@ import { ToastProvider } from "@/components/ui/toast";
|
|||||||
|
|
||||||
export function Provider({ children }: { children: React.ReactNode }) {
|
export function Provider({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<SessionProvider>
|
<ChakraProvider value={system}>
|
||||||
<ReactQueryProvider>
|
<SessionProvider>
|
||||||
<ThemeProvider
|
<ReactQueryProvider>
|
||||||
attribute="class"
|
<ThemeProvider
|
||||||
defaultTheme="dark"
|
attribute="class"
|
||||||
enableSystem={false}
|
defaultTheme="dark"
|
||||||
disableTransitionOnChange
|
enableSystem={false}
|
||||||
>
|
disableTransitionOnChange
|
||||||
<ToastProvider>{children}</ToastProvider>
|
>
|
||||||
</ThemeProvider>
|
<ToastProvider>{children}</ToastProvider>
|
||||||
</ReactQueryProvider>
|
</ThemeProvider>
|
||||||
</SessionProvider>
|
</ReactQueryProvider>
|
||||||
|
</SessionProvider>
|
||||||
|
</ChakraProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
type CreateFromDocumentPayload,
|
type CreateFromDocumentPayload,
|
||||||
type ExtractDocumentTopicsPayload,
|
type ExtractDocumentTopicsPayload,
|
||||||
type CreateFromExtractedTextPayload,
|
type CreateFromExtractedTextPayload,
|
||||||
|
type CreateFromTextPayload,
|
||||||
type ExtractDocumentTopicsResponse,
|
type ExtractDocumentTopicsResponse,
|
||||||
type Template,
|
type Template,
|
||||||
type PaginatedResponse,
|
type PaginatedResponse,
|
||||||
@@ -434,6 +435,18 @@ export function useCreateFromExtractedText() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Serbest metin veya fikir üzerinden proje üret */
|
||||||
|
export function useCreateFromText() {
|
||||||
|
const qc = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (data: CreateFromTextPayload) => projectsApi.createFromText(data),
|
||||||
|
onSuccess: () => {
|
||||||
|
qc.invalidateQueries({ queryKey: queryKeys.projects.all });
|
||||||
|
qc.invalidateQueries({ queryKey: queryKeys.dashboard.stats });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════
|
||||||
// NOTIFICATIONS — Bildirim hook'ları
|
// NOTIFICATIONS — Bildirim hook'ları
|
||||||
// ═══════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════
|
||||||
|
|||||||
@@ -271,6 +271,16 @@ export interface CreateFromDocumentPayload {
|
|||||||
targetDuration?: number;
|
targetDuration?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CreateFromTextPayload {
|
||||||
|
text: string;
|
||||||
|
title?: string;
|
||||||
|
language?: string;
|
||||||
|
aspectRatio?: string;
|
||||||
|
videoStyle?: string;
|
||||||
|
cinematicReference?: string;
|
||||||
|
targetDuration?: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ExtractDocumentTopicsPayload {
|
export interface ExtractDocumentTopicsPayload {
|
||||||
file: File;
|
file: File;
|
||||||
}
|
}
|
||||||
@@ -378,6 +388,9 @@ export const projectsApi = {
|
|||||||
}).then((r) => r.data);
|
}).then((r) => r.data);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
createFromText: (data: CreateFromTextPayload) =>
|
||||||
|
apiClient.post<Project>('/projects/from-text', data).then((r) => r.data),
|
||||||
|
|
||||||
extractDocumentTopics: (data: ExtractDocumentTopicsPayload) => {
|
extractDocumentTopics: (data: ExtractDocumentTopicsPayload) => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', data.file);
|
formData.append('file', data.file);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { createSystem, defaultConfig, SystemConfig } from '@chakra-ui/react';
|
import { createSystem, defaultConfig, SystemConfig } from '@chakra-ui/react';
|
||||||
|
|
||||||
const customConfig: SystemConfig = {
|
const customConfig: SystemConfig = {
|
||||||
|
preflight: false,
|
||||||
theme: {
|
theme: {
|
||||||
breakpoints: {
|
breakpoints: {
|
||||||
tablet: '768px',
|
tablet: '768px',
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
const puppeteer = require('puppeteer');
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const browser = await puppeteer.launch();
|
||||||
|
const page = await browser.newPage();
|
||||||
|
page.on('console', msg => console.log('PAGE LOG:', msg.text()));
|
||||||
|
page.on('pageerror', error => console.log('PAGE ERROR:', error.message));
|
||||||
|
page.on('requestfailed', request => console.log('REQUEST FAILED:', request.url(), request.failure().errorText));
|
||||||
|
|
||||||
|
await page.goto('http://localhost:3001/tr/dashboard/text-to-video');
|
||||||
|
await new Promise(r => setTimeout(r, 2000));
|
||||||
|
await browser.close();
|
||||||
|
})();
|
||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user