-
-
- {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 (
- {/* Medya önizleme (varsa) */}
- {scene.mediaAssets && scene.mediaAssets.length > 0 && (
-
- {scene.mediaAssets.slice(0, 3).map((asset) => (
-
- {asset.url ? (
-

- ) : (
-
- )}
+ {/* Görsel / Upscale Alanı */}
+
+ {thumbnailAsset?.url ? (
+
+

setLightboxOpen(true)}
+ />
+
+
- ))}
-
- )}
+
+ ) : (
+
+
+
Görsel Henüz Üretilmedi
+
+ )}
+
+ {isEditable && (
+
+
+ {thumbnailAsset?.url && (
+
+ )}
+
+ )}
+
)}
@@ -191,6 +250,36 @@ export function SceneCard({ scene, isEditable, onUpdate, onRegenerate, isRegener
{/* Sahne bağlantı çizgisi */}
+
+ {/* Lightbox Modal */}
+
+ {lightboxOpen && thumbnailAsset?.url && (
+ setLightboxOpen(false)}
+ className="fixed inset-0 z-[100] flex items-center justify-center bg-black/80 backdrop-blur-sm p-4 md:p-10 cursor-zoom-out"
+ >
+ e.stopPropagation()} // Click image prevents closing
+ />
+
+
+ )}
+
);
}
diff --git a/src/hooks/use-api.ts b/src/hooks/use-api.ts
index 101f7e7..e6979c0 100644
--- a/src/hooks/use-api.ts
+++ b/src/hooks/use-api.ts
@@ -146,6 +146,54 @@ export function useApproveAndQueue() {
});
}
+/** Sahne güncelleme (narrasyon, prompt) */
+export function useUpdateScene() {
+ const qc = useQueryClient();
+ return useMutation({
+ mutationFn: ({ projectId, sceneId, data }: { projectId: string; sceneId: string; data: any }) =>
+ projectsApi.updateScene(projectId, sceneId, data),
+ onSuccess: (updatedScene, variables) => {
+ qc.invalidateQueries({ queryKey: queryKeys.projects.detail(variables.projectId) });
+ },
+ });
+}
+
+/** Sahneyi AI ile yeniden üretme (Senaryo) */
+export function useRegenerateScene() {
+ const qc = useQueryClient();
+ return useMutation({
+ mutationFn: ({ projectId, sceneId }: { projectId: string; sceneId: string }) =>
+ projectsApi.regenerateScene(projectId, sceneId),
+ onSuccess: (updatedScene, variables) => {
+ qc.invalidateQueries({ queryKey: queryKeys.projects.detail(variables.projectId) });
+ },
+ });
+}
+
+/** Sahne görseli oluştur (Gemini) */
+export function useGenerateSceneImage() {
+ const qc = useQueryClient();
+ return useMutation({
+ mutationFn: ({ projectId, sceneId, customPrompt }: { projectId: string; sceneId: string; customPrompt?: string }) =>
+ projectsApi.generateSceneImage(projectId, sceneId, customPrompt),
+ onSuccess: (updatedScene, variables) => {
+ qc.invalidateQueries({ queryKey: queryKeys.projects.detail(variables.projectId) });
+ },
+ });
+}
+
+/** Sahne görselini upscale (4K) yap */
+export function useUpscaleSceneImage() {
+ const qc = useQueryClient();
+ return useMutation({
+ mutationFn: ({ projectId, sceneId }: { projectId: string; sceneId: string }) =>
+ projectsApi.upscaleSceneImage(projectId, sceneId),
+ onSuccess: (updatedScene, variables) => {
+ qc.invalidateQueries({ queryKey: queryKeys.projects.detail(variables.projectId) });
+ },
+ });
+}
+
// ═══════════════════════════════════════════════════════
// CREDITS — Kredi hook'ları
// ═══════════════════════════════════════════════════════
diff --git a/src/lib/api/api-service.ts b/src/lib/api/api-service.ts
index e6d962a..44a6cd8 100644
--- a/src/lib/api/api-service.ts
+++ b/src/lib/api/api-service.ts
@@ -299,6 +299,12 @@ export const projectsApi = {
regenerateScene: (projectId: string, sceneId: string) =>
apiClient.post
(`/projects/${projectId}/scenes/${sceneId}/regenerate`).then((r) => r.data),
+
+ generateSceneImage: (projectId: string, sceneId: string, customPrompt?: string) =>
+ apiClient.post(`/projects/${projectId}/scenes/${sceneId}/generate-image`, { customPrompt }).then((r) => r.data),
+
+ upscaleSceneImage: (projectId: string, sceneId: string) =>
+ apiClient.post(`/projects/${projectId}/scenes/${sceneId}/upscale-image`).then((r) => r.data),
};
// Backend path: /billing/credits/balance (billing controller prefix)
diff --git a/test-fe-api.js b/test-fe-api.js
new file mode 100644
index 0000000..816ae96
--- /dev/null
+++ b/test-fe-api.js
@@ -0,0 +1,31 @@
+const axios = require('axios');
+
+async function test() {
+ const login = await axios.post('http://localhost:3000/api/auth/login', {
+ email: 'admin@contentgen.ai',
+ password: 'Admin123!',
+ });
+ const token = login.data.data.accessToken;
+
+ // Simulate interceptor
+ let responseData = await axios.get('http://localhost:3000/api/projects', {
+ headers: { Authorization: `Bearer ${token}` }
+ }).then(r => {
+ let data = r.data;
+ if (data && typeof data === 'object' && 'success' in data && 'data' in data) {
+ data = data.data;
+ }
+ return data;
+ });
+
+ console.log("Interceptor Output:", JSON.stringify(responseData, null, 2).substring(0, 500));
+
+ const raw = responseData;
+ const rawProjects = raw?.data ?? raw ?? [];
+ const projects = Array.isArray(rawProjects) ? rawProjects : [];
+
+ console.log("Is array?", Array.isArray(rawProjects));
+ console.log("Projects length:", projects.length);
+}
+
+test();
diff --git a/test-frontend-useProjects.js b/test-frontend-useProjects.js
new file mode 100644
index 0000000..f7d79a4
--- /dev/null
+++ b/test-frontend-useProjects.js
@@ -0,0 +1,47 @@
+import axios from 'axios';
+
+// Bu bir simulasyon, frontend API klientinin nasil calistigini gosterecek.
+const client = axios.create({ baseURL: 'http://localhost:3000/api' });
+client.interceptors.response.use(
+ (response) => {
+ if (
+ response.data &&
+ typeof response.data === 'object' &&
+ 'success' in response.data &&
+ 'data' in response.data
+ ) {
+ response.data = response.data.data;
+ }
+ return response;
+ }
+);
+
+async function test() {
+ const loginRes = await axios.post('http://localhost:3000/api/auth/login', {
+ email: 'admin@contentgen.ai',
+ password: 'Admin123!'
+ });
+ const token = loginRes.data.accessToken;
+
+ if (!token) {
+ console.log("No token");
+ return;
+ }
+
+ client.interceptors.request.use((config) => {
+ config.headers['Authorization'] = `Bearer ${token}`;
+ return config;
+ });
+
+ const res = await client.get('/projects?limit=100');
+ const rData = res.data;
+ console.log("rData structure:", typeof rData, Object.keys(rData));
+
+ const raw = rData;
+ const rawProjects = raw?.data ?? raw ?? [];
+ const projects = Array.isArray(rawProjects) ? rawProjects : [];
+
+ console.log("Final extracted projects count:", projects.length);
+}
+
+test().catch(console.error);