main
Some checks failed
Deploy Frontend / deploy (push) Has been cancelled

This commit is contained in:
2026-02-05 01:34:13 +03:00
commit 6e3bee17ef
52 changed files with 12306 additions and 0 deletions

396
pages/AdminDashboard.tsx Normal file
View File

@@ -0,0 +1,396 @@
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import { useAuth } from '../AuthContext';
import { Users, PlusCircle, Shield, ArrowLeft, RefreshCw, Key, DollarSign, BarChart2, Save, Activity } from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import { Tooltip } from '../components/Tooltip';
import { Layout } from '../components/Layout';
// Simple SVG Bar Chart Component
const AnalyticsChart = ({ data }: { data: any[] }) => {
if (!data || data.length === 0) return <div className="text-center text-stone-400 py-10">No data available for this period.</div>;
const maxVal = Math.max(...data.map(d => Math.max(d.revenue, d.usage)), 10); // Prevent zero division
const height = 200;
const barWidth = 40;
const gap = 20;
const width = data.length * (barWidth + gap);
return (
<div className="overflow-x-auto pb-4">
<svg width={Math.max(width, 600)} height={height + 50} className="font-mono text-[10px]">
{/* Y-Axis Grid */}
{[0, 0.5, 1].map(t => (
<line key={t} x1={0} y1={height * (1 - t)} x2="100%" y2={height * (1 - t)} stroke="#e5e5e5" strokeDasharray="4" />
))}
{data.map((d, i) => {
const x = i * (barWidth + gap);
const hRev = (d.revenue / maxVal) * height;
const hCost = (d.usage / maxVal) * height;
return (
<g key={i} transform={`translate(${x}, 0)`}>
{/* Revenue Bar (Green) */}
<rect x={0} y={height - hRev} width={barWidth / 2} height={hRev} fill="#22c55e" rx="2" />
{/* Cost Bar (Red) */}
<rect x={barWidth / 2} y={height - hCost} width={barWidth / 2} height={hCost} fill="#ef4444" rx="2" />
{/* Label */}
<text x={barWidth / 2} y={height + 15} textAnchor="middle" fill="#78716c">{d.date.slice(5)}</text>
{/* Values */}
<text x={0} y={height - hRev - 5} className="fill-green-600 font-bold" fontSize="8">${d.revenue.toFixed(1)}</text>
</g>
);
})}
</svg>
<div className="flex gap-4 justify-center mt-4 text-xs font-bold">
<div className="flex items-center gap-1"><div className="w-3 h-3 bg-green-500 rounded"></div> Revenue</div>
<div className="flex items-center gap-1"><div className="w-3 h-3 bg-red-500 rounded"></div> Usage Cost</div>
</div>
</div>
);
};
const AdminDashboard: React.FC = () => {
const { user } = useAuth();
const navigate = useNavigate();
// Tab State
const [activeTab, setActiveTab] = useState<'users' | 'pricing' | 'analytics'>('users');
// Data State
const [users, setUsers] = useState<any[]>([]);
const [configs, setConfigs] = useState<any>({});
const [analytics, setAnalytics] = useState<any[]>([]);
// Loading State
const [loading, setLoading] = useState(false);
const [editingConfig, setEditingConfig] = useState<string | null>(null);
const [configValue, setConfigValue] = useState("");
const [analyticsRange, setAnalyticsRange] = useState("30d");
// NEW: User Management State
const [selectedUser, setSelectedUser] = useState<any>(null);
useEffect(() => {
if (user && user.role !== 'ADMIN') {
navigate('/');
return;
}
fetchData();
}, [user, activeTab, analyticsRange]);
const fetchData = async () => {
setLoading(true);
const token = localStorage.getItem('token');
const headers = { Authorization: `Bearer ${token}` };
try {
if (activeTab === 'users') {
const res = await axios.get('/api/admin/users', { headers });
setUsers(res.data.users);
} else if (activeTab === 'pricing') {
const res = await axios.get('/api/admin/config', { headers });
setConfigs(res.data);
} else if (activeTab === 'analytics') {
const res = await axios.get(`/api/admin/analytics?range=${analyticsRange}`, { headers });
const data = Array.isArray(res.data.analytics) ? res.data.analytics : [];
setAnalytics(data);
}
} catch (err) {
console.error("Fetch Error:", err);
} finally {
setLoading(false);
}
};
const updateConfig = async (key: string) => {
try {
const token = localStorage.getItem('token');
await axios.post('/api/admin/config', { key, value: configValue }, {
headers: { Authorization: `Bearer ${token}` }
});
setConfigs({ ...configs, [key]: configValue });
setEditingConfig(null);
} catch (e) {
alert("Failed to update config");
}
};
const addCredits = async (userId: string, amount: number) => {
try {
const token = localStorage.getItem('token');
await axios.post('/api/admin/credits', { userId, amount }, {
headers: { Authorization: `Bearer ${token}` }
});
fetchData(); // Refresh list to show new balance
if (selectedUser) {
// Update local selected user state immediately for better UX
setSelectedUser((prev: any) => ({ ...prev, credits: prev.credits + amount }));
}
} catch (err) {
alert("Failed to add credits");
}
};
const updateUserRole = async (userId: string, role: string) => {
try {
const token = localStorage.getItem('token');
await axios.post('/api/admin/role', { userId, role }, {
headers: { Authorization: `Bearer ${token}` }
});
fetchData(); // Refresh list
if (selectedUser) {
setSelectedUser((prev: any) => ({ ...prev, role }));
}
} catch (err: any) {
alert(err.response?.data?.error || "Failed to update role");
}
};
return (
<Layout>
<div className="max-w-7xl mx-auto p-6 pt-12 space-y-8">
{/* Admin Sub-Header */}
<div className="flex flex-col md:flex-row items-start md:items-center justify-between gap-6 mb-8">
<div className="flex items-center gap-4">
<button
onClick={() => navigate('/')}
className="p-3 bg-white hover:bg-stone-100 border border-stone-200 rounded-2xl transition-all shadow-sm group"
>
<ArrowLeft className="w-5 h-5 text-stone-600 group-hover:-translate-x-1 transition-transform" />
</button>
<div>
<h1 className="text-3xl font-black tracking-tighter text-stone-900 flex items-center gap-3">
<Shield className="w-8 h-8 text-purple-600" />
Admin Command Center
</h1>
<p className="text-stone-500 font-medium text-sm">System management and financial oversight.</p>
</div>
</div>
<div className="flex items-center gap-4">
<div className="flex bg-stone-200 p-1 rounded-xl">
<button onClick={() => setActiveTab('users')} className={`px-4 py-2 rounded-lg text-[10px] font-black uppercase tracking-widest transition-all ${activeTab === 'users' ? 'bg-white text-stone-900 shadow-sm' : 'text-stone-500 hover:text-stone-700'}`}>Users</button>
<button onClick={() => setActiveTab('pricing')} className={`px-4 py-2 rounded-lg text-[10px] font-black uppercase tracking-widest transition-all ${activeTab === 'pricing' ? 'bg-white text-stone-900 shadow-sm' : 'text-stone-500 hover:text-stone-700'}`}>Pricing</button>
<button onClick={() => setActiveTab('analytics')} className={`px-4 py-2 rounded-lg text-[10px] font-black uppercase tracking-widest transition-all ${activeTab === 'analytics' ? 'bg-white text-stone-900 shadow-sm' : 'text-stone-500 hover:text-stone-700'}`}>Analytics</button>
</div>
<button onClick={fetchData} className="p-3 bg-white hover:bg-stone-50 border border-stone-200 rounded-xl transition-all shadow-sm">
<RefreshCw className={`w-4 h-4 text-stone-600 ${loading ? 'animate-spin' : ''}`} />
</button>
</div>
</div>
{/* --- USERS TAB --- */}
{activeTab === 'users' && (
<div className="bg-white rounded-xl shadow-sm border border-stone-200 overflow-hidden">
<div className="p-6 border-b border-stone-100 flex items-center gap-3">
<Users className="w-5 h-5 text-stone-400" />
<h3 className="font-bold text-lg">User Management</h3>
</div>
<div className="overflow-x-auto">
<table className="w-full text-left">
<thead className="bg-stone-50 border-b border-stone-200">
<tr>
<th className="px-6 py-3 text-xs font-black text-stone-400 uppercase tracking-wider">User</th>
<th className="px-6 py-3 text-xs font-black text-stone-400 uppercase tracking-wider">Credits</th>
<th className="px-6 py-3 text-xs font-black text-stone-400 uppercase tracking-wider">P&L (Rev/Cost)</th>
<th className="px-6 py-3 text-xs font-black text-stone-400 uppercase tracking-wider">API Key</th>
<th className="px-6 py-3 text-xs font-black text-stone-400 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-stone-100">
{users.map(u => (
<tr key={u.id} className="hover:bg-stone-50 transition-colors">
<td className="px-6 py-4">
<div className="font-bold">{u.email}</div>
<div className="text-xs text-stone-400 font-mono">{u.id}</div>
</td>
<td className="px-6 py-4 font-mono font-bold text-stone-700">{u.credits}</td>
<td className="px-6 py-4 font-mono text-xs">
<span className="text-green-600 block">+${(u.totalRevenue || 0).toFixed(2)}</span>
<span className="text-red-500 block">-${(u.totalCost || 0).toFixed(4)}</span>
</td>
<td className="px-6 py-4">
{u.apiKey ? <span className="bg-green-100 text-green-700 px-2 py-1 rounded text-xs font-bold">Key Active</span> : <span className="text-stone-400 text-xs">No Key</span>}
</td>
<td className="px-6 py-4">
<button
onClick={() => setSelectedUser(u)}
className="px-3 py-1 bg-purple-600 text-white rounded-lg text-xs font-bold hover:bg-purple-500 shadow-sm flex items-center gap-1"
>
<Key className="w-3 h-3" /> Manage
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
{/* --- USER MANAGEMENT MODAL --- */}
{selectedUser && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm">
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-lg overflow-hidden border border-stone-100">
{/* Header */}
<div className="bg-stone-50 px-6 py-4 border-b border-stone-200 flex justify-between items-center">
<div>
<h3 className="text-lg font-black text-stone-800">Manage User</h3>
<p className="text-xs text-stone-500 font-mono">{selectedUser.email}</p>
</div>
<button
onClick={() => setSelectedUser(null)}
className="p-1 hover:bg-stone-200 rounded-full transition-colors text-stone-400 hover:text-stone-600"
>
</button>
</div>
<div className="p-6 space-y-6">
{/* Role Management */}
<div className="space-y-2">
<label className="text-xs font-black uppercase tracking-widest text-stone-400">User Role</label>
<div className="flex gap-2">
<select
value={selectedUser.role}
onChange={(e) => updateUserRole(selectedUser.id, e.target.value)}
className="flex-1 bg-stone-50 border border-stone-200 rounded-xl px-4 py-2 text-sm font-bold focus:ring-2 focus:ring-purple-500 outline-none"
>
<option value="USER">USER (Standard)</option>
<option value="VIP">VIP (High Priority)</option>
<option value="MODERATOR">MODERATOR (Team)</option>
<option value="ADMIN">ADMIN (God Mode)</option>
</select>
</div>
<p className="text-[10px] text-stone-400">
Admins have full access. VIPs get priority queue.
</p>
</div>
<hr className="border-stone-100" />
{/* Credit Management */}
<div className="space-y-4">
<div className="flex justify-between items-end">
<label className="text-xs font-black uppercase tracking-widest text-stone-400">Credit Balance</label>
<span className="text-2xl font-black text-stone-800 font-mono">{selectedUser.credits}</span>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<p className="text-xs font-bold text-green-600 text-center">Add Credits</p>
<div className="flex gap-1">
<button onClick={() => addCredits(selectedUser.id, 50)} className="flex-1 py-2 bg-green-50 text-green-600 rounded-lg text-xs font-bold hover:bg-green-100 transition-colors">+50</button>
<button onClick={() => addCredits(selectedUser.id, 500)} className="flex-1 py-2 bg-green-50 text-green-600 rounded-lg text-xs font-bold hover:bg-green-100 transition-colors">+500</button>
</div>
</div>
<div className="space-y-2">
<p className="text-xs font-bold text-red-500 text-center">Deduct</p>
<div className="flex gap-1">
<button onClick={() => addCredits(selectedUser.id, -50)} className="flex-1 py-2 bg-red-50 text-red-500 rounded-lg text-xs font-bold hover:bg-red-100 transition-colors">-50</button>
<button onClick={() => addCredits(selectedUser.id, -500)} className="flex-1 py-2 bg-red-50 text-red-500 rounded-lg text-xs font-bold hover:bg-red-100 transition-colors">-500</button>
</div>
</div>
</div>
{/* Manual Input */}
<form
onSubmit={(e) => {
e.preventDefault();
const val = Number((e.target as any).amount.value);
if (val) addCredits(selectedUser.id, val);
(e.target as any).reset();
}}
className="flex gap-2 mt-2"
>
<input
name="amount"
type="number"
placeholder="Custom Amount (+/-)"
className="flex-1 bg-stone-50 border border-stone-200 rounded-xl px-4 py-2 text-sm font-mono focus:ring-2 focus:ring-stone-500 outline-none"
/>
<button type="submit" className="px-4 py-2 bg-stone-800 text-white rounded-xl text-xs font-bold hover:bg-stone-700">Apply</button>
</form>
</div>
</div>
</div>
</div>
)}
{/* --- PRICING TAB --- */}
{activeTab === 'pricing' && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{Object.entries(configs).map(([key, value]) => (
<div key={key} className="bg-white p-4 rounded-xl border border-stone-200 shadow-sm hover:shadow-md transition-shadow">
<div className="flex justify-between items-start mb-2">
<div>
<h4 className="font-bold text-sm text-stone-800 break-all">{key}</h4>
<p className="text-xs text-stone-400">System Configuration Key</p>
</div>
<button
onClick={() => { setEditingConfig(key); setConfigValue(String(value)); }}
className="text-blue-500 hover:text-blue-600 text-xs font-bold"
>
Edit
</button>
</div>
{editingConfig === key ? (
<div className="flex gap-2 mt-2">
<input
value={configValue}
onChange={(e) => setConfigValue(e.target.value)}
className="flex-1 bg-stone-50 border border-stone-300 rounded px-2 py-1 text-sm font-mono"
/>
<button onClick={() => updateConfig(key)} className="bg-green-500 text-white px-2 rounded text-xs"><Save className="w-4 h-4" /></button>
<button onClick={() => setEditingConfig(null)} className="bg-stone-300 text-stone-700 px-2 rounded text-xs"></button>
</div>
) : (
<div className="bg-stone-50 p-2 rounded border border-stone-100 font-mono text-xs text-stone-600 break-all">
{String(value)}
</div>
)}
</div>
))}
</div>
)}
{/* --- ANALYTICS TAB --- */}
{activeTab === 'analytics' && (
<div className="space-y-6">
<div className="bg-white rounded-xl shadow-sm border border-stone-200 p-6">
<div className="flex items-center justify-between mb-8">
<div className="flex items-center gap-3">
<Activity className="w-6 h-6 text-blue-500" />
<div>
<h3 className="font-bold text-lg">Financial Performance</h3>
<p className="text-sm text-stone-500">Revenue (Stripe/Purchase) vs Cost (Gemini API)</p>
</div>
</div>
<div className="flex bg-stone-100 rounded p-1">
{['24h', '7d', '30d'].map(r => (
<button
key={r}
onClick={() => setAnalyticsRange(r)}
className={`px-3 py-1 text-xs font-bold rounded ${analyticsRange === r ? 'bg-white shadow text-black' : 'text-stone-500'}`}
>
{r}
</button>
))}
</div>
</div>
<AnalyticsChart data={analytics} />
</div>
</div>
)}
</div>
</Layout>
);
};
export default AdminDashboard;

279
pages/AnalyticsPage.tsx Normal file
View File

@@ -0,0 +1,279 @@
import React, { useEffect, useState } from 'react';
import axios from 'axios';
import { useTranslation } from 'react-i18next';
import {
Activity, Users, DollarSign, Server, Cpu,
ArrowUpRight, ArrowDownRight, RefreshCw, Layers
} from 'lucide-react';
import Layout from '../components/Layout';
import { useAuth } from '../AuthContext';
// Simple simulated chart component
const MicroChart = ({ color, data }: { color: string, data: number[] }) => (
<div className="flex items-end gap-1 h-12 w-full mt-2 opacity-50">
{data.map((val, i) => (
<div
key={i}
style={{
height: `${val}%`,
backgroundColor: color
}}
className="flex-1 rounded-sm transition-all duration-500"
/>
))}
</div>
);
export default function AnalyticsPage() {
const { t } = useTranslation();
const { user } = useAuth();
const [stats, setStats] = useState<any>(null);
const [loading, setLoading] = useState(true);
const [refreshKey, setRefreshKey] = useState(0);
// Mock Chart Data
const [chartData] = useState(() => Array.from({ length: 12 }, () => Math.floor(Math.random() * 60) + 20));
useEffect(() => {
const fetchStats = async () => {
try {
const res = await axios.get('/api/admin/analytics');
setStats(res.data);
} catch (err) {
console.error("Failed to load analytics", err);
} finally {
setLoading(false);
}
};
fetchStats();
// Auto-refresh every 30s
const interval = setInterval(fetchStats, 30000);
return () => clearInterval(interval);
}, [refreshKey]);
const formatCurrency = (val: number) => {
return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(val);
};
const formatTime = (seconds: number) => {
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
return `${h}h ${m}m`;
};
if (loading && !stats) return (
<Layout>
<div className="flex h-[80vh] items-center justify-center">
<RefreshCw className="w-8 h-8 text-stone-300 animate-spin" />
</div>
</Layout>
);
return (
<Layout>
<div className="max-w-7xl mx-auto p-8 relative overflow-hidden min-h-screen">
{/* Background Ambient Glow */}
<div className="absolute top-0 right-0 w-[500px] h-[500px] bg-purple-500/5 rounded-full blur-[120px] -z-10" />
<div className="absolute bottom-0 left-0 w-[500px] h-[500px] bg-blue-500/5 rounded-full blur-[120px] -z-10" />
{/* Header Section */}
<div className="flex justify-between items-end mb-10">
<div>
<div className="flex items-center gap-3 mb-2">
<div className="bg-stone-900 text-white px-2 py-0.5 rounded text-[10px] font-black uppercase tracking-widest">
v{stats.system.version}
</div>
<span className="flex items-center gap-1.5 text-green-600 text-[10px] font-bold uppercase tracking-widest">
<span className="relative flex h-2 w-2">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span>
<span className="relative inline-flex rounded-full h-2 w-2 bg-green-500"></span>
</span>
System Operational
</span>
</div>
<h1 className="text-4xl font-black text-stone-900 tracking-tighter">Engine Analytics</h1>
<p className="text-stone-500 font-medium">Mission Control & System Health</p>
</div>
<button
onClick={() => setRefreshKey(p => p + 1)}
className="flex items-center gap-2 px-4 py-2 bg-white/50 hover:bg-white border border-stone-200/50 rounded-xl transition-all shadow-sm backdrop-blur-sm text-sm font-bold text-stone-600 hover:text-stone-900"
>
<RefreshCw className="w-4 h-4" />
Refresh Data
</button>
</div>
{/* KPI Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-10">
{/* KPI 1: Users */}
<div className="bg-white/60 backdrop-blur-xl border border-white/40 shadow-[0_8px_30px_rgb(0,0,0,0.04)] rounded-3xl p-6 relative overflow-hidden group">
<div className="flex justify-between items-start mb-4">
<div className="p-3 bg-indigo-50 rounded-2xl text-indigo-600 group-hover:scale-110 transition-transform">
<Users className="w-6 h-6" />
</div>
<span className="flex items-center text-xs font-bold text-green-600 bg-green-50 px-2 py-1 rounded-lg">
+{stats.users.new24h} <ArrowUpRight className="w-3 h-3 ml-1" />
</span>
</div>
<div>
<p className="text-sm font-bold text-stone-400 uppercase tracking-widest mb-1">Total Users</p>
<h3 className="text-3xl font-black text-stone-900 tracking-tight">{stats.users.total}</h3>
</div>
<MicroChart color="#6366f1" data={chartData} />
</div>
{/* KPI 2: Projects */}
<div className="bg-white/60 backdrop-blur-xl border border-white/40 shadow-[0_8px_30px_rgb(0,0,0,0.04)] rounded-3xl p-6 relative overflow-hidden group">
<div className="flex justify-between items-start mb-4">
<div className="p-3 bg-purple-50 rounded-2xl text-purple-600 group-hover:scale-110 transition-transform">
<Layers className="w-6 h-6" />
</div>
<span className="flex items-center text-xs font-bold text-purple-600 bg-purple-50 px-2 py-1 rounded-lg">
{stats.projects.completionRate}% Done
</span>
</div>
<div>
<p className="text-sm font-bold text-stone-400 uppercase tracking-widest mb-1">Total Projects</p>
<h3 className="text-3xl font-black text-stone-900 tracking-tight">{stats.projects.total}</h3>
</div>
<MicroChart color="#a855f7" data={[...chartData].reverse()} />
</div>
{/* KPI 3: Liability */}
<div className="bg-white/60 backdrop-blur-xl border border-white/40 shadow-[0_8px_30px_rgb(0,0,0,0.04)] rounded-3xl p-6 relative overflow-hidden group">
<div className="flex justify-between items-start mb-4">
<div className="p-3 bg-amber-50 rounded-2xl text-amber-600 group-hover:scale-110 transition-transform">
<DollarSign className="w-6 h-6" />
</div>
</div>
<div>
<p className="text-sm font-bold text-stone-400 uppercase tracking-widest mb-1">Credit Liability</p>
<h3 className="text-3xl font-black text-stone-900 tracking-tight">{stats.financials.creditsLiability.toLocaleString()}</h3>
<p className="text-xs text-stone-400 mt-1 font-mono">Value: {formatCurrency(stats.financials.estimatedValue)}</p>
</div>
<div className="absolute -right-6 -bottom-6 opacity-5">
<DollarSign className="w-40 h-40" />
</div>
</div>
{/* KPI 4: System Health */}
<div className="bg-stone-900 backdrop-blur-xl border border-stone-800 shadow-[0_8px_30px_rgb(0,0,0,0.04)] rounded-3xl p-6 relative overflow-hidden text-white group">
<div className="flex justify-between items-start mb-4">
<div className="p-3 bg-white/10 rounded-2xl text-white group-hover:scale-110 transition-transform">
<Server className="w-6 h-6" />
</div>
<span className="flex items-center text-xs font-bold text-green-400 bg-green-900/30 px-2 py-1 rounded-lg border border-green-800">
{stats.system.cpuLoad}% Load
</span>
</div>
<div>
<p className="text-sm font-bold text-stone-400 uppercase tracking-widest mb-1">Uptime</p>
<h3 className="text-3xl font-black tracking-tight">{formatTime(stats.system.uptime)}</h3>
<p className="text-xs text-stone-500 mt-1 font-mono">Mem: {stats.system.memory}MB</p>
</div>
<div className="absolute top-0 right-0 w-full h-full bg-[url('https://grainy-gradients.vercel.app/noise.svg')] opacity-10 mix-blend-overlay"></div>
</div>
</div>
{/* Detailed Sections */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Live Feed */}
<div className="lg:col-span-2 bg-white border border-stone-100 shadow-sm rounded-3xl p-8">
<div className="flex items-center gap-3 mb-6">
<Activity className="w-5 h-5 text-stone-400" />
<h3 className="text-lg font-black text-stone-900 tracking-tight">Live Activity Feed</h3>
</div>
<div className="space-y-4">
{stats.activityLog.map((log: any) => (
<div key={log.id} className="flex items-center justify-between p-4 rounded-2xl bg-stone-50 border border-stone-100 hover:bg-white hover:shadow-md transition-all group">
<div className="flex items-center gap-4">
<div className="w-10 h-10 rounded-full bg-blue-100 flex items-center justify-center text-blue-600 font-bold text-xs">
{log.user.substring(0, 2).toUpperCase()}
</div>
<div>
<p className="text-sm font-bold text-stone-900">{log.action.replace('_', ' ')}</p>
<p className="text-xs text-stone-500">{log.user}</p>
</div>
</div>
<div className="text-right">
<p className="text-xs font-mono font-bold text-stone-400">{new Date(log.timestamp).toLocaleTimeString()}</p>
<p className="text-[10px] font-bold text-stone-300 uppercase tracking-widest">{log.details}</p>
</div>
</div>
))}
{stats.activityLog.length === 0 && (
<div className="text-center py-10 text-stone-400 text-sm">No recent activity found.</div>
)}
</div>
</div>
{/* System Resources */}
<div className="bg-white border border-stone-100 shadow-sm rounded-3xl p-8">
<div className="flex items-center gap-3 mb-6">
<Cpu className="w-5 h-5 text-stone-400" />
<h3 className="text-lg font-black text-stone-900 tracking-tight">Resource Monitor</h3>
</div>
<div className="space-y-6">
<div>
<div className="flex justify-between text-xs font-bold text-stone-500 mb-2 uppercase tracking-widest">
<span>CPU Usage</span>
<span>{stats.system.cpuLoad}%</span>
</div>
<div className="h-2 w-full bg-stone-100 rounded-full overflow-hidden">
<div
className="h-full bg-stone-900 rounded-full transition-all duration-1000 ease-out"
style={{ width: `${stats.system.cpuLoad}%` }}
/>
</div>
</div>
<div>
<div className="flex justify-between text-xs font-bold text-stone-500 mb-2 uppercase tracking-widest">
<span>Memory Allocation</span>
<span>{stats.system.memory} MB</span>
</div>
<div className="h-2 w-full bg-stone-100 rounded-full overflow-hidden">
<div
className="h-full bg-purple-500 rounded-full transition-all duration-1000 ease-out"
style={{ width: `${Math.min((stats.system.memory / 512) * 100, 100)}%` }}
/>
</div>
</div>
<div>
<div className="flex justify-between text-xs font-bold text-stone-500 mb-2 uppercase tracking-widest">
<span>Storage (Projects)</span>
<span>{stats.projects.total} Items</span>
</div>
<div className="h-2 w-full bg-stone-100 rounded-full overflow-hidden">
<div
className="h-full bg-blue-500 rounded-full transition-all duration-1000 ease-out"
style={{ width: '45%' }}
/>
</div>
</div>
<div className="mt-8 p-4 bg-amber-50 rounded-2xl border border-amber-100">
<h4 className="flex items-center gap-2 text-xs font-black text-amber-700 uppercase tracking-widest mb-1">
<Activity className="w-3 h-3" /> System Status
</h4>
<p className="text-xs text-amber-600/80 leading-relaxed">
All services operate within nominal parameters. No critical warnings detected in the last 24 hours.
</p>
</div>
</div>
</div>
</div>
</div>
</Layout>
);
}

184
pages/ConfigPage.tsx Normal file
View File

@@ -0,0 +1,184 @@
import React, { useEffect, useState } from 'react';
import axios from 'axios';
import Layout from '../components/Layout';
import BrandKit from '../components/BrandKit';
import { Settings, Save, Plus, Trash2, ToggleLeft, ToggleRight, Search } from 'lucide-react';
interface ConfigItem {
key: string;
value: string;
description?: string;
}
export default function ConfigPage() {
const [configs, setConfigs] = useState<ConfigItem[]>([]);
const [loading, setLoading] = useState(true);
const [search, setSearch] = useState('');
const [newItem, setNewItem] = useState({ key: '', value: '', description: '' });
const [editingItem, setEditingItem] = useState<string | null>(null);
useEffect(() => {
fetchConfigs();
}, []);
const fetchConfigs = async () => {
try {
const res = await axios.get('/api/admin/config');
setConfigs(res.data.configs);
} catch (err) {
console.error(err);
} finally {
setLoading(false);
}
};
const handleSave = async (key: string, value: string, description?: string) => {
try {
await axios.put('/api/admin/config', { key, value, description });
setEditingItem(null);
fetchConfigs();
if (key === newItem.key) setNewItem({ key: '', value: '', description: '' });
} catch (err) {
alert('Failed to save config');
}
};
const filteredConfigs = (configs || []).filter(c =>
c.key.toLowerCase().includes(search.toLowerCase()) ||
(c.description || '').toLowerCase().includes(search.toLowerCase())
);
return (
<Layout>
<div className="max-w-5xl mx-auto p-8">
<div className="flex justify-between items-end mb-8">
<div>
<h1 className="text-3xl font-black text-stone-900 tracking-tight flex items-center gap-3">
<Settings className="w-8 h-8 text-stone-400" />
System Configuration
</h1>
<p className="text-stone-500 mt-2">Manage global variables, feature flags, and system limits.</p>
</div>
</div>
<BrandKit />
{/* New Config Card */}
<div className="bg-white border border-stone-200 shadow-sm rounded-2xl p-6 mb-8 group focus-within:ring-2 ring-purple-500/20 transition-all">
<h3 className="text-xs font-bold text-stone-400 uppercase tracking-widest mb-4 flex items-center gap-2">
<Plus className="w-4 h-4" /> Add New Variable
</h3>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<input
placeholder="KEY_NAME (e.g. MAX_TOKENS)"
className="bg-stone-50 border border-stone-200 rounded-xl px-4 py-2 text-sm font-mono font-bold focus:outline-none focus:border-purple-500 transition-all"
value={newItem.key}
onChange={e => setNewItem({ ...newItem, key: e.target.value.toUpperCase() })}
/>
<input
placeholder="Value"
className="bg-stone-50 border border-stone-200 rounded-xl px-4 py-2 text-sm focus:outline-none focus:border-purple-500 transition-all"
value={newItem.value}
onChange={e => setNewItem({ ...newItem, value: e.target.value })}
/>
<input
placeholder="Description (Optional)"
className="bg-stone-50 border border-stone-200 rounded-xl px-4 py-2 text-sm focus:outline-none focus:border-purple-500 transition-all"
value={newItem.description}
onChange={e => setNewItem({ ...newItem, description: e.target.value })}
/>
<button
disabled={!newItem.key || !newItem.value}
onClick={() => handleSave(newItem.key, newItem.value, newItem.description)}
className="bg-stone-900 text-white rounded-xl font-bold text-sm hover:bg-purple-600 disabled:opacity-50 disabled:hover:bg-stone-900 transition-colors flex items-center justify-center gap-2"
>
<Save className="w-4 h-4" /> Save Variable
</button>
</div>
</div>
{/* Search Bar */}
<div className="relative mb-6">
<Search className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-stone-400" />
<input
type="text"
placeholder="Search configuration keys..."
className="w-full pl-10 pr-4 py-3 bg-white border border-stone-200 rounded-xl text-sm font-medium focus:outline-none focus:ring-2 focus:ring-stone-100 transition-all"
value={search}
onChange={e => setSearch(e.target.value)}
/>
</div>
{/* Config Table */}
<div className="bg-white border border-stone-200 shadow-sm rounded-3xl overflow-hidden">
<table className="w-full">
<thead className="bg-stone-50 border-b border-stone-100">
<tr>
<th className="text-left py-4 px-6 text-[10px] font-black uppercase tracking-widest text-stone-400">Key Name</th>
<th className="text-left py-4 px-6 text-[10px] font-black uppercase tracking-widest text-stone-400">Value</th>
<th className="text-left py-4 px-6 text-[10px] font-black uppercase tracking-widest text-stone-400">Description</th>
<th className="text-right py-4 px-6 text-[10px] font-black uppercase tracking-widest text-stone-400">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-stone-100">
{filteredConfigs.map(config => (
<tr key={config.key} className="hover:bg-stone-50/50 transition-colors group">
<td className="py-4 px-6 font-mono text-xs font-bold text-purple-600 select-all">
{config.key}
</td>
<td className="py-4 px-6">
{editingItem === config.key ? (
<input
defaultValue={config.value}
className="w-full bg-white border border-purple-200 rounded-lg px-2 py-1 text-sm focus:outline-none"
onBlur={(e) => handleSave(config.key, e.target.value, config.description)}
onKeyDown={(e) => {
if (e.key === 'Enter') handleSave(config.key, e.currentTarget.value, config.description);
}}
autoFocus
/>
) : (
<button
onClick={() => setEditingItem(config.key)}
className="text-sm font-medium text-stone-700 hover:text-purple-600 border-b border-stone-200 border-dashed hover:border-purple-300 transition-colors text-left"
>
{config.value === 'true' || config.value === 'false' ? (
<span className={`inline-flex items-center gap-1.5 px-2 py-0.5 rounded text-[10px] uppercase font-black tracking-wider ${config.value === 'true' ? 'bg-green-100 text-green-700' : 'bg-red-50 text-red-500'}`}>
{config.value === 'true' ? <ToggleRight className="w-4 h-4" /> : <ToggleLeft className="w-4 h-4" />}
{config.value.toUpperCase()}
</span>
) : (
<span className="truncate max-w-[200px] block" title={config.value}>{config.value}</span>
)}
</button>
)}
</td>
<td className="py-4 px-6 text-xs text-stone-400 font-medium">
{config.description || '-'}
</td>
<td className="py-4 px-6 text-right">
<button
onClick={() => setEditingItem(config.key)}
className="p-2 text-stone-300 hover:text-stone-900 transition-colors"
>
<Settings className="w-4 h-4" />
</button>
</td>
</tr>
))}
{filteredConfigs.length === 0 && (
<tr>
<td colSpan={4} className="py-10 text-center text-stone-400 text-sm">
No configuration keys found.
</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
</Layout>
);
}

94
pages/LoginPage.tsx Normal file
View File

@@ -0,0 +1,94 @@
import React, { useState } from 'react';
import axios from 'axios';
import { useAuth } from '../AuthContext';
import { useNavigate, Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { Tooltip } from '../components/Tooltip';
import { GoogleLogin } from '@react-oauth/google';
export default function Login() {
const { t } = useTranslation();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const { login } = useAuth();
const navigate = useNavigate();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
try {
const res = await axios.post('/api/auth/login', { email, password });
login(res.data.token, res.data.user);
navigate('/');
} catch (err: any) {
setError(err.response?.data?.error || 'Login failed');
}
};
return (
<div className="flex min-h-screen items-center justify-center bg-gray-100">
<div className="w-full max-w-md rounded-lg bg-white p-8 shadow-lg">
<h2 className="mb-6 text-center text-2xl font-bold text-gray-800">{t('buttons.login')}</h2>
{error && <div className="mb-4 rounded bg-red-100 p-2 text-red-600">{error}</div>}
<form onSubmit={handleSubmit}>
<div className="mb-4">
<label className="mb-1 block font-semibold text-gray-700">Email</label>
<input
type="email"
className="w-full rounded border p-2 focus:border-blue-500 focus:outline-none"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
</div>
<div className="mb-6">
<label className="mb-1 block font-semibold text-gray-700">Password</label>
<input
type="password"
className="w-full rounded border p-2 focus:border-blue-500 focus:outline-none"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
</div>
<Tooltip content={t('buttons.login')} position="top">
<button
type="submit"
className="w-full rounded bg-blue-600 py-2 font-bold text-white hover:bg-blue-700 transition"
>
{t('buttons.login')}
</button>
</Tooltip>
</form>
<div className="mt-6">
<div className="relative">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-gray-300"></div>
</div>
<div className="relative flex justify-center text-sm">
<span className="bg-white px-2 text-gray-500">Or continue with</span>
</div>
</div>
<div className="mt-6 flex justify-center">
<GoogleLogin
onSuccess={async (credentialResponse) => {
try {
const res = await axios.post('/api/auth/google', { credential: credentialResponse.credential });
login(res.data.token, res.data.user);
navigate('/');
} catch (err) {
setError('Google Login Failed');
}
}}
onError={() => setError('Google Login Failed')}
/>
</div>
</div>
<div className="mt-4 text-center">
<p className="text-gray-600">Don't have an account? <Link to="/signup" className="text-blue-600 hover:underline">Sign up</Link></p>
</div>
</div>
</div>
);
}

141
pages/PricingPage.tsx Normal file
View File

@@ -0,0 +1,141 @@
import React, { useState } from 'react';
import axios from 'axios';
import { useAuth } from '../AuthContext';
import { Check, Shield, ArrowLeft } from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import { Layout } from '../components/Layout';
const PricingPage: React.FC = () => {
const { user, refreshUser } = useAuth();
const navigate = useNavigate();
const [loading, setLoading] = useState<string | null>(null);
const handlePurchase = async (plan: string, credits: number, price: number) => {
setLoading(plan);
try {
const token = localStorage.getItem('token');
// MOCK PURCHASE ENDPOINT
await axios.post('/api/user/purchase', { plan, credits, price }, {
headers: { Authorization: `Bearer ${token}` }
});
await refreshUser();
alert(`Successfully purchased ${plan}! ${credits} credits added.`);
navigate('/');
} catch (err: any) {
console.error(err);
alert("Purchase failed: " + (err.response?.data?.error || err.message));
} finally {
setLoading(null);
}
};
return (
<Layout>
<div className="max-w-5xl mx-auto p-8 pt-12">
<div className="flex items-center gap-4 mb-12">
<button
onClick={() => navigate('/')}
className="p-3 bg-white hover:bg-stone-100 border border-stone-200 rounded-2xl transition-all shadow-sm group"
>
<ArrowLeft className="w-5 h-5 text-stone-600 group-hover:-translate-x-1 transition-transform" />
</button>
<div>
<h1 className="text-3xl font-black tracking-tighter text-stone-900">Upgrade Your Engine</h1>
<p className="text-stone-500 font-medium text-sm">Unlock higher limits and premium features.</p>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{/* ... tier content remains same ... */}
{/* Free Tier */}
<div className="bg-white rounded-2xl shadow-sm border border-stone-200 p-8 flex flex-col">
<div className="mb-4">
<h3 className="text-lg font-bold text-stone-500 uppercase tracking-wider">Starter</h3>
<p className="text-4xl font-black mt-2">$0</p>
<p className="text-sm text-stone-400">Forever free</p>
</div>
<ul className="space-y-4 mb-8 flex-1">
<li className="flex items-center gap-3 text-sm">
<span className="p-1 bg-green-100 text-green-600 rounded-full"><Check className="w-3 h-3" /></span>
10 Free Credits / Month
</li>
<li className="flex items-center gap-3 text-sm">
<span className="p-1 bg-green-100 text-green-600 rounded-full"><Check className="w-3 h-3" /></span>
Standard Quality
</li>
</ul>
<button disabled className="w-full py-3 bg-stone-100 text-stone-400 font-bold rounded-xl cursor-not-allowed">
Current Plan
</button>
</div>
{/* Pro Tier */}
<div className="bg-stone-900 text-white rounded-2xl shadow-xl border border-stone-800 p-8 flex flex-col relative transform scale-105">
<div className="absolute top-0 left-1/2 -translate-x-1/2 -translate-y-1/2 bg-gradient-to-r from-purple-500 to-indigo-500 text-white px-4 py-1 rounded-full text-xs font-bold uppercase tracking-widest shadow-lg">
Most Popular
</div>
<div className="mb-4">
<h3 className="text-lg font-bold text-purple-400 uppercase tracking-wider">Professional</h3>
<p className="text-4xl font-black mt-2">$29</p>
<p className="text-sm text-stone-400">per month</p>
</div>
<ul className="space-y-4 mb-8 flex-1">
<li className="flex items-center gap-3 text-sm">
<span className="p-1 bg-purple-500/20 text-purple-400 rounded-full"><Check className="w-3 h-3" /></span>
1,000 Credits / Month
</li>
<li className="flex items-center gap-3 text-sm">
<span className="p-1 bg-purple-500/20 text-purple-400 rounded-full"><Check className="w-3 h-3" /></span>
Priority Generation
</li>
<li className="flex items-center gap-3 text-sm">
<span className="p-1 bg-purple-500/20 text-purple-400 rounded-full"><Check className="w-3 h-3" /></span>
Access to Beta Features
</li>
</ul>
<button
onClick={() => handlePurchase('PRO', 1000, 29)}
disabled={loading === 'PRO'}
className="w-full py-3 bg-gradient-to-r from-purple-600 to-indigo-600 hover:from-purple-500 hover:to-indigo-500 text-white font-bold rounded-xl transition-all shadow-lg hover:shadow-purple-500/25 disabled:opacity-50"
>
{loading === 'PRO' ? 'Processing...' : 'Upgrade Now'}
</button>
</div>
{/* Enterprise/Bulk */}
<div className="bg-white rounded-2xl shadow-sm border border-stone-200 p-8 flex flex-col">
<div className="mb-4">
<h3 className="text-lg font-bold text-stone-500 uppercase tracking-wider">Credit Pack</h3>
<p className="text-4xl font-black mt-2">$10</p>
<p className="text-sm text-stone-400">One-time purchase</p>
</div>
<ul className="space-y-4 mb-8 flex-1">
<li className="flex items-center gap-3 text-sm">
<span className="p-1 bg-green-100 text-green-600 rounded-full"><Check className="w-3 h-3" /></span>
+300 Credits
</li>
<li className="flex items-center gap-3 text-sm">
<span className="p-1 bg-green-100 text-green-600 rounded-full"><Check className="w-3 h-3" /></span>
Never Expires
</li>
</ul>
<button
onClick={() => handlePurchase('PACK', 300, 10)}
disabled={loading === 'PACK'}
className="w-full py-3 bg-white border-2 border-stone-200 text-stone-700 hover:border-stone-900 hover:text-stone-900 font-bold rounded-xl transition-all disabled:opacity-50"
>
{loading === 'PACK' ? 'Processing...' : 'Buy Pack'}
</button>
</div>
</div>
<div className="mt-12 text-center text-xs text-stone-400 bg-stone-100 p-4 rounded-lg inline-flex items-center gap-2">
<Shield className="w-4 h-4" />
Secure Payment Processing via MockStripe (Test Mode)
</div>
</div>
</Layout>
);
};
export default PricingPage;

185
pages/ScorecardPage.tsx Normal file
View File

@@ -0,0 +1,185 @@
import React, { useState } from 'react';
import Header from '../components/Header';
import { useAuth } from '../AuthContext';
import { motion } from 'framer-motion';
import { Brain, Upload, Image as ImageIcon, AlertCircle } from 'lucide-react';
import NeuroScorecard from '../components/NeuroScorecard';
import { ApiKeyModal } from '../components/ApiKeyModal';
const ScorecardPage = () => {
const { user, token, logout, refreshUser } = useAuth();
const [imageUrl, setImageUrl] = useState<string>('');
const [loading, setLoading] = useState(false);
const [analysis, setAnalysis] = useState<any>(null);
const [error, setError] = useState<string | null>(null);
const [isApiKeyModalOpen, setIsApiKeyModalOpen] = useState(false);
const handleImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
const reader = new FileReader();
reader.onloadend = () => {
setImageUrl(reader.result as string);
setAnalysis(null); // Reset analysis when new image is uploaded
};
reader.readAsDataURL(file);
}
};
const analyzeImage = async () => {
if (!imageUrl) return;
setLoading(true);
setError(null);
try {
// Strip base64 prefix if present for API
const base64Data = imageUrl.split(',')[1];
const response = await fetch('http://localhost:3001/api/neuro-score', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
imageBase64: base64Data,
apiKey: localStorage.getItem('gemini_api_key') || user?.apiKey // BYOK support
})
});
const data = await response.json();
if (!data.success) {
throw new Error(data.error || "Analysis failed");
}
setAnalysis(data.data);
} catch (err: any) {
setError(err.message);
} finally {
setLoading(false);
}
};
return (
<div className="min-h-screen bg-stone-950 text-stone-200 font-sans selection:bg-purple-500/30">
<Header
user={user}
logout={logout}
openApiKeyModal={() => setIsApiKeyModalOpen(true)}
/>
<ApiKeyModal
isOpen={isApiKeyModalOpen}
onClose={() => setIsApiKeyModalOpen(false)}
/>
<main className="max-w-6xl mx-auto px-6 py-12">
{/* Hero Section */}
<div className="text-center mb-16">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="inline-flex items-center gap-2 px-4 py-1.5 rounded-full bg-blue-500/10 border border-blue-500/20 text-blue-400 text-sm font-medium mb-6"
>
<Brain className="w-4 h-4" />
<span>The Internal Critic</span>
</motion.div>
<h1 className="text-5xl md:text-6xl font-serif text-white mb-6">
Neuro-Scorecard
</h1>
<p className="text-xl text-stone-400 max-w-2xl mx-auto leading-relaxed">
Don't guess what sells. Let our AI predict your conversion rate by analyzing
dopamine triggers, cognitive ease, and market fit.
</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-start">
{/* Input Section */}
<div className="space-y-6">
<div className="p-8 rounded-3xl bg-stone-900/50 border border-white/5 backdrop-blur-sm relative group overflow-hidden">
<div className="absolute inset-0 bg-gradient-to-br from-blue-500/5 to-purple-500/5 opacity-0 group-hover:opacity-100 transition-opacity" />
{!imageUrl ? (
<label className="flex flex-col items-center justify-center w-full h-80 border-2 border-dashed border-white/10 rounded-xl cursor-pointer hover:border-blue-500/50 hover:bg-white/5 transition-all group-hover:scale-[1.02]">
<div className="flex flex-col items-center justify-center pt-5 pb-6">
<div className="p-4 bg-stone-800 rounded-full mb-4">
<Upload className="w-8 h-8 text-stone-400" />
</div>
<p className="mb-2 text-lg text-stone-300 font-medium">Click to upload an image</p>
<p className="text-sm text-stone-500">PNG, JPG up to 10MB</p>
</div>
<input type="file" className="hidden" accept="image/*" onChange={handleImageUpload} />
</label>
) : (
<div className="relative rounded-xl overflow-hidden shadow-2xl">
<img src={imageUrl} alt="Analysis Target" className="w-full h-auto object-cover" />
<button
onClick={() => { setImageUrl(''); setAnalysis(null); }}
className="absolute top-4 right-4 p-2 bg-black/50 backdrop-blur-md rounded-full text-white hover:bg-red-500/80 transition-colors"
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clipRule="evenodd" />
</svg>
</button>
</div>
)}
</div>
<button
onClick={analyzeImage}
disabled={!imageUrl || loading}
className={`w-full py-4 rounded-xl font-bold text-lg shadow-lg flex items-center justify-center gap-2 transition-all ${!imageUrl ? 'bg-stone-800 text-stone-500 cursor-not-allowed' :
loading ? 'bg-stone-700 text-stone-400 cursor-wait' :
'bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-500 hover:to-purple-500 text-white hover:shadow-blue-500/20 hover:scale-[1.02]'
}`}
>
{loading ? (
<>Processing Vision...</>
) : (
<>
<Brain className="w-5 h-5" />
Run Neuro-Analysis
</>
)}
</button>
{error && (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
className="p-4 bg-red-500/10 border border-red-500/20 rounded-xl text-red-400 flex items-center gap-3"
>
<AlertCircle className="w-5 h-5 flex-shrink-0" />
<p>{error}</p>
</motion.div>
)}
</div>
{/* Results Section */}
<div>
{!analysis && !loading && (
<div className="h-full flex flex-col items-center justify-center text-stone-600 p-12 border border-white/5 rounded-3xl bg-stone-900/30 border-dashed">
<ImageIcon className="w-16 h-16 mb-4 opacity-20" />
<p className="text-lg font-medium">Ready to Score</p>
<p className="text-sm">Upload an image to see its commercial prediction.</p>
</div>
)}
{(analysis || loading) && (
<NeuroScorecard analysis={analysis} loading={loading} />
)}
</div>
</div>
</main>
</div>
);
};
export default ScorecardPage;

