generated from fahricansecer/boilerplate-fe
main
Some checks failed
UI Deploy (Next-Auth Support) 🎨 / build-and-deploy (push) Has been cancelled
Some checks failed
UI Deploy (Next-Auth Support) 🎨 / build-and-deploy (push) Has been cancelled
This commit is contained in:
2
next-env.d.ts
vendored
2
next-env.d.ts
vendored
@@ -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.
|
||||||
|
|||||||
@@ -1,231 +1,268 @@
|
|||||||
"use client";
|
"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 { 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({
|
export default function SignInPage() {
|
||||||
email: yup.string().email().required(),
|
|
||||||
password: yup.string().required(),
|
|
||||||
});
|
|
||||||
|
|
||||||
type SignInForm = yup.InferType<typeof schema>;
|
|
||||||
|
|
||||||
const defaultValues = {
|
|
||||||
email: "test@test.com.ue",
|
|
||||||
password: "test1234",
|
|
||||||
};
|
|
||||||
|
|
||||||
function SignInPage() {
|
|
||||||
const t = useTranslations();
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const [email, setEmail] = useState("");
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
|
||||||
const {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
handleSubmit,
|
e.preventDefault();
|
||||||
register,
|
setError("");
|
||||||
formState: { errors },
|
|
||||||
} = useForm<SignInForm>({
|
if (!email || !password) {
|
||||||
resolver: yupResolver(schema),
|
setError("Email ve şifre gereklidir.");
|
||||||
mode: "onChange",
|
return;
|
||||||
defaultValues,
|
}
|
||||||
});
|
|
||||||
|
|
||||||
const onSubmit = async (formData: SignInForm) => {
|
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const res = await signIn("credentials", {
|
const res = await signIn("credentials", {
|
||||||
redirect: false,
|
redirect: false,
|
||||||
email: formData.email,
|
email,
|
||||||
password: formData.password,
|
password,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res?.error) {
|
if (res?.error) {
|
||||||
throw new Error(res.error);
|
setError("Giriş başarısız. Email veya şifrenizi kontrol edin.");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
router.replace("/home");
|
router.replace("/dashboard");
|
||||||
} catch (error) {
|
} catch {
|
||||||
toaster.error({
|
setError("Bağlantı hatası. Lütfen tekrar deneyin.");
|
||||||
title: (error as Error).message || "Giriş yaparken hata oluştu!",
|
|
||||||
type: "error",
|
|
||||||
});
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box position="relative">
|
<div
|
||||||
<Flex
|
style={{
|
||||||
h={{ sm: "initial", md: "75vh", lg: "85vh" }}
|
minHeight: "100vh",
|
||||||
w="100%"
|
display: "flex",
|
||||||
maxW="1044px"
|
alignItems: "center",
|
||||||
mx="auto"
|
justifyContent: "center",
|
||||||
justifyContent="space-between"
|
background: "linear-gradient(135deg, #0a0a14 0%, #0d0d1f 50%, #0a0a14 100%)",
|
||||||
mb="30px"
|
padding: "1rem",
|
||||||
pt={{ sm: "100px", md: "0px" }}
|
}}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
maxWidth: "420px",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Flex
|
{/* Logo */}
|
||||||
as="form"
|
<div style={{ textAlign: "center", marginBottom: "2rem" }}>
|
||||||
onSubmit={handleSubmit(onSubmit)}
|
<div
|
||||||
alignItems="center"
|
style={{
|
||||||
justifyContent="start"
|
width: 56,
|
||||||
style={{ userSelect: "none" }}
|
height: 56,
|
||||||
w={{ base: "100%", md: "50%", lg: "42%" }}
|
borderRadius: 16,
|
||||||
>
|
background: "linear-gradient(135deg, #8b5cf6, #06b6d4)",
|
||||||
<Flex
|
display: "inline-flex",
|
||||||
direction="column"
|
alignItems: "center",
|
||||||
w="100%"
|
justifyContent: "center",
|
||||||
background="transparent"
|
marginBottom: "1rem",
|
||||||
p="10"
|
boxShadow: "0 8px 32px rgba(139, 92, 246, 0.3)",
|
||||||
mt={{ md: "150px", lg: "80px" }}
|
}}
|
||||||
>
|
>
|
||||||
<Heading
|
<Sparkles size={24} color="white" />
|
||||||
color={{ base: "primary.400", _dark: "primary.200" }}
|
</div>
|
||||||
fontSize="32px"
|
<h1
|
||||||
mb="10px"
|
style={{
|
||||||
fontWeight="bold"
|
fontSize: "1.75rem",
|
||||||
>
|
fontWeight: 800,
|
||||||
{t("auth.welcome-back")}
|
color: "#f0f0f5",
|
||||||
</Heading>
|
letterSpacing: "-0.02em",
|
||||||
<Text
|
marginBottom: "0.5rem",
|
||||||
mb="36px"
|
}}
|
||||||
ms="4px"
|
>
|
||||||
color={{ base: "gray.400", _dark: "white" }}
|
ContentGen AI
|
||||||
fontWeight="bold"
|
</h1>
|
||||||
fontSize="14px"
|
<p style={{ fontSize: "0.875rem", color: "#6b7280" }}>
|
||||||
>
|
Hesabınıza giriş yapın
|
||||||
{t("auth.subtitle")}
|
</p>
|
||||||
</Text>
|
</div>
|
||||||
<Field
|
|
||||||
mb="24px"
|
{/* Form Card */}
|
||||||
label={t("email")}
|
<div
|
||||||
errorText={errors.email?.message}
|
style={{
|
||||||
invalid={!!errors.email}
|
background: "rgba(255,255,255,0.03)",
|
||||||
>
|
border: "1px solid rgba(255,255,255,0.06)",
|
||||||
<InputGroup w="full" startElement={<MdMail size="1rem" />}>
|
borderRadius: 20,
|
||||||
<Input
|
padding: "2rem",
|
||||||
borderRadius="15px"
|
backdropFilter: "blur(20px)",
|
||||||
fontSize="sm"
|
}}
|
||||||
type="text"
|
|
||||||
placeholder={t("email")}
|
|
||||||
size="lg"
|
|
||||||
{...register("email")}
|
|
||||||
/>
|
|
||||||
</InputGroup>
|
|
||||||
</Field>
|
|
||||||
<Field
|
|
||||||
mb="24px"
|
|
||||||
label={t("password")}
|
|
||||||
errorText={errors.password?.message}
|
|
||||||
invalid={!!errors.password}
|
|
||||||
>
|
|
||||||
<InputGroup w="full" startElement={<BiLock size="1rem" />}>
|
|
||||||
<PasswordInput
|
|
||||||
borderRadius="15px"
|
|
||||||
fontSize="sm"
|
|
||||||
placeholder={t("password")}
|
|
||||||
size="lg"
|
|
||||||
{...register("password")}
|
|
||||||
/>
|
|
||||||
</InputGroup>
|
|
||||||
</Field>
|
|
||||||
<Field mb="24px">
|
|
||||||
<Switch colorPalette="teal" label={t("auth.remember-me")}>
|
|
||||||
{t("auth.remember-me")}
|
|
||||||
</Switch>
|
|
||||||
</Field>
|
|
||||||
<Field mb="24px">
|
|
||||||
<ClientOnly fallback={<Skeleton height="45px" width="100%" />}>
|
|
||||||
<Button
|
|
||||||
loading={loading}
|
|
||||||
type="submit"
|
|
||||||
bg="primary.400"
|
|
||||||
w="100%"
|
|
||||||
h="45px"
|
|
||||||
color="white"
|
|
||||||
_hover={{
|
|
||||||
bg: "primary.500",
|
|
||||||
}}
|
|
||||||
_active={{
|
|
||||||
bg: "primary.400",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t("auth.sign-in")}
|
|
||||||
</Button>
|
|
||||||
</ClientOnly>
|
|
||||||
</Field>
|
|
||||||
<Flex
|
|
||||||
flexDirection="column"
|
|
||||||
justifyContent="center"
|
|
||||||
alignItems="center"
|
|
||||||
maxW="100%"
|
|
||||||
>
|
|
||||||
<Text
|
|
||||||
color={{ base: "gray.400", _dark: "white" }}
|
|
||||||
fontWeight="medium"
|
|
||||||
>
|
|
||||||
{t("auth.dont-have-account")}
|
|
||||||
<ChakraLink
|
|
||||||
as={Link}
|
|
||||||
href="/signup"
|
|
||||||
color={{ base: "primary.400", _dark: "primary.200" }}
|
|
||||||
ms="5px"
|
|
||||||
fontWeight="bold"
|
|
||||||
focusRing="none"
|
|
||||||
>
|
|
||||||
{t("auth.sign-up")}
|
|
||||||
</ChakraLink>
|
|
||||||
</Text>
|
|
||||||
</Flex>
|
|
||||||
</Flex>
|
|
||||||
</Flex>
|
|
||||||
<Box
|
|
||||||
display={{ base: "none", md: "block" }}
|
|
||||||
overflowX="hidden"
|
|
||||||
h="100%"
|
|
||||||
w="40vw"
|
|
||||||
position="absolute"
|
|
||||||
right="0px"
|
|
||||||
>
|
>
|
||||||
<Box
|
<form onSubmit={handleSubmit}>
|
||||||
bgImage={`url(${signInImage.src})`}
|
{/* Error */}
|
||||||
w="100%"
|
{error && (
|
||||||
h="100%"
|
<div
|
||||||
bgSize="cover"
|
style={{
|
||||||
bgPos="50%"
|
padding: "0.75rem 1rem",
|
||||||
position="absolute"
|
borderRadius: 12,
|
||||||
borderBottomLeftRadius="20px"
|
background: "rgba(239, 68, 68, 0.1)",
|
||||||
/>
|
border: "1px solid rgba(239, 68, 68, 0.2)",
|
||||||
</Box>
|
color: "#f87171",
|
||||||
</Flex>
|
fontSize: "0.8125rem",
|
||||||
</Box>
|
marginBottom: "1.25rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Email */}
|
||||||
|
<div style={{ marginBottom: "1.25rem" }}>
|
||||||
|
<label
|
||||||
|
style={{
|
||||||
|
display: "block",
|
||||||
|
fontSize: "0.8125rem",
|
||||||
|
fontWeight: 600,
|
||||||
|
color: "#d1d5db",
|
||||||
|
marginBottom: "0.5rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
E-posta
|
||||||
|
</label>
|
||||||
|
<div style={{ position: "relative" }}>
|
||||||
|
<Mail
|
||||||
|
size={16}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: 14,
|
||||||
|
top: "50%",
|
||||||
|
transform: "translateY(-50%)",
|
||||||
|
color: "#6b7280",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => 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)")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Password */}
|
||||||
|
<div style={{ marginBottom: "1.5rem" }}>
|
||||||
|
<label
|
||||||
|
style={{
|
||||||
|
display: "block",
|
||||||
|
fontSize: "0.8125rem",
|
||||||
|
fontWeight: 600,
|
||||||
|
color: "#d1d5db",
|
||||||
|
marginBottom: "0.5rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Şifre
|
||||||
|
</label>
|
||||||
|
<div style={{ position: "relative" }}>
|
||||||
|
<Lock
|
||||||
|
size={16}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: 14,
|
||||||
|
top: "50%",
|
||||||
|
transform: "translateY(-50%)",
|
||||||
|
color: "#6b7280",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => 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)")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Submit Button */}
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={loading}
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
padding: "0.875rem",
|
||||||
|
borderRadius: 14,
|
||||||
|
border: "none",
|
||||||
|
background: "linear-gradient(135deg, #8b5cf6, #7c3aed)",
|
||||||
|
color: "white",
|
||||||
|
fontSize: "0.9375rem",
|
||||||
|
fontWeight: 700,
|
||||||
|
cursor: loading ? "not-allowed" : "pointer",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
gap: "0.5rem",
|
||||||
|
opacity: loading ? 0.7 : 1,
|
||||||
|
transition: "opacity 0.2s, transform 0.1s",
|
||||||
|
boxShadow: "0 4px 16px rgba(139, 92, 246, 0.3)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<Loader2 size={18} className="animate-spin" />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
Giriş Yap
|
||||||
|
<ArrowRight size={16} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer text */}
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
textAlign: "center",
|
||||||
|
fontSize: "0.75rem",
|
||||||
|
color: "#4b5563",
|
||||||
|
marginTop: "1.5rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
ContentGen AI Studio © 2026
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SignInPage;
|
|
||||||
|
|||||||
@@ -18,7 +18,16 @@ import {
|
|||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useState } from 'react';
|
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 { useRenderProgress } from '@/hooks/use-render-progress';
|
||||||
import { SceneCard } from '@/components/project/scene-card';
|
import { SceneCard } from '@/components/project/scene-card';
|
||||||
import { RenderProgress } from '@/components/project/render-progress';
|
import { RenderProgress } from '@/components/project/render-progress';
|
||||||
@@ -42,16 +51,61 @@ const STATUS_MAP: Record<string, { label: string; color: string; icon: React.Ele
|
|||||||
FAILED: { label: 'Başarısız', color: 'text-red-400', icon: AlertCircle, bgClass: 'bg-red-500/10 border-red-500/20' },
|
FAILED: { label: 'Başarısız', color: 'text-red-400', icon: AlertCircle, bgClass: 'bg-red-500/10 border-red-500/20' },
|
||||||
};
|
};
|
||||||
|
|
||||||
const STYLE_LABELS: Record<string, string> = {
|
const videoStyles = [
|
||||||
CINEMATIC: '🎬 Sinematik',
|
// Film & Sinema
|
||||||
DOCUMENTARY: '📹 Belgesel',
|
{ id: "CINEMATIC", label: "Sinematik", emoji: "🎬", desc: "Film kalitesinde görseller", category: "Film & Sinema" },
|
||||||
EDUCATIONAL: '📚 Eğitici',
|
{ id: "DOCUMENTARY", label: "Belgesel", emoji: "📹", desc: "Bilimsel ve ciddi ton", category: "Film & Sinema" },
|
||||||
STORYTELLING: '📖 Hikaye',
|
{ id: "STORYTELLING", label: "Hikâye Anlatımı", emoji: "📖", desc: "Anlatı odaklı, sürükleyici", category: "Film & Sinema" },
|
||||||
NEWS: '📰 Haber',
|
{ id: "NEWS", label: "Haber", emoji: "📰", desc: "Güncel ve bilgilendirici", category: "Film & Sinema" },
|
||||||
PROMOTIONAL: '📢 Tanıtım',
|
{ id: "ARTISTIC", label: "Sanatsal", emoji: "🎨", desc: "Yaratıcı ve sıra dışı", category: "Film & Sinema" },
|
||||||
ARTISTIC: '🎨 Sanatsal',
|
{ id: "NOIR", label: "Film Noir", emoji: "🖤", desc: "Karanlık, dramatik", category: "Film & Sinema" },
|
||||||
MINIMALIST: '✨ Minimalist',
|
{ 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 = {
|
const stagger = {
|
||||||
hidden: { opacity: 0 },
|
hidden: { opacity: 0 },
|
||||||
@@ -75,6 +129,13 @@ export default function ProjectDetailPage() {
|
|||||||
const approveMutation = useApproveAndQueue();
|
const approveMutation = useApproveAndQueue();
|
||||||
const deleteMutation = useDeleteProject();
|
const deleteMutation = useDeleteProject();
|
||||||
|
|
||||||
|
const generateImageMutation = useGenerateSceneImage();
|
||||||
|
const upscaleImageMutation = useUpscaleSceneImage();
|
||||||
|
const regenerateSceneMutation = useRegenerateScene();
|
||||||
|
|
||||||
|
const [generatingImageId, setGeneratingImageId] = useState<string | null>(null);
|
||||||
|
const [upscalingImageId, setUpscalingImageId] = useState<string | null>(null);
|
||||||
|
|
||||||
// WebSocket progress
|
// WebSocket progress
|
||||||
const renderState = useRenderProgress(
|
const renderState = useRenderProgress(
|
||||||
project?.status && ['PENDING', 'GENERATING_MEDIA', 'RENDERING'].includes(project.status) ? id : undefined,
|
project?.status && ['PENDING', 'GENERATING_MEDIA', 'RENDERING'].includes(project.status) ? id : undefined,
|
||||||
@@ -93,17 +154,37 @@ export default function ProjectDetailPage() {
|
|||||||
// Sahne yeniden üretim
|
// Sahne yeniden üretim
|
||||||
const handleSceneRegenerate = async (sceneId: string) => {
|
const handleSceneRegenerate = async (sceneId: string) => {
|
||||||
setRegeneratingSceneId(sceneId);
|
setRegeneratingSceneId(sceneId);
|
||||||
try {
|
regenerateSceneMutation.mutate(
|
||||||
await fetch(`${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000/api'}/projects/${id}/scenes/${sceneId}/regenerate`, {
|
{ projectId: id, sceneId },
|
||||||
method: 'POST',
|
{
|
||||||
headers: { 'Content-Type': 'application/json' },
|
onSettled: () => setRegeneratingSceneId(null),
|
||||||
});
|
onSuccess: () => refetch(),
|
||||||
refetch();
|
}
|
||||||
} catch (err) {
|
);
|
||||||
console.error('Sahne yeniden üretim hatası:', err);
|
};
|
||||||
} finally {
|
|
||||||
setRegeneratingSceneId(null);
|
// 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
|
// Senaryo üret
|
||||||
@@ -161,12 +242,24 @@ export default function ProjectDetailPage() {
|
|||||||
|
|
||||||
const statusInfo = STATUS_MAP[project.status] || STATUS_MAP.DRAFT;
|
const statusInfo = STATUS_MAP[project.status] || STATUS_MAP.DRAFT;
|
||||||
const StatusIcon = statusInfo.icon;
|
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 hasScript = project.scenes && project.scenes.length > 0;
|
||||||
const isRendering = ['PENDING', 'GENERATING_MEDIA', 'RENDERING'].includes(project.status);
|
|
||||||
const isCompleted = project.status === 'COMPLETED';
|
const isCompleted = project.status === 'COMPLETED';
|
||||||
const tweetData = project.sourceTweetData as Record<string, any> | undefined;
|
const tweetData = project.sourceTweetData as Record<string, any> | 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 (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
variants={stagger}
|
variants={stagger}
|
||||||
@@ -246,7 +339,21 @@ export default function ProjectDetailPage() {
|
|||||||
<span className="flex items-center gap-1">
|
<span className="flex items-center gap-1">
|
||||||
<Clock size={12} /> {project.targetDuration}s
|
<Clock size={12} /> {project.targetDuration}s
|
||||||
</span>
|
</span>
|
||||||
<span>{STYLE_LABELS[project.videoStyle] || project.videoStyle}</span>
|
{isEditable ? (
|
||||||
|
<select
|
||||||
|
value={project.videoStyle}
|
||||||
|
onChange={(e) => handleStyleChange(e.target.value)}
|
||||||
|
className="bg-[var(--color-bg-base)] border border-[var(--color-border-faint)] rounded-md px-2 py-0.5 text-xs text-[var(--color-text-secondary)] focus:outline-none focus:border-violet-500/50 cursor-pointer"
|
||||||
|
>
|
||||||
|
{videoStyles.map((s) => (
|
||||||
|
<option key={s.id} value={s.id}>
|
||||||
|
{s.emoji} {s.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
) : (
|
||||||
|
<span>{currentStyle ? `${currentStyle.emoji} ${currentStyle.label}` : project.videoStyle}</span>
|
||||||
|
)}
|
||||||
<span className="uppercase text-[10px] tracking-wider">{project.language}</span>
|
<span className="uppercase text-[10px] tracking-wider">{project.language}</span>
|
||||||
<span className="text-[10px]">
|
<span className="text-[10px]">
|
||||||
{new Date(project.createdAt).toLocaleDateString('tr-TR', {
|
{new Date(project.createdAt).toLocaleDateString('tr-TR', {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState, useMemo } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
import {
|
import {
|
||||||
@@ -16,6 +16,8 @@ import {
|
|||||||
Loader2,
|
Loader2,
|
||||||
Check,
|
Check,
|
||||||
Wand2,
|
Wand2,
|
||||||
|
Search,
|
||||||
|
ChevronDown,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
@@ -37,12 +39,59 @@ const languages = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const videoStyles = [
|
const videoStyles = [
|
||||||
{ id: "CINEMATIC", label: "Sinematik", emoji: "🎬", desc: "Film kalitesinde görseller" },
|
// Film & Sinema
|
||||||
{ id: "DOCUMENTARY", label: "Belgesel", emoji: "📹", desc: "Bilimsel ve ciddi ton" },
|
{ id: "CINEMATIC", label: "Sinematik", emoji: "🎬", desc: "Film kalitesinde görseller", category: "Film & Sinema" },
|
||||||
{ id: "EDUCATIONAL", label: "Eğitim", emoji: "📚", desc: "Öğretici ve açıklayıcı" },
|
{ id: "DOCUMENTARY", label: "Belgesel", emoji: "📹", desc: "Bilimsel ve ciddi ton", category: "Film & Sinema" },
|
||||||
{ id: "STORYTELLING", label: "Hikaye", emoji: "📖", desc: "Anlatı odaklı, sürükleyici" },
|
{ 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" },
|
{ 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ışı" },
|
{ 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 = [
|
const aspectRatios = [
|
||||||
@@ -65,6 +114,26 @@ export default function NewProjectPage() {
|
|||||||
const [duration, setDuration] = useState(60);
|
const [duration, setDuration] = useState(60);
|
||||||
const [aspectRatio, setAspectRatio] = useState("PORTRAIT_9_16");
|
const [aspectRatio, setAspectRatio] = useState("PORTRAIT_9_16");
|
||||||
|
|
||||||
|
// Search & Category state
|
||||||
|
const [styleSearch, setStyleSearch] = useState("");
|
||||||
|
const [expandedCategory, setExpandedCategory] = useState<string | null>("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<string, typeof videoStyles>);
|
||||||
|
}, [filteredStyles]);
|
||||||
|
|
||||||
const canProceed = currentStep === 0 ? topic.trim().length >= 5 : true;
|
const canProceed = currentStep === 0 ? topic.trim().length >= 5 : true;
|
||||||
const isGenerating = createProject.isPending;
|
const isGenerating = createProject.isPending;
|
||||||
|
|
||||||
@@ -211,27 +280,80 @@ export default function NewProjectPage() {
|
|||||||
>
|
>
|
||||||
{/* Video Stili */}
|
{/* Video Stili */}
|
||||||
<div>
|
<div>
|
||||||
<label className="text-sm font-medium text-[var(--color-text-secondary)] mb-3 block">
|
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2 mb-3">
|
||||||
<Palette size={14} className="inline mr-1.5 text-violet-400" />
|
<label className="text-sm font-medium text-[var(--color-text-secondary)] block">
|
||||||
Video Stili
|
<Palette size={14} className="inline mr-1.5 text-violet-400" />
|
||||||
</label>
|
Video Stili
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2">
|
</label>
|
||||||
{videoStyles.map((s) => (
|
<div className="relative w-full sm:w-56">
|
||||||
<button
|
<div className="absolute inset-y-0 left-0 pl-2.5 flex items-center pointer-events-none">
|
||||||
key={s.id}
|
<Search size={14} className="text-[var(--color-text-ghost)]" />
|
||||||
onClick={() => setStyle(s.id)}
|
</div>
|
||||||
className={cn(
|
<input
|
||||||
"flex flex-col items-start gap-1 p-3 rounded-xl text-left transition-all",
|
type="text"
|
||||||
style === s.id
|
placeholder="Stil ara..."
|
||||||
? "bg-violet-500/12 border border-violet-500/30 glow-violet"
|
value={styleSearch}
|
||||||
: "bg-[var(--color-bg-surface)] border border-[var(--color-border-faint)] hover:border-[var(--color-border-default)]"
|
onChange={(e) => 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"
|
||||||
>
|
/>
|
||||||
<span className="text-xl mb-0.5">{s.emoji}</span>
|
</div>
|
||||||
<span className="text-sm font-medium">{s.label}</span>
|
</div>
|
||||||
<span className="text-[10px] text-[var(--color-text-ghost)]">{s.desc}</span>
|
|
||||||
</button>
|
<div className="space-y-3 max-h-[360px] overflow-y-auto pr-1 custom-scrollbar">
|
||||||
))}
|
{Object.entries(groupedStyles).map(([category, items]) => {
|
||||||
|
const isExpanded = styleSearch ? true : expandedCategory === category;
|
||||||
|
return (
|
||||||
|
<div key={category} className="bg-[var(--color-bg-surface)] border border-[var(--color-border-faint)] rounded-xl overflow-hidden">
|
||||||
|
<button
|
||||||
|
onClick={() => !styleSearch && setExpandedCategory(isExpanded ? null : category)}
|
||||||
|
className="w-full flex items-center justify-between p-3 bg-[var(--color-bg-elevated)] hover:bg-[var(--color-bg-surface-hover)] transition-colors"
|
||||||
|
>
|
||||||
|
<span className="text-sm font-medium text-[var(--color-text-secondary)]">{category} <span className="text-[11px] text-[var(--color-text-ghost)] ml-1">({items.length})</span></span>
|
||||||
|
{!styleSearch && (
|
||||||
|
<ChevronDown
|
||||||
|
size={16}
|
||||||
|
className={cn("text-[var(--color-text-ghost)] transition-transform duration-200", isExpanded && "rotate-180")}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<AnimatePresence initial={false}>
|
||||||
|
{isExpanded && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ height: 0, opacity: 0 }}
|
||||||
|
animate={{ height: "auto", opacity: 1 }}
|
||||||
|
exit={{ height: 0, opacity: 0 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
>
|
||||||
|
<div className="p-3 pt-0 mt-3 grid grid-cols-2 sm:grid-cols-3 gap-2">
|
||||||
|
{items.map((s) => (
|
||||||
|
<button
|
||||||
|
key={s.id}
|
||||||
|
onClick={() => setStyle(s.id)}
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col items-start gap-1 p-2.5 rounded-xl text-left transition-all",
|
||||||
|
style === s.id
|
||||||
|
? "bg-violet-500/12 border border-violet-500/30 glow-violet"
|
||||||
|
: "bg-[var(--color-bg-base)] border border-[var(--color-border-faint)] hover:border-[var(--color-border-default)]"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span className="text-xl mb-0.5">{s.emoji}</span>
|
||||||
|
<span className="text-xs font-semibold leading-tight">{s.label}</span>
|
||||||
|
<span className="text-[10px] text-[var(--color-text-ghost)] leading-tight">{s.desc}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{Object.keys(groupedStyles).length === 0 && (
|
||||||
|
<div className="text-center py-8 text-[var(--color-text-ghost)] text-sm">
|
||||||
|
"{styleSearch}" için sonuç bulunamadı.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -90,11 +90,6 @@ export default function XToVideoPage() {
|
|||||||
toast.success("Tweet → Video projesi oluşturuldu!");
|
toast.success("Tweet → Video projesi oluşturuldu!");
|
||||||
const projectId = result?.id;
|
const projectId = result?.id;
|
||||||
if (projectId) {
|
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}`);
|
router.push(`/dashboard/projects/${projectId}`);
|
||||||
} else {
|
} else {
|
||||||
router.push("/dashboard/projects");
|
router.push("/dashboard/projects");
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
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 {
|
interface SceneCardProps {
|
||||||
scene: {
|
scene: {
|
||||||
@@ -19,13 +19,28 @@ interface SceneCardProps {
|
|||||||
isEditable: boolean;
|
isEditable: boolean;
|
||||||
onUpdate?: (sceneId: string, data: { narrationText?: string; visualPrompt?: string; subtitleText?: string }) => void;
|
onUpdate?: (sceneId: string, data: { narrationText?: string; visualPrompt?: string; subtitleText?: string }) => void;
|
||||||
onRegenerate?: (sceneId: string) => void;
|
onRegenerate?: (sceneId: string) => void;
|
||||||
|
onGenerateImage?: (sceneId: string, customPrompt?: string) => void;
|
||||||
|
onUpscaleImage?: (sceneId: string) => void;
|
||||||
isRegenerating?: boolean;
|
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 [isEditing, setIsEditing] = useState(false);
|
||||||
const [editNarration, setEditNarration] = useState(scene.narrationText);
|
const [editNarration, setEditNarration] = useState(scene.narrationText);
|
||||||
const [editVisual, setEditVisual] = useState(scene.visualPrompt);
|
const [editVisual, setEditVisual] = useState(scene.visualPrompt);
|
||||||
|
const [lightboxOpen, setLightboxOpen] = useState(false);
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
onUpdate?.(scene.id, {
|
onUpdate?.(scene.id, {
|
||||||
@@ -33,6 +48,7 @@ export function SceneCard({ scene, isEditable, onUpdate, onRegenerate, isRegener
|
|||||||
visualPrompt: editVisual,
|
visualPrompt: editVisual,
|
||||||
subtitleText: editNarration,
|
subtitleText: editNarration,
|
||||||
});
|
});
|
||||||
|
// If user edited visual prompt, and maybe wants to generate, they can click generate visual later.
|
||||||
setIsEditing(false);
|
setIsEditing(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -42,6 +58,8 @@ export function SceneCard({ scene, isEditable, onUpdate, onRegenerate, isRegener
|
|||||||
setIsEditing(false);
|
setIsEditing(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const thumbnailAsset = scene.mediaAssets?.find(a => a.type === 'THUMBNAIL');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
layout
|
layout
|
||||||
@@ -119,7 +137,7 @@ export function SceneCard({ scene, isEditable, onUpdate, onRegenerate, isRegener
|
|||||||
{/* Görsel prompt düzenleme */}
|
{/* Görsel prompt düzenleme */}
|
||||||
<div>
|
<div>
|
||||||
<label className="flex items-center gap-1.5 text-xs font-medium text-[var(--color-text-muted)] mb-1.5">
|
<label className="flex items-center gap-1.5 text-xs font-medium text-[var(--color-text-muted)] mb-1.5">
|
||||||
<Image size={12} /> Görsel Prompt
|
<ImageIcon size={12} /> Görsel Prompt
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
value={editVisual}
|
value={editVisual}
|
||||||
@@ -160,30 +178,71 @@ export function SceneCard({ scene, isEditable, onUpdate, onRegenerate, isRegener
|
|||||||
{/* Görsel Prompt */}
|
{/* Görsel Prompt */}
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<div className="w-5 h-5 rounded-md bg-cyan-500/10 flex items-center justify-center shrink-0 mt-0.5">
|
<div className="w-5 h-5 rounded-md bg-cyan-500/10 flex items-center justify-center shrink-0 mt-0.5">
|
||||||
<Image size={11} className="text-cyan-400" />
|
<ImageIcon size={11} className="text-cyan-400" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 group/prompt relative">
|
||||||
|
<p className="text-xs text-[var(--color-text-ghost)] leading-relaxed italic pr-6">
|
||||||
|
{scene.visualPrompt}
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
navigator.clipboard.writeText(scene.visualPrompt);
|
||||||
|
}}
|
||||||
|
className="absolute top-0 right-0 p-1 opacity-0 group-hover/prompt:opacity-100 transition-opacity bg-[var(--color-bg-elevated)] rounded-md text-[var(--color-text-muted)] hover:text-cyan-400 hover:bg-cyan-500/10"
|
||||||
|
title="Prompt'u Kopyala"
|
||||||
|
>
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<rect width="14" height="14" x="8" y="8" rx="2" ry="2" />
|
||||||
|
<path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-[var(--color-text-ghost)] leading-relaxed italic">
|
|
||||||
{scene.visualPrompt}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Medya önizleme (varsa) */}
|
{/* Görsel / Upscale Alanı */}
|
||||||
{scene.mediaAssets && scene.mediaAssets.length > 0 && (
|
<div className="flex flex-col gap-2 pt-2">
|
||||||
<div className="flex gap-2 pt-1">
|
{thumbnailAsset?.url ? (
|
||||||
{scene.mediaAssets.slice(0, 3).map((asset) => (
|
<div className="relative group/thumb rounded-lg overflow-hidden border border-[var(--color-border-faint)] aspect-video max-w-sm">
|
||||||
<div
|
<img
|
||||||
key={asset.id}
|
src={thumbnailAsset.url}
|
||||||
className="w-16 h-16 rounded-lg bg-[var(--color-bg-deep)] border border-[var(--color-border-faint)] flex items-center justify-center overflow-hidden"
|
alt="Scene Thumbnail"
|
||||||
>
|
className="w-full h-full object-cover cursor-pointer hover:scale-105 transition-transform duration-500"
|
||||||
{asset.url ? (
|
onClick={() => setLightboxOpen(true)}
|
||||||
<img src={asset.url} alt="" className="w-full h-full object-cover" />
|
/>
|
||||||
) : (
|
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover/thumb:opacity-100 transition-opacity flex items-center justify-center pointer-events-none">
|
||||||
<Wand2 size={14} className="text-[var(--color-text-ghost)]" />
|
<Maximize2 size={24} className="text-white" />
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
</div>
|
||||||
</div>
|
) : (
|
||||||
)}
|
<div className="rounded-lg border border-dashed border-[var(--color-border-faint)] bg-[var(--color-bg-deep)] aspect-video max-w-sm flex flex-col items-center justify-center p-4">
|
||||||
|
<ImageIcon size={24} className="text-[var(--color-text-ghost)] mb-2" />
|
||||||
|
<p className="text-xs text-[var(--color-text-muted)] text-center">Görsel Henüz Üretilmedi</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isEditable && (
|
||||||
|
<div className="flex items-center gap-2 mt-1">
|
||||||
|
<button
|
||||||
|
onClick={() => onGenerateImage?.(scene.id, scene.visualPrompt)}
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
{isGeneratingImage ? <RefreshCw size={13} className="animate-spin" /> : <ImageIcon size={13} />}
|
||||||
|
{thumbnailAsset ? "Görseli Yeniden Üret" : "Görsel Üret"}
|
||||||
|
</button>
|
||||||
|
{thumbnailAsset?.url && (
|
||||||
|
<button
|
||||||
|
onClick={() => onUpscaleImage?.(scene.id)}
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
{isUpscalingImage ? <RefreshCw size={13} className="animate-spin" /> : <Wand2 size={13} />}
|
||||||
|
Upscale (4K)
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
@@ -191,6 +250,36 @@ export function SceneCard({ scene, isEditable, onUpdate, onRegenerate, isRegener
|
|||||||
|
|
||||||
{/* Sahne bağlantı çizgisi */}
|
{/* Sahne bağlantı çizgisi */}
|
||||||
<div className="absolute left-7 -bottom-3 w-px h-3 bg-gradient-to-b from-[var(--color-border-faint)] to-transparent" />
|
<div className="absolute left-7 -bottom-3 w-px h-3 bg-gradient-to-b from-[var(--color-border-faint)] to-transparent" />
|
||||||
|
|
||||||
|
{/* Lightbox Modal */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{lightboxOpen && thumbnailAsset?.url && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
onClick={() => 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"
|
||||||
|
>
|
||||||
|
<motion.img
|
||||||
|
initial={{ scale: 0.9, opacity: 0 }}
|
||||||
|
animate={{ scale: 1, opacity: 1 }}
|
||||||
|
exit={{ scale: 0.9, opacity: 0 }}
|
||||||
|
transition={{ type: "spring", damping: 25, stiffness: 300 }}
|
||||||
|
src={thumbnailAsset.url}
|
||||||
|
alt="Fullscreen Scene"
|
||||||
|
className="max-w-full max-h-full object-contain rounded-xl shadow-2xl"
|
||||||
|
onClick={(e) => e.stopPropagation()} // Click image prevents closing
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={() => setLightboxOpen(false)}
|
||||||
|
className="absolute top-6 right-6 p-2 rounded-full bg-black/50 text-white/70 hover:text-white hover:bg-black/70 transition-colors"
|
||||||
|
>
|
||||||
|
<X size={24} />
|
||||||
|
</button>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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ı
|
// CREDITS — Kredi hook'ları
|
||||||
// ═══════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════
|
||||||
|
|||||||
@@ -299,6 +299,12 @@ export const projectsApi = {
|
|||||||
|
|
||||||
regenerateScene: (projectId: string, sceneId: string) =>
|
regenerateScene: (projectId: string, sceneId: string) =>
|
||||||
apiClient.post<Scene>(`/projects/${projectId}/scenes/${sceneId}/regenerate`).then((r) => r.data),
|
apiClient.post<Scene>(`/projects/${projectId}/scenes/${sceneId}/regenerate`).then((r) => r.data),
|
||||||
|
|
||||||
|
generateSceneImage: (projectId: string, sceneId: string, customPrompt?: string) =>
|
||||||
|
apiClient.post<Scene>(`/projects/${projectId}/scenes/${sceneId}/generate-image`, { customPrompt }).then((r) => r.data),
|
||||||
|
|
||||||
|
upscaleSceneImage: (projectId: string, sceneId: string) =>
|
||||||
|
apiClient.post<Scene>(`/projects/${projectId}/scenes/${sceneId}/upscale-image`).then((r) => r.data),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Backend path: /billing/credits/balance (billing controller prefix)
|
// Backend path: /billing/credits/balance (billing controller prefix)
|
||||||
|
|||||||
31
test-fe-api.js
Normal file
31
test-fe-api.js
Normal file
@@ -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();
|
||||||
47
test-frontend-useProjects.js
Normal file
47
test-frontend-useProjects.js
Normal file
@@ -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);
|
||||||
Reference in New Issue
Block a user