From dd8878d403080dc6dccd7180caf18798df85cf58 Mon Sep 17 00:00:00 2001 From: Harun CAN Date: Sun, 5 Apr 2026 20:37:03 +0300 Subject: [PATCH] main --- next-env.d.ts | 2 +- src/app/[locale]/(auth)/signin/page.tsx | 443 ++++++++++-------- .../dashboard/projects/[id]/page.tsx | 157 ++++++- .../dashboard/projects/new/page.tsx | 178 +++++-- .../(dashboard)/dashboard/x-to-video/page.tsx | 5 - src/components/project/scene-card.tsx | 135 +++++- src/hooks/use-api.ts | 48 ++ src/lib/api/api-service.ts | 6 + test-fe-api.js | 31 ++ test-frontend-useProjects.js | 47 ++ 10 files changed, 767 insertions(+), 285 deletions(-) create mode 100644 test-fe-api.js create mode 100644 test-frontend-useProjects.js diff --git a/next-env.d.ts b/next-env.d.ts index c4b7818..9edff1c 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import "./.next/dev/types/routes.d.ts"; +import "./.next/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/src/app/[locale]/(auth)/signin/page.tsx b/src/app/[locale]/(auth)/signin/page.tsx index 71a3a8f..fd2fe98 100644 --- a/src/app/[locale]/(auth)/signin/page.tsx +++ b/src/app/[locale]/(auth)/signin/page.tsx @@ -1,231 +1,268 @@ "use client"; -import { - Box, - Flex, - Heading, - Input, - Link as ChakraLink, - Text, - ClientOnly, -} from "@chakra-ui/react"; -import { Button } from "@/components/ui/buttons/button"; -import { Switch } from "@/components/ui/forms/switch"; -import { Field } from "@/components/ui/forms/field"; -import { useTranslations } from "next-intl"; -import signInImage from "/public/assets/img/sign-in-image.png"; -import { InputGroup } from "@/components/ui/forms/input-group"; -import { BiLock } from "react-icons/bi"; -import { useForm } from "react-hook-form"; -import { yupResolver } from "@hookform/resolvers/yup"; -import * as yup from "yup"; -import { Link, useRouter } from "@/i18n/navigation"; -import { MdMail } from "react-icons/md"; -import { PasswordInput } from "@/components/ui/forms/password-input"; -import { Skeleton } from "@/components/ui/feedback/skeleton"; -import { signIn } from "next-auth/react"; -import { toaster } from "@/components/ui/feedback/toaster"; import { useState } from "react"; +import { useRouter } from "next/navigation"; +import { signIn } from "next-auth/react"; +import { motion } from "framer-motion"; +import { Sparkles, Mail, Lock, Loader2, ArrowRight } from "lucide-react"; -const schema = yup.object({ - email: yup.string().email().required(), - password: yup.string().required(), -}); - -type SignInForm = yup.InferType; - -const defaultValues = { - email: "test@test.com.ue", - password: "test1234", -}; - -function SignInPage() { - const t = useTranslations(); +export default function SignInPage() { const router = useRouter(); - + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); const [loading, setLoading] = useState(false); + const [error, setError] = useState(""); - const { - handleSubmit, - register, - formState: { errors }, - } = useForm({ - resolver: yupResolver(schema), - mode: "onChange", - defaultValues, - }); + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(""); + + if (!email || !password) { + setError("Email ve şifre gereklidir."); + return; + } - const onSubmit = async (formData: SignInForm) => { try { setLoading(true); const res = await signIn("credentials", { redirect: false, - email: formData.email, - password: formData.password, + email, + password, }); if (res?.error) { - throw new Error(res.error); + setError("Giriş başarısız. Email veya şifrenizi kontrol edin."); + return; } - router.replace("/home"); - } catch (error) { - toaster.error({ - title: (error as Error).message || "Giriş yaparken hata oluştu!", - type: "error", - }); + router.replace("/dashboard"); + } catch { + setError("Bağlantı hatası. Lütfen tekrar deneyin."); } finally { setLoading(false); } }; return ( - - + - - +
- - {t("auth.welcome-back")} - - - {t("auth.subtitle")} - - - }> - - - - - }> - - - - - - {t("auth.remember-me")} - - - - }> - - - - - - {t("auth.dont-have-account")} - - {t("auth.sign-up")} - - - - - - +
+

