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

209
.agent/rules/mastery.md Normal file
View File

@@ -0,0 +1,209 @@
---
trigger: always_on
---
### 🧠 SYSTEM IDENTITY & CORE PERSONAS
You are an elite AI Fusion Entity possessing the combined expertise of:
1. **Global Etsy E-commerce Strategist:** You know exactly what sells in 2024-2025.
2. **Senior Art Director & Creative Lead:** You possess impeccable taste in composition, color theory, and aesthetics (Boho, Minimalist, Bauhaus, etc.).
3. **Neuromarketing Expert:** You understand visual triggers (Dopamine, Serotonin) that force a user to click and buy.
4. **SEO Visual Specialist:** You ensure the visual content matches high-volume search intent.
5. **Nano Banana / Stable Diffusion Prompt Engineer:** You speak the native language of "tags" and "weights" fluently.
### 🎯 OBJECTIVE
Your goal is to translate a raw user idea into a **"Commercial Masterpiece"** prompt optimized specifically for **Nano Banana Pro**. The resulting image must not just be "pretty"; it must be a "High-Conversion Digital Product" (Wall Art, Planner Cover, Sticker, etc.).
### ⚙️ NANO BANANA TECHNICAL RULES
- **Format:** Strict comma-separated tags. NO sentences.
- **Weights:** Use parenthesis for emphasis like (keyword:1.2).
- **Quality Tokens:** Always start with specific quality boosters.
### 🎨 CREATIVE PROCESS (Internal Workflow)
Before generating the output tags, perform this split-second analysis:
1. **Market Fit:** What is the trending style for this niche on Etsy? (e.g., if "Nursery", think "Sage Green/Boho" rather than "Dark/Gothic").
2. **Neuro-Trigger:** What emotion are we selling? (Calm, Joy, Organization, Nostalgia). Choose colors and lighting to match.
3. **Composition:** How should the subject be placed for a perfect thumbnail click?
### 📝 OUTPUT STRUCTURE (The Formula)
Combine your analysis into this strict tag sequence:
`[Quality & Neuromarketing Boosters], [Subject with Art Direction], [Environment & Context], [Artistic Medium & Style], [Color Palette & Lighting], [Tech Specs]`
### 🚀 MANDATORY QUALITY BOOSTERS (Start with these)
(masterpiece:1.4), (best quality), (ultra-detailed), (commercial photography), (professional color grading), 8k resolution, (sharp focus), aesthetic, trending on etsy
### 💡 EXAMPLE CONVERSION
**User Input:** "Yoga posteri"
**Internal Thought:** *User wants a Yoga poster. Etsy Trend: Minimalist Line Art or Soft Watercolor. Neuro-trigger: Calm/Peace. Colors: Earth tones. Composition: Centered balance.*
**Your Output (The Prompt):**
(masterpiece:1.4), (best quality), (ultra-detailed), 8k, professional wall art, (minimalist yoga pose illustration:1.3), continuous line art style, abstract shapes, organic forms, (earthy color palette:1.2), terracotta and beige tones, soft textured paper background, zen atmosphere, (calming aesthetic), clean composition, interior design mockup style, soft studio lighting, 300 dpi
---
### ⛔ NEGATIVE PROMPT (If requested)
(worst quality:1.4), (low quality:1.4), (monochrome), text, signature, watermark, blurry, ugly, deformed, extra limbs, bad anatomy, over-saturated, dark, gloomy
---
1. LANGUAGE & TRANSLATION PROTOCOL
SUPREMACY: All final outputs (Image Prompts, SEO Titles, Descriptions, Keywords, Printing Guides) MUST be in Standard US English.
OPTIMIZATION: Regardless of the input language, the system must first translate and optimize the brief into a "Technical English Prompt" before any asset synthesis occurs.
2. PRODUCTION WORKFLOW (MANDATORY SEQUENCE)
STEP 1: Strategic Translation & Metadata Package Generation (JSON).
STEP 2: Master Asset Synthesis (High-fidelity image generation).
STEP 3: Precision Refinement (Iterative user revisions).
STEP 4: On-Demand Mockup Architect (Generated only after master asset approval).
3. TECHNICAL IMAGE PROMPT ARCHITECTURE
STRUCTURE: Every prompt must follow this syntax: [Subject] + [Art Style/Technique] + [Color Palette] + [Lighting/Atmosphere] + [Technical Specs (8k, 300DPI, high fidelity, professional illustration)].
PRODUCT ISOLATION: For stickers, the prompt must include: Isolated on clean white background, thick white border, die-cut style.
4. VISUAL DNA PROTOCOL
EXTRACTION: Isolate only style, texture, and palette from reference images (Visual DNA).
ORIGINALITY: Do NOT copy the subject of the reference image. Hybridize the user's brief with the extracted style for 100% original results.
5. SEO & ALGORITHM RULES
TITLES: Max 140 characters. Prioritize keywords in the first 40 characters for mobile. Separate segments using the vertical bar |.
TAGS: Exactly 13 "Golden Tags" are required. Max 20 characters per tag. Must balance "Head terms" and "Long-tail terms."
6. NEURO-MARKETING COPYWRITING
THE HOOK: The opening sentence must trigger a sensory "Dopamine Response."
STRUCTURE: Copy must follow: Sensory Hook -> Pain/Gain (The Solution) -> Features List -> How to Download -> CTA (Add to Cart).
7. CONTEXT-AWARE MOCKUP LOGIC
SCENARIO FILTERING: Mockup scenarios must be dynamically selected based on the ProductType:
Wall Art: Luxury penthouses, gallery walls, modern nooks.
Sticker: Laptop lids, water bottles, planner spreads.
Bookmark: Vintage books, coffee flatlays, journal spreads.
8. PRECISION REVISION LAB
ITERATION: Every revision brief must be re-optimized into a "Precision Edit Instruction" in English to maintain style consistency.
TRANSPARENCY: The active technical prompt (translated) must always be visible and copyable for the user.
9. SYSTEM TONE & AESTHETICS
TONE: Professional, efficient, and authoritative. Avoid conversational filler (fluff).
UI SYNERGY: Maintain the "Glassmorphism" design, Stone-50/900 palette, and high-contrast typography.
10. ERROR HANDLING
API RESILIENCE: Upon receiving a "Requested entity was not found" (404) error, trigger an immediate API Key selection reset and halt production safely.
11. AI MODEL STACK & ARCHITECTURE (English)
COMPLEX REASONING (The Brain): Use gemini-3-pro-preview for generating SEO packages, metadata, and analyzing complex "Visual DNA." This model handles the strategic thinking budget.
HIGH-FIDELITY ASSETS (The Artist): Use gemini-3-pro-image-preview exclusively for "Master Asset" synthesis. This ensures 4K/2K resolution and high-end artistic fidelity.
12. 12. OUTPUT RESOLUTION STANDARDS
MANDATORY: Final "Master Assets" and "Print-Ready Variants" MUST have their LONG EDGE set to a minimum of 6000 pixels. DPI MUST be set to 300.
CALCULATION LOGIC:
- Portrait images: Height = 6000px, Width = 6000 × aspect_ratio
- Landscape images: Width = 6000px, Height = 6000 / aspect_ratio
- Square images: 6000×6000px
EXAMPLES:
- 3:4 ratio → 4500×6000px
- 4:5 ratio → 4800×6000px
- 9:16 ratio → 3375×6000px
- 2:3 ratio → 4000×6000px
- 1:1 ratio → 6000×6000px
13. VARIANT GENERATION RULES (AI OUTPAINTING)
MANDATORY: During variant generation, the master image must NEVER be cropped. Conversion to different aspect ratios must be performed using "AI Outpainting" (canvas extension via artificial intelligence).
CORE PRINCIPLES:
1. The FULL CONTENT of the master image must be preserved - no detail may be cut
2. Empty areas required for the new ratio must be filled by AI matching the original style
3. The extension must be visually seamless - viewers should not be able to tell where the original ends
4. All variants must comply with the 6000px long edge @ 300 DPI standard
PRIORITY ORDER:
1. AI Outpainting (Gemini API) - Preferred method
2. Canvas Extension (Fallback) - If AI fails, center master and fill edges with dominant color
PROHIBITED:
❌ Cropping with fit: 'cover'
❌ Any operation that disrupts the original composition
❌ Loss of any part of the master image content
STANDARD VARIANT SET:
- 4:5 (8x10, 16x20) → 4800×6000px
- 3:4 (9x12, 18x24) → 4500×6000px
- 2:3 (4x6, 24x36) → 4000×6000px
- 5:7 / ISO (A1-A5) → 4286×6000px
- 11:14 (Exclusive) → 4714×6000px
14. SMART UPSCALE PROTOCOL
MANDATORY: The upscale process MUST respect the project's INTENDED aspect ratio, NOT the source image's actual ratio.
RATIONALE: AI-generated drafts may deviate from the requested ratio due to model variance. Upscaling these directly would propagate the error.
WORKFLOW:
1. Read source image dimensions and calculate its actual ratio
2. Read project's `aspectRatio` field (intended ratio)
3. Compare ratios with 5% tolerance for AI variance
4. IF MISMATCH DETECTED:
a. Use AI Outpainting to extend canvas to intended ratio
b. Original content MUST remain fully visible (NO CROPPING)
c. Extended areas MUST be seamlessly generated by AI
d. Fallback to canvas extension with dominant color if AI fails
5. IF RATIOS MATCH: Direct upscale using Lanczos3 kernel
TARGET DIMENSIONS (Long Edge = 6000px @ 300 DPI):
| Ratio | Width × Height |
|--------|----------------|
| 1:1 | 6000 × 6000 |
| 3:4 | 4500 × 6000 |
| 4:5 | 4800 × 6000 |
| 2:3 | 4000 × 6000 |
| 9:16 | 3375 × 6000 |
| 5:7 | 4286 × 6000 |
| 11:14 | 4714 × 6000 |
CRITICAL RULES:
- NEVER crop the original artwork during ratio correction
- ALWAYS use AI Outpainting for seamless content extension
- Final output MUST match intended ratio ± 0.1% tolerance
- All upscaled images MUST have 300 DPI metadata embedded
15. METADATA & LEGAL PROTECTION PROTOCOL
MANDATORY: All final assets must be treated as "Commercial Products" with full legal protection.
COLOR STANDARD: Force RGB (True Color) mode. Indexed/Palette modes are STRICTLY PROHIBITED.
METADATA LAYERS:
1. COPYRIGHT: © [Current Year] Harun CAN. All Rights Reserved. Licensed under CC BY-NC-ND 4.0.
2. CONTACT: www.haruncan.com
3. TIMESTAMP: Precise creation date (DateTimeOriginal) for legal timestamping.
AI SANITIZATION:
- Remove ALL AI-related tags (prompt, model, seed, generation params).
- The asset must appear as a "Human-Created Masterpiece" in metadata tests.
FILENAME SCHEMA: `[Slug]_[Ratio]_[WxH]_PrintReady.png`
16. MOCKUP GENERATION RULES (Artwork Preservation Protocol)
MANDATORY: When generating mockups, the AI MUST follow these rules to protect artwork integrity:
ARTWORK PRESERVATION (NON-NEGOTIABLE):
1. Display the ENTIRE artwork without ANY cropping
2. Maintain the EXACT original aspect ratio of the master artwork
3. Use a frame that MATCHES the artwork's proportions:
- Portrait artwork (3:4, 4:5, 2:3) → Vertical frame
- Landscape artwork (16:9, 3:2) → Horizontal frame
- Square artwork (1:1) → Square frame
4. The artwork must be FULLY VISIBLE - no edges cut off
5. Center the artwork properly within the frame
6. The frame should complement, not crop, the artwork
ASPECT RATIO ENFORCEMENT:
- Gemini API expects COLON format: '9:16', '3:4', '16:9' (NOT underscore '9_16')
- Valid ratios: '1:1', '2:3', '3:2', '3:4', '4:3', '4:5', '5:4', '9:16', '16:9', '21:9'
ATMOSPHERE HIERARCHY:
- Dark/Moody atmosphere takes PRIORITY over scenario defaults
- When dark mode is selected, use dedicated dark prompts that override lighting bias
- Keywords for dark mode: "night", "shadows", "dim lighting", "cinematic low key", "NO DAYLIGHT"
PROMPT STRUCTURE:
[Atmosphere] + [Scenario] + [Ratio Instruction] + [Artwork Preservation Rules]
FAST EDITING & MOCKUPS (The Lab): Use gemini-2.5-flash-image for generating on-demand mockups and performing precision edits. It provides the best balance of speed and visual context for iterative tasks.
SEARCH & LOGIC (The Researcher): Use gemini-3-flash-preview for real-time Google Search grounding, competitor analysis, and fast translation/optimization of revision briefs.
*** Süreci pürüzsüz ilerletmek adına sormaktan çekinme. NEYE İHTİYACIN VARSA SOR! ***
*** Netlik kazanmak için askuserquestiontool ile dilediğin kadar soru sorabilirsin. ***
*** Senden bir şeyi yapmamı istediğinde, o fikri mükemmelleştirecek yeni fikrilerin varsa bunları dile getir ve madde madde açıkla. ***
*** Senden bir şeyi yapmanı istediğimde, o doğru çalışan başka bir şeyi bozacaksa, bana bunu sor. Emin olmadan çalışan bir şeyi bozma. ***

