diff --git a/package-lock.json b/package-lock.json index 613dc41..06a324b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "@radix-ui/react-radio-group": "^1.3.7", "@radix-ui/react-select": "^2.2.5", "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-tabs": "^1.1.12", "@tldraw/tldraw": "^3.15.4", "axios": "^1.11.0", @@ -28,6 +29,7 @@ "next": "15.4.6", "react": "19.1.0", "react-dom": "19.1.0", + "socket.io-client": "^4.8.1", "svg-path-parser": "^1.1.0", "tailwind-merge": "^3.3.1", "zustand": "^5.0.8" @@ -2572,6 +2574,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, "node_modules/@swc/helpers": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", @@ -2809,6 +2817,66 @@ "node": ">=14.0.0" } }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.4.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.0.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.4.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.0.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.11", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.9.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { + "version": "0.9.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { + "version": "2.8.0", + "dev": true, + "inBundle": true, + "license": "0BSD", + "optional": true + }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { "version": "4.1.11", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.11.tgz", @@ -4833,6 +4901,45 @@ "dev": true, "license": "MIT" }, + "node_modules/engine.io-client": { + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz", + "integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-client/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/enhanced-resolve": { "version": "5.18.3", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", @@ -7061,7 +7168,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/nanoid": { @@ -8318,6 +8424,68 @@ "is-arrayish": "^0.3.1" } }, + "node_modules/socket.io-client": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz", + "integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.2", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-client/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -9074,6 +9242,35 @@ "node": ">=0.10.0" } }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/yallist": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", diff --git a/package.json b/package.json index 7f8877f..e0eaa46 100644 --- a/package.json +++ b/package.json @@ -3,9 +3,9 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "next dev --turbopack", + "dev": "next dev --turbopack -p 3001", "build": "next build", - "start": "next start", + "start": "next start -p 3001", "lint": "next lint" }, "dependencies": { @@ -20,6 +20,7 @@ "@radix-ui/react-radio-group": "^1.3.7", "@radix-ui/react-select": "^2.2.5", "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-tabs": "^1.1.12", "@tldraw/tldraw": "^3.15.4", "axios": "^1.11.0", @@ -29,6 +30,7 @@ "next": "15.4.6", "react": "19.1.0", "react-dom": "19.1.0", + "socket.io-client": "^4.8.1", "svg-path-parser": "^1.1.0", "tailwind-merge": "^3.3.1", "zustand": "^5.0.8" diff --git a/src/app/admin/analytics/page.tsx b/src/app/admin/analytics/page.tsx new file mode 100644 index 0000000..edbe972 --- /dev/null +++ b/src/app/admin/analytics/page.tsx @@ -0,0 +1,88 @@ +"use client" + +import { AdminSidebarLayout } from '@/components/layout/admin-sidebar-layout' +import { AdminNotificationProvider } from '@/contexts/AdminNotificationContext' +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { BarChart3, TrendingUp, Activity, Eye } from 'lucide-react' + +export default function AdminAnalyticsPage() { + return ( + + +
+
+
+

Analytics Dashboard

+

Monitor system performance and user engagement

+
+
+ + {/* Analytics Stats */} +
+ + + Page Views + + + +
45,231
+

+20.1% from last month

+
+
+ + + + Active Sessions + + + +
2,350
+

+180.1% from last month

+
+
+ + + + Template Usage + + + +
12,234
+

+19% from last month

+
+
+ + + + Growth Rate + + + +
+12.5%
+

+2.1% from last month

+
+
+
+ + {/* Analytics Content */} + + + Analytics Features + + +

Advanced analytics functionality will be implemented here.

+

Features will include:

+
    +
  • Real-time user activity tracking
  • +
  • Template usage statistics
  • +
  • Performance metrics
  • +
  • Custom reports generation
  • +
  • Data visualization charts
  • +
+
+
+
+
+
+ ) +} diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index 8e7d221..3dae46c 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -1,59 +1,15 @@ "use client" -import { useEffect } from 'react' -import { useRouter } from 'next/navigation' -import { useAuth } from '@/contexts/auth-context' import { AdminDashboard } from '@/components/admin/admin-dashboard' +import { AdminSidebarLayout } from '@/components/layout/admin-sidebar-layout' +import { AdminNotificationProvider } from '@/contexts/AdminNotificationContext' export default function AdminPage() { - const { user, isAdmin } = useAuth() - const router = useRouter() - - useEffect(() => { - // Redirect non-admin users to home page - if (user && !isAdmin) { - router.push('/') - } - // Redirect unauthenticated users to signin - if (!user) { - router.push('/signin') - } - }, [user, isAdmin, router]) - - // Show loading while checking auth - if (!user) { - return ( -
-
-
-

Loading...

-
-
- ) - } - - // Show access denied for non-admin users - if (!isAdmin) { - return ( -
-
-
🚫
-

Access Denied

-

You don't have permission to access this page.

- -
-
- ) - } - return ( -
- -
+ + + + + ) } diff --git a/src/app/admin/settings/page.tsx b/src/app/admin/settings/page.tsx new file mode 100644 index 0000000..6f55624 --- /dev/null +++ b/src/app/admin/settings/page.tsx @@ -0,0 +1,186 @@ +"use client" + +import { AdminSidebarLayout } from '@/components/layout/admin-sidebar-layout' +import { AdminNotificationProvider } from '@/contexts/AdminNotificationContext' +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { Switch } from '@/components/ui/switch' +import { Settings, Save, Database, Mail, Shield } from 'lucide-react' + +export default function AdminSettingsPage() { + return ( + + +
+
+
+

System Settings

+

Configure system preferences and security settings

+
+ +
+ +
+ {/* General Settings */} + + + + + General Settings + + + +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + {/* Security Settings */} + + + + + Security Settings + + + +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + {/* Email Settings */} + + + + + Email Settings + + + +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + {/* Database Settings */} + + + + + Database Settings + + + +
+ + +
+
+ + +
+
+ + +
+ +
+
+
+
+
+
+ ) +} diff --git a/src/app/admin/templates/page.tsx b/src/app/admin/templates/page.tsx new file mode 100644 index 0000000..14a3b41 --- /dev/null +++ b/src/app/admin/templates/page.tsx @@ -0,0 +1,15 @@ +"use client" + +import { AdminSidebarLayout } from '@/components/layout/admin-sidebar-layout' +import { AdminNotificationProvider } from '@/contexts/AdminNotificationContext' +import { AdminTemplatesManager } from '@/components/admin/admin-templates-manager' + +export default function AdminTemplatesPage() { + return ( + + + + + + ) +} diff --git a/src/app/admin/users/page.tsx b/src/app/admin/users/page.tsx new file mode 100644 index 0000000..1de2e02 --- /dev/null +++ b/src/app/admin/users/page.tsx @@ -0,0 +1,88 @@ +"use client" + +import { AdminSidebarLayout } from '@/components/layout/admin-sidebar-layout' +import { AdminNotificationProvider } from '@/contexts/AdminNotificationContext' +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { Users, UserPlus, UserCheck, UserX } from 'lucide-react' + +export default function AdminUsersPage() { + return ( + + +
+
+
+

User Management

+

Manage user accounts and permissions

+
+
+ + {/* Stats Cards */} +
+ + + Total Users + + + +
1,234
+

+12% from last month

+
+
+ + + + New Users + + + +
89
+

This month

+
+
+ + + + Active Users + + + +
987
+

80% of total

+
+
+ + + + Inactive Users + + + +
247
+

20% of total

+
+
+
+ + {/* User Management Content */} + + + User Management Features + + +

User management functionality will be implemented here.

+

Features will include:

+
    +
  • View all users
  • +
  • Search and filter users
  • +
  • Edit user permissions
  • +
  • Activate/deactivate accounts
  • +
  • View user activity logs
  • +
+
+
+
+
+
+ ) +} diff --git a/src/app/api/ai/analyze/route.ts b/src/app/api/ai/analyze/route.ts index f876b2d..b05eed5 100644 --- a/src/app/api/ai/analyze/route.ts +++ b/src/app/api/ai/analyze/route.ts @@ -77,7 +77,8 @@ Return ONLY a JSON object in this exact format: } const parsed = JSON.parse(jsonMatch[0]) - const complexity = (parsed.complexity as 'low' | 'medium' | 'high') || 'medium' + // Use the user-selected complexity if provided, otherwise use AI's suggestion + const complexity = (body.complexity || parsed.complexity as 'low' | 'medium' | 'high') || 'medium' const logicRules = Array.isArray(parsed.logicRules) ? parsed.logicRules : [] return NextResponse.json({ success: true, data: { complexity, logicRules } }) diff --git a/src/app/auth/emailVerification.tsx b/src/app/auth/emailVerification.tsx index 37b2313..03f5dac 100644 --- a/src/app/auth/emailVerification.tsx +++ b/src/app/auth/emailVerification.tsx @@ -2,6 +2,7 @@ import React, { useEffect, useRef, useState } from "react"; import { useSearchParams, useRouter } from "next/navigation"; +import { BACKEND_URL } from "@/config/backend"; type VerifyState = "idle" | "loading" | "success" | "error"; @@ -9,14 +10,11 @@ interface VerificationResponse { success: boolean; data: { message: string; user: { email: string; username: string } }; message: string; + redirect?: string; } -interface ErrorResponse { - success: false; - error: string; - message: string; -} +// Removed unused ErrorResponse interface -const API_BASE_URL = "http://localhost:8011"; + const EmailVerification: React.FC = () => { const searchParams = useSearchParams(); @@ -51,30 +49,37 @@ const EmailVerification: React.FC = () => { try { const res = await fetch( - `${API_BASE_URL}/api/auth/verify-email?token=${verificationToken}`, - { method: "GET", headers: { "Content-Type": "application/json" }, signal: ctrl.signal } + `${BACKEND_URL}/api/auth/verify-email?token=${verificationToken}&format=json`, + { method: "GET", headers: { "Accept": "application/json" }, signal: ctrl.signal } ); const txt = await res.text(); - let data: any = {}; + console.log("Raw response text:", txt); + console.log("Response status:", res.status); + console.log("Response headers:", Object.fromEntries(res.headers.entries())); + + let data: Record = {}; try { data = JSON.parse(txt || "{}"); } catch { /* ignore non-JSON */ } + console.log("Parsed response data:", data); - if (res.ok && (data as VerificationResponse)?.success) { - router.replace("/auth?verified=1"); + if (res.ok && (data as unknown as VerificationResponse)?.success) { + router.replace("/signin?verified=true"); return; } const msg = String(data?.message || "").toLowerCase(); if (msg.includes("already verified")) { - router.replace("/auth?verified=1"); + // Check if there's a redirect URL in the response for already verified case too + router.replace("/signin?verified=true"); return; } setStatus("error"); - setError(data?.message || `Verification failed (HTTP ${res.status}).`); - } catch (e: any) { + setError(String(data?.message) || `Verification failed (HTTP ${res.status}).`); + } catch (e: unknown) { setStatus("error"); - setError(e?.name === "AbortError" ? "Request timed out. Please try again." : "Network error. Please try again."); + const error = e as { name?: string }; + setError(error?.name === "AbortError" ? "Request timed out. Please try again." : "Network error. Please try again."); console.error("Email verification error:", e); } finally { clearTimeout(timeout); @@ -94,7 +99,7 @@ const EmailVerification: React.FC = () => { return; } - const res = await fetch(`${API_BASE_URL}/api/auth/resend-verification`, { + const res = await fetch(`${BACKEND_URL}/api/auth/resend-verification`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email }), diff --git a/src/app/layout.tsx b/src/app/layout.tsx index bed4124..2b442e2 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -3,6 +3,7 @@ import type { Metadata } from "next" import { Poppins } from "next/font/google" import { AuthProvider } from "@/contexts/auth-context" import { AppLayout } from "@/components/layout/app-layout" +import { ToastProvider } from "@/components/ui/toast" import "./globals.css" const poppins = Poppins({ @@ -33,11 +34,13 @@ html { `} - - -
{children}
-
-
+ + + +
{children}
+
+
+
) diff --git a/src/app/signin/page.tsx b/src/app/signin/page.tsx index 03bd6a4..b29be2c 100644 --- a/src/app/signin/page.tsx +++ b/src/app/signin/page.tsx @@ -1,5 +1,10 @@ +import { Suspense } from "react" import { SignInPage } from "@/components/auth/signin-page" export default function SignInPageRoute() { - return + return ( + Loading...}> + + + ) } diff --git a/src/app/verify-email/page.tsx b/src/app/verify-email/page.tsx index f6ae03c..90e2646 100644 --- a/src/app/verify-email/page.tsx +++ b/src/app/verify-email/page.tsx @@ -1,5 +1,10 @@ +import { Suspense } from "react"; import EmailVerification from "@/app/auth/emailVerification"; export default function VerifyEmailPage() { - return ; + return ( + Loading...}> + + + ); } diff --git a/src/components/admin/admin-dashboard.tsx b/src/components/admin/admin-dashboard.tsx index 042aa46..11c610a 100644 --- a/src/components/admin/admin-dashboard.tsx +++ b/src/components/admin/admin-dashboard.tsx @@ -6,34 +6,55 @@ import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { - AlertCircle, CheckCircle, - Clock, XCircle, - Copy, - Bell, + Clock, RefreshCw, + AlertCircle, + Copy, + Filter, Search, - Filter + Edit, + Zap, + Files, + Bell, + BarChart3 } from 'lucide-react' import { adminApi, formatDate, getStatusColor, getComplexityColor } from '@/lib/api/admin' -import { AdminFeature, AdminNotification, AdminStats } from '@/types/admin.types' +import { AdminFeature, AdminTemplate, AdminStats } from '@/types/admin.types' import { FeatureReviewDialog } from './feature-review-dialog' import { AdminNotificationsPanel } from './admin-notifications-panel' +import { FeatureEditDialog } from './feature-edit-dialog' +import { TemplateEditDialog } from './template-edit-dialog' +import { RejectDialog } from './reject-dialog' import { Input } from '@/components/ui/input' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' +import { AdminNotificationProvider, useAdminNotifications } from '@/contexts/AdminNotificationContext' +import { useSearchParams } from 'next/navigation' -export function AdminDashboard() { +function AdminDashboardContent() { + const searchParams = useSearchParams() + const activeTab = searchParams.get('tab') || 'dashboard' + const filterParam = searchParams.get('filter') || 'all' + const [pendingFeatures, setPendingFeatures] = useState([]) - const [notifications, setNotifications] = useState([]) + const [customTemplates, setCustomTemplates] = useState([]) const [stats, setStats] = useState(null) + const [templateStats, setTemplateStats] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [selectedFeature, setSelectedFeature] = useState(null) + const [selectedTemplate, setSelectedTemplate] = useState(null) const [showReviewDialog, setShowReviewDialog] = useState(false) + const [showFeatureEditDialog, setShowFeatureEditDialog] = useState(false) + const [showTemplateEditDialog, setShowTemplateEditDialog] = useState(false) + const [showRejectDialog, setShowRejectDialog] = useState(false) + const [rejectItem, setRejectItem] = useState<{ id: string; name: string; type: 'feature' | 'template' } | null>(null) const [showNotifications, setShowNotifications] = useState(false) const [searchQuery, setSearchQuery] = useState('') - const [statusFilter, setStatusFilter] = useState('all') + const [statusFilter, setStatusFilter] = useState(filterParam) + + const { unreadCount, removeByReference } = useAdminNotifications() // Load dashboard data const loadDashboardData = async () => { @@ -41,15 +62,17 @@ export function AdminDashboard() { setLoading(true) setError(null) - const [pendingData, notificationsData, statsData] = await Promise.all([ - adminApi.getPendingFeatures(), - adminApi.getNotifications(true, 10), // Get 10 unread notifications - adminApi.getFeatureStats() + const [featuresResponse, templatesResponse, featureStatsResponse, templateStatsResponse] = await Promise.all([ + adminApi.getCustomFeatures('pending'), + adminApi.getCustomTemplates('pending'), + adminApi.getCustomFeatureStats(), + adminApi.getTemplateStats() ]) - setPendingFeatures(pendingData) - setNotifications(notificationsData) - setStats(statsData) + setPendingFeatures(featuresResponse || []) + setCustomTemplates(templatesResponse || []) + setStats(featureStatsResponse) + setTemplateStats(templateStatsResponse) } catch (err) { setError(err instanceof Error ? err.message : 'Failed to load dashboard data') console.error('Error loading dashboard data:', err) @@ -63,12 +86,14 @@ export function AdminDashboard() { }, []) // Handle feature review - const handleFeatureReview = async (featureId: string, reviewData: any) => { + const handleFeatureReview = async (featureId: string, reviewData: { status: 'approved' | 'rejected' | 'duplicate'; admin_notes?: string; admin_reviewed_by: string }) => { try { await adminApi.reviewFeature(featureId, reviewData) - // Remove the reviewed feature from pending list - setPendingFeatures(prev => prev.filter(f => f.id !== featureId)) + // Update the feature in the list + setPendingFeatures(prev => prev.map(f => + f.id === featureId ? { ...f, status: reviewData.status as AdminFeature['status'], admin_notes: reviewData.admin_notes } : f + )) // Reload stats const newStats = await adminApi.getFeatureStats() @@ -79,6 +104,141 @@ export function AdminDashboard() { } catch (err) { console.error('Error reviewing feature:', err) // Handle error (show toast notification, etc.) + alert('Error reviewing feature') + } + } + + // Handle template review + const handleTemplateReview = async (templateId: string, reviewData: { status: 'pending' | 'approved' | 'rejected' | 'duplicate'; admin_notes?: string }) => { + try { + await adminApi.reviewTemplate(templateId, reviewData) + + // Update the template in the list + setCustomTemplates(prev => prev.map(t => + t.id === templateId ? { ...t, status: reviewData.status as AdminTemplate['status'], admin_notes: reviewData.admin_notes } : t + )) + + // Reload stats + const newStats = await adminApi.getTemplateStats() + setTemplateStats(newStats) + + } catch (err) { + console.error('Error reviewing template:', err) + // Handle error (show toast notification, etc.) + alert('Error reviewing template') + } + } + + // Handle feature update + const handleFeatureUpdate = (featureId: string, updatedFeature: AdminFeature) => { + setPendingFeatures(prev => prev.map(f => + f.id === featureId ? updatedFeature : f + )) + } + + // Handle template update + const handleTemplateUpdate = (templateId: string, updatedTemplate: AdminTemplate) => { + // Hide the edited item from the pending list immediately + setCustomTemplates(prev => prev.filter(t => t.id !== templateId)) + } + + // Handle reject action + const handleReject = async (adminNotes: string) => { + if (!rejectItem) return + + try { + if (rejectItem.type === 'feature') { + await adminApi.rejectCustomFeature(rejectItem.id, adminNotes) + setPendingFeatures(prev => prev.map(f => + f.id === rejectItem.id ? { ...f, status: 'rejected', admin_notes: adminNotes } : f + )) + // Reload feature stats + const newStats = await adminApi.getCustomFeatureStats() + setStats(newStats) + } else { + await adminApi.rejectTemplate(rejectItem.id, adminNotes) + setCustomTemplates(prev => prev.map(t => + t.id === rejectItem.id ? { ...t, status: 'rejected', admin_notes: adminNotes } : t + )) + // Reload template stats + const newStats = await adminApi.getTemplateStats() + setTemplateStats(newStats) + } + } catch (err) { + console.error(`Error rejecting ${rejectItem.type}:`, err) + // Handle error (show toast notification, etc.) + alert(`Error rejecting ${rejectItem.type}`) + } + } + + // Handle approve action + const handleApprove = async (item: { id: string; type: 'feature' | 'template' }) => { + // Optimistic UI: remove immediately + if (item.type === 'feature') { + setPendingFeatures(prev => prev.filter(f => f.id !== item.id)) + } else { + setCustomTemplates(prev => prev.filter(t => t.id !== item.id)) + } + try { + if (item.type === 'feature') { + // Get the feature details first + const feature = pendingFeatures.find(f => f.id === item.id) + if (feature) { + // Create new approved feature in main template_features table + await adminApi.createApprovedFeature({ + template_id: feature.template_id, + name: feature.name, + description: feature.description, + complexity: feature.complexity, + business_rules: feature.business_rules, + technical_requirements: feature.technical_requirements + }) + + // Update the custom feature status to approved + await adminApi.reviewCustomFeature(item.id, { status: 'approved', admin_notes: 'Approved and created in main templates' }) + + // already optimistically removed + // Remove related notifications for this feature + removeByReference('custom_feature', item.id) + } + + // Reload feature stats + const newStats = await adminApi.getCustomFeatureStats() + setStats(newStats) + } else { + // Get the template details first + const template = customTemplates.find(t => t.id === item.id) + if (template) { + // Create new approved template in main templates table + await adminApi.createApprovedTemplate(item.id, { + title: template.title || '', + description: template.description, + category: template.category || '', + type: template.type || '', + icon: template.icon, + gradient: template.gradient, + border: template.border, + text: template.text, + subtext: template.subtext + }) + + // Update the custom template status to approved + await adminApi.reviewTemplate(item.id, { status: 'approved', admin_notes: 'Approved and created in main templates' }) + + // already optimistically removed + // Remove related notifications for this template + removeByReference('custom_template', item.id) + } + + // Reload template stats + const newStats = await adminApi.getTemplateStats() + setTemplateStats(newStats) + } + } catch (err) { + console.error(`Error approving ${item.type}:`, err) + // Handle error (show toast notification, etc.) + // Recover UI by reloading if optimistic removal was wrong + await loadDashboardData() } } @@ -93,15 +253,17 @@ export function AdminDashboard() { return matchesSearch && matchesStatus }) - // Get status counts - const getStatusCount = (status: string) => { - return stats?.features.find(s => s.status === status)?.count || 0 + // Get status counts for features + const getFeatureStatusCount = (status: string) => { + return stats?.features?.find(s => s.status === status)?.count || 0 } - const getUnreadNotificationCount = () => { - return stats?.notifications.unread || 0 + // Get status counts for templates + const getTemplateStatusCount = (status: string) => { + return (templateStats as AdminStats & { templates?: Array<{ status: string; count: number }> })?.templates?.find((s) => s.status === status)?.count || 0 } + if (loading) { return (
@@ -133,29 +295,16 @@ export function AdminDashboard() { ) } - return ( + const renderDashboardOverview = () => (
{/* Header */}
-

Admin Dashboard

-

Manage feature approvals and system notifications

+

Admin Dashboard

+

Manage feature approvals and system notifications

- - @@ -163,70 +312,166 @@ export function AdminDashboard() {
{/* Stats Cards */} -
- - -
- -
-

Pending

-

{getStatusCount('pending')}

+
+ {/* Features Stats */} + + + Custom Features + + +
+
+ +
+

Pending

+

{getFeatureStatusCount('pending')}

+
+
+
+ +
+

Approved

+

{getFeatureStatusCount('approved')}

+
+
+
+ +
+

Rejected

+

{getFeatureStatusCount('rejected')}

+
+
+
+ +
+

Duplicates

+

{getFeatureStatusCount('duplicate')}

+
- - -
- -
-

Approved

-

{getStatusCount('approved')}

+ {/* Templates Stats */} + + + Custom Templates + + +
+
+ +
+

Pending

+

{getTemplateStatusCount('pending')}

+
-
-
-
- - - -
- -
-

Rejected

-

{getStatusCount('rejected')}

+
+ +
+

Approved

+

{getTemplateStatusCount('approved')}

+
-
- - - - - -
- -
-

Duplicates

-

{getStatusCount('duplicate')}

+
+ +
+

Rejected

+

{getTemplateStatusCount('rejected')}

+
+
+
+ +
+

Duplicates

+

{getTemplateStatusCount('duplicate')}

+
- {/* Main Content */} - - - - - Pending Review ({pendingFeatures.length}) - - - - All Features - - + {/* Quick Actions */} + + + Quick Actions + + +
+ + + + +
+
+
+
+ ) + + return ( +
+ {activeTab === 'dashboard' && renderDashboardOverview()} + + {(activeTab === 'features' || activeTab === 'templates') && ( + <> + {/* Header */} +
+
+

+ {activeTab === 'features' ? 'Custom Features' : 'Custom Templates'} +

+

+ {activeTab === 'features' + ? 'Review and manage custom feature requests' + : 'Review and manage custom template submissions' + } +

+
+
+ +
+
+ + )} + + {/* Main Content - Only show for features/templates tabs */} + {activeTab === 'features' && ( +
- {/* Filters */}
@@ -304,15 +549,38 @@ export function AdminDashboard() {
+ +
@@ -321,40 +589,38 @@ export function AdminDashboard() { )) )}
- +
+ )} - -

- View and manage all features across different statuses. Use the filters above to narrow down results. -

- {/* TODO: Implement all features view with pagination */} -
- - - {/* Review Dialog */} + {/* Feature Edit Dialog */} {selectedFeature && ( - )} - {/* Notifications Panel */} - { - try { - await adminApi.markNotificationAsRead(id) - setNotifications(prev => prev.filter(n => n.id !== id)) - } catch (err) { - console.error('Error marking notification as read:', err) - } - }} - /> + {/* Reject Dialog */} + {rejectItem && ( + + )}
) } + +export function AdminDashboard() { + return ( + + + + ) +} diff --git a/src/components/admin/admin-feature-selection.tsx b/src/components/admin/admin-feature-selection.tsx new file mode 100644 index 0000000..b6d5548 --- /dev/null +++ b/src/components/admin/admin-feature-selection.tsx @@ -0,0 +1,305 @@ +"use client" + +import { useState, useEffect } from 'react' +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { Button } from '@/components/ui/button' +import { Badge } from '@/components/ui/badge' +import { Input } from '@/components/ui/input' +import { Checkbox } from '@/components/ui/checkbox' +import { ArrowLeft, Plus, Edit, Trash2 } from 'lucide-react' +import { DatabaseTemplate, TemplateFeature } from '@/lib/template-service' +import { AICustomFeatureCreator } from '@/components/ai/AICustomFeatureCreator' +import { getApiUrl } from '@/config/backend' + +interface AdminFeatureSelectionProps { + template: { + id: string + title: string + description?: string + type: string + category?: string + icon?: string + gradient?: string + border?: string + text?: string + subtext?: string + } + onBack: () => void +} + +export function AdminFeatureSelection({ template, onBack }: AdminFeatureSelectionProps) { + // Admin template service functions using admin API endpoints + const fetchFeatures = async (templateId: string): Promise => { + const response = await fetch(getApiUrl(`api/templates/${templateId}/features`)) + if (!response.ok) throw new Error('Failed to fetch features') + const data = await response.json() + // Handle different response structures + return Array.isArray(data) ? data : (data.data || data.features || []) + } + + const createFeature = async (templateId: string, feature: Partial) => { + const response = await fetch(getApiUrl('api/features'), { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ ...feature, template_id: templateId }) + }) + if (!response.ok) throw new Error('Failed to create feature') + return response.json() + } + + const updateFeature = async (templateId: string, featureId: string, feature: Partial) => { + const response = await fetch(getApiUrl(`api/templates/${templateId}/features/${featureId}`), { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(feature) + }) + if (!response.ok) throw new Error('Failed to update feature') + return response.json() + } + + const deleteFeature = async (templateId: string, featureId: string) => { + const response = await fetch(getApiUrl(`api/templates/${templateId}/features/${featureId}`), { + method: 'DELETE' + }) + if (!response.ok) throw new Error('Failed to delete feature') + } + + const bulkCreateFeatures = async (templateId: string, features: Partial[]) => { + const response = await fetch(getApiUrl(`api/templates/${templateId}/features/bulk`), { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ features }) + }) + if (!response.ok) throw new Error('Failed to create features') + return response.json() + } + + const [features, setFeatures] = useState([]) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + const [newFeature, setNewFeature] = useState({ + name: '', + description: '', + complexity: 'medium' as 'low' | 'medium' | 'high' + }) + const [selectedIds, setSelectedIds] = useState>(new Set()) + const [showAIModal, setShowAIModal] = useState(false) + + const load = async () => { + try { + setLoading(true) + setError(null) + const data = await fetchFeatures(template.id) + // Ensure we always have an array + setFeatures(Array.isArray(data) ? data : []) + } catch (err) { + console.error('Error loading features:', err) + const message = err instanceof Error ? err.message : 'Failed to load features' + setError(message) + setFeatures([]) // Reset to empty array on error + } finally { + setLoading(false) + } + } + + useEffect(() => { load() }, [template.id]) + + const handleAddCustom = async () => { + if (!newFeature.name.trim()) return + + // Check if template ID is valid UUID format (database templates use UUIDs) + const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i + if (!uuidRegex.test(template.id)) { + alert(`Cannot add features to demo templates. Template ID: ${template.id} is not a valid UUID. Please select a real template from your database.`) + return + } + + try { + const created = await createFeature(template.id, { + name: newFeature.name, + description: newFeature.description, + feature_type: 'essential', + complexity: newFeature.complexity, + is_default: true, + created_by_user: true, + }) + setNewFeature({ name: '', description: '', complexity: 'medium' }) + setSelectedIds((prev) => { + const next = new Set(prev) + if (created?.id) next.add(created.id) + return next + }) + await load() + } catch (error) { + console.error('Error creating custom feature:', error) + const errorMessage = error instanceof Error ? error.message : 'Template ID not found in database' + alert(`Failed to create custom feature: ${errorMessage}`) + } + } + + const handleAddAIAnalyzed = async (payload: { name: string; description: string; complexity: 'low' | 'medium' | 'high' }) => { + // Check if template ID is valid UUID format + const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i + if (!uuidRegex.test(template.id)) { + alert("Cannot add features to demo templates. Please select a real template from your database.") + return + } + + try { + await createFeature(template.id, { + name: payload.name, + description: payload.description, + feature_type: 'essential', + complexity: payload.complexity, + is_default: true, + created_by_user: true, + }) + await load() + } catch (error) { + console.error('Error creating AI-analyzed feature:', error) + const errorMessage = error instanceof Error ? error.message : 'Template ID not found in database' + alert(`Failed to create AI-analyzed feature: ${errorMessage}`) + } + } + + const handleUpdate = async (f: TemplateFeature, updates: Partial) => { + try { + await updateFeature(template.id, f.id, updates) + await load() + } catch (err) { + console.error('Error updating feature:', err) + const message = err instanceof Error ? err.message : 'Failed to update feature' + alert(`Error updating feature: ${message}`) + } + } + + const handleDelete = async (f: TemplateFeature) => { + try { + await deleteFeature(template.id, f.id) + setSelectedIds((prev) => { + const next = new Set(prev) + next.delete(f.id) + return next + }) + await load() + } catch (err) { + console.error('Error deleting feature:', err) + const message = err instanceof Error ? err.message : 'Failed to delete feature' + alert(`Error deleting feature: ${message}`) + } + } + + const toggleSelect = (f: TemplateFeature) => { + setSelectedIds((prev) => { + const next = new Set(prev) + if (next.has(f.id)) next.delete(f.id) + else next.add(f.id) + return next + }) + } + + if (loading) { + return
Loading features...
+ } + if (error) { + return
{error}
+ } + + // Ensure features is always an array before filtering + const safeFeatures = Array.isArray(features) ? features : [] + const essentialFeatures = safeFeatures.filter(f => f.feature_type === 'essential') + const suggestedFeatures = safeFeatures.filter(f => f.feature_type === 'suggested') + const customFeatures = safeFeatures.filter(f => f.feature_type === 'custom') + + const section = (title: string, list: TemplateFeature[]) => ( +
+