493
pages/SettingsPage.tsx Normal file
View File

@@ -0,0 +1,493 @@
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import { useAuth } from '../AuthContext';
import { Save, Key, Shield, User, ArrowLeft, CreditCard, Upload, Image as ImageIcon } from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import { Layout } from '../components/Layout';
import { ProductType } from '../types';
import { Tag } from 'lucide-react';
const SettingsPage: React.FC = () => {
const { user, refreshUser } = useAuth();
const navigate = useNavigate();
const [apiKey, setApiKey] = useState('');
const [savedKeyMasked, setSavedKeyMasked] = useState<string | null>(null);
const [etsyShopName, setEtsyShopName] = useState('');
const [etsyShopLink, setEtsyShopLink] = useState('');
const [etsyShopLogo, setEtsyShopLogo] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const [brandingLoading, setBrandingLoading] = useState(false);
const [logoUploading, setLogoUploading] = useState(false);
const [message, setMessage] = useState<{ type: 'success' | 'error', text: string } | null>(null);
// SKU State
const [skuConfig, setSkuConfig] = useState<Record<string, { prefix: string, next: number }>>({});
const [skuLoading, setSkuLoading] = useState(false);
const PRODUCT_TYPES: ProductType[] = ["Wall Art", "Bookmark", "Sticker", "Planner", "Phone Wallpaper", "Social Media Kit", "Label"];
useEffect(() => {
fetchApiKey();
fetchBranding();
// Check local storage for migration
const localKey = localStorage.getItem('gemini_api_key');
if (localKey && !apiKey && !savedKeyMasked) {
setApiKey(localKey);
}
}, []);
const fetchApiKey = async () => {
try {
const token = localStorage.getItem('token');
const res = await axios.get('/api/user/apikey', {
headers: { Authorization: `Bearer ${token}` }
});
if (res.data.apiKey) {
setSavedKeyMasked(res.data.apiKey);
}
} catch (err) {
console.error("Failed to fetch key", err);
}
};
const fetchBranding = async () => {
try {
const token = localStorage.getItem('token');
const res = await axios.get('/api/user/branding', {
headers: { Authorization: `Bearer ${token}` }
});
setEtsyShopName(res.data.etsyShopName || '');
setEtsyShopLink(res.data.etsyShopLink || '');
setEtsyShopLogo(res.data.etsyShopLogo || null);
} catch (err) {
console.error("Failed to fetch branding", err);
}
};
useEffect(() => {
if (user) {
setEtsyShopName(user.etsyShopName || '');
setEtsyShopLink(user.etsyShopLink || '');
setEtsyShopLogo(user.etsyShopLogo || null);
if (user.apiKey) setSavedKeyMasked(user.apiKey.replace(/.(?=.{4})/g, '*'));
try {
if (user.skuSettings) {
setSkuConfig(JSON.parse(user.skuSettings));
}
} catch (e) { }
}
}, [user]);
const handleSaveSku = async () => {
setSkuLoading(true);
setMessage(null);
try {
const token = localStorage.getItem('token');
await axios.post('/api/user/sku', { skuSettings: JSON.stringify(skuConfig) }, {
headers: { Authorization: `Bearer ${token}` }
});
setMessage({ type: 'success', text: 'SKU Settings saved successfully.' });
await refreshUser();
} catch (error: any) {
setMessage({ type: 'error', text: error.response?.data?.error || 'Failed to save SKU settings.' });
} finally {
setSkuLoading(false);
}
};
const handleSaveKey = async () => {
setLoading(true);
setMessage(null);
try {
const token = localStorage.getItem('token');
await axios.post('/api/user/apikey', { apiKey }, {
headers: { Authorization: `Bearer ${token}` }
});
setMessage({ type: 'success', text: 'API Key saved securely to your profile.' });
setSavedKeyMasked(`${apiKey.substring(0, 4)}...${apiKey.substring(apiKey.length - 4)}`);
setApiKey(''); // Clear input for security
// Also update local storage for redundancy if needed, or clear it to enforce server side?
// Let's keep it in sync for now as a fallback if we revert logic.
localStorage.setItem('gemini_api_key', apiKey);
} catch (err: any) {
setMessage({ type: 'error', text: err.response?.data?.error || 'Failed to save key' });
} finally {
setLoading(false);
}
};
const handleSaveBranding = async () => {
setBrandingLoading(true);
setMessage(null);
try {
const token = localStorage.getItem('token');
await axios.post('/api/user/branding', { etsyShopName, etsyShopLink }, {
headers: { Authorization: `Bearer ${token}` }
});
setMessage({ type: 'success', text: 'Store branding updated successfully.' });
await refreshUser(); // Update global context field
} catch (err: any) {
setMessage({ type: 'error', text: err.response?.data?.error || 'Failed to update branding' });
} finally {
setBrandingLoading(false);
}
};
const handleGodMode = async () => {
try {
const token = localStorage.getItem('token');
const res = await axios.post('http://localhost:3001/api/admin/grant-me-god-mode', {}, {
headers: { Authorization: `Bearer ${token}` }
});
if (res.data.token) {
localStorage.setItem('token', res.data.token);
// Force update context if possible, or just reload
}
alert(res.data.message);
window.location.reload();
} catch (err: any) {
console.error(err);
alert(err.response?.data?.error || "Failed to grant God Mode");
}
};
return (
<Layout>
<div className="max-w-3xl mx-auto p-6 pt-12 space-y-8">
<div className="flex items-center gap-4 mb-4">
<button
onClick={() => navigate('/')}
className="p-3 bg-white hover:bg-stone-100 border border-stone-200 rounded-2xl transition-all shadow-sm group"
>
<ArrowLeft className="w-5 h-5 text-stone-600 group-hover:-translate-x-1 transition-transform" />
</button>
<div>
<h1 className="text-3xl font-black tracking-tighter text-stone-900">Account Settings</h1>
<p className="text-stone-500 font-medium text-sm">Manage your profile and API security.</p>
</div>
</div>
{/* Profile Card */}
<section className="bg-white rounded-xl shadow-sm border border-stone-200 p-6">
<div className="flex items-center gap-4 mb-6">
<div className="w-12 h-12 bg-purple-100 rounded-full flex items-center justify-center text-purple-600">
<User className="w-6 h-6" />
</div>
<div>
<h2 className="text-lg font-semibold">{user?.email}</h2>
<div className="flex items-center gap-2 mt-1">
<span className="px-2 py-0.5 bg-stone-100 border border-stone-200 rounded text-xs text-stone-600 font-medium">
{user?.role}
</span>
<span className="px-2 py-0.5 bg-indigo-50 border border-indigo-100 rounded text-xs text-indigo-600 font-medium">
{user?.plan} Plan
</span>
</div>
</div>
</div>
<div className="p-4 bg-stone-50 rounded-lg border border-stone-100 flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="p-2 bg-yellow-100 rounded-lg text-yellow-700">
<CreditCard className="w-5 h-5" />
</div>
<div>
<p className="text-sm text-stone-500 font-medium">Available Credits</p>
<p className="text-xl font-bold text-stone-800">{user?.credits}</p>
</div>
</div>
<button className="text-sm font-medium text-purple-600 hover:underline">
Top Up (Soon)
</button>
</div>
</section>
{/* API Key Management */}
<section className="bg-white rounded-xl shadow-sm border border-stone-200 p-6">
<div className="flex items-center gap-3 mb-4">
<Key className="w-5 h-5 text-purple-600" />
<h3 className="text-lg font-semibold">Gemini API Key</h3>
</div>
<div className="mb-6 p-4 bg-blue-50 text-blue-800 rounded-lg text-sm border border-blue-100 flex gap-3">
<Shield className="w-5 h-5 flex-shrink-0" />
<p>
<strong>Beta Mode Requirement:</strong> Your personal request key is now stored securely in your profile.
This allows us to process your requests on our servers without you needing to re-enter it on every device.
</p>
</div>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-stone-700 mb-1">
{savedKeyMasked ? 'Update API Key' : 'Add API Key'}
</label>
<div className="relative">
<input
type="text"
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
placeholder={savedKeyMasked ? `Current: ${savedKeyMasked}` : "Paste your Google Gemini API Key here"}
className="w-full px-4 py-2 border border-stone-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:outline-none"
/>
{savedKeyMasked && !apiKey && (
<span className="absolute right-3 top-2.5 text-xs text-green-600 font-medium flex items-center gap-1">
<Shield className="w-3 h-3" /> Securely Saved
</span>
)}
</div>
<p className="mt-2 text-xs text-stone-500">
Get your key from <a href="https://aistudio.google.com/app/apikey" target="_blank" rel="noreferrer" className="text-purple-600 hover:underline">Google AI Studio</a>.
</p>
</div>
{message && (
<div className={`text-sm p-3 rounded-lg ${message.type === 'success' ? 'bg-green-50 text-green-700' : 'bg-red-50 text-red-700'}`}>
{message.text}
</div>
)}
<div className="flex justify-end">
<button
onClick={handleSaveKey}
disabled={loading || apiKey.length < 10}
className={`flex items-center gap-2 px-6 py-2 rounded-lg font-medium text-white transition-all ${loading || apiKey.length < 10
? 'bg-stone-300 cursor-not-allowed'
: 'bg-stone-900 hover:bg-stone-800'
}`}
>
<Save className="w-4 h-4" />
{loading ? 'Saving...' : 'Save Key'}
</button>
</div>
</div>
</section>
{/* Store Branding */}
<section className="bg-white rounded-xl shadow-sm border border-stone-200 p-6">
<div className="flex items-center gap-3 mb-4">
<Save className="w-5 h-5 text-indigo-600" />
<h3 className="text-lg font-semibold">Store Branding</h3>
</div>
<div className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-stone-700 mb-1">
Etsy Shop Name
</label>
<input
type="text"
value={etsyShopName}
onChange={(e) => setEtsyShopName(e.target.value)}
placeholder="e.g. MyAmazingStudio"
className="w-full px-4 py-2 border border-stone-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:outline-none"
/>
</div>
<div>
<label className="block text-sm font-medium text-stone-700 mb-1">
Etsy Shop Link
</label>
<input
type="url"
value={etsyShopLink}
onChange={(e) => setEtsyShopLink(e.target.value)}
placeholder="https://www.etsy.com/shop/..."
className="w-full px-4 py-2 border border-stone-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:outline-none"
/>
</div>
</div>
{/* Logo Upload Section */}
<div className="mt-4 border-t border-stone-100 pt-4">
<label className="block text-sm font-medium text-stone-700 mb-3">
Shop Logo
</label>
<div className="flex items-center gap-6">
<div className="relative w-24 h-24 bg-stone-100 rounded-full border border-stone-200 flex items-center justify-center overflow-hidden shrink-0">
{etsyShopLogo ? (
<img
src={`/api/storage/${etsyShopLogo}`}
alt="Shop Logo"
className="w-full h-full object-cover"
/>
) : (
<ImageIcon className="w-8 h-8 text-stone-300" />
)}
{logoUploading && (
<div className="absolute inset-0 bg-black/50 flex items-center justify-center">
<div className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin" />
</div>
)}
</div>
<div className="flex-1">
<div className="flex items-center gap-3">
<label className="cursor-pointer inline-flex items-center gap-2 px-4 py-2 bg-white border border-stone-300 rounded-lg text-sm font-medium text-stone-700 hover:bg-stone-50 transition-colors shadow-sm">
<Upload className="w-4 h-4" />
{etsyShopLogo ? 'Change Logo' : 'Upload Logo'}
<input
type="file"
accept="image/*"
className="hidden"
onChange={async (e) => {
if (!e.target.files || !e.target.files[0]) return;
const file = e.target.files[0];
setLogoUploading(true);
setMessage(null);
const formData = new FormData();
formData.append('logo', file);
try {
const token = localStorage.getItem('token');
const res = await axios.post('/api/user/logo', formData, {
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'multipart/form-data'
}
});
setEtsyShopLogo(res.data.logoPath);
setMessage({ type: 'success', text: 'Logo uploaded successfully.' });
await refreshUser(); // Update global context field
} catch (err: any) {
setMessage({ type: 'error', text: err.response?.data?.error || 'Failed to upload logo' });
} finally {
setLogoUploading(false);
}
}}
/>
</label>
<p className="text-xs text-stone-500">
Recommended: 500x500px, PNG or JPG.
</p>
</div>
</div>
</div>
</div>
<div className="flex justify-end">
<button
onClick={handleSaveBranding}
disabled={brandingLoading}
className={`flex items-center gap-2 px-6 py-2 rounded-lg font-medium text-white transition-all ${brandingLoading
? 'bg-stone-300 cursor-not-allowed'
: 'bg-stone-900 hover:bg-stone-800'
}`}
>
<Save className="w-4 h-4" />
{brandingLoading ? 'Updating...' : 'Save Branding'}
</button>
</div>
</div>
</section>
{/* SKU MANAGEMENT */}
<section className="bg-white rounded-xl shadow-sm border border-stone-200 p-6">
<div className="flex items-center gap-3 mb-4">
<Tag className="w-5 h-5 text-emerald-600" />
<h3 className="text-lg font-semibold">SKU Configuration</h3>
</div>
<div className="space-y-4">
<p className="text-sm text-stone-500">
Set up prefixes and starting numbers for automatic SKU generation (e.g., WLR105).
</p>
<div className="bg-stone-50 rounded-xl overflow-hidden border border-stone-200">
<table className="w-full text-sm text-left">
<thead className="bg-stone-100 text-stone-600 font-bold uppercase text-xs">
<tr>
<th className="px-4 py-3">Product Type</th>
<th className="px-4 py-3">Prefix (3-4 Chars)</th>
<th className="px-4 py-3">Next Number</th>
</tr>
</thead>
<tbody className="divide-y divide-stone-200">
{PRODUCT_TYPES.map((type) => {
const config = skuConfig[type] || { prefix: '', next: 1 };
return (
<tr key={type} className="hover:bg-white transition-colors">
<td className="px-4 py-3 font-medium text-stone-700">{type}</td>
<td className="px-4 py-3">
<input
type="text"
maxLength={4}
placeholder="e.g. WLR"
value={config.prefix}
onChange={(e) => {
const val = e.target.value.toUpperCase().replace(/[^A-Z]/g, '');
setSkuConfig(prev => ({
...prev,
[type]: { ...prev[type], prefix: val, next: prev[type]?.next || 1 }
}));
}}
className="w-24 px-2 py-1 border border-stone-300 rounded font-mono uppercase focus:ring-2 focus:ring-emerald-500 focus:outline-none"
/>
</td>
<td className="px-4 py-3">
<input
type="number"
min="1"
value={config.next}
onChange={(e) => {
const val = parseInt(e.target.value) || 1;
setSkuConfig(prev => ({
...prev,
[type]: { ...prev[type], prefix: prev[type]?.prefix || '', next: val }
}));
}}
className="w-24 px-2 py-1 border border-stone-300 rounded font-mono focus:ring-2 focus:ring-emerald-500 focus:outline-none"
/>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
<div className="flex justify-end">
<button
onClick={handleSaveSku}
disabled={skuLoading}
className={`flex items-center gap-2 px-6 py-2 rounded-lg font-medium text-white transition-all ${skuLoading
? 'bg-stone-300 cursor-not-allowed'
: 'bg-stone-900 hover:bg-stone-800'
}`}
>
<Save className="w-4 h-4" />
{skuLoading ? 'Saving...' : 'Save Configuration'}
</button>
</div>
</div>
</section>
<section className="bg-red-50 rounded-xl shadow-sm border border-red-100 p-6">
<div className="flex items-center gap-3 mb-2">
<Shield className="w-5 h-5 text-red-600" />
<h3 className="text-lg font-bold text-red-900">Emergency Admin Access</h3>
</div>
<p className="text-sm text-red-700 mb-4">
If you are unable to access admin features or are stuck with credit limits, use this button to force-upgrade your account to ADMIN status.
</p>
<button
onClick={handleGodMode}
className="px-4 py-2 bg-red-600 hover:bg-red-700 text-white font-bold rounded-lg transition-colors shadow-sm"
>
Grant Me Admin God Mode
</button>
</section>
</div>
</Layout >
);
};
export default SettingsPage;

243
pages/SignupPage.tsx Normal file
View File

@@ -0,0 +1,243 @@
import React, { useState } from 'react';
import axios from 'axios';
import { useAuth } from '../AuthContext';
import { useNavigate, Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { Tooltip } from '../components/Tooltip';
import { LegalModal } from '../components/LegalModal';
import { ShieldCheck, FileText, AlertTriangle, Key } from 'lucide-react';
import { GoogleLogin } from '@react-oauth/google';
export default function Signup() {
const { t } = useTranslation();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [etsyShopName, setEtsyShopName] = useState('');
const [etsyShopLink, setEtsyShopLink] = useState('');
// New Fields for Phase 6
const [apiKey, setApiKey] = useState('');
const [termsAccepted, setTermsAccepted] = useState(false);
const [kvkkAccepted, setKvkkAccepted] = useState(false);
// UI State
const [error, setError] = useState<{ message: string, code?: string } | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
const [modalType, setModalType] = useState<'terms' | 'kvkk' | null>(null);
const { login } = useAuth();
const navigate = useNavigate();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError(null);
// Frontend Verification
if (!termsAccepted || !kvkkAccepted) {
setError({ message: "You must accept both the User Agreement and KVKK to register." });
return;
}
if (apiKey.length < 20) {
setError({ message: "Please enter a valid Gemini API Key." });
return;
}
setIsSubmitting(true);
try {
const res = await axios.post('/api/auth/register', {
email,
password,
apiKey,
etsyShopName,
etsyShopLink,
termsAccepted: true // Backend checks this
});
login(res.data.token, res.data.user);
navigate('/');
} catch (err: any) {
const data = err.response?.data;
const code = data?.code || 'UNKNOWN_ERROR';
const msg = data?.error || 'Registration failed. Please try again.';
setError({ message: msg, code });
} finally {
setIsSubmitting(false);
}
};
return (
<div className="flex min-h-screen items-center justify-center bg-stone-50 p-4">
<LegalModal
isOpen={!!modalType}
onClose={() => setModalType(null)}
type={modalType}
/>
<div className="w-full max-w-lg rounded-2xl bg-white p-8 shadow-xl border border-stone-100">
<div className="text-center mb-8">
<h2 className="text-3xl font-black text-stone-900 tracking-tight">Create Account</h2>
<p className="text-stone-500 mt-2 text-sm">Join the Beta access program</p>
</div>
{error && (
<div className={`mb-6 rounded-xl p-4 flex gap-3 items-start ${error.code === 'QUOTA_EXCEEDED' ? 'bg-amber-50 text-amber-800 border-amber-200 border' : 'bg-red-50 text-red-800 border-red-200 border'}`}>
<AlertTriangle className="w-5 h-5 flex-shrink-0 mt-0.5" />
<div>
<h4 className="font-bold text-sm uppercase tracking-wide mb-1">
{error.code === 'QUOTA_EXCEEDED' ? 'Quota Exceeded' : 'Registration Failed'}
</h4>
<p className="text-sm opacity-90">{error.message}</p>
{error.code === 'INVALID_KEY' && (
<p className="text-xs mt-2 font-mono bg-white/50 p-1 rounded inline-block">
Tip: Keys usually start with AIza...
</p>
)}
</div>
</div>
)}
<form onSubmit={handleSubmit} className="space-y-5">
{/* Security Note */}
<div className="bg-blue-50 border border-blue-100 rounded-lg p-3 flex gap-2 items-center text-xs text-blue-700">
<ShieldCheck className="w-4 h-4" />
<span>Your data is encrypted. We strictly follow KVKK/GDPR.</span>
</div>
<div>
<label className="mb-1 block text-sm font-bold text-stone-700">Email Address</label>
<input
type="email"
className="w-full rounded-lg border border-stone-300 p-3 text-sm focus:border-stone-900 focus:ring-1 focus:ring-stone-900 focus:outline-none transition-all"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
placeholder="you@example.com"
/>
</div>
<div>
<label className="mb-1 block text-sm font-bold text-stone-700">Password</label>
<input
type="password"
className="w-full rounded-lg border border-stone-300 p-3 text-sm focus:border-stone-900 focus:ring-1 focus:ring-stone-900 focus:outline-none transition-all"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
placeholder="••••••••"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="mb-1 block text-sm font-bold text-stone-700">Etsy Shop Name</label>
<input
type="text"
className="w-full rounded-lg border border-stone-300 p-3 text-sm focus:border-stone-900 focus:ring-1 focus:ring-stone-900 focus:outline-none transition-all"
value={etsyShopName}
onChange={(e) => setEtsyShopName(e.target.value)}
placeholder="e.g. PixelParadise"
/>
</div>
<div>
<label className="mb-1 block text-sm font-bold text-stone-700">Etsy Shop Link</label>
<input
type="url"
className="w-full rounded-lg border border-stone-300 p-3 text-sm focus:border-stone-900 focus:ring-1 focus:ring-stone-900 focus:outline-none transition-all"
value={etsyShopLink}
onChange={(e) => setEtsyShopLink(e.target.value)}
placeholder="https://etsy.com/shop/..."
/>
</div>
</div>
<div>
<div className="flex justify-between items-center mb-1">
<label className="block text-sm font-bold text-stone-700">Gemini API Key (Required)</label>
<a href="https://aistudio.google.com/app/apikey" target="_blank" rel="noreferrer" className="text-[10px] font-bold text-blue-600 hover:underline uppercase">
Get Key
</a>
</div>
<div className="relative">
<Key className="absolute left-3 top-3 w-4 h-4 text-stone-400" />
<input
type="password"
className="w-full rounded-lg border border-stone-300 p-3 pl-10 text-sm font-mono focus:border-stone-900 focus:ring-1 focus:ring-stone-900 focus:outline-none transition-all"
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
required
placeholder="AIzaSy..."
/>
</div>
<p className="text-[10px] text-stone-400 mt-1">
The system will send a test request to validate this key immediately.
</p>
</div>
{/* Legal Checkboxes */}
<div className="space-y-3 pt-2">
<label className="flex items-start gap-3 cursor-pointer group">
<input
type="checkbox"
className="mt-1 w-4 h-4 rounded border-stone-300 text-stone-900 focus:ring-stone-900"
checked={termsAccepted}
onChange={(e) => setTermsAccepted(e.target.checked)}
/>
<span className="text-sm text-stone-600 group-hover:text-stone-800 transition-colors">
I accept the <button type="button" onClick={(e) => { e.preventDefault(); setModalType('terms'); }} className="font-bold underline">User Agreement & IP Rights</button>.
</span>
</label>
<label className="flex items-start gap-3 cursor-pointer group">
<input
type="checkbox"
className="mt-1 w-4 h-4 rounded border-stone-300 text-stone-900 focus:ring-stone-900"
checked={kvkkAccepted}
onChange={(e) => setKvkkAccepted(e.target.checked)}
/>
<span className="text-sm text-stone-600 group-hover:text-stone-800 transition-colors">
I have read and accept the <button type="button" onClick={(e) => { e.preventDefault(); setModalType('kvkk'); }} className="font-bold underline">KVKK (PDPL) & Privacy Policy</button>.
</span>
</label>
</div>
<button
type="submit"
disabled={isSubmitting || !termsAccepted || !kvkkAccepted || !apiKey}
className="w-full rounded-xl bg-stone-900 py-3.5 font-bold text-white uppercase tracking-widest hover:bg-stone-800 disabled:opacity-50 disabled:cursor-not-allowed transition-all shadow-lg hover:shadow-xl transform active:scale-[0.98]"
>
{isSubmitting ? 'Verifying Key...' : 'Create Account'}
</button>
</form>
<div className="mt-8">
<div className="relative">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-stone-200"></div>
</div>
<div className="relative flex justify-center text-sm">
<span className="bg-white px-2 text-stone-500 font-bold uppercase text-[10px] tracking-widest">Or continue with</span>
</div>
</div>
<div className="mt-6 flex justify-center">
<GoogleLogin
onSuccess={async (credentialResponse) => {
try {
const res = await axios.post('/api/auth/google', { credential: credentialResponse.credential });
login(res.data.token, res.data.user);
navigate('/');
} catch (err) {
setError({ message: 'Google Signup Failed' });
}
}}
onError={() => setError({ message: 'Google Signup Failed' })}
/>
</div>
</div>
<div className="mt-6 text-center pt-6 border-t border-stone-100">
<p className="text-stone-500 text-sm">Already have an account? <Link to="/login" className="font-bold text-stone-900 hover:underline">Log in</Link></p>
</div>
</div>
</div>
);
}

212
pages/XRayPage.tsx Normal file
View File

@@ -0,0 +1,212 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Sparkles, ArrowRight, Activity, Copy, CheckCircle, AlertTriangle, ScanEye } from 'lucide-react';
import axios from 'axios';
import { useNavigate } from 'react-router-dom';
const XRayPage: React.FC = () => {
const { t } = useTranslation();
const navigate = useNavigate();
const [url, setUrl] = useState('');
const [loading, setLoading] = useState(false);
const [result, setResult] = useState<any>(null);
const [error, setError] = useState('');
const [copied, setCopied] = useState(false);
// Mock result for dev preview if needed, but we connect to real API
const handleAnalyze = async () => {
if (!url) return;
setLoading(true);
setError('');
setResult(null);
try {
const response = await axios.post('/api/xray', { url }, {
headers: { Authorization: `Bearer ${localStorage.getItem('token')}` }
});
setResult(response.data);
} catch (err: any) {
console.error(err);
setError(err.response?.data?.error || 'Failed to analyze. Please check the URL and try again.');
} finally {
setLoading(false);
}
};
const copyPrompt = () => {
if (result?.analysis?.superiorPrompt) {
navigator.clipboard.writeText(result.analysis.superiorPrompt);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
};
return (
<div className="min-h-screen bg-stone-50 p-8 text-stone-900 font-sans">
<div className="max-w-6xl mx-auto space-y-12">
{/* Header Section */}
<div className="text-center space-y-4">
<div className="inline-flex items-center justify-center p-3 bg-indigo-100 rounded-full mb-4">
<ScanEye className="w-8 h-8 text-indigo-600" />
</div>
<h1 className="text-4xl font-serif font-bold tracking-tight text-stone-900">
Competitor X-Ray
</h1>
<p className="text-lg text-stone-500 max-w-2xl mx-auto">
Paste any Etsy or Pinterest product URL. Our AI will deconstruct its success formula and generate a superior prompt for you.
</p>
</div>
{/* Input Section */}
<div className="bg-white/80 backdrop-blur-xl rounded-2xl shadow-xl border border-stone-200 p-2 flex items-center max-w-3xl mx-auto transition-all focus-within:ring-4 ring-indigo-500/10">
<input
type="text"
placeholder="Paste Etsy or Pinterest URL here..."
className="flex-1 bg-transparent border-none text-lg px-6 py-4 focus:ring-0 placeholder:text-stone-400"
value={url}
onChange={(e) => setUrl(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleAnalyze()}
/>
<button
onClick={handleAnalyze}
disabled={loading || !url}
className={`
px-8 py-4 rounded-xl font-medium text-white shadow-lg transition-all flex items-center gap-2
${loading || !url ? 'bg-stone-300 cursor-not-allowed' : 'bg-indigo-600 hover:bg-indigo-700 hover:shadow-indigo-500/30'}
`}
>
{loading ? (
<>Running X-Ray...</>
) : (
<>Analyze <ArrowRight className="w-5 h-5" /></>
)}
</button>
</div>
{error && (
<div className="max-w-3xl mx-auto bg-red-50 text-red-600 p-4 rounded-xl border border-red-100 flex items-center gap-3">
<AlertTriangle className="w-5 h-5" />
{error}
</div>
)}
{/* Results Section */}
{result && (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 animate-in fade-in slide-in-from-bottom-8 duration-700">
{/* Left: Competitor Analysis */}
<div className="space-y-6">
<div className="bg-white rounded-2xl shadow-sm border border-stone-100 overflow-hidden">
<div className="aspect-[4/3] bg-stone-100 relative overflow-hidden">
<img
src={result.metadata.image}
alt="Competitor"
className="w-full h-full object-cover transform hover:scale-105 transition-transform duration-700"
/>
<div className="absolute top-4 left-4 bg-black/70 backdrop-blur-md text-white px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider">
Competitor Asset
</div>
</div>
<div className="p-6">
<h3 className="text-xl font-bold font-serif mb-2 line-clamp-2">{result.metadata.title}</h3>
<p className="text-stone-500 text-sm line-clamp-3 mb-4">{result.metadata.description}</p>
<div className="space-y-4">
<div>
<h4 className="text-xs font-bold text-stone-400 uppercase tracking-wider mb-2">Visual DNA Detected</h4>
<div className="flex flex-wrap gap-2">
{result.analysis.visualDna?.map((tag: string, i: number) => (
<span key={i} className="px-3 py-1 bg-stone-100 text-stone-600 rounded-full text-xs font-medium border border-stone-200">
{tag}
</span>
))}
</div>
</div>
<div>
<h4 className="text-xs font-bold text-red-400 uppercase tracking-wider mb-2">Identified Weaknesses (Sentiment Gap)</h4>
<div className="bg-red-50/50 p-4 rounded-xl border border-red-100 text-sm text-stone-700 italic">
"{result.analysis.sentimentGap}"
</div>
</div>
</div>
</div>
</div>
</div>
{/* Right: The Solution (Superior Prompt) */}
<div className="space-y-6">
<div className="bg-indigo-900 text-white rounded-2xl shadow-xl overflow-hidden relative">
{/* Decorative background glow */}
<div className="absolute top-0 right-0 w-64 h-64 bg-indigo-500/20 blur-3xl rounded-full translate-x-1/2 -translate-y-1/2"></div>
<div className="p-8 relative z-10">
<div className="flex items-center gap-3 mb-6">
<div className="p-2 bg-indigo-500/20 rounded-lg">
<Sparkles className="w-6 h-6 text-indigo-300" />
</div>
<div>
<h3 className="text-2xl font-serif font-bold">Superior Formula</h3>
<p className="text-indigo-200 text-sm">Optimized for high-conversion & aesthetics</p>
</div>
</div>
<div className="bg-black/30 backdrop-blur-sm p-6 rounded-xl border border-white/10 relative group">
<p className="font-mono text-sm text-indigo-100 leading-relaxed">
{result.analysis.superiorPrompt}
</p>
<button
onClick={copyPrompt}
className="absolute top-4 right-4 p-2 bg-white/10 hover:bg-white/20 rounded-lg transition-colors text-white"
title="Copy Prompt"
>
{copied ? <CheckCircle className="w-5 h-5 text-green-400" /> : <Copy className="w-5 h-5" />}
</button>
</div>
<div className="mt-8">
<h4 className="text-xs font-bold text-indigo-300 uppercase tracking-wider mb-3">Why This Wins</h4>
<div className="space-y-3">
<p className="text-sm text-indigo-100/80 leading-relaxed border-l-2 border-indigo-500 pl-4">
{result.analysis.gapAnalysis}
</p>
</div>
</div>
</div>
<div className="bg-indigo-950/50 p-4 flex justify-between items-center border-t border-white/10">
<span className="text-xs text-indigo-400 font-medium">Ready to dominate?</span>
<div className="flex gap-3">
<button
onClick={() => navigate('/', { state: { prompt: result.analysis.superiorPrompt } })} // Assuming Home takes state
className="px-4 py-2 bg-white text-indigo-900 rounded-lg text-sm font-bold hover:bg-indigo-50 transition-colors flex items-center gap-2"
>
Generate Asset <ArrowRight className="w-4 h-4" />
</button>
</div>
</div>
</div>
{/* Pro Tip */}
<div className="bg-yellow-50 border border-yellow-100 rounded-xl p-4 flex items-start gap-3">
<Activity className="w-5 h-5 text-yellow-600 mt-0.5" />
<div>
<h4 className="font-bold text-yellow-800 text-sm">Pro Tip</h4>
<p className="text-xs text-yellow-700 mt-1">
Use this prompt in the "Analyst" mode for best results. Consider generating 4 variations to test different lighting setups.
</p>
</div>
</div>
</div>
</div>
)}
</div>
</div>
);
};
export default XRayPage;