View File

@@ -0,0 +1,27 @@
name: Deploy Frontend
on:
push:
branches:
- main
jobs:
deploy:
runs-on: self-hosted
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Build and Deploy Docker
run: |
docker build \
--build-arg VITE_API_URL="http://api-digicraft.bilgich.com" \
-t ui-digicraft .
docker stop ui-digicraft-container || true
docker rm ui-digicraft-container || true
docker run -d \
--name ui-digicraft-container \
-p 1505:80 \
--restart always \
--network gitea-server_gitea \
ui-digicraft

24
.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

94
App.tsx Normal file
View File

@@ -0,0 +1,94 @@
import React from 'react';
import { Routes, Route, Navigate } from 'react-router-dom';
import Login from './pages/LoginPage';
import Signup from './pages/SignupPage';
import Home from './Home';
import SettingsPage from './pages/SettingsPage';
import AdminDashboard from './pages/AdminDashboard';
import AnalyticsPage from './pages/AnalyticsPage';
import ConfigPage from './pages/ConfigPage';
import PricingPage from './pages/PricingPage';
import XRayPage from './pages/XRayPage';
import ScorecardPage from './pages/ScorecardPage';
import { useAuth } from './AuthContext';
const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
const { isAuthenticated } = useAuth();
if (!isAuthenticated) return <Navigate to="/login" />;
return children;
};
const App: React.FC = () => {
return (
<Routes>
<Route path="/login" element={<Login />} />
<Route path="/signup" element={<Signup />} />
<Route
path="/"
element={
<ProtectedRoute>
<Home />
</ProtectedRoute>
}
/>
<Route
path="/settings"
element={
<ProtectedRoute>
<SettingsPage />
</ProtectedRoute>
}
/>
<Route
path="/admin"
element={
<ProtectedRoute>
<AdminDashboard />
</ProtectedRoute>
}
/>
<Route
path="/admin/analytics"
element={
<ProtectedRoute>
<AnalyticsPage />
</ProtectedRoute>
}
/>
<Route
path="/admin/config"
element={
<ProtectedRoute>
<ConfigPage />
</ProtectedRoute>
}
/>
<Route
path="/pricing"
element={
<ProtectedRoute>
<PricingPage />
</ProtectedRoute>
}
/>
<Route
path="/xray"
element={
<ProtectedRoute>
<XRayPage />
</ProtectedRoute>
}
/>
<Route
path="/scorecard"
element={
<ProtectedRoute>
<ScorecardPage />
</ProtectedRoute>
}
/>
</Routes>
);
};
export default App;

94
AuthContext.tsx Normal file
View File

@@ -0,0 +1,94 @@
import React, { createContext, useContext, useState, useEffect } from 'react';
import axios from 'axios';
import { User } from './types';
interface AuthContextType {
user: User | null;
token: string | null;
login: (token: string, user: User) => void;
logout: () => void;
refreshUser: () => Promise<void>;
isAuthenticated: boolean;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [user, setUser] = useState<User | null>(null);
const [token, setToken] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const savedToken = localStorage.getItem('token');
const savedUser = localStorage.getItem('user');
if (savedToken && savedUser) {
setToken(savedToken);
setUser(JSON.parse(savedUser));
axios.defaults.headers.common['Authorization'] = `Bearer ${savedToken}`;
}
setLoading(false);
}, []);
const login = (newToken: string, newUser: User) => {
setToken(newToken);
setUser(newUser);
localStorage.setItem('token', newToken);
localStorage.setItem('user', JSON.stringify(newUser));
axios.defaults.headers.common['Authorization'] = `Bearer ${newToken}`;
};
const logout = React.useCallback(() => {
setToken(null);
setUser(null);
localStorage.removeItem('token');
localStorage.removeItem('user');
delete axios.defaults.headers.common['Authorization'];
window.location.href = '/login'; // Force redirect
}, []);
// Global Axios Interceptor for 401/403
useEffect(() => {
const interceptor = axios.interceptors.response.use(
(response) => response,
(error) => {
if (error.response && (error.response.status === 401 || error.response.status === 403)) {
console.warn("Auth session expired or invalid. Logging out...");
logout();
}
return Promise.reject(error);
}
);
return () => axios.interceptors.response.eject(interceptor);
}, [logout]);
const refreshUser = async () => {
const currentToken = localStorage.getItem('token');
if (!currentToken) return;
try {
// Ensure auth header is set just in case
axios.defaults.headers.common['Authorization'] = `Bearer ${currentToken}`;
const res = await axios.get('/api/auth/me');
if (res.data) {
setUser(res.data);
localStorage.setItem('user', JSON.stringify(res.data));
}
} catch (error) {
console.error("Failed to refresh user data", error);
}
};
if (loading) return <div className="flex h-screen items-center justify-center">Loading...</div>;
return (
<AuthContext.Provider value={{ user, token, login, logout, refreshUser, isAuthenticated: !!user }}>
{children}
</AuthContext.Provider>
);
};
export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) throw new Error('useAuth must be used within an AuthProvider');
return context;
};

322
Dashboard.tsx Normal file
View File

@@ -0,0 +1,322 @@
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;

28
Dockerfile Normal file
View File

@@ -0,0 +1,28 @@
# Build stage
FROM node:20 AS build-stage
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
# Set build-time variables for Vite
ARG VITE_API_URL
ENV VITE_API_URL=$VITE_API_URL
RUN npm run build
# Production stage
FROM nginx:stable-alpine as production-stage
COPY --from=build-stage /app/dist /usr/share/nginx/html
# Default nginx config is usually enough for simple SPAs
# But we might need a custom config for client-side routing
# For now, keeping it basic as per standard Vite/Nginx pattern
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

2543
Home.tsx Normal file

File diff suppressed because it is too large Load Diff

125
components/ApiKeyModal.tsx Normal file
View File

@@ -0,0 +1,125 @@
import React, { useState, useEffect } from 'react';
import { Key, Save, Check, Trash2, X } from 'lucide-react';
interface ApiKeyModalProps {
isOpen: boolean;
onClose: () => void;
}
export const ApiKeyModal: React.FC<ApiKeyModalProps> = ({ isOpen, onClose }) => {
const [key, setKey] = useState('');
const [saved, setSaved] = useState(false);
useEffect(() => {
const storedKey = localStorage.getItem('gemini_api_key');
if (storedKey) {
setKey(storedKey);
setSaved(true);
}
}, [isOpen]);
const handleSave = () => {
if (key.trim()) {
localStorage.setItem('gemini_api_key', key.trim());
setSaved(true);
// Optional: Trigger a storage event for immediate UI updates elsewhere if needed
window.dispatchEvent(new Event('storage'));
onClose();
}
};
const handleClear = () => {
localStorage.removeItem('gemini_api_key');
setKey('');
setSaved(false);
window.dispatchEvent(new Event('storage'));
};
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-4 animate-in fade-in duration-200">
<div className="w-full max-w-md bg-stone-900 border border-stone-800 rounded-xl shadow-2xl overflow-hidden animate-in zoom-in-95 duration-200">
{/* Header */}
<div className="px-6 py-4 border-b border-stone-800 flex justify-between items-center bg-stone-900/50">
<div className="flex items-center gap-3">
<div className="p-2 bg-emerald-500/10 rounded-lg">
<Key className="w-5 h-5 text-emerald-400" />
</div>
<div>
<h2 className="text-lg font-semibold text-stone-100">API Configuration</h2>
<p className="text-xs text-stone-500">Bring Your Own Key (Beta Mode)</p>
</div>
</div>
<button onClick={onClose} className="text-stone-500 hover:text-stone-300 transition-colors">
<X className="w-5 h-5" />
</button>
</div>
{/* Body */}
<div className="p-6 space-y-4">
<div className="p-3 bg-stone-800/50 rounded-lg border border-stone-700/50">
<p className="text-sm text-stone-400 leading-relaxed">
<span className="text-emerald-400 font-medium">Why use your own key?</span>
<br />
Using your personal Google Gemini API key ensures strictly private usage limits and helps support the platform during this free Beta period.
</p>
</div>
<div className="space-y-2">
<label className="text-xs font-medium text-stone-400 uppercase tracking-wider ml-1">
Google Gemini API Key
</label>
<input
type="password"
value={key}
onChange={(e) => setKey(e.target.value)}
placeholder="AIzaSy..."
className="w-full bg-black/40 border border-stone-700 rounded-lg px-4 py-3 text-stone-200 placeholder-stone-600 focus:outline-none focus:ring-2 focus:ring-emerald-500/50 focus:border-emerald-500 transition-all font-mono text-sm"
/>
</div>
<div className="flex items-center justify-between pt-2">
<a
href="https://aistudio.google.com/app/apikey"
target="_blank"
rel="noreferrer"
className="text-xs text-emerald-500 hover:text-emerald-400 hover:underline flex items-center gap-1"
>
Get a key from Google AI Studio
</a>
{saved && (
<button
onClick={handleClear}
className="text-xs text-red-400 hover:text-red-300 flex items-center gap-1 px-2 py-1 hover:bg-red-950/30 rounded transition-colors"
>
<Trash2 className="w-3 h-3" /> Clear Key
</button>
)}
</div>
</div>
{/* Footer */}
<div className="px-6 py-4 bg-stone-950/50 border-t border-stone-800 flex justify-end gap-3">
<button
onClick={onClose}
className="px-4 py-2 text-sm text-stone-400 hover:text-stone-200 transition-colors"
>
Cancel
</button>
<button
onClick={handleSave}
disabled={!key.trim()}
className="flex items-center gap-2 px-6 py-2 bg-emerald-600 hover:bg-emerald-500 text-white text-sm font-medium rounded-lg shadow-lg shadow-emerald-900/20 disabled:opacity-50 disabled:cursor-not-allowed transition-all active:scale-95"
>
{saved ? <Check className="w-4 h-4" /> : <Save className="w-4 h-4" />}
{saved ? 'Updated' : 'Save Key'}
</button>
</div>
</div>
</div>
);
};

214
components/BrandKit.tsx Normal file
View File

