fix: toast api call syntax
UI Deploy (Next-Auth Support) 🎨 / build-and-deploy (push) Has been cancelled

This commit is contained in:
Harun CAN
2026-04-30 16:48:44 +02:00
parent 7d161fdb3d
commit bc9a1587a8
5 changed files with 394 additions and 5 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
/// <reference types="next" />
/// <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
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
@@ -17,6 +17,13 @@ import {
MoreVertical,
X,
Languages,
Search,
Tag,
Copy,
Check,
TrendingUp,
Zap,
Hash,
} from 'lucide-react';
import Link from 'next/link';
import { useState, useEffect } from 'react';
@@ -29,7 +36,9 @@ import {
useGenerateSceneImage,
useUpscaleSceneImage,
useRegenerateScene,
useCancelRender
useCancelRender,
useGenerateSeoTitles,
useSelectSeoTitle
} from '@/hooks/use-api';
import { useRenderProgress } from '@/hooks/use-render-progress';
import { SceneCard } from '@/components/project/scene-card';
@@ -47,6 +56,18 @@ const XIcon = ({ size = 16 }: { size?: number }) => (
</svg>
);
const YouTubeIcon = ({ size = 16 }: { size?: number }) => (
<svg width={size} height={size} viewBox="0 0 24 24" fill="currentColor">
<path d="M23.5 6.19a3.02 3.02 0 0 0-2.12-2.14C19.53 3.5 12 3.5 12 3.5s-7.53 0-9.38.55A3.02 3.02 0 0 0 .5 6.19 31.7 31.7 0 0 0 0 12a31.7 31.7 0 0 0 .5 5.81 3.02 3.02 0 0 0 2.12 2.14c1.85.55 9.38.55 9.38.55s7.53 0 9.38-.55a3.02 3.02 0 0 0 2.12-2.14A31.7 31.7 0 0 0 24 12a31.7 31.7 0 0 0-.5-5.81zM9.55 15.57V8.43L15.82 12l-6.27 3.57z" />
</svg>
);
const InstagramIcon = ({ size = 16 }: { size?: number }) => (
<svg width={size} height={size} viewBox="0 0 24 24" fill="currentColor">
<path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zM12 0C8.741 0 8.333.014 7.053.072 2.695.272.273 2.69.073 7.052.014 8.333 0 8.741 0 12c0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98C8.333 23.986 8.741 24 12 24c3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98C15.668.014 15.259 0 12 0zm0 5.838a6.162 6.162 0 1 0 0 12.324 6.162 6.162 0 0 0 0-12.324zM12 16a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm6.406-11.845a1.44 1.44 0 1 0 0 2.881 1.44 1.44 0 0 0 0-2.881z" />
</svg>
);
const STATUS_MAP: Record<string, { label: string; color: string; icon: React.ElementType; bgClass: string }> = {
DRAFT: { label: 'Taslak', color: 'text-neutral-400', icon: FileText, bgClass: 'bg-neutral-500/10 border-neutral-500/20' },
GENERATING_SCRIPT: { label: 'Senaryo Üretiliyor', color: 'text-neutral-300', icon: Sparkles, bgClass: 'bg-neutral-500/10 border-neutral-500/20' },
@@ -139,7 +160,7 @@ export default function ProjectDetailPage() {
try {
setIsTranslating(true);
const res = await apiClient.post(`/projects/${id}/translate`, { targetLanguage });
toast.success("Proje başarıyla çevrildi!");
toast.success({ title: "Proje başarıyla çevrildi!" });
setShowTranslateModal(false);
setTargetLanguage("");
// refetch() to maybe update some states, or router.push to the new project
@@ -172,9 +193,13 @@ export default function ProjectDetailPage() {
const generateImageMutation = useGenerateSceneImage();
const upscaleImageMutation = useUpscaleSceneImage();
const regenerateSceneMutation = useRegenerateScene();
const seoTitlesMutation = useGenerateSeoTitles();
const selectTitleMutation = useSelectSeoTitle();
const [generatingImageId, setGeneratingImageId] = useState<string | null>(null);
const [upscalingImageId, setUpscalingImageId] = useState<string | null>(null);
const [copiedField, setCopiedField] = useState<string | null>(null);
const [activeCaptionTab, setActiveCaptionTab] = useState<'youtube' | 'tiktok' | 'instagram' | 'twitter'>('youtube');
// WebSocket progress
const renderState = useRenderProgress(
@@ -234,6 +259,40 @@ export default function ProjectDetailPage() {
});
};
// Panoya kopyala
const copyToClipboard = (text: string, field: string) => {
navigator.clipboard.writeText(text);
setCopiedField(field);
toast.success({ title: 'Panoya kopyalandı!' });
setTimeout(() => setCopiedField(null), 2000);
};
// SEO başlıkları üret
const handleGenerateSeoTitles = () => {
seoTitlesMutation.mutate(id, {
onSuccess: () => {
refetch();
toast.success({ title: '5 yeni SEO başlığı üretildi!' });
},
onError: () => {
toast.error({ title: 'SEO başlık üretimi başarısız.' });
},
});
};
// SEO başlık seç
const handleSelectTitle = (title: string) => {
selectTitleMutation.mutate({ projectId: id, title }, {
onSuccess: () => {
refetch();
toast.success({ title: 'Başlık güncellendi!' });
},
onError: () => {
toast.error({ title: 'Başlık güncellenemedi.' });
},
});
};
// Onayla ve gönder
const handleApprove = () => {
approveMutation.mutate(id, {
@@ -612,6 +671,283 @@ export default function ProjectDetailPage() {
</motion.div>
)}
{/* ── SEO & Sosyal Medya Power Engine ── */}
{hasScript && (
<motion.div variants={fadeUp}>
<div className="card-surface p-5 rounded-2xl border border-[var(--color-border-subtle)]">
{/* Header */}
<div className="flex items-center justify-between mb-5">
<h2 className="text-sm font-semibold text-[var(--color-text-primary)] flex items-center gap-2">
<TrendingUp size={15} className="text-emerald-400" />
SEO & Sosyal Medya
</h2>
{project.seoScore != null && (
<div className="flex items-center gap-2">
<div className="relative w-10 h-10">
<svg className="w-10 h-10 -rotate-90" viewBox="0 0 36 36">
<path
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
fill="none"
stroke="rgba(255,255,255,0.1)"
strokeWidth="3"
/>
<path
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
fill="none"
stroke={project.seoScore >= 80 ? '#34d399' : project.seoScore >= 50 ? '#fbbf24' : '#f87171'}
strokeWidth="3"
strokeDasharray={`${project.seoScore}, 100`}
strokeLinecap="round"
/>
</svg>
<span className="absolute inset-0 flex items-center justify-center text-[10px] font-bold text-white">
{project.seoScore}
</span>
</div>
<span className="text-[10px] text-[var(--color-text-muted)]">SEO<br/>Skoru</span>
</div>
)}
</div>
{/* ─── Başlık Yönetimi ─── */}
<div className="mb-5">
<div className="flex items-center justify-between mb-2">
<h3 className="text-xs font-semibold text-[var(--color-text-secondary)] uppercase tracking-wider">
📌 SEO Başlıkları
</h3>
<button
onClick={handleGenerateSeoTitles}
disabled={seoTitlesMutation.isPending}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-[11px] font-medium bg-gradient-to-r from-violet-500/20 to-purple-500/20 text-violet-300 hover:from-violet-500/30 hover:to-purple-500/30 border border-violet-500/20 transition-all disabled:opacity-50"
>
{seoTitlesMutation.isPending ? (
<Loader2 size={12} className="animate-spin" />
) : (
<Zap size={12} />
)}
5 Yeni Başlık Üret
</button>
</div>
{/* Mevcut başlık */}
<div className="bg-[var(--color-bg-elevated)] rounded-xl p-3 mb-2 border border-emerald-500/20">
<div className="flex items-center gap-2 mb-1">
<CheckCircle2 size={12} className="text-emerald-400" />
<span className="text-[10px] text-emerald-400 font-medium uppercase">Aktif Başlık</span>
</div>
<p className="text-sm font-medium text-white">{project.seoTitle || project.title}</p>
</div>
{/* Alternatif başlıklar */}
{project.seoTitleAlts && project.seoTitleAlts.length > 0 && (
<div className="space-y-1.5">
{project.seoTitleAlts.map((alt, i) => (
<button
key={i}
onClick={() => handleSelectTitle(alt)}
disabled={selectTitleMutation.isPending || alt === (project.seoTitle || project.title)}
className={`w-full text-left p-2.5 rounded-lg border text-xs transition-all ${
alt === (project.seoTitle || project.title)
? 'border-emerald-500/30 bg-emerald-500/10 text-emerald-300'
: 'border-[var(--color-border-subtle)] hover:border-violet-500/30 hover:bg-violet-500/5 text-[var(--color-text-secondary)]'
} disabled:opacity-40`}
>
<span className="text-[10px] text-[var(--color-text-ghost)] mr-2">#{i + 1}</span>
{alt}
</button>
))}
</div>
)}
</div>
{/* ─── SEO Keywords ─── */}
{project.seoKeywords && project.seoKeywords.length > 0 && (
<div className="mb-5">
<h3 className="text-xs font-semibold text-[var(--color-text-secondary)] uppercase tracking-wider mb-2 flex items-center gap-1.5">
<Search size={12} />
Anahtar Kelimeler ({project.seoKeywords.length})
</h3>
<div className="flex flex-wrap gap-1.5">
{project.seoKeywords.map((kw, i) => {
const colors = [
'bg-blue-500/15 text-blue-300 border-blue-500/20',
'bg-emerald-500/15 text-emerald-300 border-emerald-500/20',
'bg-amber-500/15 text-amber-300 border-amber-500/20',
'bg-violet-500/15 text-violet-300 border-violet-500/20',
'bg-rose-500/15 text-rose-300 border-rose-500/20',
'bg-cyan-500/15 text-cyan-300 border-cyan-500/20',
];
return (
<span
key={i}
className={`inline-flex items-center gap-1 px-2.5 py-1 rounded-full text-[11px] font-medium border ${colors[i % colors.length]}`}
>
<Tag size={10} />
{kw}
</span>
);
})}
</div>
</div>
)}
{/* ─── Hashtag'ler ─── */}
{project.scriptJson?.seo?.hashtags && project.scriptJson.seo.hashtags.length > 0 && (
<div className="mb-5">
<div className="flex items-center justify-between mb-2">
<h3 className="text-xs font-semibold text-[var(--color-text-secondary)] uppercase tracking-wider flex items-center gap-1.5">
<Hash size={12} />
Hashtag&apos;ler
</h3>
<button
onClick={() => copyToClipboard(
project.scriptJson!.seo.hashtags.map((h: string) => `#${h}`).join(' '),
'hashtags'
)}
className="flex items-center gap-1 text-[10px] text-[var(--color-text-muted)] hover:text-white transition-colors"
>
{copiedField === 'hashtags' ? <Check size={10} className="text-emerald-400" /> : <Copy size={10} />}
Tümünü Kopyala
</button>
</div>
<div className="flex flex-wrap gap-1.5">
{project.scriptJson.seo.hashtags.map((tag: string, i: number) => (
<span
key={i}
className="px-2.5 py-1 rounded-full text-[11px] font-medium bg-sky-500/10 text-sky-300 border border-sky-500/20"
>
#{tag}
</span>
))}
{/* Trending hashtag'ler */}
{project.scriptJson.seo.trendingHashtags?.map((tag: string, i: number) => (
<span
key={`trend-${i}`}
className="px-2.5 py-1 rounded-full text-[11px] font-medium bg-orange-500/15 text-orange-300 border border-orange-500/20 flex items-center gap-1"
>
<TrendingUp size={10} />
#{tag}
</span>
))}
</div>
</div>
)}
{/* ─── Sosyal Medya Caption'ları ─── */}
{project.socialContent && (
<div>
<h3 className="text-xs font-semibold text-[var(--color-text-secondary)] uppercase tracking-wider mb-3 flex items-center gap-1.5">
<Sparkles size={12} />
Sosyal Medya İçerikleri
</h3>
{/* Tab navigasyonu */}
<div className="flex gap-1 mb-3 p-1 bg-[var(--color-bg-elevated)] rounded-xl">
{[
{ key: 'youtube' as const, label: 'YouTube', icon: YouTubeIcon, color: 'text-red-400' },
{ key: 'tiktok' as const, label: 'TikTok', icon: Film, color: 'text-cyan-400' },
{ key: 'instagram' as const, label: 'Instagram', icon: InstagramIcon, color: 'text-pink-400' },
{ key: 'twitter' as const, label: 'X', icon: XIcon, color: 'text-white' },
].map((tab) => (
<button
key={tab.key}
onClick={() => setActiveCaptionTab(tab.key)}
className={`flex-1 flex items-center justify-center gap-1.5 py-2 rounded-lg text-[11px] font-medium transition-all ${
activeCaptionTab === tab.key
? 'bg-[var(--color-bg-surface)] text-white shadow-sm'
: 'text-[var(--color-text-muted)] hover:text-white'
}`}
>
<tab.icon size={12} className={activeCaptionTab === tab.key ? tab.color : ''} />
<span className="hidden sm:inline">{tab.label}</span>
</button>
))}
</div>
{/* Caption içeriği */}
<div className="bg-[var(--color-bg-elevated)] rounded-xl p-4 border border-[var(--color-border-subtle)]">
{activeCaptionTab === 'youtube' && (
<div className="space-y-3">
{project.socialContent.youtubeTitle && (
<div>
<div className="flex items-center justify-between mb-1">
<span className="text-[10px] text-[var(--color-text-muted)] uppercase">Başlık</span>
<button onClick={() => copyToClipboard(project.socialContent!.youtubeTitle!, 'yt-title')} className="p-1 hover:bg-white/5 rounded">
{copiedField === 'yt-title' ? <Check size={10} className="text-emerald-400" /> : <Copy size={10} className="text-[var(--color-text-muted)]" />}
</button>
</div>
<p className="text-sm font-medium text-white">{project.socialContent.youtubeTitle}</p>
</div>
)}
{project.socialContent.youtubeDescription && (
<div>
<div className="flex items-center justify-between mb-1">
<span className="text-[10px] text-[var(--color-text-muted)] uppercase">Açıklama</span>
<button onClick={() => copyToClipboard(project.socialContent!.youtubeDescription!, 'yt-desc')} className="p-1 hover:bg-white/5 rounded">
{copiedField === 'yt-desc' ? <Check size={10} className="text-emerald-400" /> : <Copy size={10} className="text-[var(--color-text-muted)]" />}
</button>
</div>
<p className="text-xs text-[var(--color-text-secondary)] whitespace-pre-line leading-relaxed">{project.socialContent.youtubeDescription}</p>
</div>
)}
</div>
)}
{activeCaptionTab === 'tiktok' && project.socialContent.tiktokCaption && (
<div>
<div className="flex items-center justify-between mb-1">
<span className="text-[10px] text-[var(--color-text-muted)] uppercase">TikTok Caption</span>
<button onClick={() => copyToClipboard(project.socialContent!.tiktokCaption!, 'tiktok')} className="p-1 hover:bg-white/5 rounded">
{copiedField === 'tiktok' ? <Check size={10} className="text-emerald-400" /> : <Copy size={10} className="text-[var(--color-text-muted)]" />}
</button>
</div>
<p className="text-xs text-[var(--color-text-secondary)] whitespace-pre-line leading-relaxed">{project.socialContent.tiktokCaption}</p>
</div>
)}
{activeCaptionTab === 'instagram' && project.socialContent.instagramCaption && (
<div>
<div className="flex items-center justify-between mb-1">
<span className="text-[10px] text-[var(--color-text-muted)] uppercase">Instagram Caption</span>
<button onClick={() => copyToClipboard(project.socialContent!.instagramCaption!, 'instagram')} className="p-1 hover:bg-white/5 rounded">
{copiedField === 'instagram' ? <Check size={10} className="text-emerald-400" /> : <Copy size={10} className="text-[var(--color-text-muted)]" />}
</button>
</div>
<p className="text-xs text-[var(--color-text-secondary)] whitespace-pre-line leading-relaxed">{project.socialContent.instagramCaption}</p>
</div>
)}
{activeCaptionTab === 'twitter' && project.socialContent.twitterText && (
<div>
<div className="flex items-center justify-between mb-1">
<span className="text-[10px] text-[var(--color-text-muted)] uppercase">X (Twitter) Paylaşımı</span>
<button onClick={() => copyToClipboard(project.socialContent!.twitterText!, 'twitter')} className="p-1 hover:bg-white/5 rounded">
{copiedField === 'twitter' ? <Check size={10} className="text-emerald-400" /> : <Copy size={10} className="text-[var(--color-text-muted)]" />}
</button>
</div>
<p className="text-xs text-[var(--color-text-secondary)] whitespace-pre-line leading-relaxed">{project.socialContent.twitterText}</p>
</div>
)}
</div>
</div>
)}
{/* ─── SEO Açıklama ─── */}
{project.seoDescription && (
<div className="mt-4 pt-4 border-t border-[var(--color-border-subtle)]">
<div className="flex items-center justify-between mb-1.5">
<span className="text-[10px] text-[var(--color-text-muted)] uppercase">Meta Description</span>
<button onClick={() => copyToClipboard(project.seoDescription!, 'seo-desc')} className="p-1 hover:bg-white/5 rounded">
{copiedField === 'seo-desc' ? <Check size={10} className="text-emerald-400" /> : <Copy size={10} className="text-[var(--color-text-muted)]" />}
</button>
</div>
<p className="text-xs text-[var(--color-text-secondary)] leading-relaxed">{project.seoDescription}</p>
</div>
)}
</div>
</motion.div>
)}
{/* ── Boş durum (senaryo yok) ── */}
{!hasScript && isEditable && (
<motion.div variants={fadeUp} className="card-surface p-8 text-center">
@@ -107,7 +107,7 @@ export default function ProjectsPage() {
try {
setIsTranslating(true);
const res = await apiClient.post(`/projects/${translateTarget.id}/translate`, { targetLanguage });
toast.success("Proje başarıyla çevrildi!");
toast.success({ title: "Proje başarıyla çevrildi!" });
setTranslateTarget(null);
setTargetLanguage("");
refetch();
+25
View File
@@ -182,6 +182,31 @@ export function useCancelRender() {
});
}
/** AI ile 5 yeni SEO başlığı üret */
export function useGenerateSeoTitles() {
const qc = useQueryClient();
return useMutation({
mutationFn: (projectId: string) =>
projectsApi.generateSeoTitles(projectId),
onSuccess: (_data, projectId) => {
qc.invalidateQueries({ queryKey: queryKeys.projects.detail(projectId) });
},
});
}
/** Alternatif SEO başlıklarından birini seç ve proje başlığını güncelle */
export function useSelectSeoTitle() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ projectId, title }: { projectId: string; title: string }) =>
projectsApi.selectSeoTitle(projectId, title),
onSuccess: (_data, variables) => {
qc.invalidateQueries({ queryKey: queryKeys.projects.detail(variables.projectId) });
qc.invalidateQueries({ queryKey: queryKeys.projects.all });
},
});
}
/** Sahne güncelleme (narrasyon, prompt) */
export function useUpdateScene() {
const qc = useQueryClient();
+29 -1
View File
@@ -30,6 +30,19 @@ export interface Project {
renderJobs?: RenderJob[];
sourceType?: 'MANUAL' | 'X_TWEET' | 'YOUTUBE';
sourceTweetData?: Record<string, unknown>;
// SEO Power Engine
seoTitle?: string;
seoDescription?: string;
seoKeywords?: string[];
seoTitleAlts?: string[];
seoScore?: number;
socialContent?: {
youtubeTitle?: string;
youtubeDescription?: string;
tiktokCaption?: string;
instagramCaption?: string;
twitterText?: string;
};
createdAt: string;
updatedAt: string;
completedAt?: string;
@@ -99,13 +112,17 @@ export interface ScriptJson {
language: string;
hashtags: string[];
};
seo?: {
seo: {
title: string;
description: string;
keywords: string[];
hashtags: string[];
trendingHashtags?: string[];
estimatedSearchVolume?: string;
schemaMarkup: Record<string, unknown>;
};
seoTitleAlternatives?: string[];
seoScore?: number;
scenes: Array<{
order: number;
title?: string;
@@ -363,6 +380,17 @@ export const projectsApi = {
`/projects/${id}/cancel-render`,
).then((r) => r.data),
generateSeoTitles: (id: string) =>
apiClient.post<{ titles: string[]; seoScore: number; currentTitle: string }>(
`/projects/${id}/generate-seo-titles`,
).then((r) => r.data),
selectSeoTitle: (id: string, title: string) =>
apiClient.patch<Project>(
`/projects/${id}/select-title`,
{ title },
).then((r) => r.data),
getRenderQueue: () =>
apiClient.get<any>('/projects/render-queue').then((r) => r.data),