Files
digicraft-fe/Dashboard.tsx
Fahri Can Seçer 6e3bee17ef
Some checks failed
Deploy Frontend / deploy (push) Has been cancelled
main
2026-02-05 01:34:13 +03:00

323 lines
20 KiB
TypeScript

import React, { useEffect, useState } from 'react';
import axios from 'axios';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import { Tooltip } from './components/Tooltip';
import { useAuth } from './AuthContext';
import { Shield, Users, BarChart3, Database, Sparkles, ScanEye, BrainCircuit, ArrowRight } from 'lucide-react';
interface ProjectSummary {
id: string;
niche: string;
productType: string;
createdAt: string;
masterPath: string | null;
seoTitle: string;
}
interface DashboardProps {
onSelectProject: (id: string) => void;
onNewProject: () => void;
}
const Dashboard: React.FC<DashboardProps> = ({ onSelectProject, onNewProject }) => {
const { t } = useTranslation();
const { user } = useAuth();
const navigate = useNavigate();
const [projects, setProjects] = useState<ProjectSummary[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [remixTargetId, setRemixTargetId] = useState<string | null>(null);
const [targetProductType, setTargetProductType] = useState("Phone Wallpaper");
const [isRemixing, setIsRemixing] = useState(false);
useEffect(() => {
fetchProjects();
}, []);
const fetchProjects = async () => {
try {
setLoading(true);
const res = await axios.get('/api/projects');
setProjects(res.data.projects);
} catch (err: any) {
setError(err.message);
} finally {
setLoading(false);
}
};
const handleDeleteProject = async (e: React.MouseEvent, id: string) => {
e.stopPropagation(); // Prevent card click
if (!window.confirm("Are you sure you want to delete this project? This action cannot be undone.")) return;
try {
await axios.delete(`/api/projects/${id}`);
// Optimistic update
setProjects(prev => prev.filter(p => p.id !== id));
} catch (err: any) {
alert(`Failed to delete project: ${err.message}`);
}
};
const handleRemixProject = async () => {
if (!remixTargetId) return;
setIsRemixing(true);
try {
await axios.post(`/api/projects/${remixTargetId}/remix`, { targetProductType });
alert(`Project Remixed for ${targetProductType}!`);
setRemixTargetId(null);
fetchProjects(); // Refresh list to see new project
} catch (err: any) {
alert(`Remix Failed: ${err.message}`);
} finally {
setIsRemixing(false);
}
};
return (
<div className="max-w-7xl mx-auto px-6 min-h-[60vh] space-y-12">
{/* The Analyst Suite Section */}
<section>
<div className="flex items-center gap-3 mb-6">
<div className="p-2 bg-indigo-100 rounded-lg text-indigo-600">
<ScanEye className="w-6 h-6" />
</div>
<div>
<h2 className="text-xl font-black text-stone-900 uppercase tracking-widest">Mastermind Intelligence Suite</h2>
<p className="text-xs font-bold text-stone-400">Phase 1: Analysis & Optimization Tools</p>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{/* Competitor X-Ray Card */}
<div
onClick={() => navigate('/xray')}
className="group bg-white p-6 rounded-3xl border border-stone-100 shadow-lg hover:shadow-xl hover:-translate-y-1 transition-all cursor-pointer relative overflow-hidden"
>
<div className="absolute top-0 right-0 p-32 bg-indigo-50/50 rounded-full blur-3xl -mr-16 -mt-16 transition-all group-hover:bg-indigo-100/50" />
<div className="relative z-10">
<div className="w-12 h-12 bg-white rounded-2xl shadow-sm border border-stone-100 flex items-center justify-center mb-4 group-hover:scale-110 transition-transform text-indigo-600">
<ScanEye className="w-6 h-6" />
</div>
<h3 className="text-lg font-black text-stone-800 mb-2">Competitor X-Ray</h3>
<p className="text-sm text-stone-500 font-medium leading-relaxed mb-6">
Deconstruct competitor listings to uncover their "Visual DNA" and generate superior prompts.
</p>
<div className="flex items-center text-xs font-black uppercase tracking-widest text-indigo-600 group-hover:gap-2 transition-all">
Launch Tool <ArrowRight className="w-4 h-4 ml-1" />
</div>
</div>
</div>
{/* Neuro-Scorecard Card */}
<div
onClick={() => navigate('/scorecard')}
className="group bg-white p-6 rounded-3xl border border-stone-100 shadow-lg hover:shadow-xl hover:-translate-y-1 transition-all cursor-pointer relative overflow-hidden"
>
<div className="absolute top-0 right-0 p-32 bg-emerald-50/50 rounded-full blur-3xl -mr-16 -mt-16 transition-all group-hover:bg-emerald-100/50" />
<div className="relative z-10">
<div className="w-12 h-12 bg-white rounded-2xl shadow-sm border border-stone-100 flex items-center justify-center mb-4 group-hover:scale-110 transition-transform text-emerald-600">
<BrainCircuit className="w-6 h-6" />
</div>
<h3 className="text-lg font-black text-stone-800 mb-2">Neuro-Scorecard</h3>
<p className="text-sm text-stone-500 font-medium leading-relaxed mb-6">
Predict "Click-Through Rate" with AI-powered neuro-marketing scoring (Dopamine, Serotonin).
</p>
<div className="flex items-center text-xs font-black uppercase tracking-widest text-emerald-600 group-hover:gap-2 transition-all">
Launch Tool <ArrowRight className="w-4 h-4 ml-1" />
</div>
</div>
</div>
</div>
</section>
<div className="w-full h-px bg-stone-100" />
<section>
<div className="flex flex-col md:flex-row items-start md:items-center justify-between gap-6 mb-8">
<div>
<h1 className="text-3xl font-black tracking-tighter text-stone-900 leading-none">{t('dashboard.subtitle')}</h1>
</div>
<div className="flex flex-wrap gap-3">
{/* ETSY CONNECT BUTTON */}
<button
onClick={async () => {
try {
const res = await fetch('/api/etsy/url');
const data = await res.json();
if (data.url) window.location.href = data.url;
} catch (e) {
alert("Failed to start Etsy connection");
}
}}
className="flex items-center gap-2 px-6 py-3 bg-[#F1641E] hover:bg-[#D55619] text-white rounded-2xl transition-all shadow-lg hover:shadow-xl hover:-translate-y-0.5 font-bold text-xs uppercase tracking-widest"
>
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
<path d="M9.362 16.966c-2.31 0-3.327-1.168-3.327-3.235 0-2.616 1.76-4.502 4.417-4.502 2.657 0 3.737 1.76 3.737 3.655 0 .208-.02.416-.04.603H6.012c.1 1.54 1.228 2.616 2.946 2.616 1.109 0 2.059-.475 2.534-1.306l.872.535c-.753 1.287-2.099 2.03-3.963 2.03zm.18-6.195c-1.129 0-2.06.713-2.356 1.742h4.752c-.06-1.148-.832-1.742-2.396-1.742zM21.92 14.53c0 1.624-.317 2.872-1.188 3.783-.89.93-2.337 1.406-4.337 1.406-3.88 0-5.742-1.92-5.742-5.723 0-4.04 1.96-6.08 5.485-6.08 1.94 0 3.425.594 4.316 1.565v-4.575h2.476v16.337h-2.198v-1.782c-.93 1.247-2.396 1.94-4.218 1.94-1.96 0-3.604-.831-4.633-2.376-.872-1.287-1.287-3.03-1.287-5.069 0-2.455.594-4.574 1.841-6.198 1.109-1.426 2.693-2.178 4.673-2.178 1.782 0 3.169.653 4.099 1.861v5.089z" />
</svg>
Connect Etsy
</button>
<Tooltip content={t('buttons.new_project')} position="top">
<button
onClick={onNewProject}
className="bg-stone-900 text-white px-8 py-3 rounded-2xl font-black uppercase tracking-widest hover:bg-stone-800 transition-all shadow-lg hover:shadow-xl hover:-translate-y-0.5 text-xs"
>
+ {t('buttons.new_project')}
</button>
</Tooltip>
</div>
</div>
{loading && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 animate-pulse">
{[1, 2, 3, 4].map(i => (
<div key={i} className="aspect-[3/4] bg-stone-200 rounded-3xl"></div>
))}
</div>
)}
{error && (
<div className="bg-red-50 text-red-600 p-6 rounded-2xl border border-red-100 mb-8">
Error loading projects: {error}
</div>
)}
{!loading && projects.length === 0 && (
<div className="text-center py-24 bg-white rounded-[3rem] border border-stone-100 shadow-sm">
<h3 className="text-xl font-bold text-stone-400 mb-4">{t('dashboard.no_projects')}</h3>
<p className="text-stone-300">{t('dashboard.start_masterpiece')}</p>
</div>
)}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
{projects.map((p) => (
<div
key={p.id}
onClick={() => onSelectProject(p.id)}
className="group relative bg-white rounded-3xl p-3 shadow-lg hover:shadow-2xl hover:-translate-y-2 transition-all duration-300 text-left cursor-pointer"
>
<div className="aspect-[3/4] bg-stone-100 rounded-2xl overflow-hidden mb-4 relative">
{p.masterPath ? (
<img
src={`/storage/${p.masterPath}`}
alt={p.niche}
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
/>
) : (
<div className="w-full h-full flex items-center justify-center text-stone-300 font-bold text-xs uppercase tracking-widest">
Pending
</div>
)}
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/10 transition-colors" />
{/* DELETE BUTTON */}
<button
title={t('buttons.delete_project')}
onClick={(e) => handleDeleteProject(e, p.id)}
className="absolute top-2 right-2 z-50 p-2.5 rounded-full bg-white/30 backdrop-blur-md border border-white/40 shadow-[0_8px_32px_0_rgba(31,38,135,0.15)] text-stone-600 hover:text-red-500 hover:bg-white/60 hover:shadow-red-500/20 hover:scale-110 active:scale-95 transition-all duration-300 group/btn"
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-5 h-5 drop-shadow-sm">
<path strokeLinecap="round" strokeLinejoin="round" d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" />
</svg>
</button>
{/* REMIX BUTTON */}
<button
title="Remix / Repurpose"
onClick={(e) => { e.stopPropagation(); setRemixTargetId(p.id); }}
className="absolute top-2 left-2 z-50 p-2.5 rounded-full bg-white/30 backdrop-blur-md border border-white/40 shadow-[0_8px_32px_0_rgba(31,38,135,0.15)] text-stone-600 hover:text-purple-600 hover:bg-white/60 hover:shadow-purple-500/20 hover:scale-110 active:scale-95 transition-all duration-300 group/btn"
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-5 h-5 drop-shadow-sm">
<path strokeLinecap="round" strokeLinejoin="round" d="M9.813 15.904 9 18.75l-.813-2.846a4.5 4.5 0 0 0-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 0 0 3.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 0 0 3.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 0 0-3.09 3.09ZM18.259 8.715 18 9.75l-.259-1.035a3.375 3.375 0 0 0-2.455-2.456L14.25 6l1.036-.259a3.375 3.375 0 0 0 2.455-2.456L18 2.25l.259 1.035a3.375 3.375 0 0 0 2.456 2.456L21.75 6l-1.035.259a3.375 3.375 0 0 0-2.456 2.456ZM16.894 20.567 16.5 21.75l-.394-1.183a2.25 2.25 0 0 0-1.423-1.423L13.5 18.75l1.183-.394a2.25 2.25 0 0 0 1.423-1.423l.394-1.183.394 1.183a2.25 2.25 0 0 0 1.423 1.423l1.183.394-1.183.394a2.25 2.25 0 0 0-1.423 1.423Z" />
</svg>
</button>
{/* DUPLICATE BUTTON */}
<button
title="Duplicate Project"
onClick={async (e) => {
e.stopPropagation();
if (!confirm("Duplicate this project?")) return;
try {
await axios.post(`/api/projects/${p.id}/duplicate`);
fetchProjects();
} catch (err: any) {
alert("Duplication failed: " + err.message);
}
}}
className="absolute top-2 left-14 z-50 p-2.5 rounded-full bg-white/30 backdrop-blur-md border border-white/40 shadow-[0_8px_32px_0_rgba(31,38,135,0.15)] text-stone-600 hover:text-blue-600 hover:bg-white/60 hover:shadow-blue-500/20 hover:scale-110 active:scale-95 transition-all duration-300 group/btn"
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-5 h-5 drop-shadow-sm">
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 0 1-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 0 1 1.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 0 0-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 0 1-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 0 0-3.375-3.375h-1.5" />
</svg>
</button>
</div>
<div className="px-2 pb-2">
<div className="flex justify-between items-start mb-2">
<span className="text-[10px] font-black uppercase tracking-wider text-purple-600 bg-purple-50 px-2 py-1 rounded-md">
{p.productType}
</span>
<span className="text-[10px] font-bold text-stone-400">
{new Date(p.createdAt).toLocaleDateString()}
</span>
</div>
<h3 className="font-bold text-stone-800 leading-tight line-clamp-2 min-h-[2.5rem]">
{p.seoTitle}
</h3>
<p className="text-xs text-stone-400 mt-1 truncate">{p.niche}</p>
</div>
</div>
))}
</div>
</section>
{/* REMIX MODAL */}
{remixTargetId && (
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4 backdrop-blur-sm">
<div className="bg-white rounded-[2rem] p-8 w-full max-w-md shadow-2xl relative">
<button
onClick={() => setRemixTargetId(null)}
className="absolute top-4 right-4 text-stone-400 hover:text-stone-900"
>
</button>
<h2 className="text-2xl font-black mb-1"> Smart Remix</h2>
<p className="text-stone-500 text-sm mb-6">Transform this design into a new product format while keeping its Visual DNA.</p>
<div className="space-y-4">
<label className="text-xs font-bold uppercase tracking-widest text-stone-400">Target Product</label>
<div className="grid grid-cols-2 gap-3">
{["Wall Art", "Phone Wallpaper", "Sticker", "Bookmark", "Planner", "Social Media Kit"].map(type => (
<button
key={type}
onClick={() => setTargetProductType(type)}
className={`p-3 rounded-xl text-xs font-bold border-2 transition-all ${targetProductType === type ? 'border-purple-500 bg-purple-50 text-purple-700' : 'border-stone-100 text-stone-500 hover:border-stone-200'}`}
>
{type}
</button>
))}
</div>
</div>
<button
onClick={handleRemixProject}
disabled={isRemixing}
className="w-full mt-8 bg-purple-600 text-white py-4 rounded-xl font-black uppercase tracking-widest hover:bg-purple-700 transition-all disabled:opacity-50 disabled:cursor-not-allowed"
>
{isRemixing ? "Remixing (Wait)..." : "Generate Remix 🚀"}
</button>
</div>
</div>
)}
</div>
);
};
export default Dashboard;