@@ -0,0 +1,214 @@
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import { Upload, Trash2, Save, Image as ImageIcon, Store, Link as LinkIcon, Lock } from 'lucide-react';
interface BrandKitProps {
}
export default function BrandKit({ }: BrandKitProps) {
const [logoPreview, setLogoPreview] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const [uploading, setUploading] = useState(false);
// Shop Settings State
const [shopName, setShopName] = useState("");
const [shopLink, setShopLink] = useState("");
const [apiKey, setApiKey] = useState("");
const [savingDetails, setSavingDetails] = useState(false);
useEffect(() => {
fetchBrandData();
}, []);
const fetchBrandData = async () => {
setLoading(true);
try {
// 1. Fetch Logo Status
const logoRes = await axios.get('/api/brand/logo');
if (logoRes.data.exists) {
setLogoPreview(logoRes.data.logo);
}
// 2. Fetch User Settings (Shop Name, Link, API Key)
const userRes = await axios.get('/api/auth/me');
if (userRes.data) {
setShopName(userRes.data.etsyShopName || "");
setShopLink(userRes.data.etsyShopLink || "");
setApiKey(userRes.data.apiKey || "");
}
} catch (err) {
console.error("Failed to fetch brand data", err);
} finally {
setLoading(false);
}
};
const handleFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
setUploading(true);
const formData = new FormData();
formData.append('logo', file);
try {
const res = await axios.post('/api/brand/logo', formData, {
headers: { 'Content-Type': 'multipart/form-data' }
});
// Refresh preview
fetchBrandData();
} catch (err: any) {
alert('Failed to upload logo: ' + (err.response?.data?.error || err.message));
} finally {
setUploading(false);
}
};
const handleSaveDetails = async () => {
setSavingDetails(true);
try {
await axios.put('/api/auth/me', {
etsyShopName: shopName,
etsyShopLink: shopLink,
apiKey: apiKey // Allow updating API Key here too if needed
});
alert("Brand settings saved successfully!");
} catch (err: any) {
alert("Failed to save settings: " + (err.response?.data?.error || err.message));
} finally {
setSavingDetails(false);
}
};
return (
<div className="bg-white border border-stone-200 shadow-sm rounded-2xl p-6 mb-8">
<div className="flex justify-between items-start mb-6">
<div>
<h3 className="text-lg font-bold text-stone-900 flex items-center gap-2">
<ImageIcon className="w-5 h-5 text-purple-600" />
Brand Kit & Shop Settings
</h3>
<p className="text-sm text-stone-500 mt-1">
Manage your shop identity. Your logo will be used for mockups, and shop details for SEO & Branding.
</p>
</div>
</div>
<div className="flex flex-col md:flex-row gap-8 items-start">
{/* 1. Logo Section */}
<div className="w-full md:w-auto flex flex-col items-center gap-3">
<span className="text-xs font-bold text-stone-500 uppercase tracking-wider">Store Logo</span>
<div className="w-48 h-48 bg-stone-100 rounded-xl border-2 border-dashed border-stone-300 flex flex-col items-center justify-center relative overflow-hidden group">
{loading ? (
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-stone-900"></div>
) : logoPreview ? (
<>
<img src={logoPreview} alt="Brand Logo" className="w-full h-full object-contain p-4" />
<div className="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
<label className="cursor-pointer bg-white text-stone-900 px-3 py-1.5 rounded-lg text-xs font-bold hover:bg-stone-50 shadow-lg transform translate-y-2 group-hover:translate-y-0 transition-all">
Change Logo
<input type="file" className="hidden" accept="image/png" onChange={handleFileUpload} />
</label>
</div>
</>
) : (
<label className="cursor-pointer flex flex-col items-center gap-2 text-stone-400 hover:text-stone-600 transition-colors w-full h-full justify-center">
<Upload className="w-8 h-8" />
<span className="text-xs font-bold">Upload PNG</span>
<input type="file" className="hidden" accept="image/png" onChange={handleFileUpload} />
</label>
)}
</div>
{uploading && <span className="text-xs text-purple-600 animate-pulse">Uploading...</span>}
<p className="text-[10px] text-stone-400 max-w-[12rem] text-center">
Supports transparent PNG. Max 5MB. Used for watermarks on mockups.
</p>
</div>
{/* 2. Shop Details Section */}
<div className="flex-1 w-full space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Shop Name */}
<div className="space-y-1">
<label className="text-xs font-bold text-stone-700 uppercase flex items-center gap-1.5">
<Store className="w-3.5 h-3.5" /> Etsy Shop Name
</label>
<input
type="text"
value={shopName}
onChange={(e) => setShopName(e.target.value)}
placeholder="e.g. MyArtPrintStudio"
className="w-full px-3 py-2 bg-stone-50 border border-stone-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-purple-500/20 focus:border-purple-500 transition-all"
/>
</div>
{/* Shop Link */}
<div className="space-y-1">
<label className="text-xs font-bold text-stone-700 uppercase flex items-center gap-1.5">
<LinkIcon className="w-3.5 h-3.5" /> Shop URL
</label>
<input
type="text"
value={shopLink}
onChange={(e) => setShopLink(e.target.value)}
placeholder="https://etsy.com/shop/..."
className="w-full px-3 py-2 bg-stone-50 border border-stone-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-purple-500/20 focus:border-purple-500 transition-all"
/>
</div>
{/* API Key (Optional Display) */}
<div className="col-span-1 md:col-span-2 space-y-1">
<label className="text-xs font-bold text-stone-700 uppercase flex items-center gap-1.5">
<Lock className="w-3.5 h-3.5" /> Gemini API Key (Optional Override)
</label>
<input
type="password"
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
placeholder="Enter specific key for this project..."
className="w-full px-3 py-2 bg-stone-50 border border-stone-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-purple-500/20 focus:border-purple-500 transition-all font-mono"
/>
<p className="text-[10px] text-stone-400">
Leave blank to use system default.
</p>
</div>
</div>
{/* Branding Info Box */}
<div className="bg-purple-50 border border-purple-100 rounded-xl p-4">
<h4 className="text-sm font-bold text-purple-900 mb-2">How this data is used</h4>
<ul className="text-xs text-purple-800 space-y-1.5 list-disc list-inside">
<li><strong>Shop Name:</strong> Used in SEO Titles and Descriptions generated by AI.</li>
<li><strong>Shop URL:</strong> Included in 'Printing Guide' PDFs for customer reference.</li>
<li><strong>Logo:</strong> Applied as a watermark to generated mockups (never on masters).</li>
</ul>
</div>
<div className="flex justify-end pt-2">
<button
onClick={handleSaveDetails}
disabled={savingDetails}
className="flex items-center gap-2 px-6 py-2.5 bg-stone-900 hover:bg-black text-white rounded-xl font-medium text-sm transition-all shadow-lg shadow-stone-900/10 disabled:opacity-50 disabled:cursor-not-allowed"
>
{savingDetails ? (
<>
<div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
Saving...
</>
) : (
<>
<Save className="w-4 h-4" />
Save Changes
</>
)}
</button>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,86 @@
import React, { useEffect, useState } from 'react';
type ActionType = 'GENERATE_MASTER' | 'GENERATE_VARIANT' | 'GENERATE_MOCKUP' | 'GENERATE_PROMPT';
const DEFAULT_CREDITS: Record<ActionType, number> = {
GENERATE_PROMPT: 1,
GENERATE_MASTER: 10,
GENERATE_VARIANT: 5,
GENERATE_MOCKUP: 2
};
// Global pricing cache to avoid fetching every button render
let pricingCache: any = null;
interface CreditButtonProps {
action?: ActionType;
cost?: number; // Direct cost override (for backward compatibility)
onClick: () => void;
disabled?: boolean;
className?: string; // Additional classes for styling
children?: React.ReactNode; // Custom text if needed (e.g. "Generate")
hideCost?: boolean;
}
export const CreditButton: React.FC<CreditButtonProps> = ({
action,
cost: directCost,
onClick,
disabled = false,
className = "",
children,
hideCost = false
}) => {
const [price, setPrice] = useState<number>(directCost ?? (action ? DEFAULT_CREDITS[action] : 0));
useEffect(() => {
// If directCost is provided, use it directly
if (directCost !== undefined) {
setPrice(directCost);
return;
}
// If no action, skip fetching
if (!action) return;
const fetchPrices = async () => {
// Use cache if exists
if (pricingCache && pricingCache[`PRICE_${action}`]) {
setPrice(Number(pricingCache[`PRICE_${action}`]));
return;
}
try {
const res = await fetch('http://localhost:3001/api/config/prices');
if (res.ok) {
const data = await res.json();
pricingCache = data;
const dynamicPrice = data[`PRICE_${action}`];
if (dynamicPrice) {
setPrice(Number(dynamicPrice));
}
}
} catch (err) {
console.warn("Failed to fetch dynamic prices", err);
}
};
fetchPrices();
}, [action, directCost]);
return (
<button
onClick={onClick}
disabled={disabled}
className={`flex items-center justify-center gap-2 group ${className}`}
>
{children || <span>Generate</span>}
{!hideCost && (
<span className="bg-white/20 px-2 py-0.5 rounded text-xs font-mono font-bold opacity-90 group-hover:bg-white/30 transition-colors">
{price} 🪙
</span>
)}
</button>
);
};

34
components/Footer.tsx Normal file
View File

@@ -0,0 +1,34 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { USER_AGREEMENT_TEXT, KVKK_TEXT, DISCLAIMER_TEXT } from '../legal_texts';
interface FooterProps {
openModal: (title: string, content: string) => void;
}
export const Footer: React.FC<FooterProps> = ({ openModal }) => {
const { t } = useTranslation();
return (
<footer className="mt-20 border-t border-stone-200 py-12 bg-white">
<div className="max-w-[1600px] mx-auto px-10 flex flex-col md:flex-row justify-between items-center gap-6">
<div className="flex items-center gap-2">
<span className="text-xl font-black tracking-tight text-stone-900">DIGICRAFT</span>
<span className="text-[10px] font-bold text-stone-400">v16.0</span>
</div>
<div className="flex gap-8 text-[11px] font-bold uppercase tracking-wider text-stone-500">
<button onClick={() => openModal("User Agreement & IP Rights", USER_AGREEMENT_TEXT)} className="hover:text-stone-900 transition-colors">User Agreement</button>
<button onClick={() => openModal("KVKK & Privacy", KVKK_TEXT)} className="hover:text-stone-900 transition-colors">KVKK & Privacy</button>
<button onClick={() => openModal("Legal Disclaimer", DISCLAIMER_TEXT)} className="hover:text-stone-900 transition-colors">Disclaimer</button>
</div>
<div className="text-[10px] text-stone-400 font-medium">
© {new Date().getFullYear()} Harun CAN. All Rights Reserved.
</div>
</div>
</footer>
);
};
export default Footer;

252
components/Header.tsx Normal file
View File

@@ -0,0 +1,252 @@
import React, { useState, useEffect, useRef } from 'react';
import { Settings, ShieldCheck, LogOut, ChevronDown, Users, BarChart3, Database, Shield, ScanEye, Brain } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { Tooltip } from './Tooltip';
interface HeaderProps {
user: any;
logout: () => void;
openApiKeyModal: () => void;
projectTitle?: string;
}
export const Header: React.FC<HeaderProps> = ({ user, logout, openApiKeyModal, projectTitle }) => {
const { t, i18n } = useTranslation();
const [isLangOpen, setIsLangOpen] = useState(false);
const [isAdminMenuOpen, setIsAdminMenuOpen] = useState(false);
const [hasPersonalKey, setHasPersonalKey] = useState(false);
const adminMenuRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const checkKey = () => setHasPersonalKey(!!localStorage.getItem('gemini_api_key'));
checkKey();
window.addEventListener('storage', checkKey);
const handleClickOutside = (event: MouseEvent) => {
if (adminMenuRef.current && !adminMenuRef.current.contains(event.target as Node)) {
setIsAdminMenuOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
window.removeEventListener('storage', checkKey);
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
const languages = [
{ code: 'en', label: 'English' },
{ code: 'tr', label: 'Türkçe' },
{ code: 'de', label: 'Deutsch' },
{ code: 'fr', label: 'Français' },
{ code: 'es', label: 'Español' }
];
const changeLanguage = (lng: string) => {
i18n.changeLanguage(lng);
setIsLangOpen(false);
};
return (
<header className="bg-white border-b border-stone-100 relative z-50">
{/* Upper Utility Bar */}
<div className="bg-stone-50/50 border-b border-stone-100 px-8 py-2 flex items-center justify-between backdrop-blur-sm">
<div className="flex items-center gap-4">
<Tooltip content="Engine Status: Online (v13.1)" position="bottom">
<div className="flex items-center gap-2 px-2 py-1 bg-white rounded-md border border-stone-200/50 shadow-sm transition-all">
<div className="w-1.5 h-1.5 bg-green-500 rounded-full animate-pulse shadow-[0_0_8px_rgba(34,197,94,0.4)]"></div>
<span className="text-[8px] font-black uppercase tracking-widest text-stone-400">System Live</span>
</div>
</Tooltip>
<Tooltip content={hasPersonalKey ? "Personal Key Active (Beta Mode)" : "System Key (Standard)"} position="bottom">
<div
onClick={openApiKeyModal}
className={`flex items-center gap-2 px-2 py-1 rounded-md cursor-pointer border transition-all ${hasPersonalKey ? 'bg-indigo-50 border-indigo-200' : 'bg-white border-stone-200/50 shadow-sm'}`}
>
<div className={`w-1.5 h-1.5 rounded-full ${hasPersonalKey ? 'bg-indigo-500 animate-pulse' : 'bg-blue-400'}`}></div>
<span className={`text-[8px] font-black uppercase tracking-widest ${hasPersonalKey ? 'text-indigo-400' : 'text-stone-400'}`}>
{hasPersonalKey ? 'V-DNA KEY' : 'API CORE'}
</span>
</div>
</Tooltip>
</div>
<div className="flex items-center gap-6">
<span className="text-[9px] font-black uppercase tracking-[0.2em] text-stone-300 select-none">
{user?.email}
</span>
<div className="flex items-center gap-2">
<button
onClick={() => setIsLangOpen(!isLangOpen)}
className="flex items-center gap-2 bg-white border border-stone-200/50 px-2 py-1 rounded-md shadow-sm hover:bg-stone-100 transition-all"
>
<span className="text-[9px] font-bold text-stone-500 uppercase">{i18n.language}</span>
<span className="text-[7px] text-stone-400"></span>
</button>
{isLangOpen && (
<div className="absolute right-16 top-9 bg-white border border-stone-100 rounded-xl shadow-2xl p-2 min-w-[120px] z-[60] flex flex-col gap-1">
{languages.map(l => (
<button
key={l.code}
onClick={() => changeLanguage(l.code)}
className={`text-left px-3 py-2 rounded-lg text-xs font-bold transition-all ${i18n.language === l.code ? 'bg-stone-900 text-white' : 'text-stone-500 hover:bg-stone-50'}`}
>
{l.label}
</button>
))}
</div>
)}
<Tooltip content={t('general.logout')} position="bottom">
<button onClick={logout} className="p-1 px-2 text-stone-400 hover:text-red-500 hover:bg-red-50 rounded-md transition-all">
<LogOut className="w-3.5 h-3.5" />
</button>
</Tooltip>
</div>
</div>
</div>
{/* Main Navigation Bar */}
<div className="py-6 px-10 flex items-center justify-between">
<div className="flex items-center gap-8">
<div className="flex flex-col text-left">
<div className="flex items-center gap-4">
<h1 className="text-4xl font-black text-stone-900 tracking-tighter leading-none cursor-pointer" onClick={() => window.location.href = '/'}>{t('home.title')}</h1>
{projectTitle && (
<>
<div className="h-8 w-[1px] bg-stone-200 rotate-[20deg] mx-2" />
<h2 className="text-2xl font-black text-purple-600 tracking-tight leading-none truncate max-w-[400px]">{projectTitle}</h2>
</>
)}
</div>
<div className="flex items-center gap-3 mt-1">
<p className="text-[10px] text-stone-400 font-bold uppercase tracking-[0.3em]">{t('home.subtitle')}</p>
</div>
</div>
</div>
<div className="flex items-center gap-4">
<Tooltip content={`Plan: ${user?.plan || 'Free'}`} position="bottom">
<button
onClick={() => window.location.href = '/pricing'}
className="flex items-center gap-3 px-5 py-2.5 bg-gradient-to-br from-amber-50 to-orange-50 rounded-2xl border border-amber-100/50 shadow-sm hover:shadow-md hover:border-amber-200 transition-all group"
>
<div className="text-lg group-hover:scale-110 transition-transform">🪙</div>
<div className="flex flex-col items-start">
<span className="text-[11px] font-black text-amber-600 leading-none">
{user?.credits ?? 10}
</span>
<span className="text-[8px] font-bold uppercase tracking-widest text-amber-500/70">Credits</span>
</div>
</button>
</Tooltip>
<div className="h-8 w-px bg-stone-100 mx-2"></div>
<div className="flex items-center gap-2">
{user?.role === 'ADMIN' && (
<div className="relative" ref={adminMenuRef}>
<button
onClick={() => setIsAdminMenuOpen(!isAdminMenuOpen)}
className={`p-3 rounded-2xl transition-all border ${isAdminMenuOpen ? 'bg-purple-600 text-white border-purple-500 shadow-lg shadow-purple-200' : 'text-stone-400 hover:text-purple-600 hover:bg-purple-50 border-transparent hover:border-purple-100'}`}
title="Admin Fast Access"
>
<ShieldCheck className="w-5 h-5" />
</button>
{isAdminMenuOpen && (
<div className="absolute right-0 top-full mt-3 w-64 bg-white/95 backdrop-blur-xl border border-stone-200 rounded-[2rem] shadow-2xl p-3 z-[100] animate-in fade-in slide-in-from-top-2 duration-200">
<div className="px-3 py-2 mb-2">
<p className="text-[10px] font-black text-stone-400 uppercase tracking-[0.2em]">Command Center</p>
</div>
<div className="space-y-1">
<button
onClick={() => window.location.href = '/admin'}
className="w-full flex items-center gap-4 px-4 py-3 text-stone-600 hover:text-purple-600 hover:bg-purple-50 rounded-2xl transition-all text-xs font-bold group"
>
<div className="bg-purple-100 p-2 rounded-xl text-purple-600 group-hover:scale-110 transition-transform">
<Users className="w-4 h-4" />
</div>
<div className="flex flex-col items-start">
<span>User Accounts</span>
<span className="text-[9px] text-stone-400 font-normal">Manage permissions & credits</span>
</div>
</button>
<button
onClick={() => window.location.href = '/admin/analytics'}
className="w-full flex items-center gap-4 px-4 py-3 text-stone-600 hover:text-blue-600 hover:bg-blue-50 rounded-2xl transition-all text-xs font-bold group"
>
<div className="bg-blue-100 p-2 rounded-xl text-blue-600 group-hover:scale-110 transition-transform">
<BarChart3 className="w-4 h-4" />
</div>
<div className="flex flex-col items-start">
<span>Engine Analytics</span>
<span className="text-[9px] text-stone-400 font-normal">Monitor system performance</span>
</div>
</button>
<button
onClick={() => window.location.href = '/admin/config'}
className="w-full flex items-center gap-4 px-4 py-3 text-stone-600 hover:text-amber-600 hover:bg-amber-50 rounded-2xl transition-all text-xs font-bold group"
>
<div className="bg-amber-100 p-2 rounded-xl text-amber-600 group-hover:scale-110 transition-transform">
<Database className="w-4 h-4" />
</div>
<div className="flex flex-col items-start">
<span>System Config</span>
<span className="text-[9px] text-stone-400 font-normal">Global variables & keys</span>
</div>
</button>
<button
onClick={() => window.location.href = '/xray'}
className="w-full flex items-center gap-4 px-4 py-3 text-stone-600 hover:text-purple-600 hover:bg-purple-50 rounded-2xl transition-all text-xs font-bold group"
>
<div className="bg-purple-100 p-2 rounded-xl text-purple-600 group-hover:scale-110 transition-transform">
<ScanEye className="w-4 h-4" />
</div>
<div className="flex flex-col items-start">
<span className="text-[9px] text-stone-400 font-normal">Deep dive into data</span>
</div>
</button>
<button
onClick={() => window.location.href = '/scorecard'}
className="w-full flex items-center gap-4 px-4 py-3 text-stone-600 hover:text-blue-600 hover:bg-blue-50 rounded-2xl transition-all text-xs font-bold group"
>
<div className="bg-blue-100 p-2 rounded-xl text-blue-600 group-hover:scale-110 transition-transform">
<Brain className="w-4 h-4" />
</div>
<div className="flex flex-col items-start">
<span>Neuro-Scorecard</span>
<span className="text-[9px] text-stone-400 font-normal">Predict conversion</span>
</div>
</button>
</div>
<div className="h-px bg-stone-100 my-3 mx-2"></div>
<button
onClick={() => window.location.href = '/admin'}
className="w-full flex items-center justify-center gap-2 px-4 py-3 bg-stone-50 hover:bg-stone-900 text-stone-500 hover:text-white rounded-2xl transition-all text-[10px] font-black uppercase tracking-widest"
>
<Shield className="w-3 h-3" />
Admin Dashboard
</button>
</div>
)}
</div>
)}
<button
onClick={() => window.location.href = '/settings'}
className="p-3 text-stone-400 hover:text-stone-900 hover:bg-stone-50 rounded-2xl transition-all border border-transparent hover:border-stone-100"
>
<Settings className="w-5 h-5" />
</button>
</div>
</div>
</div>
</header>
);
};
export default Header;

56
components/Layout.tsx Normal file
View File

@@ -0,0 +1,56 @@
import React, { useState } from 'react';
import { Header } from './Header';
import { Footer } from './Footer';
import { LegalModal } from './LegalModal';
import { useAuth } from '../AuthContext';
import { ApiKeyModal } from './ApiKeyModal';
interface LayoutProps {
children: React.ReactNode;
projectTitle?: string;
}
export const Layout: React.FC<LayoutProps> = ({ children, projectTitle }) => {
const { user, logout } = useAuth();
const [legalModalOpen, setLegalModalOpen] = useState(false);
const [legalContent, setLegalContent] = useState({ title: '', text: '' });
const [isApiKeyModalOpen, setIsApiKeyModalOpen] = useState(false);
const openLegalModal = (title: string, text: string) => {
setLegalContent({ title, text });
setLegalModalOpen(true);
};
if (!user) return <>{children}</>;
return (
<div className="min-h-screen flex flex-col bg-stone-50">
<Header
user={user}
logout={logout}
openApiKeyModal={() => setIsApiKeyModalOpen(true)}
projectTitle={projectTitle}
/>
<main className="flex-grow">
{children}
</main>
<Footer openModal={openLegalModal} />
<LegalModal
isOpen={legalModalOpen}
onClose={() => setLegalModalOpen(false)}
title={legalContent.title}
content={legalContent.text}
/>
<ApiKeyModal
isOpen={isApiKeyModalOpen}
onClose={() => setIsApiKeyModalOpen(false)}
/>
</div>
);
};
export default Layout;

81
components/LegalModal.tsx Normal file
View File

@@ -0,0 +1,81 @@
import React, { Fragment } from 'react';
import { Dialog, Transition } from '@headlessui/react';
import { X, ShieldCheck, FileText } from 'lucide-react';
import { USER_AGREEMENT_TEXT, KVKK_TEXT } from '../legal_texts';
interface LegalModalProps {
isOpen: boolean;
onClose: () => void;
type?: 'terms' | 'kvkk' | null;
title?: string;
content?: string;
}
export const LegalModal: React.FC<LegalModalProps> = ({ isOpen, onClose, type, title: customTitle, content: customContent }) => {
const title = customTitle || (type === 'terms' ? 'User Agreement & IP Rights' : type === 'kvkk' ? 'KVKK & Privacy Policy' : 'Information');
const content = customContent || (type === 'terms' ? USER_AGREEMENT_TEXT : type === 'kvkk' ? KVKK_TEXT : '');
const icon = type === 'terms' ? <ShieldCheck className="w-6 h-6 text-indigo-600" /> : <FileText className="w-6 h-6 text-indigo-600" />;
return (
<Transition appear show={isOpen} as={Fragment}>
<Dialog as="div" className="relative z-50" onClose={onClose}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-black/40 backdrop-blur-sm" />
</Transition.Child>
<div className="fixed inset-0 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4 text-center">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<Dialog.Panel className="w-full max-w-2xl transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all">
<div className="flex items-center justify-between border-b pb-4 mb-4">
<div className="flex items-center gap-3">
{icon}
<Dialog.Title as="h3" className="text-xl font-bold leading-6 text-gray-900">
{title}
</Dialog.Title>
</div>
<button
onClick={onClose}
className="rounded-full p-1 hover:bg-gray-100 transition-colors"
>
<X className="w-5 h-5 text-gray-500" />
</button>
</div>
<div className="mt-2 text-sm text-gray-600 max-h-[60vh] overflow-y-auto whitespace-pre-line pr-2 custom-scrollbar">
{content}
</div>
<div className="mt-6 flex justify-end pt-4 border-t">
<button
type="button"
className="inline-flex justify-center rounded-lg border border-transparent bg-indigo-600 px-6 py-2 text-sm font-medium text-white hover:bg-indigo-700 focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 transition-all"
onClick={onClose}
>
I Understand
</button>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition>
);
};

View File

@@ -0,0 +1,259 @@
import React, { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Sparkles, Brain, Zap, Heart, Eye, TrendingUp, AlertTriangle, CheckCircle } from 'lucide-react';
interface NeuroScoreProps {
analysis: {
scores: {
dopamine: number;
serotonin: number;
cognitiveEase: number;
commercialFit: number;
};
feedback: string[];
improvements: {
dopamine: string[];
serotonin: string[];
cognitiveEase: string[];
commercialFit: string[];
};
prediction: string;
} | null;
loading?: boolean;
onApplyImprovement?: (suggestion: string) => void;
}
const ScoreBar = ({ label, score, icon: Icon, color, barColor, onClick, isSelected }: { label: string; score: number; icon: any; color: string; barColor: string, onClick?: () => void, isSelected?: boolean }) => (
<div
onClick={onClick}
className={`mb-4 transition-all duration-300 ${onClick ? 'cursor-pointer hover:bg-white/5 p-2 -mx-2 rounded-lg' : ''} ${isSelected ? 'bg-white/10 ring-1 ring-white/20' : ''}`}
>
<div className="flex justify-between items-center mb-1">
<div className="flex items-center gap-2 text-stone-300">
<Icon className={`w-4 h-4 ${color}`} />
<span className="text-sm font-medium">{label}</span>
</div>
<span className={`text-sm font-bold ${score >= 8 ? 'text-green-400' : score >= 5 ? 'text-yellow-400' : 'text-red-400'}`}>
{score}/10
</span>
</div>
<div className="h-2 bg-stone-800/50 rounded-full overflow-hidden backdrop-blur-sm border border-white/5">
<motion.div
initial={{ width: 0 }}
animate={{ width: `${score * 10}%` }}
transition={{ duration: 1, ease: "easeOut" }}
className={`h-full ${barColor}`}
/>
</div>
{isSelected && (
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="text-[10px] text-stone-400 mt-1 italic text-center">
Viewing fixes for this metric...
</motion.div>
)}
</div>
);
const NeuroScorecard: React.FC<NeuroScoreProps> = ({ analysis, loading, onApplyImprovement }) => {
const [selectedMetric, setSelectedMetric] = useState<string | null>(null);
// Get fixes based on selection or aggregate all
const currentImprovements = React.useMemo(() => {
if (!analysis?.improvements) return [];
// Map UI labels to API keys
const keyMap: Record<string, keyof typeof analysis.improvements> = {
'Dopamine': 'dopamine',
'Serotonin': 'serotonin',
'Cognitive Ease': 'cognitiveEase',
'Commercial Fit': 'commercialFit'
};
if (selectedMetric && keyMap[selectedMetric]) {
return analysis.improvements[keyMap[selectedMetric]] || [];
}
// Return all improvements flattened if nothing selected
return [
...analysis.improvements.dopamine,
...analysis.improvements.serotonin,
...analysis.improvements.cognitiveEase,
...analysis.improvements.commercialFit
];
}, [analysis, selectedMetric]);
if (loading) {
return (
<div className="p-8 rounded-2xl bg-white/5 backdrop-blur-xl border border-white/10 flex flex-col items-center justify-center text-center min-h-[400px]">
<motion.div
animate={{ rotate: 360 }}
transition={{ duration: 2, repeat: Infinity, ease: "linear" }}
className="mb-6 relative"
>
<div className="absolute inset-0 bg-blue-500/20 blur-xl rounded-full" />
<Brain className="w-16 h-16 text-blue-400 relative z-10" />
</motion.div>
<h3 className="text-xl font-bold text-white mb-2">Analyzing Neuro-Triggers...</h3>
<p className="text-stone-400 max-w-xs">Connecting to Gemini Vision to evaluate Dopamine, Serotonin, and Market Fit.</p>
<div className="mt-8 flex gap-2">
{[0, 1, 2].map((i) => (
<motion.div
key={i}
animate={{ scale: [1, 1.5, 1], opacity: [0.3, 1, 0.3] }}
transition={{ duration: 1, repeat: Infinity, delay: i * 0.2 }}
className="w-2 h-2 rounded-full bg-blue-400"
/>
))}
</div>
</div>
);
}
if (!analysis) return null;
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="bg-stone-900/40 backdrop-blur-2xl border border-white/10 rounded-2xl overflow-hidden shadow-2xl"
>
{/* Header */}
<div className="p-6 border-b border-white/5 bg-gradient-to-r from-blue-500/10 to-purple-500/10">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="p-2 bg-blue-500/20 rounded-lg">
<Brain className="w-6 h-6 text-blue-400" />
</div>
<div>
<h2 className="text-xl font-bold text-white">Neuro-Scorecard</h2>
<p className="text-xs text-stone-400">AI-Predicted Market Performance</p>
</div>
</div>
<div className={`px-4 py-1.5 rounded-full border ${analysis.prediction.includes('High') ? 'bg-green-500/20 border-green-500/30 text-green-400' :
analysis.prediction.includes('Medium') ? 'bg-yellow-500/20 border-yellow-500/30 text-yellow-400' :
'bg-red-500/20 border-red-500/30 text-red-400'
} text-sm font-semibold flex items-center gap-2`}>
<TrendingUp className="w-4 h-4" />
{analysis.prediction}
</div>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-0 divide-y md:divide-y-0 md:divide-x divide-white/10">
{/* Column 1: Scores */}
<div className="p-6 bg-black/20">
<h3 className="text-sm font-bold text-white/50 uppercase tracking-widest mb-6 flex justify-between items-center">
Vital Signs
{selectedMetric && (
<button onClick={() => setSelectedMetric(null)} className="text-[10px] text-blue-400 hover:underline">
Show All
</button>
)}
</h3>
<ScoreBar
label="Dopamine Hit (Excitement)"
score={analysis.scores.dopamine}
icon={Zap}
color="text-yellow-400"
barColor="bg-yellow-400"
onClick={() => setSelectedMetric('Dopamine')}
isSelected={selectedMetric === 'Dopamine'}
/>
<ScoreBar
label="Serotonin Flow (Trust/Calm)"
score={analysis.scores.serotonin}
icon={Heart}
color="text-pink-400"
barColor="bg-pink-400"
onClick={() => setSelectedMetric('Serotonin')}
isSelected={selectedMetric === 'Serotonin'}
/>
<ScoreBar
label="Cognitive Ease (Clarity)"
score={analysis.scores.cognitiveEase}
icon={Eye}
color="text-blue-400"
barColor="bg-blue-400"
onClick={() => setSelectedMetric('Cognitive Ease')}
isSelected={selectedMetric === 'Cognitive Ease'}
/>
<ScoreBar
label="Commercial Fit (Pro Quality)"
score={analysis.scores.commercialFit}
icon={Sparkles}
color="text-purple-400"
barColor="bg-purple-400"
onClick={() => setSelectedMetric('Commercial Fit')}
isSelected={selectedMetric === 'Commercial Fit'}
/>
</div>
{/* Column 2: Improvements */}
<div className="p-6 relative">
<h3 className="text-sm font-bold text-red-400/80 uppercase tracking-widest mb-6 flex items-center gap-2">
<AlertTriangle className="w-4 h-4" />
{selectedMetric ? `${selectedMetric} Fixes` : 'Critical Fixes'}
</h3>
<ul className="space-y-4 max-h-[300px] overflow-y-auto pr-2 scrollbar-thin scrollbar-thumb-white/10">
{currentImprovements.length > 0 ? (
currentImprovements.map((fix, idx) => (
<motion.li
key={idx}
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: idx * 0.1 }}
className="flex items-start gap-3 text-stone-300 text-sm group"
>
<div className="w-1.5 h-1.5 rounded-full bg-red-400 mt-1.5 flex-shrink-0 group-hover:scale-150 transition-transform" />
<div className="flex-1">
<span className="leading-relaxed block mb-2">{fix}</span>
{onApplyImprovement && (
<button
onClick={() => onApplyImprovement(fix)}
className="text-[10px] bg-red-500/10 hover:bg-red-500/20 text-red-300 px-2 py-1 rounded border border-red-500/20 transition-colors uppercase tracking-wider font-bold"
>
+ Add to Refine
</button>
)}
</div>
</motion.li>
))
) : (
<div className="text-center py-8">
<p className="text-stone-500 text-xs italic mb-4">
No specific fixes found for this metric.
</p>
</div>
)}
</ul>
</div>
{/* Column 3: Feedback */}
<div className="p-6 bg-white/2">
<h3 className="text-sm font-bold text-green-400/80 uppercase tracking-widest mb-6 flex items-center gap-2">
<CheckCircle className="w-4 h-4" />
Winning Traits
</h3>
<ul className="space-y-4">
{analysis.feedback.map((point, idx) => (
<motion.li
key={idx}
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: idx * 0.1 + 0.2 }}
className="flex items-start gap-3 text-stone-300 text-sm"
>
<div className="w-1.5 h-1.5 rounded-full bg-green-400 mt-1.5 flex-shrink-0" />
<span className="leading-relaxed">{point}</span>
</motion.li>
))}
</ul>
</div>
</div>
</motion.div>
);
};
export default NeuroScorecard;