{title} ({list.length})

+
+ {list.map((f) => ( + + + +
+ toggleSelect(f)} + className="border-white/20 data-[state=checked]:bg-orange-500 data-[state=checked]:border-orange-500" + /> + {f.name} +
+ {f.feature_type === 'custom' && ( +
+ + +
+ )} +
+
+ +

{f.description || 'No description provided.'}

+
+ {f.feature_type} + {f.complexity} +
+
+
+ ))} +
+
+ ) + + return ( +
+
+

Select Features for {template.title}

+

Choose defaults or add your own essential features.

+
+ + {section('Essential Features', essentialFeatures)} + {section('Suggested Features', suggestedFeatures)} + +
+

Add Essential Feature

+

Use AI to analyze and create essential features for your project.

+
+ +
+

AI will analyze your requirements and create essential features

+
+ + {section('Your Custom Features', customFeatures)} + + {showAIModal && ( + { await handleAddAIAnalyzed(f); setShowAIModal(false) }} + onClose={() => setShowAIModal(false)} + /> + )} + +
+
+ +
+
Selected: {selectedIds.size} | Essential: {essentialFeatures.length} | Suggested: {suggestedFeatures.length} | Custom: {customFeatures.length}
+
+
+ ) +} diff --git a/src/components/admin/admin-notifications-panel.tsx b/src/components/admin/admin-notifications-panel.tsx index 0278bff..b558967 100644 --- a/src/components/admin/admin-notifications-panel.tsx +++ b/src/components/admin/admin-notifications-panel.tsx @@ -14,34 +14,38 @@ import { Card, CardContent } from '@/components/ui/card' import { Bell, CheckCircle, - XCircle, - Copy, Clock, Check, - Trash2 + Trash2, + Wifi, + WifiOff } from 'lucide-react' -import { AdminNotification } from '@/types/admin.types' import { formatDate } from '@/lib/api/admin' +import { useAdminNotifications } from '@/contexts/AdminNotificationContext' interface AdminNotificationsPanelProps { open: boolean onOpenChange: (open: boolean) => void - notifications: AdminNotification[] - onNotificationRead: (id: string) => Promise } export function AdminNotificationsPanel({ open, - onOpenChange, - notifications, - onNotificationRead + onOpenChange }: AdminNotificationsPanelProps) { const [markingAsRead, setMarkingAsRead] = useState(null) + const { + notifications, + unreadCount, + isConnected, + markAsRead, + markAllAsRead, + clearAll + } = useAdminNotifications() const handleMarkAsRead = async (id: string) => { try { setMarkingAsRead(id) - await onNotificationRead(id) + await markAsRead(id) } catch (error) { console.error('Error marking notification as read:', error) } finally { @@ -49,6 +53,22 @@ export function AdminNotificationsPanel({ } } + const handleMarkAllAsRead = async () => { + try { + await markAllAsRead() + } catch (error) { + console.error('Error marking all notifications as read:', error) + } + } + + const handleClearAll = async () => { + try { + await clearAll() + } catch (error) { + console.error('Error clearing all notifications:', error) + } + } + const getNotificationIcon = (type: string) => { switch (type) { case 'new_feature': @@ -81,14 +101,27 @@ export function AdminNotificationsPanel({ Admin Notifications - {unreadNotifications.length > 0 && ( + {unreadCount > 0 && ( - {unreadNotifications.length} + {unreadCount} )} +
+ {isConnected ? ( +
+ + Live +
+ ) : ( +
+ + Offline +
+ )} +
- System notifications and feature review updates + System notifications and feature review updates • Real-time updates {isConnected ? 'enabled' : 'disabled'} @@ -187,7 +220,7 @@ export function AdminNotificationsPanel({

No notifications

- You're all caught up! New notifications will appear here. + You're all caught up! New notifications will appear here.

)} @@ -203,14 +236,20 @@ export function AdminNotificationsPanel({ +
)} diff --git a/src/components/admin/admin-templates-list.tsx b/src/components/admin/admin-templates-list.tsx new file mode 100644 index 0000000..1446803 --- /dev/null +++ b/src/components/admin/admin-templates-list.tsx @@ -0,0 +1,657 @@ +"use client" + +import { useState, useEffect } from 'react' +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { Button } from '@/components/ui/button' +import { Badge } from '@/components/ui/badge' +import { Input } from '@/components/ui/input' +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog' +import { + Search, + Settings, + RefreshCw, + AlertCircle, + Zap, + Globe, + BarChart3, + Code, + ShoppingCart, + Briefcase, + GraduationCap, + Plus, + Save, + X +} from 'lucide-react' +import { adminApi, formatDate, getComplexityColor } from '@/lib/api/admin' +import { AdminTemplate, AdminStats } from '@/types/admin.types' +import { AdminFeatureSelection } from './admin-feature-selection' + +interface AdminTemplatesListProps { + onTemplateSelect?: (template: AdminTemplate) => void +} + +export function AdminTemplatesList({ onTemplateSelect }: AdminTemplatesListProps) { + const [templates, setTemplates] = useState([]) + const [stats, setStats] = useState(null) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + const [searchQuery, setSearchQuery] = useState('') + const [categoryFilter, setCategoryFilter] = useState('all') + const [showFeaturesManager, setShowFeaturesManager] = useState(false) + const [selectedTemplate, setSelectedTemplate] = useState(null) + const [showFeatureSelection, setShowFeatureSelection] = useState(false) + const [showCreateModal, setShowCreateModal] = useState(false) + + // Create template form state + const [newTemplate, setNewTemplate] = useState({ + type: '', + title: '', + description: '', + category: '', + icon: '', + gradient: '', + border: '', + text: '', + subtext: '' + }) + + const categories = [ + "Food Delivery", + "E-commerce", + "SaaS Platform", + "Mobile App", + "Dashboard", + "CRM System", + "Learning Platform", + "Healthcare", + "Real Estate", + "Travel", + "Entertainment", + "Finance", + "Social Media", + "Marketplace", + "Other" + ] + + // Load templates and stats + const loadData = async () => { + try { + setLoading(true) + setError(null) + + console.log('Loading admin templates data...') + + const [templatesResponse, statsResponse] = await Promise.all([ + adminApi.getAdminTemplates(50, 0, categoryFilter, searchQuery), + adminApi.getAdminTemplateStats() + ]) + + console.log('Admin templates response:', templatesResponse) + console.log('Admin template stats response:', statsResponse) + + setTemplates(templatesResponse || []) + setStats(statsResponse) + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to load admin templates') + console.error('Error loading admin templates:', err) + } finally { + setLoading(false) + } + } + + useEffect(() => { + loadData() + }, [categoryFilter, searchQuery]) + + // Handle template selection for features + const handleManageFeatures = (template: AdminTemplate) => { + // Convert AdminTemplate to Template format for feature management + const templateForFeatures = { + id: template.id, + title: template.title, + description: template.description, + type: template.type, + category: template.category, + icon: template.icon, + gradient: template.gradient, + border: template.border, + text: template.text, + subtext: template.subtext + } + setSelectedTemplate(templateForFeatures) + setShowFeatureSelection(true) + } + + // Handle create template form submission + const handleCreateTemplate = async (e: React.FormEvent) => { + e.preventDefault() + try { + setLoading(true) + + // Create template payload + const templateData = { + ...newTemplate, + created_at: new Date().toISOString(), + updated_at: new Date().toISOString() + } + + // Call API to create template (you'll need to implement this endpoint) + const response = await fetch('/api/templates', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(templateData) + }) + + if (!response.ok) { + throw new Error('Failed to create template') + } + + // Reset form and hide it + setNewTemplate({ + type: '', + title: '', + description: '', + category: '', + icon: '', + gradient: '', + border: '', + text: '', + subtext: '' + }) + setShowCreateModal(false) + + // Reload data to show new template + await loadData() + + } catch (error) { + console.error('Error creating template:', error) + setError(error instanceof Error ? error.message : 'Failed to create template') + } finally { + setLoading(false) + } + } + + // Get category icon + const getCategoryIcon = (category: string) => { + switch (category.toLowerCase()) { + case 'marketing': + return Zap + case 'software': + return Code + case 'seo': + return BarChart3 + case 'ecommerce': + return ShoppingCart + case 'portfolio': + return Briefcase + case 'business': + return Briefcase + case 'education': + return GraduationCap + default: + return Globe + } + } + + // Get category stats for filters + const getCategoryStats = () => { + const categoryStats = [ + { id: 'all', name: 'All Templates', count: templates.length, icon: Globe }, + { id: 'marketing', name: 'Marketing', count: 0, icon: Zap }, + { id: 'software', name: 'Software', count: 0, icon: Code }, + { id: 'seo', name: 'SEO', count: 0, icon: BarChart3 }, + { id: 'ecommerce', name: 'E-commerce', count: 0, icon: ShoppingCart }, + { id: 'portfolio', name: 'Portfolio', count: 0, icon: Briefcase }, + { id: 'business', name: 'Business', count: 0, icon: Briefcase }, + { id: 'education', name: 'Education', count: 0, icon: GraduationCap } + ] + + // Count templates by category + templates.forEach(template => { + const categoryId = template.category?.toLowerCase() + const categoryItem = categoryStats.find(cat => cat.id === categoryId) + if (categoryItem) { + categoryItem.count++ + } + }) + + return categoryStats + } + + // Filter templates based on search + const filteredTemplates = templates.filter(template => { + const matchesSearch = !searchQuery || + template.title?.toLowerCase().includes(searchQuery.toLowerCase()) || + template.description?.toLowerCase().includes(searchQuery.toLowerCase()) || + template.type?.toLowerCase().includes(searchQuery.toLowerCase()) + + return matchesSearch + }) + + const TemplateCard = ({ template }: { template: AdminTemplate }) => ( + + +
+
+ + {template.title} + +
+ + {template.type} + + + {template.category} + +
+
+ +
+ {template.description && ( +

{template.description}

+ )} +
+ + +
+
+ + {(template as any).feature_count || 0} features +
+
+ {template.created_at && formatDate(template.created_at)} +
+
+ + {template.gradient && ( +
+ Style: {template.gradient} +
+ )} +
+
+ ) + + if (loading) { + return ( +
+
+ + Loading admin templates... +
+
+ ) + } + + if (error) { + return ( +
+ + +
+ + Error loading admin templates +
+

{error}

+ +
+
+
+ ) + } + + // Show feature selection view if a template is selected + if (showFeatureSelection && selectedTemplate) { + return ( + { + setShowFeatureSelection(false) + setSelectedTemplate(null) + }} + /> + ) + } + + return ( +
+ {/* Header */} +
+
+

Admin Templates

+

Manage features for your templates

+
+
+ + +
+
+ + {/* Stats Cards */} + {stats && ( +
+ + + Total Templates + + + +
{(stats as any).total_templates || templates.length}
+
+
+ + + + Categories + + + +
{(stats as any).total_categories || categories.length - 1}
+
+
+ + + + Avg Features + + + +
+ {Math.round((stats as any).avg_features_per_template || 0)} +
+
+
+ + + + With Features + + + +
{(stats as any).templates_with_features || 0}
+
+
+
+ )} + + {/* Filters */} +
+
+
+ + setSearchQuery(e.target.value)} + className="pl-10 bg-white/5 border-white/10 text-white" + /> +
+
+ +
+ + {/* Category Filters */} +
+ {getCategoryStats().map((category) => { + const Icon = category.icon + const active = categoryFilter === category.id + return ( + + ) + })} +
+ + {/* Create Template Modal */} + + + + Create New Template + + +
+
+
+ + setNewTemplate(prev => ({ ...prev, type: e.target.value }))} + className="bg-white/5 border-white/10 text-white placeholder:text-white/40" + required + /> +

Unique identifier for the template

+
+ +
+ + setNewTemplate(prev => ({ ...prev, title: e.target.value }))} + className="bg-white/5 border-white/10 text-white placeholder:text-white/40" + required + /> +
+
+ +
+ +