+ ContentGen AI +

+

+ Hesabınıza giriş yapın +

+ + + {/* Form Card */} +
- - - - +
+ {/* Error */} + {error && ( +
+ {error} +
+ )} + + {/* Email */} +
+ +
+ + setEmail(e.target.value)} + placeholder="admin@contentgen.ai" + style={{ + width: "100%", + padding: "0.75rem 1rem 0.75rem 2.5rem", + borderRadius: 12, + border: "1px solid rgba(255,255,255,0.08)", + background: "rgba(255,255,255,0.04)", + color: "#f0f0f5", + fontSize: "0.875rem", + outline: "none", + transition: "border-color 0.2s", + }} + onFocus={(e) => (e.target.style.borderColor = "rgba(139, 92, 246, 0.4)")} + onBlur={(e) => (e.target.style.borderColor = "rgba(255,255,255,0.08)")} + /> +
+
+ + {/* Password */} +
+ +
+ + setPassword(e.target.value)} + placeholder="••••••••" + style={{ + width: "100%", + padding: "0.75rem 1rem 0.75rem 2.5rem", + borderRadius: 12, + border: "1px solid rgba(255,255,255,0.08)", + background: "rgba(255,255,255,0.04)", + color: "#f0f0f5", + fontSize: "0.875rem", + outline: "none", + transition: "border-color 0.2s", + }} + onFocus={(e) => (e.target.style.borderColor = "rgba(139, 92, 246, 0.4)")} + onBlur={(e) => (e.target.style.borderColor = "rgba(255,255,255,0.08)")} + /> +
+
+ + {/* Submit Button */} + +
+
+ + {/* Footer text */} +

+ ContentGen AI Studio © 2026 +