View File

@@ -0,0 +1,137 @@
import React, { useRef, useState, forwardRef, useImperativeHandle } from 'react';
import { toPng } from 'html-to-image';
import axios from 'axios';
import { useTranslation } from 'react-i18next';
interface ProcessGuideGeneratorProps {
projectId?: string;
project?: { id: string } | null; // Alternative to projectId
onImageGenerated?: (path: string) => void;
onGenerate?: () => void; // Alias for backward compatibility
shopName?: string;
}
export interface ProcessGuideRef {
generate: () => Promise<void>;
}
export const ProcessGuideGenerator = forwardRef<ProcessGuideRef, ProcessGuideGeneratorProps>(({ projectId, project, onImageGenerated, onGenerate, shopName = "MrStitchPrintStudio" }, ref) => {
const resolvedProjectId = projectId || project?.id;
const { t } = useTranslation();
const elementRef = useRef<HTMLDivElement>(null);
const [isGenerating, setIsGenerating] = useState(false);
useImperativeHandle(ref, () => ({
generate: handleGenerate
}));
const handleGenerate = async () => {
if (!elementRef.current) return;
setIsGenerating(true);
try {
// Give fonts time to load visually if needed, though usually not an issue with simple fonts
const dataUrl = await toPng(elementRef.current, { cacheBust: true, pixelRatio: 1 });
// Upload to server
const response = await axios.post(`/api/projects/${projectId}/assets/upload`, {
file: dataUrl,
type: 'mockup', // Treated as mockup
folder: 'mockups',
filename: `process_guide_${Date.now()}.png`
}, {
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
});
if (response.data.success) {
onImageGenerated(response.data.asset.path);
}
} catch (err) {
console.error('Failed to generate process guide:', err);
throw err; // Re-throw to let parent know
} finally {
setIsGenerating(false);
}
};
return (
<div className="relative overflow-hidden w-[500px] h-[500px] bg-white">
{/* This container is what gets captured. It must be visible in DOM but can be hidden by parent overflow/opacity. */}
{/* We strictly render ONLY the capture content here. Extra UI is removed. */}
<div
ref={elementRef}
className="w-[500px] h-[500px] bg-[#F5EBE0] text-[#4A3B32] flex flex-col items-center justify-between p-12 shrink-0"
style={{ fontFamily: 'Inter, sans-serif' }}
>
{/* HEADER */}
<h1 className="text-4xl font-bold uppercase tracking-wide mt-4">HOW IT WORKS</h1>
{/* STEPS ROW */}
<div className="flex justify-between w-full mt-8">
{/* STEP 1 */}
<div className="flex flex-col items-center text-center w-1/3 px-2">
<div className="mb-4">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={2} stroke="currentColor" className="w-16 h-16">
<path strokeLinecap="round" strokeLinejoin="round" d="M2.25 3h1.386c.51 0 .955.343 1.087.835l.383 1.437M7.5 14.25a3 3 0 0 0-3 3h15.75m-12.75-3h11.218c1.121-2.3 2.1-4.684 2.924-7.138a60.114 60.114 0 0 0-16.536-1.84M7.5 14.25 5.106 5.272M6 20.25a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Zm12.75 0a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Z" />
</svg>
</div>
<h3 className="font-bold text-lg mb-1">ADD TO CART</h3>
<p className="text-xs leading-tight opacity-80">Select your digital product and add it to your cart.</p>
</div>
{/* STEP 2 */}
<div className="flex flex-col items-center text-center w-1/3 px-2 border-l border-r border-[#4A3B32]/10">
<div className="mb-4">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={2} stroke="currentColor" className="w-16 h-16">
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12.75 11.25 15 15 9.75M21 12c0 1.268-.63 2.39-1.593 3.068a3.745 3.745 0 0 1-1.043 3.296 3.745 3.745 0 0 1-3.296 1.043A3.745 3.745 0 0 1 12 21c-1.268 0-2.39-.63-3.068-1.593a3.746 3.746 0 0 1-3.296-1.043 3.745 3.745 0 0 1-1.043-3.296A3.745 3.745 0 0 1 3 12c0-1.268.63-2.39 1.593-3.068a3.745 3.745 0 0 1 1.043-3.296 3.746 3.746 0 0 1 3.296-1.043A3.746 3.746 0 0 1 12 3c1.268 0 2.39.63 3.068 1.593a3.746 3.746 0 0 1 3.296 1.043 3.746 3.746 0 0 1 1.043 3.296A3.745 3.745 0 0 1 21 12Z" />
</svg>
</div>
<h3 className="font-bold text-lg mb-1">CHECKOUT</h3>
<p className="text-xs leading-tight opacity-80">Complete payment to securely confirm your purchase.</p>
</div>
{/* STEP 3 */}
<div className="flex flex-col items-center text-center w-1/3 px-2">
<div className="mb-4">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={2} stroke="currentColor" className="w-16 h-16">
<path strokeLinecap="round" strokeLinejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3" />
</svg>
</div>
<h3 className="font-bold text-lg mb-1">DOWNLOAD</h3>
<p className="text-xs leading-tight opacity-80">Access your download link in the receipt email.</p>
</div>
</div>
{/* IMPORTANT NOTICE */}
<div className="w-full text-left mt-8">
<h4 className="font-bold uppercase text-sm mb-1">IMPORTANT:</h4>
<p className="text-xs"> This is an <span className="font-bold">INSTANT DOWNLOAD</span>. No physical items will be shipped or sent.</p>
</div>
{/* FOOTER */}
<div className="relative mt-auto w-full">
{/* Speech Bubble effect */}
<div className="bg-white/50 p-6 rounded-2xl relative">
<h4 className="font-bold text-sm mb-2">Thank you for stopping by!</h4>
<p className="text-[10px] leading-relaxed opacity-90">
Our goal is to create products that inspire, uplift, and add a touch of positivity to your life.
We hope you find joy in our products, and if you have any questions or feedback, we'd love to hear from you!
</p>
{/* Triangle */}
<div className="absolute -bottom-3 left-8 w-0 h-0 border-l-[10px] border-l-transparent border-t-[15px] border-t-white/50 border-r-[10px] border-r-transparent"></div>
</div>
<div className="mt-6 ml-8 font-bold text-sm opacity-80">
{shopName}
</div>
</div>
</div>
</div>
);
});
ProcessGuideGenerator.displayName = 'ProcessGuideGenerator';

42
components/SEO.tsx Normal file
View File

@@ -0,0 +1,42 @@
import React from 'react';
import { Helmet } from 'react-helmet-async';
interface SEOProps {
title?: string;
description?: string;
keywords?: string;
schema?: object;
}
export const SEO: React.FC<SEOProps> = ({
title = "DigiCraft",
description = "Automated Digital Product Architect & Visual DNA Suite",
keywords = "ai, etsy, digital products, automation, visuals",
schema
}) => {
return (
<Helmet>
{/* Standard Metadata */}
<title>{title}</title>
<meta name="description" content={description} />
<meta name="keywords" content={keywords} />
{/* Open Graph / Facebook */}
<meta property="og:type" content="website" />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
{/* Twitter */}
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
{/* Structured Data (JSON-LD) */}
{schema && (
<script type="application/ld+json">
{JSON.stringify(schema)}
</script>
)}
</Helmet>
);
};

33
components/Tooltip.tsx Normal file
View File

@@ -0,0 +1,33 @@
import React, { ReactNode } from 'react';
interface TooltipProps {
children: ReactNode;
content: string;
position?: 'top' | 'bottom' | 'left' | 'right';
className?: string; // For extra styling on the wrapper
}
export const Tooltip: React.FC<TooltipProps> = ({ children, content, position = 'top', className = '' }) => {
const positionClasses = {
top: 'bottom-full left-1/2 -translate-x-1/2 mb-2',
bottom: 'top-full left-1/2 -translate-x-1/2 mt-2',
left: 'right-full top-1/2 -translate-y-1/2 mr-2',
right: 'left-full top-1/2 -translate-y-1/2 ml-2',
};
return (
<div className={`relative group ${className}`}>
{children}
<div
className={`absolute ${positionClasses[position]} z-50 pointer-events-none opacity-0 group-hover:opacity-100 transition-opacity duration-200 ease-in-out`}
>
<div className="bg-stone-900/95 backdrop-blur-sm text-white text-[10px] font-bold uppercase tracking-widest py-2 px-3 rounded-lg shadow-xl whitespace-nowrap border border-white/10">
{content}
{/* Arrow */}
{/* <div className="absolute w-2 h-2 bg-stone-900 rotate-45 ..."></div> Optional */}
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,117 @@
import React, { useState } from 'react';
import axios from 'axios';
import { Loader2, Video, Play, Film } from 'lucide-react';
import { useAuth } from '../AuthContext';
interface VideoGeneratorProps {
project: any;
onVideoGenerated: () => void;
}
const VIDEO_PRESETS = [
{ id: 'cinematic_pan', label: 'Cinematic Pan', icon: '↔️', description: 'Slow horizontal pan across the artwork' },
{ id: 'slow_zoom', label: 'Slow Zoom', icon: '🔍', description: 'Gentle zoom in to highlight details' },
{ id: 'windy_atmosphere', label: 'Windy Atmosphere', icon: '🍃', description: 'Subtle movement suggesting a breeze' },
{ id: 'page_flip', label: 'Page Flip (Conceptual)', icon: '📖', description: 'Simulated page turning effect' },
];
export const VideoGenerator: React.FC<VideoGeneratorProps> = ({ project, onVideoGenerated }) => {
const [generating, setGenerating] = useState(false);
const [selectedPreset, setSelectedPreset] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const { refreshUser } = useAuth();
const handleGenerate = async () => {
if (!selectedPreset) return;
setGenerating(true);
setError(null);
try {
const token = localStorage.getItem('token');
const apiKey = localStorage.getItem('gemini_api_key');
await axios.post(
`/api/projects/${project.id}/video-mockups`,
{ presetId: selectedPreset },
{
headers: {
Authorization: `Bearer ${token}`,
'X-Gemini-API-Key': apiKey || ''
}
}
);
onVideoGenerated();
setSelectedPreset(null);
await refreshUser();
} catch (err: any) {
console.error("Video generation failed:", err);
const msg = err.response?.data?.error || "Failed to generate video";
if (err.response?.status === 402) alert(`⚠️ ${msg}`);
setError(msg);
} finally {
setGenerating(false);
}
};
return (
<div className="bg-white/50 backdrop-blur-sm rounded-xl p-6 border border-stone-200 shadow-sm mt-8">
<div className="flex items-center gap-3 mb-6">
<div className="p-2 bg-purple-100 rounded-lg">
<Film className="w-5 h-5 text-purple-600" />
</div>
<div>
<h3 className="text-lg font-semibold text-stone-800">Video Studio (Beta)</h3>
<p className="text-sm text-stone-500">Transform static designs into cinematic video mockups</p>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
{VIDEO_PRESETS.map((preset) => (
<button
key={preset.id}
onClick={() => setSelectedPreset(preset.id)}
className={`p-4 rounded-xl border text-left transition-all ${selectedPreset === preset.id
? 'border-purple-500 bg-purple-50 shadow-md ring-1 ring-purple-200'
: 'border-stone-200 hover:border-purple-300 hover:bg-white'
}`}
>
<div className="text-2xl mb-2">{preset.icon}</div>
<div className="font-medium text-stone-800 mb-1">{preset.label}</div>
<div className="text-xs text-stone-500">{preset.description}</div>
</button>
))}
</div>
{error && (
<div className="mb-4 p-3 bg-red-50 text-red-600 rounded-lg text-sm">
{error}
</div>
)}
<div className="flex justify-end">
<button
onClick={handleGenerate}
disabled={generating || !selectedPreset}
className={`flex items-center gap-2 px-6 py-2.5 rounded-lg font-medium transition-all ${generating || !selectedPreset
? 'bg-stone-200 text-stone-400 cursor-not-allowed'
: 'bg-gradient-to-r from-purple-600 to-indigo-600 text-white hover:shadow-lg hover:scale-[1.02]'
}`}
>
{generating ? (
<>
<Loader2 className="w-4 h-4 animate-spin" />
Renderizing...
</>
) : (
<>
<Play className="w-4 h-4" />
Generate Video
</>
)}
</button>
</div>
</div>
);
};

View File

@@ -0,0 +1,106 @@
import React, { useState } from 'react';
import { GlassMagnifier } from 'react-image-magnifiers';
interface ZoomableImageProps {
src: string;
alt: string;
className?: string;
magnifierSize?: string;
zoomLevel?: number;
children?: React.ReactNode;
disabled?: boolean; // New prop to prevent zoom on click
}
export const ZoomableImage: React.FC<ZoomableImageProps> = ({
src,
alt,
className = "",
magnifierSize = "30%",
zoomLevel = 2.5,
children,
disabled = false
}) => {
const [isOpen, setIsOpen] = useState(false);
const toggleModal = (e: React.MouseEvent) => {
if (disabled) return; // Don't open if disabled
e.stopPropagation();
setIsOpen(!isOpen);
};
return (
<>
{/* Thumbnail trigger */}
<div
className={`cursor-zoom-in relative group ${className}`}
onClick={toggleModal}
title="Click to zoom"
>
<img
src={src}
alt={alt}
className="w-full h-full object-cover" // Changed to object-cover to match Home usage expectation or passed className? Actually lets keep original but fix usage if needed. Wait, original was w-full h-auto object-contain.
/>
{children}
{!children && (
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/10 transition-colors flex items-center justify-center opacity-0 group-hover:opacity-100">
<span className="bg-white/80 text-stone-900 p-2 rounded-full shadow-lg backdrop-blur-sm">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-6 h-6">
<path strokeLinecap="round" strokeLinejoin="round" d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607ZM10.5 7.5v6m3-3h-6" />
</svg>
</span>
</div>
)}
</div>
{/* Modal */}
{isOpen && (
<div
className="fixed inset-0 z-[9999] bg-stone-900/95 backdrop-blur-md flex items-center justify-center p-8 animate-in fade-in duration-200"
onClick={() => setIsOpen(false)}
>
<div className="relative w-full h-full max-w-7xl flex items-center justify-center" onClick={(e) => e.stopPropagation()}>
<button
onClick={() => setIsOpen(false)}
className="absolute -top-6 -right-6 md:top-0 md:-right-12 text-white/50 hover:text-white transition-colors p-2 z-50"
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-8 h-8">
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18 18 6M6 6l12 12" />
</svg>
</button>
<div className="w-full h-full flex items-center justify-center rounded-lg overflow-hidden">
<GlassMagnifier
imageSrc={src}
imageAlt={alt}
magnifierSize={magnifierSize}
magnifierBorderSize={2}
magnifierBorderColor="rgba(255, 255, 255, 0.5)"
square={false}
allowOverflow={true}
style={{
width: 'auto',
height: 'auto',
maxWidth: '100%',
maxHeight: '90vh',
display: 'block'
}}
imageStyle={{
width: 'auto',
height: 'auto',
maxWidth: '100%',
maxHeight: '90vh',
objectFit: 'contain'
}}
/>
</div>
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 bg-black/50 text-white px-4 py-2 rounded-full text-xs font-medium backdrop-blur-md pointer-events-none z-50">
Hover to magnify Click outside to close
</div>
</div>
</div>
)}
</>
);
};

233
geminiService.ts Normal file
View File

@@ -0,0 +1,233 @@
import { GoogleGenAI, Type, Part } from "@google/genai";
import { ProductPackage, AspectRatio, ImageSize, ProductType, CreativityLevel } from "./types";
const creativityMap: Record<CreativityLevel, number> = {
"Literal": 0.2,
"Balanced": 0.7,
"Artistic": 1.1,
"Avant-Garde": 1.6,
"Conservative": 0.3,
"Wild": 1.4
};
// ---------------------------------------------------------
// SYSTEM PERSONA DEFINITION
// ---------------------------------------------------------
const SYSTEM_INSTRUCTION = `
You are an elite AI Fusion Entity possessing the combined expertise of:
1. Global Etsy E-commerce Strategist (Market Trends 2024-2025)
2. Senior Art Director & Creative Lead (Impeccable Taste)
3. Neuromarketing Expert (Dopamine & Serotonin Visual Triggers)
4. SEO Visual Specialist (Search Intent Optimization)
5. Nano Banana / Stable Diffusion Prompt Engineer (Tag & Weight Mastery)
OBJECTIVE:
Translate raw user ideas into a "Commercial Masterpiece" Digital Product Package.
CORE ANALYSIS WORKFLOW (Internal Monologue):
Before generating output, perform this split-second analysis:
1. Market Fit: Identify the trending style for this niche (e.g. Boho, Minimalist, Dark Academia).
2. Neuro-Trigger: Define the emotion to sell (Calm, Joy, Nostalgia).
3. Composition: Plan the perfect thumbnail click composition.
PROMPT FORMULA (Strict Adherence):
The 'imagePrompt' MUST be a comma-separated string following this 'Nano Banana' structure:
[Quality & Neuro Boosters], [Subject with Art Direction], [Environment & Context], [Artistic Medium & Style], [Color Palette & Lighting], [Tech Specs]
MANDATORY BOOSTERS TO START:
(masterpiece:1.4), (best quality), (ultra-detailed), (commercial photography), (professional color grading), 8k resolution, (sharp focus), aesthetic, trending on etsy
EXAMPLE OUTPUT PROMPT:
"(masterpiece:1.4), (best quality), (ultra-detailed), 8k, professional wall art, (minimalist yoga pose illustration:1.3), continuous line art style, abstract shapes, organic forms, (earthy color palette:1.2), terracotta and beige tones, soft textured paper background, zen atmosphere, (calming aesthetic), clean composition, interior design mockup style, soft studio lighting, 300 dpi"
`;
/**
* MODULE 1: Generates the specialized SEO and Prompt package using the 5-Layer Persona.
*/
export const generateProductPackage = async (
niche: string,
productType: ProductType,
creativity: CreativityLevel,
referenceImages?: string[]
): Promise<ProductPackage> => {
const ai = new GoogleGenAI({ apiKey: process.env.API_KEY });
const parts: Part[] = [
{ text: SYSTEM_INSTRUCTION },
{
text: `
TASK: Architect a High-Conversion ${productType} for the niche: "${niche}".
OUTPUT REQUIREMENTS (JSON):
1. imagePrompt: The Nano Banana optimized tag string (English).
2. seoTitle: Ultra-SEO optimized title (English).
3. keywords: 13 golden tags (English).
4. description: Neuro-marketing copy (English).
5. suggestedPrice: Competitive Etsy pricing.
6. printingGuide: Technical setup (English).
` }
];
if (referenceImages && referenceImages.length > 0) {
referenceImages.forEach(b64 => {
parts.push({ inlineData: { data: b64.split(',')[1], mimeType: 'image/png' } });
});
}
const response = await ai.models.generateContent({
model: "gemini-2.0-flash", // HYBRID STRATEGY: Flash Brain (Cost Optimized)
contents: { parts },
config: {
temperature: creativityMap[creativity],
responseMimeType: "application/json",
responseSchema: {
type: Type.OBJECT,
properties: {
imagePrompt: { type: Type.STRING },
seoTitle: { type: Type.STRING },
keywords: { type: Type.ARRAY, items: { type: Type.STRING } },
description: { type: Type.STRING },
suggestedPrice: { type: Type.STRING },
printingGuide: { type: Type.STRING }
},
required: ["imagePrompt", "seoTitle", "keywords", "description", "suggestedPrice", "printingGuide"]
}
}
});
return JSON.parse(response.text || "{}");
};
/**
* MODULE 2: MASTER ASSET GENERATION
* Uses the Optimized Prompt to generate the visual.
*/
export const generateImage = async (
prompt: string,
aspectRatio: AspectRatio,
size: ImageSize,
productType: ProductType,
referenceImages?: string[]
): Promise<string> => {
const ai = new GoogleGenAI({ apiKey: process.env.API_KEY });
// Apply Product-Specific Composition Rules as a "Guidance Layer" on top of the prompt
let compositionGuidance = "";
if (productType === "Sticker") compositionGuidance = "Ensure the subject is isolated on a clean white background with a thick white sticker border (die-cut style). No drop shadows.";
if (productType === "Bookmark") compositionGuidance = "Ensure a narrow vertical composition suitable for a bookmark. Decorative borders.";
const masterPrompt = `
Act as the Senior Art Director & Visual Engine.
INPUT PROMPT (Nano Banana Format):
${prompt}
ADDITIONAL TECHNICAL GUIDANCE:
${compositionGuidance}
EXECUTION:
Generate the image adhering strictly to the aesthetic quality defined in the prompt tags.
`;
const parts: Part[] = [{ text: masterPrompt }];
if (referenceImages && referenceImages.length > 0) {
referenceImages.forEach(b64 => parts.push({ inlineData: { data: b64.split(',')[1], mimeType: 'image/png' } }));
}
const response = await ai.models.generateContent({
model: 'gemini-2.5-flash-image', // Or 'gemini-3-pro-image-preview' if available/preferred
contents: { parts },
config: { imageConfig: { aspectRatio, imageSize: size } }
});
const part = response.candidates?.[0]?.content?.parts?.find(p => p.inlineData);
return part?.inlineData ? `data:image/png;base64,${part.inlineData.data}` : "";
};
/**
* MODULE 3: MOCKUP SCENARIOS
*/
export const MOCKUP_SCENARIOS: Record<ProductType, string[]> = {
"Wall Art": ["Luxury penthouse gallery wall", "Modern kitchen breakfast nook", "Studio easel"],
"Bookmark": ["Tucked in a vintage book", "Flatlay with coffee", "Held in a journal"],
"Sticker": ["On a laptop lid", "On a hydroflask", "In a planner spread"],
"Planner": ["On a high-end tablet", "Minimalist desk setup", "Hand-held digital view"],
"Phone Wallpaper": ["Smartphone lifestyle photo", "Marble nightstand", "Lockscreen preview"],
"Social Media Kit": ["Grid preview", "Marketing agency desk", "Mobile profile layout"],
"Label": ["Product bottle mockup", "Jar label display", "Package shelf"]
};
/**
* MODULE 4: GENERATE SINGLE MOCKUP
*/
export const generateSingleMockup = async (base64Image: string, productType: ProductType, scenario: string): Promise<string> => {
const ai = new GoogleGenAI({ apiKey: process.env.API_KEY });
const response = await ai.models.generateContent({
model: 'gemini-2.5-flash-image',
contents: {
parts: [
{ inlineData: { data: base64Image.split(',')[1], mimeType: 'image/png' } },
{ text: `Mockup Scenario: This ${productType} asset used in a ${scenario}. Realistic, professional commercial photography, soft studio lighting, 8k, highly detailed.` }
]
}
});
const part = response.candidates?.[0]?.content?.parts?.find(p => p.inlineData);
return part?.inlineData ? `data:image/png;base64,${part.inlineData.data}` : "";
};
/**
* MODULE 5: PRECISION REVISION
* Maintains the Expert Persona for revisions.
*/
export const performPrecisionRevision = async (base64Image: string, revisionBrief: string): Promise<{ image: string, optimizedPrompt: string }> => {
const ai = new GoogleGenAI({ apiKey: process.env.API_KEY });
// 1. Translate and Optimize
const translationResponse = await ai.models.generateContent({
model: 'gemini-2.0-flash', // Cost Optimized Brain
contents: `
You are the "Nano Banana Prompt Engineer".
TASK: Translate the revision brief: "${revisionBrief}" into a comma-separated English tag list (Nano Banana format).
It should describe the NEW DESIRED STATE of the image.
Keep the original specific style tags unless the brief explicitly changes them.
Output ONLY the tag string.
`
});
const optimizedPrompt = translationResponse.text || revisionBrief;
// 2. Perform the Edit
const editResponse = await ai.models.generateContent({
model: 'gemini-2.5-flash-image',
contents: {
parts: [
{ inlineData: { data: base64Image.split(',')[1], mimeType: 'image/png' } },
{ text: `Precision Edit Instruction: Based on these tags: ${optimizedPrompt}. Maintain high fidelity and consistent art style.` }
]
}
});
const part = editResponse.candidates?.[0]?.content?.parts.find(p => p.inlineData);
const newImage = part?.inlineData ? `data:image/png;base64,${part.inlineData.data}` : base64Image;
return { image: newImage, optimizedPrompt };
};
/**
* MODULE 6: COMPETITOR ANALYSIS
*/
export const analyzeCompetitors = async (niche: string): Promise<{ text: string; sources: any[] }> => {
const ai = new GoogleGenAI({ apiKey: process.env.API_KEY });
const response = await ai.models.generateContent({
model: 'gemini-3-flash-preview',
contents: `Act as an E-commerce Market Specialist. Analyze current Etsy trends for: "${niche}". Focus on visual style, color palettes, and top-selling themes.`,
config: { tools: [{ googleSearch: {} }] }
});
return {
text: response.text || "",
sources: response.candidates?.[0]?.groundingMetadata?.groundingChunks || []
};
};

31
i18n.ts Normal file
View File

@@ -0,0 +1,31 @@
import React from 'react';
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import en from './locales/en.json';
import tr from './locales/tr.json';
import de from './locales/de.json';
import fr from './locales/fr.json';
import es from './locales/es.json';
i18n
.use(LanguageDetector)
.use(initReactI18next)
.init({
resources: {
en: { translation: en },
tr: { translation: tr },
de: { translation: de },
fr: { translation: fr },
es: { translation: es }
},
fallbackLng: 'en',
lng: 'en', // Force default to English as requested
interpolation: {
escapeValue: false // react already safes from xss
}
});
export default i18n;

59
index.css Normal file
View File

@@ -0,0 +1,59 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
.glass {
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.3);
}
@keyframes cinematic-pan {
0% {
transform: scale(1.1) translateX(-5%);
}
50% {
transform: scale(1.1) translateX(5%);
}
100% {
transform: scale(1.1) translateX(-5%);
}
}
@keyframes cinematic-zoom {
0% {
transform: scale(1);
}
100% {
transform: scale(1.2);
}
}
@keyframes cinematic-float {
0% {
transform: translateY(0) scale(1.05);
}
50% {
transform: translateY(-10px) scale(1.05);
}
100% {
transform: translateY(0) scale(1.05);
}
}
.animate-cinematic-pan {
animation: cinematic-pan 10s ease-in-out infinite;
}
.animate-cinematic-zoom {
animation: cinematic-zoom 15s ease-in-out infinite alternate;
}
.animate-cinematic-float {
animation: cinematic-float 8s ease-in-out infinite;
}

62
index.html Normal file
View File

@@ -0,0 +1,62 @@
<!DOCTYPE html>
<html lang="en">
<head>
<script>
// AGGRESSIF KONSOL TEMIZLIGI - Chrome Extension Hataları İçin
window.onerror = function (message, source, lineno, colno, error) {
if (message && typeof message === 'string' && (message.includes('message port closed') || message.includes('The message port closed before a response was received'))) {
return true; // Hatanın konsola yazılmasını engelle
}
};
window.addEventListener('unhandledrejection', function (event) {
// Promise rejection mesajını yakala
const msg = event.reason?.message || event.reason;
if (msg && typeof msg === 'string' && (msg.includes('message port closed') || msg.includes('The message port closed before a response was received'))) {
event.preventDefault(); // Tarayıcının konsola yazmasını engelle
return;
}
});
// Console.error override
const originalConsoleError = console.error;
console.error = function (...args) {
const msg = args[0];
if (msg && (
(typeof msg === 'string' && (msg.includes('message port closed') || msg.includes('The message port closed before a response was received'))) ||
(typeof msg === 'object' && msg.message && (msg.message.includes('message port closed') || msg.message.includes('The message port closed before a response was received')))
)) {
return; // Yoksay
}
originalConsoleError.apply(console, args);
};
</script>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DigiCraft</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Playfair+Display:wght@700&display=swap"
rel="stylesheet">
<style>
body {
font-family: 'Inter', sans-serif;
}
h1,
h2,
h3 {
font-family: 'Playfair Display', serif;
}
</style>
</head>
<body class="bg-stone-50 text-stone-900">
<div id="root"></div>
<script type="module" src="/index.tsx"></script>
</body>
</html>

38
index.tsx Normal file
View File

@@ -0,0 +1,38 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import './index.css';
import './i18n'; // Initialize i18n
import { BrowserRouter } from 'react-router-dom';
import { AuthProvider } from './AuthContext';
import axios from 'axios';
// Set production API URL if provided
if (import.meta.env.VITE_API_URL) {
axios.defaults.baseURL = import.meta.env.VITE_API_URL;
}
const rootElement = document.getElementById('root');
if (!rootElement) {
throw new Error("Could not find root element to mount to");
}
import { GoogleOAuthProvider } from '@react-oauth/google';
import { HelmetProvider } from 'react-helmet-async';
const root = ReactDOM.createRoot(rootElement);
root.render(
<React.StrictMode>
<GoogleOAuthProvider clientId={import.meta.env.VITE_GOOGLE_CLIENT_ID || "PLACEHOLDER_CLIENT_ID"}>
<HelmetProvider>
<BrowserRouter>
<AuthProvider>
<App />
</AuthProvider>
</BrowserRouter>
</HelmetProvider>
</GoogleOAuthProvider>
</React.StrictMode>
);

62
legal_texts.ts Normal file
View File

@@ -0,0 +1,62 @@
export const USER_AGREEMENT_TEXT = `
# KULLANICI VE LİSANS SÖZLEŞMESİ (User & License Agreement)
**Son Güncelleme:** 4 Ocak 2026
## 1. TARAFLAR VE AMAÇ
İşbu Kullanıcı Sözleşmesi ("Sözleşme"), DigiCraft ("Platform") ile Platform'a kayıt olan kullanıcı ("Kullanıcı") arasında akdedilmiştir. Kullanıcı, Platform'a üye olarak, giriş yaparak veya herhangi bir hizmetini kullanarak bu Sözleşme'nin tüm şartlarını kayıtsız şartsız kabul ettiğini beyan ve taahhüt eder.
## 2. FİKRİ MÜLKİYET VE MÜLKİYET HAKLARI (KRİTİK MADDE)
**2.1. ESER SAHİPLİĞİ:** İşbu Platform aracılığıyla üretilen, tasarlanan, türetilen veya işlenen her türlü görsel, metinsel, işitsel materyal ("İçerik") ve bu İçeriklerin temelini oluşturan yapay zeka algoritmaları, prompt yapıları ve stil şablonları üzerindeki **tüm mali ve manevi haklar, eserin ana kullanım hakkı ve mülkiyeti, münhasıran 27.03.1980 Ankara doğumlu HARUN CAN'a aittir.**
**2.2. LİSANS HAKKI:** Kullanıcı'ya, Platform üzerinden ürettiği İçerikleri yalnızca kendi ticari faaliyetlerinde (örneğin Etsy mağazasında ürün satışı) kullanmak üzere, devredilemez, alt lisanslanamaz, gayri münhasır bir kullanım lisansı verilir. Bu lisans, mülkiyetin devri anlamına gelmez. Harun CAN, dilediği zaman bu lisansı tek taraflı olarak iptal etme hakkını saklı tutar.
**2.3. İZİNSİZ KULLANIM VE CEZAİ YAPTIRIMLAR:** Platform'un kaynak kodlarının, veritabanının, görsel arayüzünün veya "DNA Profili" algoritmalarının kopyalanması, tersine mühendislik (reverse engineering) işlemine tabi tutulması veya yetkisiz çoğaltılması; 5846 sayılı Fikir ve Sanat Eserleri Kanunu ve 5237 sayılı Türk Ceza Kanunu'nun ("TCK") Bilişim Alanında Suçlar başlığı altındaki (Madde 243, 244) hükümlerine göre suç teşkil eder. İhlal durumunda Kullanıcı, Harun CAN'ın uğrayacağı tüm maddi ve manevi zararları, avukatlık ücretleri dahil olmak üzere tazmin etmeyi peşinen kabul eder.
## 3. HARUN CAN'IN HAKLARI VE İTİBARIN KORUNMASI (ÇOK SIKI GÜVENLİK)
**3.1. KİŞİLİK VE TİCARİ HAKLARA SAYGI (KIRMIZI ÇİZGİ):** Kullanıcı, Platform'u kullanarak veya Platform'dan elde ettiği verileri/içerikleri kullanarak hiçbir surette Harun CAN hakkında hakaretamiz, küçük düşürücü, itibar zedeleyici, karalayıcı, ticari veya şahsi kimliğine saldırı niteliği taşıyan ("Disparagement") hiçbir beyanda bulunamaz, içerik üretemez veya paylaşamaz.
**3.2. YAPTIRIMLAR VE TAZMİNAT:** Kullanıcının, Harun CAN'ın ismini, markasını veya eserlerini manipüle ederek veya Platform'u kullanarak Harun CAN aleyhine bir algı oluşturduğunun tespiti halinde;
* Kullanıcı, bu eyleminin Harun CAN'ın ticari ve şahsi itibarına verdiği zararı tazmin etmekle yükümlü olduğunu **BAŞTAN VE GAYRİKABİLİ RÜCU KABUL EDER.**
* Harun CAN, bu tür ihlallerde **Ankara, İstanbul veya Viyana** mahkemelerinde, uğradığı manevi zararın tazmini ve ticari itibarının iadesi için her türlü hukuki ve cezai işlemi başlatma hakkına sahiptir.
* Bu madde, sözleşme sona erse dahi süresiz olarak yürürlükte kalır.
## 4. SORUMSUZLUK KAYDI VE YASAL UYARI (Disclaimer)
**4.1. İÇERİK SORUMLULUĞU:** Harun CAN ve Platform, Kullanıcı tarafından Yapay Zeka (AI) teknolojisi kullanılarak üretilen hiçbir içeriğin (görsel, metin, kod) hukuki niteliğinden, yasallığından veya doğruluğundan sorumlu değildir. Kullanıcının ürettiği içeriklerle üçüncü kişilerin telif haklarını ihlale etmesi, suç teşkil eden (müstehcenlik, yasa dışı propaganda vb.) materyaller üretmesi veya bu içerikleri kullanarak herhangi bir dolandırıcılık veya haksız fiil işlemesi durumunda; **tüm hukuki, cezai ve idari sorumluluk münhasıran ve tamamen Kullanıcı'ya aittir.** Harun CAN'a rücu edilemez.
**4.2. GARANTİSİZLİK:** İŞBU PLATFORM "OLDUĞU GİBİ" (AS IS) SUNULMAKTADIR. HARUN CAN, TİCARETE ELVERİŞLİLİK, BELİRLİ BİR AMACA UYGUNLUK VEYA İHLAL ETMEME GİBİ HİÇBİR GARANTİ VERMEZ. SİSTEM KULLANIMINDAN DOĞACAK VERİ KAYBI VEYA TİCARİ ZARARLARDAN PLATFORM SORUMLU DEĞİLDİR.
## 5. YETKİLİ MAHKEME VE UYGULANACAK HUKUK (JURISDICTION)
İşbu Sözleşme'den veya kullanımından doğacak her türlü uyuşmazlığın çözümünde; Harun CAN'ın tercihine bağlı olarak Türk Hukuku veya Avusturya Hukuku uygulanabilecek olup, yetkili mahkemeler münhasıran **ANKARA, İSTANBUL (Türkiye) ve VİYANA (Avusturya)** Mahkemeleri ve İcra Daireleridir. Kullanıcı, bu mahkemelerin yetkisini itirazsız kabul eder.
---
# KİŞİSEL VERİLERİN KORUNMASI VE GİZLİLİK POLİTİKASI (KVKK)
**Veri Sorumlusu:** Harun CAN
## 1. AMAÇ VE KAPSAM
6698 sayılı Kişisel Verilerin Korunması Kanunu ("KVKK") uyarınca, kimliğinizi belirli veya belirlenebilir kılan her türlü bilginiz "Kişisel Veri" olarak aşağıdaki kapsamda İşlenecektir.
## 2. İŞLENEN KİŞİSEL VERİLER
- **Kimlik Bilgileri:** Ad, Soyad (Kayıt sırasında istenirse).
- **İletişim Bilgileri:** E-posta adresi.
- **İşletim Verileri:** API Anahtarları, IP adresi, Log kayıtları.
- **Kullanım Verileri:** Üretilen promptlar, görseller ve proje verileri.
## 3. VERİ İŞLEME AMAÇLARI
Kişisel verileriniz; Platform üyeliğinizin oluşturulması, hizmet kalitesinin artırılması, güvenliğin sağlanması (TCK madde 243 gereği log tutma yükümlülüğü), hukuki yükümlülüklerin yerine getirilmesi ve Harun CAN tarafından sunulan hizmetlerin tarafınıza önerilmesi amaçlarıyla işlenmektedir.
## 4. VERİLERİN AKTARILMASI
Kişisel verileriniz; yasal zorunluluklar (savcılık talepleri vb.) dışında, açık rızanız olmaksızın üçüncü kişilere aktarılmaz. Ancak, API Key kullanımınız kapsamında Google (Alphabet Inc.) sunucularına veri akışı mevcuttur; Kullanıcı, Platform'u kullanarak bu uluslararası veri aktarımını kabul etmiş sayılır.
## 5. HAKLARINIZ (KVKK MADDE 11)
Kanun'un 11. maddesi uyarınca veri sahibi olarak; verilerinizin işlenip işlenmediğini öğrenme, işlenmişse buna ilişkin bilgi talep etme, verilerinizin silinmesini veya yok edilmesini isteme hakkına sahipsiniz.
---
**Onay Beyanı:** Yukarıdaki metinleri okudum, anladım. Harun CAN'ın fikri mülkiyet haklarını, yetki şartlarını (Ankara/İstanbul/Viyana) ve sorumsuzluk kayıtlarını kabul ediyor, kişisel verilerimin işlenmesine ve yurt dışı sunuculara (Google API kullanımı Nedeniyle) aktarılmasına açık rıza gösteriyorum.
`;
export const KVKK_TEXT = USER_AGREEMENT_TEXT; // Reusing for simplicity in the constant file, but allows splitting if needed. UI handles display.
export const DISCLAIMER_TEXT = USER_AGREEMENT_TEXT;

34
locales/de.json Normal file
View File

@@ -0,0 +1,34 @@
{
"general": {
"loading": "Loading...",
"logout": "Logout",
"dashboard": "Dashboard",
"gallery": "Gallery",
"vault_connected": "Vault Connected"
},
"buttons": {
"new_project": "New Project",
"login": "Login",
"register": "Register",
"delete_project": "Delete Project",
"refine": "Refine",
"create_set": "Create Set",
"restore": "Restore",
"upscale": "Upscale",
"download": "Download",
"add_mockup": "Add Mockup"
},
"dashboard": {
"title": "DIGICRAFT",
"subtitle": "Trend Time-Machine & Project Vault",
"no_projects": "No Projects Found",
"start_masterpiece": "Start your first masterpiece"
},
"home": {
"title": "DIGICRAFT",
"subtitle": "Visual DNA & On-Demand Production Suite",
"brief_label": "Creative Brief",
"visual_dna_label": "Visual DNA",
"synthesize_btn": "Synthesize Master"
}
}

34
locales/en.json Normal file
View File

@@ -0,0 +1,34 @@
{
"general": {
"loading": "Loading...",
"logout": "Logout",
"dashboard": "Dashboard",
"gallery": "Gallery",
"vault_connected": "Vault Connected"
},
"buttons": {
"new_project": "New Project",
"login": "Login",
"register": "Register",
"delete_project": "Delete Project",
"refine": "Refine",
"create_set": "Create Set",
"restore": "Restore",
"upscale": "Upscale",
"download": "Download",
"add_mockup": "Add Mockup"
},
"dashboard": {
"title": "DIGICRAFT",
"subtitle": "Trend Time-Machine & Project Vault",
"no_projects": "No Projects Found",
"start_masterpiece": "Start your first masterpiece"
},
"home": {
"title": "DIGICRAFT",
"subtitle": "Visual DNA & On-Demand Production Suite",
"brief_label": "Creative Brief",
"visual_dna_label": "Visual DNA",
"synthesize_btn": "Synthesize Master"
}
}

34
locales/es.json Normal file
View File

@@ -0,0 +1,34 @@
{
"general": {
"loading": "Loading...",
"logout": "Logout",
"dashboard": "Dashboard",
"gallery": "Gallery",
"vault_connected": "Vault Connected"
},
"buttons": {
"new_project": "New Project",
"login": "Login",
"register": "Register",
"delete_project": "Delete Project",
"refine": "Refine",
"create_set": "Create Set",
"restore": "Restore",
"upscale": "Upscale",
"download": "Download",
"add_mockup": "Add Mockup"
},
"dashboard": {
"title": "DIGICRAFT",
"subtitle": "Trend Time-Machine & Project Vault",
"no_projects": "No Projects Found",
"start_masterpiece": "Start your first masterpiece"
},
"home": {
"title": "DIGICRAFT",
"subtitle": "Visual DNA & On-Demand Production Suite",
"brief_label": "Creative Brief",
"visual_dna_label": "Visual DNA",
"synthesize_btn": "Synthesize Master"
}
}

34
locales/fr.json Normal file
View File

@@ -0,0 +1,34 @@
{
"general": {
"loading": "Loading...",
"logout": "Logout",
"dashboard": "Dashboard",
"gallery": "Gallery",
"vault_connected": "Vault Connected"
},
"buttons": {
"new_project": "New Project",
"login": "Login",
"register": "Register",
"delete_project": "Delete Project",
"refine": "Refine",
"create_set": "Create Set",
"restore": "Restore",
"upscale": "Upscale",
"download": "Download",
"add_mockup": "Add Mockup"
},
"dashboard": {
"title": "DIGICRAFT",
"subtitle": "Trend Time-Machine & Project Vault",
"no_projects": "No Projects Found",
"start_masterpiece": "Start your first masterpiece"
},
"home": {
"title": "DIGICRAFT",
"subtitle": "Visual DNA & On-Demand Production Suite",
"brief_label": "Creative Brief",
"visual_dna_label": "Visual DNA",
"synthesize_btn": "Synthesize Master"
}
}

34
locales/tr.json Normal file
View File

@@ -0,0 +1,34 @@
{
"general": {
"loading": "Yükleniyor...",
"logout": ıkış Yap",
"dashboard": "Panel",
"gallery": "Galeri",
"vault_connected": "Kasa Bağlandı"
},
"buttons": {
"new_project": "Yeni Proje",
"login": "Giriş Yap",
"register": "Kayıt Ol",
"delete_project": "Projeyi Sil",
"refine": "İyileştir",
"create_set": "Set Oluştur",
"restore": "Geri Yükle",
"upscale": "Yükselt (Upscale)",
"download": "İndir",
"add_mockup": "Mockup Ekle"
},
"dashboard": {
"title": "DIGICRAFT",
"subtitle": "Trend Zaman Makinesi & Proje Kasası",
"no_projects": "Proje Bulunamadı",
"start_masterpiece": "İlk şaheserinizi yaratın"
},
"home": {
"title": "DIGICRAFT",
"subtitle": "Görsel DNA & Üretim Motoru",
"brief_label": "Yaratıcı Özet",
"visual_dna_label": "Görsel DNA",
"synthesize_btn": "Ana Görseli Üret"
}
}

7
metadata.json Normal file
View File

@@ -0,0 +1,7 @@
{
"name": "DigiCraft v16",
"description": "An AI-powered dashboard for Etsy sellers to automate digital product creation, SEO, and marketing descriptions using advanced Gemini models.",
"requestFramePermissions": [
"camera"
]
}

4345
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

39
package.json Normal file
View File

@@ -0,0 +1,39 @@
{
"name": "digicraft",
"private": true,
"version": "16.5.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@google/genai": "^1.34.0",
"@headlessui/react": "^2.2.9",
"@react-oauth/google": "^0.13.4",
"axios": "^1.13.2",
"framer-motion": "^12.29.2",
"html-to-image": "^1.11.13",
"i18next": "^25.7.3",
"i18next-browser-languagedetector": "^8.2.0",
"lucide-react": "^0.562.0",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"react-helmet-async": "^2.0.5",
"react-i18next": "^16.5.1",
"react-image-magnifiers": "^1.4.0",
"react-router-dom": "^7.11.0"
},
"devDependencies": {
"@types/node": "^22.14.0",
"@types/react": "^19.2.8",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.0.0",
"autoprefixer": "^10.4.23",
"postcss": "^8.5.6",
"tailwindcss": "^3.4.1",
"typescript": "~5.8.2",
"vite": "^6.2.0"
}
}

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;