+
+ ); } - -export default SignInPage; diff --git a/src/app/[locale]/(dashboard)/dashboard/projects/[id]/page.tsx b/src/app/[locale]/(dashboard)/dashboard/projects/[id]/page.tsx index 7e9a433..bf2f07f 100644 --- a/src/app/[locale]/(dashboard)/dashboard/projects/[id]/page.tsx +++ b/src/app/[locale]/(dashboard)/dashboard/projects/[id]/page.tsx @@ -18,7 +18,16 @@ import { } from 'lucide-react'; import Link from 'next/link'; import { useState } from 'react'; -import { useProject, useGenerateScript, useApproveAndQueue, useUpdateProject, useDeleteProject } from '@/hooks/use-api'; +import { + useProject, + useGenerateScript, + useApproveAndQueue, + useUpdateProject, + useDeleteProject, + useGenerateSceneImage, + useUpscaleSceneImage, + useRegenerateScene +} from '@/hooks/use-api'; import { useRenderProgress } from '@/hooks/use-render-progress'; import { SceneCard } from '@/components/project/scene-card'; import { RenderProgress } from '@/components/project/render-progress'; @@ -42,16 +51,61 @@ const STATUS_MAP: Record = { - CINEMATIC: '🎬 Sinematik', - DOCUMENTARY: '📹 Belgesel', - EDUCATIONAL: '📚 Eğitici', - STORYTELLING: '📖 Hikaye', - NEWS: '📰 Haber', - PROMOTIONAL: '📢 Tanıtım', - ARTISTIC: '🎨 Sanatsal', - MINIMALIST: '✨ Minimalist', -}; +const videoStyles = [ + // Film & Sinema + { id: "CINEMATIC", label: "Sinematik", emoji: "🎬", desc: "Film kalitesinde görseller", category: "Film & Sinema" }, + { id: "DOCUMENTARY", label: "Belgesel", emoji: "📹", desc: "Bilimsel ve ciddi ton", category: "Film & Sinema" }, + { id: "STORYTELLING", label: "Hikâye Anlatımı", emoji: "📖", desc: "Anlatı odaklı, sürükleyici", category: "Film & Sinema" }, + { id: "NEWS", label: "Haber", emoji: "📰", desc: "Güncel ve bilgilendirici", category: "Film & Sinema" }, + { id: "ARTISTIC", label: "Sanatsal", emoji: "🎨", desc: "Yaratıcı ve sıra dışı", category: "Film & Sinema" }, + { id: "NOIR", label: "Film Noir", emoji: "🖤", desc: "Karanlık, dramatik", category: "Film & Sinema" }, + { id: "VLOG", label: "Vlog", emoji: "📱", desc: "Günlük, samimi", category: "Film & Sinema" }, + // Animasyon + { id: "ANIME", label: "Anime", emoji: "⛩️", desc: "Japon animasyon stili", category: "Animasyon" }, + { id: "ANIMATION_3D", label: "3D Animasyon", emoji: "🧊", desc: "Pixar kalitesi", category: "Animasyon" }, + { id: "ANIMATION_2D", label: "2D Animasyon", emoji: "✏️", desc: "Klasik el çizimi", category: "Animasyon" }, + { id: "STOP_MOTION", label: "Stop Motion", emoji: "🧸", desc: "Kare kare animasyon", category: "Animasyon" }, + { id: "MOTION_COMIC", label: "Hareketli Çizgi Roman", emoji: "💥", desc: "Panel bazlı anlatım", category: "Animasyon" }, + { id: "CARTOON", label: "Karikatür", emoji: "🎭", desc: "Çizgi film stili", category: "Animasyon" }, + { id: "CLAYMATION", label: "Claymation", emoji: "🏺", desc: "Kil animasyon", category: "Animasyon" }, + { id: "PIXEL_ART", label: "Pixel Art", emoji: "👾", desc: "8-bit retro oyun", category: "Animasyon" }, + { id: "ISOMETRIC", label: "İzometrik", emoji: "🔷", desc: "İzometrik animasyon", category: "Animasyon" }, + // Eğitim & Bilgi + { id: "EDUCATIONAL", label: "Eğitim", emoji: "🎓", desc: "Öğretici ve açıklayıcı", category: "Eğitim & Bilgi" }, + { id: "INFOGRAPHIC", label: "İnfografik", emoji: "📊", desc: "Veri görselleştirme", category: "Eğitim & Bilgi" }, + { id: "WHITEBOARD", label: "Whiteboard", emoji: "📝", desc: "Tahta animasyonu", category: "Eğitim & Bilgi" }, + { id: "EXPLAINER", label: "Explainer", emoji: "💡", desc: "Ürün/konsept anlatımı", category: "Eğitim & Bilgi" }, + { id: "DATA_VIZ", label: "Veri Görselleştirme", emoji: "📈", desc: "Grafikler ve tablolar", category: "Eğitim & Bilgi" }, + // Retro & Nostaljik + { id: "RETRO_80S", label: "Retro 80s", emoji: "🕹️", desc: "Synthwave estetik", category: "Retro & Nostaljik" }, + { id: "VINTAGE_FILM", label: "Vintage Film", emoji: "📽️", desc: "Super 8 filmi", category: "Retro & Nostaljik" }, + { id: "VHS", label: "VHS", emoji: "📼", desc: "Kaset estetik", category: "Retro & Nostaljik" }, + { id: "POLAROID", label: "Polaroid", emoji: "📸", desc: "Analog fotoğraf", category: "Retro & Nostaljik" }, + { id: "RETRO_90S", label: "Retro 90s Y2K", emoji: "💿", desc: "Y2K & internet", category: "Retro & Nostaljik" }, + // Sanat Akımları + { id: "WATERCOLOR", label: "Suluboya", emoji: "🎨", desc: "Suluboya resim", category: "Sanat Akımları" }, + { id: "OIL_PAINTING", label: "Yağlı Boya", emoji: "🖌️", desc: "Klasik tuval", category: "Sanat Akımları" }, + { id: "IMPRESSIONIST", label: "Empresyonist", emoji: "🌅", desc: "Monet tarzı", category: "Sanat Akımları" }, + { id: "POP_ART", label: "Pop Art", emoji: "🎯", desc: "Warhol stili", category: "Sanat Akımları" }, + { id: "UKIYO_E", label: "Ukiyo-e", emoji: "🏯", desc: "Japon gravür", category: "Sanat Akımları" }, + { id: "ART_DECO", label: "Art Deco", emoji: "✨", desc: "1920s zarafet", category: "Sanat Akımları" }, + { id: "SURREAL", label: "Sürrealist", emoji: "🌀", desc: "Dalí tarzı", category: "Sanat Akımları" }, + { id: "COMIC_BOOK", label: "Çizgi Roman", emoji: "💬", desc: "Marvel/DC stili", category: "Sanat Akımları" }, + { id: "SKETCH", label: "Karakalem", emoji: "✍️", desc: "Kalem çizim", category: "Sanat Akımları" }, + // Modern & Minimal + { id: "MINIMALIST", label: "Minimalist", emoji: "⚪", desc: "Apple estetiği", category: "Modern & Minimal" }, + { id: "GLASSMORPHISM", label: "Glassmorphism", emoji: "🔮", desc: "Cam efekti", category: "Modern & Minimal" }, + { id: "NEON", label: "Neon Glow", emoji: "💜", desc: "Neon ışıkları", category: "Modern & Minimal" }, + { id: "CYBERPUNK", label: "Cyberpunk", emoji: "🤖", desc: "Gelecek distopya", category: "Modern & Minimal" }, + { id: "STEAMPUNK", label: "Steampunk", emoji: "⚙️", desc: "Viktoryan mekanik", category: "Modern & Minimal" }, + { id: "ABSTRACT", label: "Soyut", emoji: "🔵", desc: "Abstract sanat", category: "Modern & Minimal" }, + // Fotoğrafik + { id: "PRODUCT", label: "Ürün Fotoğrafı", emoji: "📦", desc: "Studio çekim", category: "Fotoğrafik" }, + { id: "FASHION", label: "Moda", emoji: "👗", desc: "Editöryal çekim", category: "Fotoğrafik" }, + { id: "AERIAL", label: "Havadan", emoji: "🚁", desc: "Drone görüntüsü", category: "Fotoğrafik" }, + { id: "MACRO", label: "Makro", emoji: "🔬", desc: "Yakın çekim", category: "Fotoğrafik" }, + { id: "PORTRAIT", label: "Portre", emoji: "🧑", desc: "Portre fotoğraf", category: "Fotoğrafik" }, +]; const stagger = { hidden: { opacity: 0 }, @@ -75,6 +129,13 @@ export default function ProjectDetailPage() { const approveMutation = useApproveAndQueue(); const deleteMutation = useDeleteProject(); + const generateImageMutation = useGenerateSceneImage(); + const upscaleImageMutation = useUpscaleSceneImage(); + const regenerateSceneMutation = useRegenerateScene(); + + const [generatingImageId, setGeneratingImageId] = useState(null); + const [upscalingImageId, setUpscalingImageId] = useState(null); + // WebSocket progress const renderState = useRenderProgress( project?.status && ['PENDING', 'GENERATING_MEDIA', 'RENDERING'].includes(project.status) ? id : undefined, @@ -93,17 +154,37 @@ export default function ProjectDetailPage() { // Sahne yeniden üretim const handleSceneRegenerate = async (sceneId: string) => { setRegeneratingSceneId(sceneId); - try { - await fetch(`${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000/api'}/projects/${id}/scenes/${sceneId}/regenerate`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - }); - refetch(); - } catch (err) { - console.error('Sahne yeniden üretim hatası:', err); - } finally { - setRegeneratingSceneId(null); - } + regenerateSceneMutation.mutate( + { projectId: id, sceneId }, + { + onSettled: () => setRegeneratingSceneId(null), + onSuccess: () => refetch(), + } + ); + }; + + // Sahne görseli oluşturma + const handleGenerateImage = (sceneId: string, customPrompt?: string) => { + setGeneratingImageId(sceneId); + generateImageMutation.mutate( + { projectId: id, sceneId, customPrompt }, + { + onSettled: () => setGeneratingImageId(null), + onSuccess: () => refetch(), + } + ); + }; + + // Sahne görseli upscale + const handleUpscaleImage = (sceneId: string) => { + setUpscalingImageId(sceneId); + upscaleImageMutation.mutate( + { projectId: id, sceneId }, + { + onSettled: () => setUpscalingImageId(null), + onSuccess: () => refetch(), + } + ); }; // Senaryo üret @@ -161,12 +242,24 @@ export default function ProjectDetailPage() { const statusInfo = STATUS_MAP[project.status] || STATUS_MAP.DRAFT; const StatusIcon = statusInfo.icon; - const isEditable = project.status === 'DRAFT' || project.status === 'FAILED'; + const isRendering = ['PENDING', 'GENERATING_MEDIA', 'RENDERING', 'GENERATING_SCRIPT'].includes(project.status); + const isEditable = !isRendering; const hasScript = project.scenes && project.scenes.length > 0; - const isRendering = ['PENDING', 'GENERATING_MEDIA', 'RENDERING'].includes(project.status); const isCompleted = project.status === 'COMPLETED'; const tweetData = project.sourceTweetData as Record | undefined; + const currentStyle = videoStyles.find(s => s.id === project.videoStyle); + + const handleStyleChange = async (newStyleId: string) => { + if (newStyleId === project.videoStyle) return; + try { + await projectsApi.update(id, { videoStyle: newStyleId } as any); + refetch(); + } catch (err) { + console.error('Üslup (Stil) değiştirme hatası:', err); + } + }; + return ( {project.targetDuration}s - {STYLE_LABELS[project.videoStyle] || project.videoStyle} + {isEditable ? ( + + ) : ( + {currentStyle ? `${currentStyle.emoji} ${currentStyle.label}` : project.videoStyle} + )} {project.language} {new Date(project.createdAt).toLocaleDateString('tr-TR', { diff --git a/src/app/[locale]/(dashboard)/dashboard/projects/new/page.tsx b/src/app/[locale]/(dashboard)/dashboard/projects/new/page.tsx index 26b0de2..ec2c661 100644 --- a/src/app/[locale]/(dashboard)/dashboard/projects/new/page.tsx +++ b/src/app/[locale]/(dashboard)/dashboard/projects/new/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState } from "react"; +import { useState, useMemo } from "react"; import { useRouter } from "next/navigation"; import { motion, AnimatePresence } from "framer-motion"; import { @@ -16,6 +16,8 @@ import { Loader2, Check, Wand2, + Search, + ChevronDown, } from "lucide-react"; import Link from "next/link"; import { cn } from "@/lib/utils"; @@ -37,12 +39,59 @@ const languages = [ ]; const videoStyles = [ - { id: "CINEMATIC", label: "Sinematik", emoji: "🎬", desc: "Film kalitesinde görseller" }, - { id: "DOCUMENTARY", label: "Belgesel", emoji: "📹", desc: "Bilimsel ve ciddi ton" }, - { id: "EDUCATIONAL", label: "Eğitim", emoji: "📚", desc: "Öğretici ve açıklayıcı" }, - { id: "STORYTELLING", label: "Hikaye", emoji: "📖", desc: "Anlatı odaklı, sürükleyici" }, - { id: "NEWS", label: "Haber", emoji: "📰", desc: "Güncel ve bilgilendirici" }, - { id: "ARTISTIC", label: "Sanatsal", emoji: "🎨", desc: "Yaratıcı ve sıra dışı" }, + // Film & Sinema + { id: "CINEMATIC", label: "Sinematik", emoji: "🎬", desc: "Film kalitesinde görseller", category: "Film & Sinema" }, + { id: "DOCUMENTARY", label: "Belgesel", emoji: "📹", desc: "Bilimsel ve ciddi ton", category: "Film & Sinema" }, + { id: "STORYTELLING", label: "Hikâye Anlatımı", emoji: "📖", desc: "Anlatı odaklı, sürükleyici", category: "Film & Sinema" }, + { id: "NEWS", label: "Haber", emoji: "📰", desc: "Güncel ve bilgilendirici", category: "Film & Sinema" }, + { id: "ARTISTIC", label: "Sanatsal", emoji: "🎨", desc: "Yaratıcı ve sıra dışı", category: "Film & Sinema" }, + { id: "NOIR", label: "Film Noir", emoji: "🖤", desc: "Karanlık, dramatik", category: "Film & Sinema" }, + { id: "VLOG", label: "Vlog", emoji: "📱", desc: "Günlük, samimi", category: "Film & Sinema" }, + // Animasyon + { id: "ANIME", label: "Anime", emoji: "⛩️", desc: "Japon animasyon stili", category: "Animasyon" }, + { id: "ANIMATION_3D", label: "3D Animasyon", emoji: "🧊", desc: "Pixar kalitesi", category: "Animasyon" }, + { id: "ANIMATION_2D", label: "2D Animasyon", emoji: "✏️", desc: "Klasik el çizimi", category: "Animasyon" }, + { id: "STOP_MOTION", label: "Stop Motion", emoji: "🧸", desc: "Kare kare animasyon", category: "Animasyon" }, + { id: "MOTION_COMIC", label: "Hareketli Çizgi Roman", emoji: "💥", desc: "Panel bazlı anlatım", category: "Animasyon" }, + { id: "CARTOON", label: "Karikatür", emoji: "🎭", desc: "Çizgi film stili", category: "Animasyon" }, + { id: "CLAYMATION", label: "Claymation", emoji: "🏺", desc: "Kil animasyon", category: "Animasyon" }, + { id: "PIXEL_ART", label: "Pixel Art", emoji: "👾", desc: "8-bit retro oyun", category: "Animasyon" }, + { id: "ISOMETRIC", label: "İzometrik", emoji: "🔷", desc: "İzometrik animasyon", category: "Animasyon" }, + // Eğitim & Bilgi + { id: "EDUCATIONAL", label: "Eğitim", emoji: "🎓", desc: "Öğretici ve açıklayıcı", category: "Eğitim & Bilgi" }, + { id: "INFOGRAPHIC", label: "İnfografik", emoji: "📊", desc: "Veri görselleştirme", category: "Eğitim & Bilgi" }, + { id: "WHITEBOARD", label: "Whiteboard", emoji: "📝", desc: "Tahta animasyonu", category: "Eğitim & Bilgi" }, + { id: "EXPLAINER", label: "Explainer", emoji: "💡", desc: "Ürün/konsept anlatımı", category: "Eğitim & Bilgi" }, + { id: "DATA_VIZ", label: "Veri Görselleştirme", emoji: "📈", desc: "Grafikler ve tablolar", category: "Eğitim & Bilgi" }, + // Retro & Nostaljik + { id: "RETRO_80S", label: "Retro 80s", emoji: "🕹️", desc: "Synthwave estetik", category: "Retro & Nostaljik" }, + { id: "VINTAGE_FILM", label: "Vintage Film", emoji: "📽️", desc: "Super 8 filmi", category: "Retro & Nostaljik" }, + { id: "VHS", label: "VHS", emoji: "📼", desc: "Kaset estetik", category: "Retro & Nostaljik" }, + { id: "POLAROID", label: "Polaroid", emoji: "📸", desc: "Analog fotoğraf", category: "Retro & Nostaljik" }, + { id: "RETRO_90S", label: "Retro 90s Y2K", emoji: "💿", desc: "Y2K & internet", category: "Retro & Nostaljik" }, + // Sanat Akımları + { id: "WATERCOLOR", label: "Suluboya", emoji: "🎨", desc: "Suluboya resim", category: "Sanat Akımları" }, + { id: "OIL_PAINTING", label: "Yağlı Boya", emoji: "🖌️", desc: "Klasik tuval", category: "Sanat Akımları" }, + { id: "IMPRESSIONIST", label: "Empresyonist", emoji: "🌅", desc: "Monet tarzı", category: "Sanat Akımları" }, + { id: "POP_ART", label: "Pop Art", emoji: "🎯", desc: "Warhol stili", category: "Sanat Akımları" }, + { id: "UKIYO_E", label: "Ukiyo-e", emoji: "🏯", desc: "Japon gravür", category: "Sanat Akımları" }, + { id: "ART_DECO", label: "Art Deco", emoji: "✨", desc: "1920s zarafet", category: "Sanat Akımları" }, + { id: "SURREAL", label: "Sürrealist", emoji: "🌀", desc: "Dalí tarzı", category: "Sanat Akımları" }, + { id: "COMIC_BOOK", label: "Çizgi Roman", emoji: "💬", desc: "Marvel/DC stili", category: "Sanat Akımları" }, + { id: "SKETCH", label: "Karakalem", emoji: "✍️", desc: "Kalem çizim", category: "Sanat Akımları" }, + // Modern & Minimal + { id: "MINIMALIST", label: "Minimalist", emoji: "⚪", desc: "Apple estetiği", category: "Modern & Minimal" }, + { id: "GLASSMORPHISM", label: "Glassmorphism", emoji: "🔮", desc: "Cam efekti", category: "Modern & Minimal" }, + { id: "NEON", label: "Neon Glow", emoji: "💜", desc: "Neon ışıkları", category: "Modern & Minimal" }, + { id: "CYBERPUNK", label: "Cyberpunk", emoji: "🤖", desc: "Gelecek distopya", category: "Modern & Minimal" }, + { id: "STEAMPUNK", label: "Steampunk", emoji: "⚙️", desc: "Viktoryan mekanik", category: "Modern & Minimal" }, + { id: "ABSTRACT", label: "Soyut", emoji: "🔵", desc: "Abstract sanat", category: "Modern & Minimal" }, + // Fotoğrafik + { id: "PRODUCT", label: "Ürün Fotoğrafı", emoji: "📦", desc: "Studio çekim", category: "Fotoğrafik" }, + { id: "FASHION", label: "Moda", emoji: "👗", desc: "Editöryal çekim", category: "Fotoğrafik" }, + { id: "AERIAL", label: "Havadan", emoji: "🚁", desc: "Drone görüntüsü", category: "Fotoğrafik" }, + { id: "MACRO", label: "Makro", emoji: "🔬", desc: "Yakın çekim", category: "Fotoğrafik" }, + { id: "PORTRAIT", label: "Portre", emoji: "🧑", desc: "Portre fotoğraf", category: "Fotoğrafik" }, ]; const aspectRatios = [ @@ -65,6 +114,26 @@ export default function NewProjectPage() { const [duration, setDuration] = useState(60); const [aspectRatio, setAspectRatio] = useState("PORTRAIT_9_16"); + // Search & Category state + const [styleSearch, setStyleSearch] = useState(""); + const [expandedCategory, setExpandedCategory] = useState("Film & Sinema"); + + const filteredStyles = useMemo(() => { + return videoStyles.filter(s => + s.label.toLowerCase().includes(styleSearch.toLowerCase()) || + s.desc.toLowerCase().includes(styleSearch.toLowerCase()) + ); + }, [styleSearch]); + + const groupedStyles = useMemo(() => { + return filteredStyles.reduce((acc, curr) => { + const cat = curr.category || "Diğer"; + if (!acc[cat]) acc[cat] = []; + acc[cat].push(curr); + return acc; + }, {} as Record); + }, [filteredStyles]); + const canProceed = currentStep === 0 ? topic.trim().length >= 5 : true; const isGenerating = createProject.isPending; @@ -211,27 +280,80 @@ export default function NewProjectPage() { > {/* Video Stili */}
- -
- {videoStyles.map((s) => ( - - ))} +
+ +
+
+ +
+ setStyleSearch(e.target.value)} + className="w-full pl-8 pr-3 py-1.5 bg-[var(--color-bg-surface)] border border-[var(--color-border-faint)] rounded-md text-xs text-[var(--color-text-primary)] placeholder:text-[var(--color-text-ghost)] focus:outline-none focus:border-violet-500/50" + /> +
+
+ +
+ {Object.entries(groupedStyles).map(([category, items]) => { + const isExpanded = styleSearch ? true : expandedCategory === category; + return ( +
+ + + + {isExpanded && ( + +
+ {items.map((s) => ( + + ))} +
+
+ )} +
+
+ ); + })} + {Object.keys(groupedStyles).length === 0 && ( +
+ "{styleSearch}" için sonuç bulunamadı. +
+ )}
diff --git a/src/app/[locale]/(dashboard)/dashboard/x-to-video/page.tsx b/src/app/[locale]/(dashboard)/dashboard/x-to-video/page.tsx index ec0cd41..6e400f1 100644 --- a/src/app/[locale]/(dashboard)/dashboard/x-to-video/page.tsx +++ b/src/app/[locale]/(dashboard)/dashboard/x-to-video/page.tsx @@ -90,11 +90,6 @@ export default function XToVideoPage() { toast.success("Tweet → Video projesi oluşturuldu!"); const projectId = result?.id; if (projectId) { - // Otomatik senaryo üretimini tetikle - const { projectsApi } = await import("@/lib/api/api-service"); - projectsApi.generateScript(projectId).catch((err) => { - console.error("Tweet→Video senaryo üretimi başlatılamadı:", err); - }); router.push(`/dashboard/projects/${projectId}`); } else { router.push("/dashboard/projects"); diff --git a/src/components/project/scene-card.tsx b/src/components/project/scene-card.tsx index 7945b1d..a855f55 100644 --- a/src/components/project/scene-card.tsx +++ b/src/components/project/scene-card.tsx @@ -2,7 +2,7 @@ import { useState } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; -import { Pencil, Check, X, RefreshCw, Clock, ArrowRight, Wand2, Image, Mic } from 'lucide-react'; +import { Pencil, Check, X, RefreshCw, Clock, ArrowRight, Wand2, Image as ImageIcon, Mic, Maximize2 } from 'lucide-react'; interface SceneCardProps { scene: { @@ -19,13 +19,28 @@ interface SceneCardProps { isEditable: boolean; onUpdate?: (sceneId: string, data: { narrationText?: string; visualPrompt?: string; subtitleText?: string }) => void; onRegenerate?: (sceneId: string) => void; + onGenerateImage?: (sceneId: string, customPrompt?: string) => void; + onUpscaleImage?: (sceneId: string) => void; isRegenerating?: boolean; + isGeneratingImage?: boolean; + isUpscalingImage?: boolean; } -export function SceneCard({ scene, isEditable, onUpdate, onRegenerate, isRegenerating }: SceneCardProps) { +export function SceneCard({ + scene, + isEditable, + onUpdate, + onRegenerate, + onGenerateImage, + onUpscaleImage, + isRegenerating, + isGeneratingImage, + isUpscalingImage, +}: SceneCardProps) { const [isEditing, setIsEditing] = useState(false); const [editNarration, setEditNarration] = useState(scene.narrationText); const [editVisual, setEditVisual] = useState(scene.visualPrompt); + const [lightboxOpen, setLightboxOpen] = useState(false); const handleSave = () => { onUpdate?.(scene.id, { @@ -33,6 +48,7 @@ export function SceneCard({ scene, isEditable, onUpdate, onRegenerate, isRegener visualPrompt: editVisual, subtitleText: editNarration, }); + // If user edited visual prompt, and maybe wants to generate, they can click generate visual later. setIsEditing(false); }; @@ -42,6 +58,8 @@ export function SceneCard({ scene, isEditable, onUpdate, onRegenerate, isRegener setIsEditing(false); }; + const thumbnailAsset = scene.mediaAssets?.find(a => a.type === 'THUMBNAIL'); + return (