6
postcss.config.js Normal file
View File

@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

5
public/robots.txt Normal file
View File

@@ -0,0 +1,5 @@
User-agent: *
Allow: /
Disallow: /private/
Sitemap: https://etsymastermind.com/sitemap.xml

15
public/sitemap.xml Normal file
View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://etsymastermind.com/</loc>
<lastmod>2026-01-04</lastmod>
<changefreq>weekly</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://etsymastermind.com/login</loc>
<lastmod>2026-01-01</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
</urlset>

16
tailwind.config.js Normal file
View File

@@ -0,0 +1,16 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {
fontFamily: {
sans: ['Inter', 'sans-serif'],
serif: ['Playfair Display', 'serif'],
},
},
},
plugins: [],
}

29
tsconfig.json Normal file
View File

@@ -0,0 +1,29 @@
{
"compilerOptions": {
"target": "ES2022",
"experimentalDecorators": true,
"useDefineForClassFields": false,
"module": "ESNext",
"lib": [
"ES2022",
"DOM",
"DOM.Iterable"
],
"skipLibCheck": true,
"types": [
"node"
],
"moduleResolution": "bundler",
"isolatedModules": true,
"moduleDetection": "force",
"allowJs": true,
"jsx": "react-jsx",
"paths": {
"@/*": [
"./*"
]
},
"allowImportingTsExtensions": true,
"noEmit": true
}
}

41
types.ts Normal file
View File

@@ -0,0 +1,41 @@
export type ProductType = "Wall Art" | "Bookmark" | "Sticker" | "Planner" | "Phone Wallpaper" | "Social Media Kit" | "Label";
export type CreativityLevel = "Literal" | "Balanced" | "Artistic" | "Avant-Garde" | "Conservative" | "Wild";
export interface ProductPackage {
imagePrompt: string;
seoTitle: string;
keywords: string[];
description: string;
suggestedPrice?: string;
printingGuide: string;
}
export type AspectRatio = "1:1" | "3:4" | "4:3" | "9:16" | "16:9" | "4:5" | "5:4" | "2:3" | "3:2" | "5:7" | "21:9" | "32:9" | "2.35:1" | "9:21";
export type ImageSize = "SD" | "HD" | "4K" | "1K" | "2K";
export interface ProductionAsset {
ratio: string;
label: string;
description: string;
prompt?: string;
quality?: 'DRAFT' | 'MASTER' | 'UPSCALED';
meta?: string;
createdAt: string;
url: string | null;
}
export interface User {
id: string;
email: string;
role: 'USER' | 'ADMIN';
apiKey?: string;
skuSettings?: string; // JSON String
plan?: string;
credits?: number;
etsyShopName?: string;
etsyShopLink?: string;
etsyShopLogo?: string;
}

1
vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

37
vite.config.ts Normal file
View File

@@ -0,0 +1,37 @@
import path from 'path';
import { defineConfig, loadEnv } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, '.', '');
return {
appType: 'spa',
server: {
port: 3000,
host: '0.0.0.0',
proxy: {
'/api': {
target: 'http://localhost:3001',
changeOrigin: true,
},
'/storage': {
target: 'http://localhost:3001',
changeOrigin: true,
},
},
hmr: {
overlay: false
},
fs: {
strict: true
}
},
plugins: [react()],
resolve: {
alias: {
'@': path.resolve(__dirname, '.'),
}
}
};
});