Compare commits
3 Commits
632302fb54
...
b051d9d2ef
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b051d9d2ef | ||
|
|
7d84d35f09 | ||
|
|
59eb32b45a |
25
app/api/contacts/route.ts
Normal file
25
app/api/contacts/route.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { prisma } from '@/lib/prisma';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
// In a real app, you would check for admin session here
|
||||
// For now, we trust the 'x-admin-request' header if it's set by the server-side component or middleware
|
||||
// but typically you'd verify the session cookie/token
|
||||
|
||||
const contacts = await prisma.contact.findMany({
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
take: 100,
|
||||
});
|
||||
|
||||
return NextResponse.json({ contacts });
|
||||
} catch (error) {
|
||||
console.error('Error fetching contacts:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch contacts' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -221,11 +221,11 @@ export default function ChatWidget() {
|
||||
setIsOpen(true);
|
||||
}
|
||||
}}
|
||||
className="fixed bottom-4 left-4 md:bottom-6 md:left-6 z-30 bg-stone-800/80 backdrop-blur-md text-stone-50 p-3.5 rounded-full shadow-[0_8px_20px_rgba(41,37,36,0.2)] hover:bg-stone-800 hover:scale-105 transition-all duration-300 group cursor-pointer border border-white/10 ring-1 ring-white/20"
|
||||
className="fixed bottom-4 left-4 md:bottom-6 md:left-6 z-30 bg-[#292524] text-[#fdfcf8] p-3.5 rounded-full shadow-[0_8px_20px_rgba(41,37,36,0.25)] hover:bg-[#44403c] hover:scale-105 transition-all duration-300 group cursor-pointer border border-[#f3f1e7]/20 ring-1 ring-[#f3f1e7]/10"
|
||||
aria-label="Open chat"
|
||||
>
|
||||
<MessageCircle size={24} />
|
||||
<span className="absolute top-0 right-0 w-3 h-3 bg-stone-400 rounded-full animate-pulse shadow-sm border-2 border-stone-800" />
|
||||
<span className="absolute top-0 right-0 w-3 h-3 bg-green-500 rounded-full animate-pulse shadow-sm border-2 border-[#292524]" />
|
||||
|
||||
{/* Tooltip */}
|
||||
<span className="absolute bottom-full left-1/2 -translate-x-1/2 mb-3 px-3 py-1.5 bg-stone-900/90 text-stone-50 text-xs font-medium rounded-lg opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none z-[100] shadow-xl backdrop-blur-sm">
|
||||
@@ -244,16 +244,16 @@ export default function ChatWidget() {
|
||||
animate={{ opacity: 1, y: 0, scale: 1, filter: "blur(0px)" }}
|
||||
exit={{ opacity: 0, y: 20, scale: 0.95, filter: "blur(10px)" }}
|
||||
transition={{ type: "spring", damping: 30, stiffness: 400 }}
|
||||
className="fixed bottom-20 left-4 right-4 md:bottom-24 md:left-6 md:right-auto z-30 md:w-[380px] h-[60vh] md:h-[550px] max-h-[600px] bg-[#fdfcf8]/30 backdrop-blur-2xl saturate-150 rounded-2xl shadow-[0_12px_40px_rgba(41,37,36,0.15)] flex flex-col overflow-hidden border border-white/40 ring-1 ring-white/50"
|
||||
className="fixed bottom-20 left-4 right-4 md:bottom-24 md:left-6 md:right-auto z-30 md:w-[380px] h-[60vh] md:h-[550px] max-h-[600px] bg-[#fdfcf8]/95 backdrop-blur-xl saturate-100 rounded-2xl shadow-[0_12px_40px_rgba(41,37,36,0.2)] flex flex-col overflow-hidden border border-[#e7e5e4] ring-1 ring-[#f3f1e7]"
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="bg-white/20 backdrop-blur-md text-stone-800 p-4 flex items-center justify-between border-b border-white/20">
|
||||
<div className="bg-[#fdfcf8] text-[#292524] p-4 flex items-center justify-between border-b border-[#e7e5e4]">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="relative">
|
||||
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-stone-100/80 to-white/80 flex items-center justify-center ring-1 ring-white shadow-sm backdrop-blur-sm">
|
||||
<Sparkles size={18} className="text-stone-600" />
|
||||
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-[#f3f1e7] to-[#fdfcf8] flex items-center justify-center ring-1 ring-[#e7e5e4] shadow-sm">
|
||||
<Sparkles size={18} className="text-[#57534e]" />
|
||||
</div>
|
||||
<span className="absolute bottom-0 right-0 w-2.5 h-2.5 bg-emerald-400 rounded-full border-2 border-white shadow-sm" />
|
||||
<span className="absolute bottom-0 right-0 w-2.5 h-2.5 bg-green-500 rounded-full border-2 border-[#fdfcf8] shadow-sm" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<h3 className="font-bold text-sm truncate text-stone-900 tracking-tight">
|
||||
@@ -293,14 +293,14 @@ export default function ChatWidget() {
|
||||
className={`flex ${message.sender === "user" ? "justify-end" : "justify-start"}`}
|
||||
>
|
||||
<div
|
||||
className={`max-w-[85%] rounded-2xl px-4 py-3 backdrop-blur-md shadow-sm ${
|
||||
className={`max-w-[85%] rounded-2xl px-4 py-3 shadow-sm ${
|
||||
message.sender === "user"
|
||||
? "bg-stone-800/90 text-stone-50 ring-1 ring-white/10"
|
||||
: "bg-white/70 text-stone-800 border border-white/50 ring-1 ring-white/40"
|
||||
? "bg-[#292524] text-[#fdfcf8]"
|
||||
: "bg-[#f3f1e7] text-[#292524] border border-[#e7e5e4]"
|
||||
}`}
|
||||
>
|
||||
<p className={`text-sm whitespace-pre-wrap break-words leading-relaxed ${
|
||||
message.sender === "user" ? "text-stone-50 font-light" : "text-stone-800 font-medium"
|
||||
message.sender === "user" ? "text-[#fdfcf8]/90 font-light" : "text-[#292524] font-medium"
|
||||
}`}>
|
||||
{message.text}
|
||||
</p>
|
||||
@@ -327,7 +327,7 @@ export default function ChatWidget() {
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="flex justify-start"
|
||||
>
|
||||
<div className="bg-white/60 backdrop-blur-md border border-white/40 rounded-2xl px-4 py-3 shadow-sm ring-1 ring-white/40">
|
||||
<div className="bg-[#f3f1e7] border border-[#e7e5e4] rounded-2xl px-4 py-3 shadow-sm">
|
||||
<div className="flex gap-1.5">
|
||||
<motion.div
|
||||
className="w-1.5 h-1.5 bg-stone-500 rounded-full"
|
||||
@@ -365,7 +365,7 @@ export default function ChatWidget() {
|
||||
</div>
|
||||
|
||||
{/* Input */}
|
||||
<div className="p-4 bg-white/20 backdrop-blur-xl border-t border-white/20">
|
||||
<div className="p-4 bg-[#fdfcf8] border-t border-[#e7e5e4]">
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
ref={inputRef}
|
||||
@@ -375,12 +375,12 @@ export default function ChatWidget() {
|
||||
onKeyPress={handleKeyPress}
|
||||
placeholder="Ask anything..."
|
||||
disabled={isLoading}
|
||||
className="flex-1 px-4 py-3 text-sm bg-white/50 backdrop-blur-sm text-stone-800 rounded-xl border border-white/40 focus:outline-none focus:ring-2 focus:ring-stone-200/50 focus:border-stone-400/50 focus:bg-white/70 disabled:opacity-50 disabled:cursor-not-allowed placeholder:text-stone-500 transition-all shadow-inner"
|
||||
className="flex-1 px-4 py-3 text-sm bg-[#f5f5f4] text-[#292524] rounded-xl border border-[#e7e5e4] focus:outline-none focus:ring-2 focus:ring-[#e7e5e4] focus:border-[#a8a29e] focus:bg-[#fdfcf8] disabled:opacity-50 disabled:cursor-not-allowed placeholder:text-[#78716c] transition-all shadow-inner"
|
||||
/>
|
||||
<button
|
||||
onClick={handleSend}
|
||||
disabled={!inputValue.trim() || isLoading}
|
||||
className="p-3 bg-stone-800 text-stone-50 rounded-xl hover:bg-stone-700 hover:shadow-lg hover:scale-105 transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100 shadow-md flex items-center justify-center aspect-square"
|
||||
className="p-3 bg-[#292524] text-[#fdfcf8] rounded-xl hover:bg-[#44403c] hover:shadow-lg hover:scale-105 transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100 shadow-md flex items-center justify-center aspect-square"
|
||||
aria-label="Send message"
|
||||
>
|
||||
{isLoading ? (
|
||||
@@ -405,7 +405,7 @@ export default function ChatWidget() {
|
||||
inputRef.current?.focus();
|
||||
}}
|
||||
disabled={isLoading}
|
||||
className="px-3 py-1.5 text-xs font-medium bg-white/40 backdrop-blur-md text-stone-700 rounded-lg hover:bg-white/60 hover:text-stone-900 border border-white/40 transition-all whitespace-nowrap disabled:opacity-50 flex-shrink-0 shadow-sm"
|
||||
className="px-3 py-1.5 text-xs font-medium bg-[#f5f5f4] text-[#57534e] rounded-lg hover:bg-[#e7e5e4] hover:text-[#292524] border border-[#e7e5e4] transition-all whitespace-nowrap disabled:opacity-50 flex-shrink-0 shadow-sm"
|
||||
>
|
||||
{suggestion}
|
||||
</button>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { motion, Variants } from "framer-motion";
|
||||
import { ExternalLink, Github, Layers, ArrowRight } from "lucide-react";
|
||||
import { ExternalLink, Github, Layers, ArrowRight, ArrowLeft, Calendar } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
|
||||
@@ -98,11 +98,8 @@ const Projects = () => {
|
||||
<motion.div
|
||||
key={project.id}
|
||||
variants={fadeInUp}
|
||||
whileHover={{
|
||||
y: -8,
|
||||
transition: { duration: 0.4, ease: "easeOut" },
|
||||
}}
|
||||
className="group relative flex flex-col bg-white/40 backdrop-blur-xl backdrop-saturate-150 rounded-2xl overflow-hidden shadow-[0_4px_20px_rgba(0,0,0,0.02)] hover:shadow-[0_20px_40px_rgba(0,0,0,0.06)] transition-all duration-500 ease-out border border-white/50 ring-1 ring-white/30"
|
||||
whileHover={{ y: -8 }}
|
||||
className="group flex flex-col bg-white/40 backdrop-blur-xl rounded-2xl overflow-hidden border border-white/60 shadow-[0_4px_20px_rgba(0,0,0,0.02)] hover:shadow-[0_20px_40px_rgba(0,0,0,0.06)] transition-all duration-500"
|
||||
>
|
||||
{/* Project Cover / Image Area */}
|
||||
<div className="relative aspect-[16/10] overflow-hidden bg-stone-100">
|
||||
@@ -114,12 +111,10 @@ const Projects = () => {
|
||||
fill
|
||||
className="object-cover transition-transform duration-1000 ease-out group-hover:scale-110"
|
||||
/>
|
||||
{/* Subtle Overlay for better text readability and depth */}
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-stone-900/20 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
|
||||
</>
|
||||
) : (
|
||||
<div className="absolute inset-0 bg-stone-200 flex items-center justify-center overflow-hidden">
|
||||
{/* Mesh Gradient Fallback */}
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-stone-300 via-stone-200 to-stone-300" />
|
||||
<div className="absolute top-[-20%] left-[-10%] w-[70%] h-[70%] bg-white/20 rounded-full blur-3xl animate-pulse" />
|
||||
<div className="absolute bottom-[-10%] right-[-5%] w-[60%] h-[60%] bg-stone-400/10 rounded-full blur-2xl" />
|
||||
@@ -141,32 +136,34 @@ const Projects = () => {
|
||||
{/* Featured Badge */}
|
||||
{project.featured && (
|
||||
<div className="absolute top-3 left-3 z-20">
|
||||
<div className="px-3 py-1 bg-red-500 text-white text-[10px] font-black uppercase tracking-widest rounded-full shadow-lg border border-red-400/50">
|
||||
<div className="px-3 py-1 bg-[#292524]/80 backdrop-blur-md text-[#fdfcf8] text-[10px] font-bold uppercase tracking-widest rounded-full shadow-sm border border-white/10">
|
||||
Featured
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Overlay Links */}
|
||||
<div className="absolute inset-0 bg-stone-900/40 opacity-0 group-hover:opacity-100 transition-opacity duration-500 ease-out flex items-center justify-center gap-4 backdrop-blur-[2px] z-10">
|
||||
<div className="absolute inset-0 bg-stone-900/40 opacity-0 group-hover:opacity-100 transition-opacity duration-500 ease-out flex items-center justify-center gap-4 backdrop-blur-[2px] z-20 pointer-events-none">
|
||||
{project.github && (
|
||||
<a
|
||||
href={project.github}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="p-3 bg-white text-stone-900 rounded-full hover:scale-110 transition-all duration-300 shadow-xl border border-white/50"
|
||||
className="p-3 bg-white text-stone-900 rounded-full hover:scale-110 transition-all duration-300 shadow-xl border border-white/50 pointer-events-auto"
|
||||
aria-label="GitHub"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Github size={20} />
|
||||
</a>
|
||||
)}
|
||||
{project.live && (
|
||||
{project.live && !project.title.toLowerCase().includes('kernel panic') && (
|
||||
<a
|
||||
href={project.live}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="p-3 bg-white text-stone-900 rounded-full hover:scale-110 transition-all duration-300 shadow-xl border border-white/50"
|
||||
className="p-3 bg-white text-stone-900 rounded-full hover:scale-110 transition-all duration-300 shadow-xl border border-white/50 pointer-events-auto"
|
||||
aria-label="Live Demo"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<ExternalLink size={20} />
|
||||
</a>
|
||||
@@ -175,47 +172,67 @@ const Projects = () => {
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex flex-col flex-1 p-6">
|
||||
<div className="flex justify-between items-start mb-3">
|
||||
<h3 className="text-xl font-bold text-stone-900 group-hover:text-stone-700 transition-colors duration-300 tracking-tight">
|
||||
<div className="p-6 flex flex-col flex-1">
|
||||
{/* Stretched Link covering the whole card (including image area) */}
|
||||
<Link
|
||||
href={`/projects/${project.title.toLowerCase().replace(/[^a-z0-9]+/g, '-')}`}
|
||||
className="absolute inset-0 z-10"
|
||||
aria-label={`View project ${project.title}`}
|
||||
/>
|
||||
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="text-xl font-bold text-stone-900 group-hover:text-stone-600 transition-colors">
|
||||
{project.title}
|
||||
</h3>
|
||||
<span className="text-xs font-mono text-stone-500 bg-white/60 border border-stone-200/50 px-2 py-1 rounded backdrop-blur-sm">
|
||||
{new Date(project.date).getFullYear()}
|
||||
</span>
|
||||
<div className="flex items-center space-x-2 text-stone-400 text-xs font-mono bg-white/50 px-2 py-1 rounded border border-stone-100">
|
||||
<Calendar size={12} />
|
||||
<span>{new Date(project.date).getFullYear()}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-stone-700 font-medium text-sm leading-relaxed mb-6 line-clamp-3 flex-1 opacity-90">
|
||||
<p className="text-stone-600 mb-6 leading-relaxed line-clamp-3 text-sm flex-1">
|
||||
{project.description}
|
||||
</p>
|
||||
|
||||
<div className="space-y-4 mt-auto">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{project.tags.slice(0, 3).map((tag, tIdx) => (
|
||||
<span
|
||||
key={`${project.id}-${tag}-${tIdx}`}
|
||||
className="text-xs px-2.5 py-1 bg-white/50 border border-white/60 rounded-md text-stone-700 font-medium hover:bg-white/80 hover:border-white transition-all duration-300 ease-out shadow-sm"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
{project.tags.length > 3 && (
|
||||
<span className="text-xs px-2 py-1 text-stone-500 font-medium">
|
||||
+ {project.tags.length - 3}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2 mb-6">
|
||||
{project.tags.slice(0, 4).map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="px-2.5 py-1 bg-white/60 border border-stone-100 text-stone-600 text-xs font-medium rounded-md"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
{project.tags.length > 4 && (
|
||||
<span className="px-2 py-1 text-stone-400 text-xs">+ {project.tags.length - 4}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Link
|
||||
href={`/projects/${project.title.toLowerCase().replace(/\s+/g, "-")}`}
|
||||
className="inline-flex items-center text-sm font-bold text-stone-800 hover:gap-3 transition-all duration-300 ease-out group/link"
|
||||
>
|
||||
Read more{" "}
|
||||
<ArrowRight
|
||||
size={16}
|
||||
className="ml-1 transition-transform duration-300 ease-out group-hover/link:translate-x-1"
|
||||
/>
|
||||
</Link>
|
||||
<div className="mt-auto pt-4 border-t border-stone-100 flex items-center justify-between relative z-20">
|
||||
<div className="flex gap-3">
|
||||
{project.github && (
|
||||
<a
|
||||
href={project.github}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-stone-400 hover:text-stone-900 transition-colors relative z-20 hover:scale-110"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Github size={18} />
|
||||
</a>
|
||||
)}
|
||||
{project.live && !project.title.toLowerCase().includes('kernel panic') && (
|
||||
<a
|
||||
href={project.live}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-stone-400 hover:text-stone-900 transition-colors relative z-20 hover:scale-110"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<ExternalLink size={18} />
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
@@ -80,7 +80,7 @@ html {
|
||||
0 20px 25px -5px rgba(0, 0, 0, 0.08),
|
||||
0 10px 10px -5px rgba(0, 0, 0, 0.02),
|
||||
inset 0 0 20px rgba(255, 255, 255, 0.8);
|
||||
transform: translateY(-4px) scale(1.005);
|
||||
transform: translateY(-4px);
|
||||
border-color: #ffffff;
|
||||
}
|
||||
|
||||
@@ -103,9 +103,6 @@ div {
|
||||
color: #44403c;
|
||||
}
|
||||
|
||||
/* Utility for the liquid melt effect container */
|
||||
/* Liquid container removed - no filters applied */
|
||||
|
||||
/* Hide scrollbar but keep functionality */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
@@ -145,18 +142,6 @@ div {
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
@keyframes liquid-pulse {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* Liquid Blobs Background */
|
||||
.liquid-bg-blob {
|
||||
position: absolute;
|
||||
@@ -188,3 +173,43 @@ div {
|
||||
.markdown pre {
|
||||
@apply bg-stone-900 text-stone-50 p-4 rounded-xl overflow-x-auto mb-6;
|
||||
}
|
||||
|
||||
/* Admin Dashboard Styles - Organic Modern */
|
||||
.animated-bg {
|
||||
background: #fdfcf8;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.admin-glass {
|
||||
background: rgba(253, 252, 248, 0.9);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border-bottom: 1px solid #e7e5e4;
|
||||
color: #292524;
|
||||
}
|
||||
|
||||
.admin-glass-light {
|
||||
background: #ffffff;
|
||||
border: 1px solid #e7e5e4;
|
||||
color: #292524;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
.admin-glass-light:hover {
|
||||
background: #fdfcf8;
|
||||
border-color: #d6d3d1;
|
||||
box-shadow: 0 4px 6px rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
.admin-glass-card {
|
||||
background: #ffffff;
|
||||
border: 1px solid #e7e5e4;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05);
|
||||
color: #292524;
|
||||
}
|
||||
|
||||
@@ -231,10 +231,10 @@ const AdminPage = () => {
|
||||
// Loading state
|
||||
if (authState.isLoading) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<div className="min-h-screen flex items-center justify-center bg-[#fdfcf8]">
|
||||
<div className="text-center">
|
||||
<Loader2 className="w-8 h-8 animate-spin mx-auto mb-4 text-blue-500" />
|
||||
<p className="text-white">Loading...</p>
|
||||
<Loader2 className="w-8 h-8 animate-spin mx-auto mb-4 text-stone-600" />
|
||||
<p className="text-stone-500">Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -243,17 +243,19 @@ const AdminPage = () => {
|
||||
// Lockout state
|
||||
if (authState.isLocked) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<div className="min-h-screen flex items-center justify-center bg-[#fdfcf8]">
|
||||
<div className="text-center">
|
||||
<Lock className="w-16 h-16 mx-auto mb-4 text-red-500" />
|
||||
<h2 className="text-2xl font-bold text-white mb-2">Account Locked</h2>
|
||||
<p className="text-white/60">Too many failed attempts. Please try again in 15 minutes.</p>
|
||||
<div className="w-16 h-16 bg-red-50 rounded-2xl flex items-center justify-center mx-auto mb-6">
|
||||
<Lock className="w-8 h-8 text-red-500" />
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-stone-900 mb-2">Account Locked</h2>
|
||||
<p className="text-stone-500">Too many failed attempts. Please try again in 15 minutes.</p>
|
||||
<button
|
||||
onClick={() => {
|
||||
localStorage.removeItem('admin_lockout');
|
||||
window.location.reload();
|
||||
}}
|
||||
className="mt-4 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
|
||||
className="mt-4 px-6 py-2 bg-stone-900 text-stone-50 rounded-xl hover:bg-stone-800 transition-colors"
|
||||
>
|
||||
Try Again
|
||||
</button>
|
||||
@@ -265,22 +267,23 @@ const AdminPage = () => {
|
||||
// Login form
|
||||
if (authState.showLogin || !authState.isAuthenticated) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<div className="min-h-screen flex items-center justify-center relative overflow-hidden bg-[#fdfcf8] z-0">
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
className="w-full max-w-md p-8"
|
||||
className="w-full max-w-md p-6"
|
||||
>
|
||||
<div className="bg-white/10 backdrop-blur-lg rounded-2xl p-8 border border-white/20">
|
||||
<div className="bg-white/80 backdrop-blur-xl rounded-3xl p-8 border border-stone-200 shadow-2xl relative z-10">
|
||||
<div className="text-center mb-8">
|
||||
<div className="w-16 h-16 bg-gradient-to-r from-blue-500 to-purple-500 rounded-2xl flex items-center justify-center mx-auto mb-6 shadow-lg">
|
||||
<Lock className="w-8 h-8 text-white" />
|
||||
<div className="w-16 h-16 bg-[#f3f1e7] rounded-2xl flex items-center justify-center mx-auto mb-6 shadow-sm border border-stone-100">
|
||||
<Lock className="w-6 h-6 text-stone-600" />
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-white mb-2">Admin Access</h1>
|
||||
<p className="text-white/60">Enter your password to continue</p>
|
||||
<h1 className="text-2xl font-bold text-stone-900 mb-2 tracking-tight">Admin Access</h1>
|
||||
<p className="text-stone-500">Enter your password to continue</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleLogin} className="space-y-6">
|
||||
<form onSubmit={handleLogin} className="space-y-5">
|
||||
<div>
|
||||
<div className="relative">
|
||||
<input
|
||||
@@ -288,37 +291,41 @@ const AdminPage = () => {
|
||||
value={authState.password}
|
||||
onChange={(e) => setAuthState(prev => ({ ...prev, password: e.target.value }))}
|
||||
placeholder="Enter password"
|
||||
className="w-full px-4 py-3 bg-white/10 border border-white/20 rounded-xl text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
className="w-full px-4 py-3.5 bg-white border border-stone-200 rounded-xl text-stone-900 placeholder:text-stone-400 focus:outline-none focus:ring-2 focus:ring-stone-200 focus:border-stone-400 transition-all shadow-sm"
|
||||
disabled={authState.isLoading}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setAuthState(prev => ({ ...prev, showPassword: !prev.showPassword }))}
|
||||
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-white/50 hover:text-white"
|
||||
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-stone-400 hover:text-stone-600 p-1"
|
||||
>
|
||||
{authState.showPassword ? '👁️' : '👁️🗨️'}
|
||||
</button>
|
||||
</div>
|
||||
{authState.error && (
|
||||
<p className="mt-2 text-red-400 text-sm">{authState.error}</p>
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: -5 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="mt-2 text-red-500 text-sm font-medium flex items-center"
|
||||
>
|
||||
<span className="w-1.5 h-1.5 bg-red-500 rounded-full mr-2" />
|
||||
{authState.error}
|
||||
</motion.p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={authState.isLoading || !authState.password}
|
||||
className="w-full bg-gradient-to-r from-blue-500 to-purple-500 text-white py-4 px-6 rounded-xl font-semibold text-lg hover:from-blue-600 hover:to-purple-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:ring-offset-transparent disabled:opacity-50 disabled:cursor-not-allowed transition-all transform hover:scale-[1.02] active:scale-[0.98] shadow-lg"
|
||||
className="w-full bg-stone-900 text-stone-50 py-3.5 px-6 rounded-xl font-semibold text-lg hover:bg-stone-800 focus:outline-none focus:ring-2 focus:ring-stone-200 focus:ring-offset-2 focus:ring-offset-white disabled:opacity-50 disabled:cursor-not-allowed transition-all shadow-lg flex items-center justify-center"
|
||||
>
|
||||
{authState.isLoading ? (
|
||||
<div className="flex items-center justify-center space-x-3">
|
||||
<div className="flex items-center justify-center space-x-2">
|
||||
<Loader2 className="w-5 h-5 animate-spin" />
|
||||
<span>Authenticating...</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-center space-x-2">
|
||||
<Lock size={18} />
|
||||
<span>Secure Login</span>
|
||||
</div>
|
||||
<span>Sign In</span>
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
@@ -184,32 +184,34 @@ const ProjectsPage = () => {
|
||||
|
||||
{project.featured && (
|
||||
<div className="absolute top-3 left-3 z-20">
|
||||
<div className="px-3 py-1 bg-red-500 text-white text-[10px] font-black uppercase tracking-widest rounded-full shadow-lg border border-red-400/50">
|
||||
<div className="px-3 py-1 bg-[#292524]/80 backdrop-blur-md text-[#fdfcf8] text-[10px] font-bold uppercase tracking-widest rounded-full shadow-sm border border-white/10">
|
||||
Featured
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Overlay Links */}
|
||||
<div className="absolute inset-0 bg-stone-900/40 opacity-0 group-hover:opacity-100 transition-opacity duration-500 ease-out flex items-center justify-center gap-4 backdrop-blur-[2px] z-10">
|
||||
<div className="absolute inset-0 bg-stone-900/40 opacity-0 group-hover:opacity-100 transition-opacity duration-500 ease-out flex items-center justify-center gap-4 backdrop-blur-[2px] z-20 pointer-events-none">
|
||||
{project.github && (
|
||||
<a
|
||||
href={project.github}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="p-3 bg-white text-stone-900 rounded-full hover:scale-110 transition-all duration-300 shadow-xl border border-white/50"
|
||||
className="p-3 bg-white text-stone-900 rounded-full hover:scale-110 transition-all duration-300 shadow-xl border border-white/50 pointer-events-auto"
|
||||
aria-label="GitHub"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Github size={20} />
|
||||
</a>
|
||||
)}
|
||||
{project.live && (
|
||||
{project.live && !project.title.toLowerCase().includes('kernel panic') && (
|
||||
<a
|
||||
href={project.live}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="p-3 bg-white text-stone-900 rounded-full hover:scale-110 transition-all duration-300 shadow-xl border border-white/50"
|
||||
className="p-3 bg-white text-stone-900 rounded-full hover:scale-110 transition-all duration-300 shadow-xl border border-white/50 pointer-events-auto"
|
||||
aria-label="Live Demo"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<ExternalLink size={20} />
|
||||
</a>
|
||||
@@ -218,6 +220,13 @@ const ProjectsPage = () => {
|
||||
</div>
|
||||
|
||||
<div className="p-6 flex flex-col flex-1">
|
||||
{/* Stretched Link covering the whole card (including image area) */}
|
||||
<Link
|
||||
href={`/projects/${project.title.toLowerCase().replace(/[^a-z0-9]+/g, '-')}`}
|
||||
className="absolute inset-0 z-10"
|
||||
aria-label={`View project ${project.title}`}
|
||||
/>
|
||||
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="text-xl font-bold text-stone-900 group-hover:text-stone-600 transition-colors">
|
||||
{project.title}
|
||||
@@ -246,27 +255,31 @@ const ProjectsPage = () => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-auto pt-4 border-t border-stone-100 flex items-center justify-between">
|
||||
<div className="mt-auto pt-4 border-t border-stone-100 flex items-center justify-between relative z-20">
|
||||
<div className="flex gap-3">
|
||||
{project.github && (
|
||||
<a href={project.github} target="_blank" rel="noopener noreferrer" className="text-stone-400 hover:text-stone-900 transition-colors">
|
||||
<a
|
||||
href={project.github}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-stone-400 hover:text-stone-900 transition-colors relative z-20 hover:scale-110"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Github size={18} />
|
||||
</a>
|
||||
)}
|
||||
{project.live && (
|
||||
<a href={project.live} target="_blank" rel="noopener noreferrer" className="text-stone-400 hover:text-stone-900 transition-colors">
|
||||
{project.live && !project.title.toLowerCase().includes('kernel panic') && (
|
||||
<a
|
||||
href={project.live}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-stone-400 hover:text-stone-900 transition-colors relative z-20 hover:scale-110"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<ExternalLink size={18} />
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Link
|
||||
href={`/projects/${project.title.toLowerCase().replace(/[^a-z0-9]+/g, '-')}`}
|
||||
className="inline-flex items-center space-x-1 text-sm font-bold text-stone-800 hover:gap-2 transition-all"
|
||||
>
|
||||
<span>Read More</span>
|
||||
<ArrowLeft size={16} className="rotate-180" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
@@ -177,24 +177,24 @@ export function AnalyticsDashboard({ isAuthenticated }: AnalyticsDashboardProps)
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="admin-glass-card p-6 rounded-xl hover:scale-105 transition-all duration-200"
|
||||
className="bg-white border border-stone-200 p-6 rounded-xl hover:shadow-md transition-all duration-200"
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center space-x-3 mb-4">
|
||||
<div className={`p-3 rounded-xl ${color}`}>
|
||||
<Icon className="w-6 h-6 text-white" size={24} />
|
||||
<Icon className="w-6 h-6" size={24} />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-white/60 text-sm font-medium">{title}</p>
|
||||
{description && <p className="text-white/40 text-xs">{description}</p>}
|
||||
<p className="text-stone-500 text-sm font-medium">{title}</p>
|
||||
{description && <p className="text-stone-400 text-xs">{description}</p>}
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-3xl font-bold text-white mb-2">{value}</p>
|
||||
<p className="text-3xl font-bold text-stone-900 mb-2">{value}</p>
|
||||
{trend && trendValue && (
|
||||
<div className={`flex items-center space-x-1 text-sm ${
|
||||
trend === 'up' ? 'text-green-400' :
|
||||
trend === 'down' ? 'text-red-400' : 'text-yellow-400'
|
||||
trend === 'up' ? 'text-green-600' :
|
||||
trend === 'down' ? 'text-red-600' : 'text-yellow-600'
|
||||
}`}>
|
||||
<TrendingUp className={`w-4 h-4 ${trend === 'down' ? 'rotate-180' : ''}`} />
|
||||
<span>{trendValue}</span>
|
||||
@@ -207,21 +207,21 @@ export function AnalyticsDashboard({ isAuthenticated }: AnalyticsDashboardProps)
|
||||
|
||||
const getDifficultyColor = (difficulty: string) => {
|
||||
switch (difficulty) {
|
||||
case 'Beginner': return 'bg-green-500/30 text-green-400 border-green-500/40';
|
||||
case 'Intermediate': return 'bg-yellow-500/30 text-yellow-400 border-yellow-500/40';
|
||||
case 'Advanced': return 'bg-orange-500/30 text-orange-400 border-orange-500/40';
|
||||
case 'Expert': return 'bg-red-500/30 text-red-400 border-red-500/40';
|
||||
default: return 'bg-gray-500/30 text-gray-400 border-gray-500/40';
|
||||
case 'Beginner': return 'bg-green-50 text-green-700 border-green-200';
|
||||
case 'Intermediate': return 'bg-yellow-50 text-yellow-700 border-yellow-200';
|
||||
case 'Advanced': return 'bg-orange-50 text-orange-700 border-orange-200';
|
||||
case 'Expert': return 'bg-red-50 text-red-700 border-red-200';
|
||||
default: return 'bg-stone-50 text-stone-600 border-stone-200';
|
||||
}
|
||||
};
|
||||
|
||||
const getCategoryColor = (index: number) => {
|
||||
const colors = [
|
||||
'bg-blue-500/30 text-blue-400',
|
||||
'bg-purple-500/30 text-purple-400',
|
||||
'bg-green-500/30 text-green-400',
|
||||
'bg-pink-500/30 text-pink-400',
|
||||
'bg-indigo-500/30 text-indigo-400'
|
||||
'bg-stone-100 text-stone-700',
|
||||
'bg-stone-200 text-stone-800',
|
||||
'bg-stone-300 text-stone-900',
|
||||
'bg-stone-100 text-stone-700',
|
||||
'bg-stone-200 text-stone-800'
|
||||
];
|
||||
return colors[index % colors.length];
|
||||
};
|
||||
@@ -233,23 +233,23 @@ export function AnalyticsDashboard({ isAuthenticated }: AnalyticsDashboardProps)
|
||||
{/* Header */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-white flex items-center">
|
||||
<BarChart3 className="w-8 h-8 mr-3 text-blue-400" />
|
||||
<h1 className="text-3xl font-bold text-stone-900 flex items-center">
|
||||
<BarChart3 className="w-8 h-8 mr-3 text-stone-600" />
|
||||
Analytics Dashboard
|
||||
</h1>
|
||||
<p className="text-white/80 mt-2">Portfolio performance and user engagement metrics</p>
|
||||
<p className="text-stone-500 mt-2">Portfolio performance and user engagement metrics</p>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
{/* Time Range Selector */}
|
||||
<div className="flex items-center space-x-1 admin-glass-light rounded-xl p-1">
|
||||
<div className="flex items-center space-x-1 bg-white border border-stone-200 rounded-xl p-1">
|
||||
{(['7d', '30d', '90d', '1y'] as const).map((range) => (
|
||||
<button
|
||||
key={range}
|
||||
onClick={() => setTimeRange(range)}
|
||||
className={`px-3 py-2 rounded-lg text-sm font-medium transition-all duration-200 ${
|
||||
timeRange === range
|
||||
? 'bg-blue-500/40 text-blue-300 shadow-lg'
|
||||
: 'text-white/70 hover:text-white hover:bg-white/10'
|
||||
? 'bg-stone-100 text-stone-900 shadow-sm'
|
||||
: 'text-stone-500 hover:text-stone-800 hover:bg-stone-50'
|
||||
}`}
|
||||
>
|
||||
{range === '7d' ? '7 Days' : range === '30d' ? '30 Days' : range === '90d' ? '90 Days' : '1 Year'}
|
||||
@@ -259,15 +259,15 @@ export function AnalyticsDashboard({ isAuthenticated }: AnalyticsDashboardProps)
|
||||
<button
|
||||
onClick={fetchAnalyticsData}
|
||||
disabled={loading}
|
||||
className="flex items-center space-x-2 px-4 py-2 admin-glass-light rounded-xl hover:scale-105 transition-all duration-200 disabled:opacity-50"
|
||||
className="flex items-center space-x-2 px-4 py-2 bg-white border border-stone-200 rounded-xl hover:bg-stone-50 transition-all duration-200 disabled:opacity-50 text-stone-600"
|
||||
>
|
||||
<RefreshCw className={`w-4 h-4 text-blue-400 ${loading ? 'animate-spin' : ''}`} />
|
||||
<span className="text-white font-medium">Refresh</span>
|
||||
<RefreshCw className={`w-4 h-4 text-stone-600 ${loading ? 'animate-spin' : ''}`} />
|
||||
<span className="font-medium">Refresh</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setShowResetModal(true)}
|
||||
className="flex items-center space-x-2 px-4 py-2 bg-red-600/20 text-red-400 border border-red-500/30 rounded-xl hover:bg-red-600/30 hover:scale-105 transition-all"
|
||||
className="flex items-center space-x-2 px-4 py-2 bg-red-50 text-red-600 border border-red-100 rounded-xl hover:bg-red-100 transition-all"
|
||||
>
|
||||
<RotateCcw className="w-4 h-4" />
|
||||
<span>Reset</span>
|
||||
@@ -276,17 +276,17 @@ export function AnalyticsDashboard({ isAuthenticated }: AnalyticsDashboardProps)
|
||||
</div>
|
||||
|
||||
{loading && (
|
||||
<div className="admin-glass-card p-8 rounded-xl">
|
||||
<div className="bg-white border border-stone-200 p-8 rounded-xl shadow-sm">
|
||||
<div className="flex items-center justify-center space-x-3">
|
||||
<RefreshCw className="w-6 h-6 text-blue-400 animate-spin" />
|
||||
<span className="text-white/80 text-lg">Loading analytics data...</span>
|
||||
<RefreshCw className="w-6 h-6 text-stone-600 animate-spin" />
|
||||
<span className="text-stone-500 text-lg">Loading analytics data...</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="admin-glass-card p-6 rounded-xl border border-red-500/40">
|
||||
<div className="flex items-center space-x-3 text-red-300">
|
||||
<div className="bg-white border border-red-200 p-6 rounded-xl">
|
||||
<div className="flex items-center space-x-3 text-red-600">
|
||||
<Activity className="w-5 h-5" />
|
||||
<span>Error: {error}</span>
|
||||
</div>
|
||||
@@ -297,8 +297,8 @@ export function AnalyticsDashboard({ isAuthenticated }: AnalyticsDashboardProps)
|
||||
<>
|
||||
{/* Overview Stats */}
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-white mb-6 flex items-center">
|
||||
<Target className="w-5 h-5 mr-2 text-purple-400" />
|
||||
<h2 className="text-xl font-bold text-stone-900 mb-6 flex items-center">
|
||||
<Target className="w-5 h-5 mr-2 text-stone-600" />
|
||||
Overview
|
||||
</h2>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4">
|
||||
@@ -306,7 +306,7 @@ export function AnalyticsDashboard({ isAuthenticated }: AnalyticsDashboardProps)
|
||||
title="Total Views"
|
||||
value={data.overview.totalViews.toLocaleString()}
|
||||
icon={Eye}
|
||||
color="bg-blue-500/30"
|
||||
color="bg-stone-100 text-stone-600"
|
||||
trend="up"
|
||||
trendValue="+12.5%"
|
||||
description="All-time page views"
|
||||
@@ -315,7 +315,7 @@ export function AnalyticsDashboard({ isAuthenticated }: AnalyticsDashboardProps)
|
||||
title="Projects"
|
||||
value={data.overview.totalProjects}
|
||||
icon={Globe}
|
||||
color="bg-green-500/30"
|
||||
color="bg-green-100 text-green-600"
|
||||
trend="up"
|
||||
trendValue="+2"
|
||||
description={`${data.overview.publishedProjects} published`}
|
||||
@@ -324,7 +324,7 @@ export function AnalyticsDashboard({ isAuthenticated }: AnalyticsDashboardProps)
|
||||
title="Engagement"
|
||||
value={data.overview.totalLikes}
|
||||
icon={Heart}
|
||||
color="bg-pink-500/30"
|
||||
color="bg-pink-100 text-pink-600"
|
||||
trend="up"
|
||||
trendValue="+8.2%"
|
||||
description="Total likes & shares"
|
||||
@@ -333,7 +333,7 @@ export function AnalyticsDashboard({ isAuthenticated }: AnalyticsDashboardProps)
|
||||
title="Performance"
|
||||
value={data.overview.avgLighthouse}
|
||||
icon={Zap}
|
||||
color="bg-orange-500/30"
|
||||
color="bg-orange-100 text-orange-600"
|
||||
trend="up"
|
||||
trendValue="+5%"
|
||||
description="Avg Lighthouse score"
|
||||
@@ -342,7 +342,7 @@ export function AnalyticsDashboard({ isAuthenticated }: AnalyticsDashboardProps)
|
||||
title="Bounce Rate"
|
||||
value={`${data.metrics.bounceRate}%`}
|
||||
icon={MousePointer}
|
||||
color="bg-purple-500/30"
|
||||
color="bg-stone-100 text-stone-600"
|
||||
trend="down"
|
||||
trendValue="-2.1%"
|
||||
description="User retention"
|
||||
@@ -353,9 +353,9 @@ export function AnalyticsDashboard({ isAuthenticated }: AnalyticsDashboardProps)
|
||||
{/* Project Performance */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
{/* Top Projects */}
|
||||
<div className="admin-glass-card p-6 rounded-xl">
|
||||
<h3 className="text-xl font-bold text-white mb-6 flex items-center">
|
||||
<Award className="w-5 h-5 mr-2 text-yellow-400" />
|
||||
<div className="bg-white border border-stone-200 p-6 rounded-xl shadow-sm">
|
||||
<h3 className="text-xl font-bold text-stone-900 mb-6 flex items-center">
|
||||
<Award className="w-5 h-5 mr-2 text-yellow-500" />
|
||||
Top Performing Projects
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
@@ -368,20 +368,20 @@ export function AnalyticsDashboard({ isAuthenticated }: AnalyticsDashboardProps)
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: index * 0.1 }}
|
||||
className="flex items-center justify-between p-4 admin-glass-light rounded-xl"
|
||||
className="flex items-center justify-between p-4 bg-stone-50 rounded-xl border border-stone-100"
|
||||
>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="w-8 h-8 bg-gradient-to-br from-blue-500 to-purple-500 rounded-lg flex items-center justify-center text-white font-bold">
|
||||
<div className="w-8 h-8 bg-stone-600 rounded-lg flex items-center justify-center text-white font-bold shadow-sm">
|
||||
#{index + 1}
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-white font-medium">{project.title}</p>
|
||||
<p className="text-white/60 text-sm">{project.category}</p>
|
||||
<p className="text-stone-900 font-medium">{project.title}</p>
|
||||
<p className="text-stone-500 text-sm">{project.category}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-white font-bold">{project.views.toLocaleString()}</p>
|
||||
<p className="text-white/60 text-sm">views</p>
|
||||
<p className="text-stone-900 font-bold">{project.views.toLocaleString()}</p>
|
||||
<p className="text-stone-500 text-sm">views</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
@@ -389,9 +389,9 @@ export function AnalyticsDashboard({ isAuthenticated }: AnalyticsDashboardProps)
|
||||
</div>
|
||||
|
||||
{/* Categories Distribution */}
|
||||
<div className="admin-glass-card p-6 rounded-xl">
|
||||
<h3 className="text-xl font-bold text-white mb-6 flex items-center">
|
||||
<BarChart3 className="w-5 h-5 mr-2 text-green-400" />
|
||||
<div className="bg-white border border-stone-200 p-6 rounded-xl shadow-sm">
|
||||
<h3 className="text-xl font-bold text-stone-900 mb-6 flex items-center">
|
||||
<BarChart3 className="w-5 h-5 mr-2 text-green-600" />
|
||||
Categories
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
@@ -405,16 +405,16 @@ export function AnalyticsDashboard({ isAuthenticated }: AnalyticsDashboardProps)
|
||||
>
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className={`w-4 h-4 rounded-full ${getCategoryColor(index)}`}></div>
|
||||
<span className="text-white font-medium">{category}</span>
|
||||
<span className="text-stone-700 font-medium">{category}</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-32 h-2 bg-white/10 rounded-full overflow-hidden">
|
||||
<div className="w-32 h-2 bg-stone-100 rounded-full overflow-hidden">
|
||||
<div
|
||||
className={`h-full ${getCategoryColor(index)} transition-all duration-500`}
|
||||
style={{ width: `${(count / Math.max(...Object.values(data.categories))) * 100}%` }}
|
||||
></div>
|
||||
</div>
|
||||
<span className="text-white/80 font-medium w-8 text-right">{count}</span>
|
||||
<span className="text-stone-500 font-medium w-8 text-right">{count}</span>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
@@ -425,9 +425,9 @@ export function AnalyticsDashboard({ isAuthenticated }: AnalyticsDashboardProps)
|
||||
{/* Difficulty & Engagement */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
{/* Difficulty Distribution */}
|
||||
<div className="admin-glass-card p-6 rounded-xl">
|
||||
<h3 className="text-xl font-bold text-white mb-6 flex items-center">
|
||||
<Target className="w-5 h-5 mr-2 text-red-400" />
|
||||
<div className="bg-white border border-stone-200 p-6 rounded-xl shadow-sm">
|
||||
<h3 className="text-xl font-bold text-stone-900 mb-6 flex items-center">
|
||||
<Target className="w-5 h-5 mr-2 text-red-500" />
|
||||
Difficulty Levels
|
||||
</h3>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
@@ -448,9 +448,9 @@ export function AnalyticsDashboard({ isAuthenticated }: AnalyticsDashboardProps)
|
||||
</div>
|
||||
|
||||
{/* Recent Activity */}
|
||||
<div className="admin-glass-card p-6 rounded-xl">
|
||||
<h3 className="text-xl font-bold text-white mb-6 flex items-center">
|
||||
<Activity className="w-5 h-5 mr-2 text-blue-400" />
|
||||
<div className="bg-white border border-stone-200 p-6 rounded-xl shadow-sm">
|
||||
<h3 className="text-xl font-bold text-stone-900 mb-6 flex items-center">
|
||||
<Activity className="w-5 h-5 mr-2 text-blue-600" />
|
||||
Recent Activity
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
@@ -463,25 +463,25 @@ export function AnalyticsDashboard({ isAuthenticated }: AnalyticsDashboardProps)
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: index * 0.1 }}
|
||||
className="flex items-center space-x-4 p-3 admin-glass-light rounded-xl"
|
||||
className="flex items-center space-x-4 p-3 bg-stone-50 rounded-xl border border-stone-100"
|
||||
>
|
||||
<div className="w-2 h-2 bg-green-400 rounded-full animate-pulse"></div>
|
||||
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
|
||||
<div className="flex-1">
|
||||
<p className="text-white font-medium text-sm">{project.title}</p>
|
||||
<p className="text-white/60 text-xs">
|
||||
<p className="text-stone-900 font-medium text-sm">{project.title}</p>
|
||||
<p className="text-stone-500 text-xs">
|
||||
Updated {new Date(project.updatedAt).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
{project.featured && (
|
||||
<span className="px-2 py-1 bg-purple-500/20 text-purple-400 rounded-full text-xs">
|
||||
<span className="px-2 py-1 bg-stone-100 text-stone-700 rounded-full text-xs font-medium">
|
||||
Featured
|
||||
</span>
|
||||
)}
|
||||
<span className={`px-2 py-1 rounded-full text-xs ${
|
||||
<span className={`px-2 py-1 rounded-full text-xs font-medium ${
|
||||
project.published
|
||||
? 'bg-green-500/20 text-green-400'
|
||||
: 'bg-yellow-500/20 text-yellow-400'
|
||||
? 'bg-green-100 text-green-700'
|
||||
: 'bg-yellow-100 text-yellow-700'
|
||||
}`}>
|
||||
{project.published ? 'Live' : 'Draft'}
|
||||
</span>
|
||||
@@ -496,30 +496,30 @@ export function AnalyticsDashboard({ isAuthenticated }: AnalyticsDashboardProps)
|
||||
|
||||
{/* Reset Modal */}
|
||||
{showResetModal && (
|
||||
<div className="fixed inset-0 bg-black/80 backdrop-blur-sm z-50 flex items-center justify-center p-4">
|
||||
<div className="fixed inset-0 bg-stone-900/20 backdrop-blur-sm z-50 flex items-center justify-center p-4">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.95 }}
|
||||
className="admin-glass-card rounded-2xl p-6 w-full max-w-md"
|
||||
className="bg-white border border-stone-200 rounded-2xl p-6 w-full max-w-md shadow-xl"
|
||||
>
|
||||
<div className="flex items-center space-x-3 mb-4">
|
||||
<div className="w-10 h-10 bg-red-500/20 rounded-lg flex items-center justify-center">
|
||||
<AlertTriangle className="w-5 h-5 text-red-400" />
|
||||
<div className="w-10 h-10 bg-red-100 rounded-lg flex items-center justify-center">
|
||||
<AlertTriangle className="w-5 h-5 text-red-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-white">Reset Analytics Data</h3>
|
||||
<p className="text-white/60 text-sm">This action cannot be undone</p>
|
||||
<h3 className="text-lg font-bold text-stone-900">Reset Analytics Data</h3>
|
||||
<p className="text-stone-500 text-sm">This action cannot be undone</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 mb-6">
|
||||
<div>
|
||||
<label className="block text-white/80 text-sm mb-2">Reset Type</label>
|
||||
<label className="block text-stone-600 text-sm mb-2">Reset Type</label>
|
||||
<select
|
||||
value={resetType}
|
||||
onChange={(e) => setResetType(e.target.value as 'all' | 'performance' | 'analytics')}
|
||||
className="w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-red-500"
|
||||
className="w-full px-3 py-2 bg-stone-50 border border-stone-200 rounded-lg text-stone-900 focus:outline-none focus:ring-2 focus:ring-red-500"
|
||||
>
|
||||
<option value="analytics">Analytics Only (views, likes, shares)</option>
|
||||
<option value="pageviews">Page Views Only</option>
|
||||
@@ -529,10 +529,10 @@ export function AnalyticsDashboard({ isAuthenticated }: AnalyticsDashboardProps)
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="bg-red-500/10 border border-red-500/20 rounded-lg p-3">
|
||||
<div className="bg-red-50 border border-red-100 rounded-lg p-3">
|
||||
<div className="flex items-start space-x-2">
|
||||
<AlertTriangle className="w-4 h-4 text-red-400 mt-0.5 flex-shrink-0" />
|
||||
<div className="text-sm text-red-300">
|
||||
<AlertTriangle className="w-4 h-4 text-red-500 mt-0.5 flex-shrink-0" />
|
||||
<div className="text-sm text-red-700">
|
||||
<p className="font-medium mb-1">Warning:</p>
|
||||
<p>This will permanently delete the selected analytics data. This action cannot be reversed.</p>
|
||||
</div>
|
||||
@@ -544,14 +544,14 @@ export function AnalyticsDashboard({ isAuthenticated }: AnalyticsDashboardProps)
|
||||
<button
|
||||
onClick={() => setShowResetModal(false)}
|
||||
disabled={resetting}
|
||||
className="flex-1 px-4 py-2 admin-glass-light text-white rounded-lg hover:scale-105 transition-all disabled:opacity-50"
|
||||
className="flex-1 px-4 py-2 bg-white border border-stone-200 text-stone-700 rounded-lg hover:bg-stone-50 transition-all disabled:opacity-50"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={resetAnalytics}
|
||||
disabled={resetting}
|
||||
className="flex-1 flex items-center justify-center space-x-2 px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg hover:scale-105 transition-all disabled:opacity-50"
|
||||
className="flex-1 flex items-center justify-center space-x-2 px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg transition-all disabled:opacity-50"
|
||||
>
|
||||
{resetting ? (
|
||||
<>
|
||||
|
||||
@@ -143,7 +143,7 @@ export const EmailManager: React.FC = () => {
|
||||
case 'high': return 'text-red-400';
|
||||
case 'medium': return 'text-yellow-400';
|
||||
case 'low': return 'text-green-400';
|
||||
default: return 'text-blue-400';
|
||||
default: return 'text-stone-400';
|
||||
}
|
||||
};
|
||||
|
||||
@@ -153,7 +153,7 @@ export const EmailManager: React.FC = () => {
|
||||
<motion.div
|
||||
animate={{ rotate: 360 }}
|
||||
transition={{ duration: 1, repeat: Infinity, ease: "linear" }}
|
||||
className="w-8 h-8 border-2 border-blue-500 border-t-transparent rounded-full"
|
||||
className="w-8 h-8 border-2 border-stone-500 border-t-transparent rounded-full"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -164,12 +164,12 @@ export const EmailManager: React.FC = () => {
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-white">Email Manager</h2>
|
||||
<p className="text-white/70 mt-1">Manage your contact messages</p>
|
||||
<h2 className="text-2xl font-bold text-stone-900">Email Manager</h2>
|
||||
<p className="text-stone-500 mt-1">Manage your contact messages</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={loadMessages}
|
||||
className="flex items-center space-x-2 px-4 py-2 bg-blue-500/20 text-blue-400 rounded-lg hover:bg-blue-500/30 transition-colors"
|
||||
className="flex items-center space-x-2 px-4 py-2 bg-stone-100 text-stone-700 rounded-lg hover:bg-stone-200 transition-colors"
|
||||
>
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
<span>Refresh</span>
|
||||
@@ -179,13 +179,13 @@ export const EmailManager: React.FC = () => {
|
||||
{/* Filters and Search */}
|
||||
<div className="flex flex-col sm:flex-row gap-4">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-white/50 w-4 h-4" />
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-stone-400 w-4 h-4" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search messages..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="w-full pl-10 pr-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
className="w-full pl-10 pr-4 py-2 bg-white border border-stone-200 rounded-lg text-stone-900 placeholder:text-stone-400 focus:outline-none focus:ring-2 focus:ring-stone-400"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex space-x-2">
|
||||
@@ -195,8 +195,8 @@ export const EmailManager: React.FC = () => {
|
||||
onClick={() => setFilter(filterType as 'all' | 'unread' | 'responded')}
|
||||
className={`px-4 py-2 rounded-lg transition-colors ${
|
||||
filter === filterType
|
||||
? 'bg-blue-500 text-white'
|
||||
: 'bg-white/10 text-white/70 hover:bg-white/20'
|
||||
? 'bg-stone-900 text-stone-50'
|
||||
: 'bg-white border border-stone-200 text-stone-600 hover:bg-stone-50'
|
||||
}`}
|
||||
>
|
||||
{filterType.charAt(0).toUpperCase() + filterType.slice(1)}
|
||||
@@ -209,7 +209,7 @@ export const EmailManager: React.FC = () => {
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<div className="lg:col-span-1 space-y-3">
|
||||
{filteredMessages.length === 0 ? (
|
||||
<div className="text-center py-12 text-white/50">
|
||||
<div className="text-center py-12 text-stone-400">
|
||||
<Mail className="w-12 h-12 mx-auto mb-4 opacity-50" />
|
||||
<p>No messages found</p>
|
||||
</div>
|
||||
@@ -219,36 +219,36 @@ export const EmailManager: React.FC = () => {
|
||||
key={message.id}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className={`p-4 rounded-lg cursor-pointer transition-all ${
|
||||
className={`p-4 rounded-lg cursor-pointer transition-all border ${
|
||||
selectedMessage?.id === message.id
|
||||
? 'bg-blue-500/20 border border-blue-500/50'
|
||||
: 'bg-white/5 border border-white/10 hover:bg-white/10'
|
||||
? 'bg-stone-100 border-stone-300 shadow-sm'
|
||||
: 'bg-white border-stone-200 hover:bg-stone-50'
|
||||
}`}
|
||||
onClick={() => handleMessageClick(message)}
|
||||
>
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<h3 className="font-semibold text-white truncate">{message.subject}</h3>
|
||||
<h3 className="font-semibold text-stone-900 truncate">{message.subject}</h3>
|
||||
<div className="flex items-center space-x-2">
|
||||
{!message.read && <Circle className="w-3 h-3 text-blue-400" />}
|
||||
{message.responded && <CheckCircle className="w-3 h-3 text-green-400" />}
|
||||
{!message.read && <Circle className="w-3 h-3 text-stone-600" />}
|
||||
{message.responded && <CheckCircle className="w-3 h-3 text-green-500" />}
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-white/70 text-sm mb-2">{message.name}</p>
|
||||
<p className="text-white/50 text-xs">{formatDate(message.createdAt)}</p>
|
||||
<p className="text-stone-600 text-sm mb-2">{message.name}</p>
|
||||
<p className="text-stone-400 text-xs">{formatDate(message.createdAt)}</p>
|
||||
</motion.div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Message Detail */}
|
||||
<div className="lg:col-span-2 admin-glass-card p-6 rounded-xl">
|
||||
<div className="lg:col-span-2 admin-glass-card p-6 rounded-xl bg-white border border-stone-200">
|
||||
{selectedMessage ? (
|
||||
<div className="space-y-6">
|
||||
{/* Message Header */}
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-xl font-bold text-white">{selectedMessage.subject}</h3>
|
||||
<div className="flex items-center space-x-4 text-sm text-white/70">
|
||||
<h3 className="text-xl font-bold text-stone-900">{selectedMessage.subject}</h3>
|
||||
<div className="flex items-center space-x-4 text-sm text-stone-500">
|
||||
<div className="flex items-center space-x-2">
|
||||
<User className="w-4 h-4" />
|
||||
<span>{selectedMessage.name}</span>
|
||||
@@ -264,15 +264,15 @@ export const EmailManager: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
{!selectedMessage.read && <Circle className="w-4 h-4 text-blue-400" />}
|
||||
{selectedMessage.responded && <CheckCircle className="w-4 h-4 text-green-400" />}
|
||||
{!selectedMessage.read && <Circle className="w-4 h-4 text-stone-600" />}
|
||||
{selectedMessage.responded && <CheckCircle className="w-4 h-4 text-green-500" />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Message Body */}
|
||||
<div className="p-4 bg-white/5 rounded-lg border border-white/10">
|
||||
<h4 className="text-white font-medium mb-3">Message:</h4>
|
||||
<div className="text-white/80 whitespace-pre-wrap leading-relaxed">
|
||||
<div className="p-4 bg-stone-50 rounded-lg border border-stone-200">
|
||||
<h4 className="text-stone-700 font-medium mb-3">Message:</h4>
|
||||
<div className="text-stone-600 whitespace-pre-wrap leading-relaxed">
|
||||
{selectedMessage.message}
|
||||
</div>
|
||||
</div>
|
||||
@@ -281,21 +281,21 @@ export const EmailManager: React.FC = () => {
|
||||
<div className="flex space-x-3">
|
||||
<button
|
||||
onClick={() => setShowReplyModal(true)}
|
||||
className="flex items-center space-x-2 px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors"
|
||||
className="flex items-center space-x-2 px-4 py-2 bg-stone-900 text-stone-50 rounded-lg hover:bg-stone-800 transition-colors"
|
||||
>
|
||||
<Reply className="w-4 h-4" />
|
||||
<span>Reply</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setSelectedMessage(null)}
|
||||
className="px-4 py-2 bg-white/10 text-white rounded-lg hover:bg-white/20 transition-colors"
|
||||
className="px-4 py-2 bg-white border border-stone-200 text-stone-600 rounded-lg hover:bg-stone-50 transition-colors"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-12 text-white/50">
|
||||
<div className="text-center py-12 text-stone-400">
|
||||
<Eye className="w-12 h-12 mx-auto mb-4 opacity-50" />
|
||||
<p>Select a message to view details</p>
|
||||
</div>
|
||||
@@ -311,23 +311,23 @@ export const EmailManager: React.FC = () => {
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center p-4"
|
||||
className="fixed inset-0 bg-stone-900/20 backdrop-blur-sm z-50 flex items-center justify-center p-4"
|
||||
onClick={() => setShowReplyModal(false)}
|
||||
>
|
||||
<motion.div
|
||||
initial={{ scale: 0.9, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
exit={{ scale: 0.9, opacity: 0 }}
|
||||
className="bg-gray-900/95 backdrop-blur-xl border border-white/20 rounded-2xl p-6 max-w-2xl w-full"
|
||||
className="bg-white border border-stone-200 rounded-2xl p-6 max-w-2xl w-full shadow-xl"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2 className="text-xl font-bold text-white">Reply to {selectedMessage.name}</h2>
|
||||
<h2 className="text-xl font-bold text-stone-900">Reply to {selectedMessage.name}</h2>
|
||||
<button
|
||||
onClick={() => setShowReplyModal(false)}
|
||||
className="p-2 hover:bg-white/10 rounded-lg transition-colors"
|
||||
className="p-2 hover:bg-stone-100 rounded-lg transition-colors"
|
||||
>
|
||||
<X className="w-5 h-5 text-white/70" />
|
||||
<X className="w-5 h-5 text-stone-500" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -336,20 +336,20 @@ export const EmailManager: React.FC = () => {
|
||||
value={replyContent}
|
||||
onChange={(e) => setReplyContent(e.target.value)}
|
||||
placeholder="Type your reply..."
|
||||
className="w-full h-32 p-3 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-blue-500 resize-none"
|
||||
className="w-full h-32 p-3 bg-stone-50 border border-stone-200 rounded-lg text-stone-900 placeholder:text-stone-400 focus:outline-none focus:ring-2 focus:ring-stone-400 resize-none"
|
||||
/>
|
||||
|
||||
<div className="flex space-x-3">
|
||||
<button
|
||||
onClick={handleReply}
|
||||
className="flex items-center space-x-2 px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors"
|
||||
className="flex items-center space-x-2 px-4 py-2 bg-stone-900 text-stone-50 rounded-lg hover:bg-stone-800 transition-colors"
|
||||
>
|
||||
<Send className="w-4 h-4" />
|
||||
<span>Send Reply</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowReplyModal(false)}
|
||||
className="px-4 py-2 bg-white/10 text-white rounded-lg hover:bg-white/20 transition-colors"
|
||||
className="px-4 py-2 bg-white border border-stone-200 text-stone-600 rounded-lg hover:bg-stone-50 transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
|
||||
@@ -85,19 +85,19 @@ export const EmailResponder: React.FC<EmailResponderProps> = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white rounded-2xl shadow-2xl max-w-2xl w-full max-h-[90vh] overflow-y-auto">
|
||||
<div className="fixed inset-0 bg-stone-900/20 backdrop-blur-sm flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white rounded-2xl shadow-2xl max-w-2xl w-full max-h-[90vh] overflow-y-auto border border-stone-200">
|
||||
|
||||
{/* Header */}
|
||||
<div className="bg-gradient-to-r from-blue-600 to-purple-600 text-white p-6 rounded-t-2xl">
|
||||
<div className="bg-stone-50 border-b border-stone-200 text-stone-900 p-6 rounded-t-2xl">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold">📧 E-Mail Antwort senden</h2>
|
||||
<p className="text-blue-100 mt-1">Wähle ein schönes Template für deine Antwort</p>
|
||||
<p className="text-stone-500 mt-1">Wähle ein schönes Template für deine Antwort</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-white hover:text-gray-200 transition-colors"
|
||||
className="text-stone-400 hover:text-stone-600 transition-colors"
|
||||
>
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
@@ -110,54 +110,54 @@ export const EmailResponder: React.FC<EmailResponderProps> = ({
|
||||
<div className="p-6">
|
||||
|
||||
{/* Contact Info */}
|
||||
<div className="bg-gray-50 rounded-xl p-4 mb-6">
|
||||
<h3 className="font-semibold text-gray-800 mb-2">📬 Kontakt-Informationen</h3>
|
||||
<div className="bg-stone-50 border border-stone-200 rounded-xl p-4 mb-6">
|
||||
<h3 className="font-semibold text-stone-800 mb-2">📬 Kontakt-Informationen</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<span className="text-sm text-gray-600">Name:</span>
|
||||
<p className="font-medium text-gray-900">{contactName}</p>
|
||||
<span className="text-sm text-stone-500">Name:</span>
|
||||
<p className="font-medium text-stone-900">{contactName}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-sm text-gray-600">E-Mail:</span>
|
||||
<p className="font-medium text-gray-900">{contactEmail}</p>
|
||||
<span className="text-sm text-stone-500">E-Mail:</span>
|
||||
<p className="font-medium text-stone-900">{contactEmail}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Original Message Preview */}
|
||||
<div className="bg-blue-50 rounded-xl p-4 mb-6">
|
||||
<h3 className="font-semibold text-blue-800 mb-2">💬 Ursprüngliche Nachricht</h3>
|
||||
<div className="bg-white rounded-lg p-3 border-l-4 border-blue-500">
|
||||
<p className="text-gray-700 text-sm whitespace-pre-wrap">{originalMessage}</p>
|
||||
<div className="bg-stone-50 border border-stone-200 rounded-xl p-4 mb-6">
|
||||
<h3 className="font-semibold text-stone-800 mb-2">💬 Ursprüngliche Nachricht</h3>
|
||||
<div className="bg-white rounded-lg p-3 border-l-4 border-blue-500 shadow-sm">
|
||||
<p className="text-stone-700 text-sm whitespace-pre-wrap">{originalMessage}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Template Selection */}
|
||||
<div className="mb-6">
|
||||
<h3 className="font-semibold text-gray-800 mb-4">🎨 Template auswählen</h3>
|
||||
<h3 className="font-semibold text-stone-800 mb-4">🎨 Template auswählen</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{Object.entries(templates).map(([key, template]) => (
|
||||
<div
|
||||
key={key}
|
||||
className={`relative cursor-pointer rounded-xl border-2 transition-all duration-200 ${
|
||||
selectedTemplate === key
|
||||
? 'border-blue-500 bg-blue-50 shadow-lg scale-105'
|
||||
: 'border-gray-200 hover:border-gray-300 hover:shadow-md'
|
||||
? 'border-stone-500 bg-stone-50 shadow-md'
|
||||
: 'border-stone-200 hover:border-stone-300 hover:shadow-sm'
|
||||
}`}
|
||||
onClick={() => setSelectedTemplate(key as keyof typeof templates)}
|
||||
>
|
||||
<div className={`bg-gradient-to-r ${template.color} text-white p-4 rounded-t-xl`}>
|
||||
<div className={`p-4 rounded-t-xl bg-white border-b border-stone-100`}>
|
||||
<div className="text-center">
|
||||
<div className="text-3xl mb-2">{template.icon}</div>
|
||||
<h4 className="font-bold text-lg">{template.name}</h4>
|
||||
<h4 className="font-bold text-lg text-stone-900">{template.name}</h4>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<p className="text-sm text-gray-600 text-center">{template.description}</p>
|
||||
<p className="text-sm text-stone-600 text-center">{template.description}</p>
|
||||
</div>
|
||||
{selectedTemplate === key && (
|
||||
<div className="absolute top-2 right-2">
|
||||
<div className="w-6 h-6 bg-blue-500 rounded-full flex items-center justify-center">
|
||||
<div className="w-6 h-6 bg-stone-600 rounded-full flex items-center justify-center">
|
||||
<svg className="w-4 h-4 text-white" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
@@ -171,15 +171,15 @@ export const EmailResponder: React.FC<EmailResponderProps> = ({
|
||||
|
||||
{/* Preview */}
|
||||
<div className="mb-6">
|
||||
<h3 className="font-semibold text-gray-800 mb-4">👀 Vorschau</h3>
|
||||
<div className="bg-gray-100 rounded-xl p-4">
|
||||
<div className="bg-white rounded-lg shadow-sm border">
|
||||
<div className={`bg-gradient-to-r ${templates[selectedTemplate].color} text-white p-4 rounded-t-lg`}>
|
||||
<h4 className="font-bold text-lg">{templates[selectedTemplate].icon} {templates[selectedTemplate].name}</h4>
|
||||
<p className="text-sm opacity-90">An: {contactName}</p>
|
||||
<h3 className="font-semibold text-stone-800 mb-4">👀 Vorschau</h3>
|
||||
<div className="bg-stone-100 rounded-xl p-4 border border-stone-200">
|
||||
<div className="bg-white rounded-lg shadow-sm border border-stone-200">
|
||||
<div className="p-4 rounded-t-lg bg-stone-50 border-b border-stone-100">
|
||||
<h4 className="font-bold text-lg text-stone-900">{templates[selectedTemplate].icon} {templates[selectedTemplate].name}</h4>
|
||||
<p className="text-sm text-stone-500">An: {contactName}</p>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<p className="text-sm text-gray-600">
|
||||
<p className="text-sm text-stone-600">
|
||||
{selectedTemplate === 'welcome' && 'Freundliche Begrüßung mit Portfolio-Links und nächsten Schritten'}
|
||||
{selectedTemplate === 'project' && 'Professionelle Projekt-Antwort mit Arbeitsprozess und CTA'}
|
||||
{selectedTemplate === 'quick' && 'Schnelle, kurze Bestätigung der Nachricht'}
|
||||
@@ -193,14 +193,14 @@ export const EmailResponder: React.FC<EmailResponderProps> = ({
|
||||
<div className="flex gap-4">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="flex-1 px-6 py-3 border border-gray-300 text-gray-700 rounded-xl hover:bg-gray-50 transition-colors font-medium"
|
||||
className="flex-1 px-6 py-3 border border-stone-300 text-stone-700 rounded-xl hover:bg-stone-50 transition-colors font-medium"
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSendEmail}
|
||||
disabled={isLoading}
|
||||
className="flex-1 px-6 py-3 bg-gradient-to-r from-blue-600 to-purple-600 text-white rounded-xl hover:from-blue-700 hover:to-purple-700 transition-all font-medium disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
||||
className="flex-1 px-6 py-3 bg-stone-900 text-white rounded-xl hover:bg-stone-800 transition-all font-medium disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
||||
>
|
||||
{isLoading ? (
|
||||
<>
|
||||
|
||||
@@ -99,23 +99,23 @@ export default function ImportExport() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="admin-glass-card rounded-lg p-6">
|
||||
<h3 className="text-lg font-semibold text-white mb-4 flex items-center">
|
||||
<FileText className="w-5 h-5 mr-2 text-blue-400" />
|
||||
<div className="bg-white border border-stone-200 rounded-xl p-6">
|
||||
<h3 className="text-lg font-semibold text-stone-900 mb-4 flex items-center">
|
||||
<FileText className="w-5 h-5 mr-2 text-stone-600" />
|
||||
Import & Export
|
||||
</h3>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* Export Section */}
|
||||
<div className="admin-glass-light rounded-lg p-4">
|
||||
<h4 className="font-medium text-white mb-2">Export Projekte</h4>
|
||||
<p className="text-sm text-white/70 mb-3">
|
||||
<div className="bg-stone-50 border border-stone-200 rounded-xl p-4">
|
||||
<h4 className="font-medium text-stone-900 mb-2">Export Projekte</h4>
|
||||
<p className="text-sm text-stone-600 mb-3">
|
||||
Alle Projekte als JSON-Datei herunterladen
|
||||
</p>
|
||||
<button
|
||||
onClick={handleExport}
|
||||
disabled={isExporting}
|
||||
className="flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 hover:scale-105 transition-all disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
className="flex items-center px-4 py-2 bg-stone-900 text-white rounded-lg hover:bg-stone-800 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
{isExporting ? 'Exportiere...' : 'Exportieren'}
|
||||
@@ -123,12 +123,12 @@ export default function ImportExport() {
|
||||
</div>
|
||||
|
||||
{/* Import Section */}
|
||||
<div className="admin-glass-light rounded-lg p-4">
|
||||
<h4 className="font-medium text-white mb-2">Import Projekte</h4>
|
||||
<p className="text-sm text-white/70 mb-3">
|
||||
<div className="bg-stone-50 border border-stone-200 rounded-xl p-4">
|
||||
<h4 className="font-medium text-stone-900 mb-2">Import Projekte</h4>
|
||||
<p className="text-sm text-stone-600 mb-3">
|
||||
JSON-Datei mit Projekten hochladen
|
||||
</p>
|
||||
<label className="flex items-center px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 hover:scale-105 transition-all cursor-pointer">
|
||||
<label className="flex items-center px-4 py-2 bg-stone-900 text-white rounded-lg hover:bg-stone-800 transition-colors cursor-pointer w-fit">
|
||||
<Upload className="w-4 h-4 mr-2" />
|
||||
{isImporting ? 'Importiere...' : 'Datei auswählen'}
|
||||
<input
|
||||
@@ -143,16 +143,16 @@ export default function ImportExport() {
|
||||
|
||||
{/* Import Results */}
|
||||
{importResult && (
|
||||
<div className="admin-glass-light rounded-lg p-4">
|
||||
<h4 className="font-medium text-white mb-2 flex items-center">
|
||||
<div className="bg-stone-50 border border-stone-200 rounded-xl p-4">
|
||||
<h4 className="font-medium text-stone-900 mb-2 flex items-center">
|
||||
{importResult.success ? (
|
||||
<CheckCircle className="w-5 h-5 mr-2 text-green-400" />
|
||||
<CheckCircle className="w-5 h-5 mr-2 text-green-600" />
|
||||
) : (
|
||||
<AlertCircle className="w-5 h-5 mr-2 text-red-400" />
|
||||
<AlertCircle className="w-5 h-5 mr-2 text-red-600" />
|
||||
)}
|
||||
Import Ergebnis
|
||||
</h4>
|
||||
<div className="text-sm text-white/70 space-y-1">
|
||||
<div className="text-sm text-stone-600 space-y-1">
|
||||
<p><strong>Importiert:</strong> {importResult.results.imported}</p>
|
||||
<p><strong>Übersprungen:</strong> {importResult.results.skipped}</p>
|
||||
{importResult.results.errors.length > 0 && (
|
||||
@@ -160,7 +160,7 @@ export default function ImportExport() {
|
||||
<p><strong>Fehler:</strong></p>
|
||||
<ul className="list-disc list-inside ml-4">
|
||||
{importResult.results.errors.map((error, index) => (
|
||||
<li key={index} className="text-red-400">{error}</li>
|
||||
<li key={index} className="text-red-600">{error}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -194,15 +194,15 @@ const ModernAdminDashboard: React.FC<ModernAdminDashboardProps> = ({ isAuthentic
|
||||
<div className="flex items-center space-x-4">
|
||||
<Link
|
||||
href="/"
|
||||
className="flex items-center space-x-2 text-white/90 hover:text-white transition-colors"
|
||||
className="flex items-center space-x-2 text-stone-900 hover:text-black transition-colors"
|
||||
>
|
||||
<Home size={20} className="text-blue-400" />
|
||||
<span className="font-medium text-white">Portfolio</span>
|
||||
<Home size={20} className="text-stone-600" />
|
||||
<span className="font-medium text-stone-900">Portfolio</span>
|
||||
</Link>
|
||||
<div className="h-6 w-px bg-white/30" />
|
||||
<div className="h-6 w-px bg-stone-300" />
|
||||
<div className="flex items-center space-x-2">
|
||||
<Shield size={20} className="text-purple-400" />
|
||||
<span className="text-white font-semibold">Admin Panel</span>
|
||||
<Shield size={20} className="text-stone-600" />
|
||||
<span className="text-stone-900 font-semibold">Admin Panel</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -214,20 +214,20 @@ const ModernAdminDashboard: React.FC<ModernAdminDashboardProps> = ({ isAuthentic
|
||||
onClick={() => setActiveTab(item.id as 'overview' | 'projects' | 'emails' | 'analytics' | 'settings')}
|
||||
className={`flex items-center space-x-2 px-4 py-2 rounded-lg transition-all duration-200 ${
|
||||
activeTab === item.id
|
||||
? 'admin-glass-light border border-blue-500/40 text-blue-300 shadow-lg'
|
||||
: 'text-white/80 hover:text-white hover:admin-glass-light'
|
||||
? 'bg-stone-100 text-stone-900 font-medium shadow-sm border border-stone-200'
|
||||
: 'text-stone-500 hover:text-stone-800 hover:bg-stone-50'
|
||||
}`}
|
||||
>
|
||||
<item.icon size={16} className={activeTab === item.id ? 'text-blue-400' : 'text-white/70'} />
|
||||
<span className="font-medium text-sm">{item.label}</span>
|
||||
<item.icon size={16} className={activeTab === item.id ? 'text-stone-800' : 'text-stone-400'} />
|
||||
<span className="text-sm">{item.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Right side - User info and Logout */}
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="hidden sm:block text-sm text-white/80">
|
||||
Welcome, <span className="text-white font-semibold">Dennis</span>
|
||||
<div className="hidden sm:block text-sm text-stone-500">
|
||||
Welcome, <span className="text-stone-800 font-semibold">Dennis</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={async () => {
|
||||
@@ -244,7 +244,7 @@ const ModernAdminDashboard: React.FC<ModernAdminDashboardProps> = ({ isAuthentic
|
||||
window.location.href = '/manage';
|
||||
}
|
||||
}}
|
||||
className="flex items-center space-x-2 px-3 py-2 rounded-lg admin-glass-light hover:bg-red-500/20 text-red-300 hover:text-red-200 transition-all duration-200"
|
||||
className="flex items-center space-x-2 px-3 py-2 rounded-lg hover:bg-red-50 text-stone-500 hover:text-red-600 transition-all duration-200 border border-transparent hover:border-red-100"
|
||||
>
|
||||
<LogOut size={16} />
|
||||
<span className="hidden sm:inline text-sm font-medium">Logout</span>
|
||||
@@ -253,7 +253,7 @@ const ModernAdminDashboard: React.FC<ModernAdminDashboardProps> = ({ isAuthentic
|
||||
{/* Mobile menu button */}
|
||||
<button
|
||||
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
||||
className="md:hidden flex items-center justify-center p-2 rounded-lg admin-glass-light text-white hover:text-blue-300 transition-colors"
|
||||
className="md:hidden flex items-center justify-center p-2 rounded-lg text-stone-600 hover:bg-stone-100 transition-colors"
|
||||
>
|
||||
{mobileMenuOpen ? <X size={20} /> : <Menu size={20} />}
|
||||
</button>
|
||||
@@ -268,7 +268,7 @@ const ModernAdminDashboard: React.FC<ModernAdminDashboardProps> = ({ isAuthentic
|
||||
initial={{ opacity: 0, height: 0 }}
|
||||
animate={{ opacity: 1, height: 'auto' }}
|
||||
exit={{ opacity: 0, height: 0 }}
|
||||
className="md:hidden border-t border-white/20 admin-glass-light"
|
||||
className="md:hidden border-t border-stone-200 bg-white"
|
||||
>
|
||||
<div className="px-4 py-4 space-y-2">
|
||||
{navigation.map((item) => (
|
||||
@@ -280,11 +280,11 @@ const ModernAdminDashboard: React.FC<ModernAdminDashboardProps> = ({ isAuthentic
|
||||
}}
|
||||
className={`w-full flex items-center space-x-3 px-4 py-3 rounded-lg transition-all duration-200 ${
|
||||
activeTab === item.id
|
||||
? 'admin-glass-light border border-blue-500/40 text-blue-300 shadow-lg'
|
||||
: 'text-white/80 hover:text-white hover:admin-glass-light'
|
||||
? 'bg-stone-100 text-stone-900 shadow-sm border border-stone-200'
|
||||
: 'text-stone-500 hover:text-stone-800 hover:bg-stone-50'
|
||||
}`}
|
||||
>
|
||||
<item.icon size={18} className={activeTab === item.id ? 'text-blue-400' : 'text-white/70'} />
|
||||
<item.icon size={18} className={activeTab === item.id ? 'text-stone-800' : 'text-stone-400'} />
|
||||
<div className="text-left">
|
||||
<div className="font-medium text-sm">{item.label}</div>
|
||||
<div className="text-xs opacity-70">{item.description}</div>
|
||||
@@ -312,96 +312,96 @@ const ModernAdminDashboard: React.FC<ModernAdminDashboardProps> = ({ isAuthentic
|
||||
<div className="space-y-8">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-white">Admin Dashboard</h1>
|
||||
<p className="text-white/80 text-lg">Manage your portfolio and monitor performance</p>
|
||||
<h1 className="text-3xl font-bold text-stone-900">Admin Dashboard</h1>
|
||||
<p className="text-stone-500 text-lg">Manage your portfolio and monitor performance</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats Grid - Mobile: 2x3, Desktop: 6x1 horizontal */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-6 gap-3 md:gap-6">
|
||||
<div
|
||||
className="admin-glass-light p-4 rounded-xl hover:scale-105 transition-all duration-200 cursor-pointer"
|
||||
className="admin-glass-light p-4 rounded-xl cursor-pointer transition-all duration-200 transform-none hover:transform-none"
|
||||
onClick={() => setActiveTab('projects')}
|
||||
>
|
||||
<div className="flex flex-col space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-white/80 text-xs md:text-sm font-medium">Projects</p>
|
||||
<Database size={20} className="text-blue-400" />
|
||||
<p className="text-stone-500 text-xs md:text-sm font-medium">Projects</p>
|
||||
<Database size={20} className="text-stone-400" />
|
||||
</div>
|
||||
<p className="text-xl md:text-2xl font-bold text-white">{stats.totalProjects}</p>
|
||||
<p className="text-green-400 text-xs font-medium">{stats.publishedProjects} published</p>
|
||||
<p className="text-xl md:text-2xl font-bold text-stone-900">{stats.totalProjects}</p>
|
||||
<p className="text-green-600 text-xs font-medium">{stats.publishedProjects} published</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="admin-glass-light p-4 rounded-xl hover:scale-105 transition-all duration-200 cursor-pointer"
|
||||
className="admin-glass-light p-4 rounded-xl cursor-pointer transition-all duration-200 transform-none hover:transform-none"
|
||||
onClick={() => setActiveTab('analytics')}
|
||||
>
|
||||
<div className="flex flex-col space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-white/80 text-xs md:text-sm font-medium">Page Views</p>
|
||||
<Activity size={20} className="text-purple-400" />
|
||||
<p className="text-stone-500 text-xs md:text-sm font-medium">Page Views</p>
|
||||
<Activity size={20} className="text-stone-400" />
|
||||
</div>
|
||||
<p className="text-xl md:text-2xl font-bold text-white">{stats.totalViews.toLocaleString()}</p>
|
||||
<p className="text-blue-400 text-xs font-medium">{stats.totalUsers} users</p>
|
||||
<p className="text-xl md:text-2xl font-bold text-stone-900">{stats.totalViews.toLocaleString()}</p>
|
||||
<p className="text-stone-600 text-xs font-medium">{stats.totalUsers} users</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="admin-glass-light p-4 rounded-xl hover:scale-105 transition-all duration-200 cursor-pointer"
|
||||
className="admin-glass-light p-4 rounded-xl cursor-pointer transition-all duration-200 transform-none hover:transform-none"
|
||||
onClick={() => setActiveTab('emails')}
|
||||
>
|
||||
<div className="flex flex-col space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-white/80 text-xs md:text-sm font-medium">Messages</p>
|
||||
<Mail size={20} className="text-green-400" />
|
||||
<p className="text-stone-500 text-xs md:text-sm font-medium">Messages</p>
|
||||
<Mail size={20} className="text-stone-400" />
|
||||
</div>
|
||||
<p className="text-xl md:text-2xl font-bold text-white">{emails.length}</p>
|
||||
<p className="text-red-400 text-xs font-medium">{stats.unreadEmails} unread</p>
|
||||
<p className="text-xl md:text-2xl font-bold text-stone-900">{emails.length}</p>
|
||||
<p className="text-red-500 text-xs font-medium">{stats.unreadEmails} unread</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="admin-glass-light p-4 rounded-xl hover:scale-105 transition-all duration-200 cursor-pointer"
|
||||
className="admin-glass-light p-4 rounded-xl cursor-pointer transition-all duration-200 transform-none hover:transform-none"
|
||||
onClick={() => setActiveTab('analytics')}
|
||||
>
|
||||
<div className="flex flex-col space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-white/80 text-xs md:text-sm font-medium">Performance</p>
|
||||
<TrendingUp size={20} className="text-orange-400" />
|
||||
<p className="text-stone-500 text-xs md:text-sm font-medium">Performance</p>
|
||||
<TrendingUp size={20} className="text-stone-400" />
|
||||
</div>
|
||||
<p className="text-xl md:text-2xl font-bold text-white">{stats.avgPerformance}</p>
|
||||
<p className="text-orange-400 text-xs font-medium">Lighthouse Score</p>
|
||||
<p className="text-xl md:text-2xl font-bold text-stone-900">{stats.avgPerformance}</p>
|
||||
<p className="text-orange-500 text-xs font-medium">Lighthouse Score</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="admin-glass-light p-4 rounded-xl hover:scale-105 transition-all duration-200 cursor-pointer"
|
||||
className="admin-glass-light p-4 rounded-xl cursor-pointer transition-all duration-200 transform-none hover:transform-none"
|
||||
onClick={() => setActiveTab('analytics')}
|
||||
>
|
||||
<div className="flex flex-col space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-white/80 text-xs md:text-sm font-medium">Bounce Rate</p>
|
||||
<Users size={20} className="text-red-400" />
|
||||
<p className="text-stone-500 text-xs md:text-sm font-medium">Bounce Rate</p>
|
||||
<Users size={20} className="text-stone-400" />
|
||||
</div>
|
||||
<p className="text-xl md:text-2xl font-bold text-white">{stats.bounceRate}%</p>
|
||||
<p className="text-red-400 text-xs font-medium">Exit rate</p>
|
||||
<p className="text-xl md:text-2xl font-bold text-stone-900">{stats.bounceRate}%</p>
|
||||
<p className="text-red-500 text-xs font-medium">Exit rate</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="admin-glass-light p-4 rounded-xl hover:scale-105 transition-all duration-200 cursor-pointer"
|
||||
className="admin-glass-light p-4 rounded-xl cursor-pointer transition-all duration-200 transform-none hover:transform-none"
|
||||
onClick={() => setActiveTab('settings')}
|
||||
>
|
||||
<div className="flex flex-col space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-white/80 text-xs md:text-sm font-medium">System</p>
|
||||
<Shield size={20} className="text-green-400" />
|
||||
<p className="text-stone-500 text-xs md:text-sm font-medium">System</p>
|
||||
<Shield size={20} className="text-stone-400" />
|
||||
</div>
|
||||
<p className="text-xl md:text-2xl font-bold text-white">Online</p>
|
||||
<p className="text-xl md:text-2xl font-bold text-stone-900">Online</p>
|
||||
<div className="flex items-center space-x-1">
|
||||
<div className="w-2 h-2 bg-green-400 rounded-full animate-pulse"></div>
|
||||
<p className="text-green-400 text-xs font-medium">All systems operational</p>
|
||||
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
|
||||
<p className="text-green-600 text-xs font-medium">Operational</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -412,10 +412,10 @@ const ModernAdminDashboard: React.FC<ModernAdminDashboardProps> = ({ isAuthentic
|
||||
{/* Recent Activity */}
|
||||
<div className="admin-glass-card p-6 rounded-xl md:col-span-2">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2 className="text-xl font-bold text-white">Recent Activity</h2>
|
||||
<h2 className="text-xl font-bold text-stone-900">Recent Activity</h2>
|
||||
<button
|
||||
onClick={() => loadAllData()}
|
||||
className="text-blue-400 hover:text-blue-300 text-sm font-medium px-3 py-1 admin-glass-light rounded-lg transition-colors"
|
||||
className="text-stone-500 hover:text-stone-800 text-sm font-medium px-3 py-1 bg-stone-100 rounded-lg transition-colors border border-stone-200"
|
||||
>
|
||||
Refresh
|
||||
</button>
|
||||
@@ -424,19 +424,19 @@ const ModernAdminDashboard: React.FC<ModernAdminDashboardProps> = ({ isAuthentic
|
||||
{/* Mobile: vertical stack, Desktop: horizontal columns */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-6">
|
||||
<h3 className="text-sm font-medium text-white/60 uppercase tracking-wider">Projects</h3>
|
||||
<h3 className="text-xs font-bold text-stone-400 uppercase tracking-wider">Projects</h3>
|
||||
<div className="space-y-4">
|
||||
{projects.slice(0, 3).map((project) => (
|
||||
<div key={project.id} className="flex items-start space-x-3 p-4 admin-glass-light rounded-lg hover:scale-[1.02] transition-all duration-200 cursor-pointer" onClick={() => setActiveTab('projects')}>
|
||||
<div key={project.id} className="flex items-start space-x-3 p-4 bg-stone-50 border border-stone-100 rounded-lg hover:shadow-sm transition-all duration-200 cursor-pointer" onClick={() => setActiveTab('projects')}>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-white font-medium text-sm truncate">{project.title}</p>
|
||||
<p className="text-white/60 text-xs">{project.published ? 'Published' : 'Draft'} • {project.analytics?.views || 0} views</p>
|
||||
<p className="text-stone-800 font-medium text-sm truncate">{project.title}</p>
|
||||
<p className="text-stone-500 text-xs">{project.published ? 'Published' : 'Draft'} • {project.analytics?.views || 0} views</p>
|
||||
<div className="flex items-center space-x-2 mt-2">
|
||||
<span className={`px-2 py-1 rounded-full text-xs ${project.published ? 'bg-green-500/20 text-green-400' : 'bg-yellow-500/20 text-yellow-400'}`}>
|
||||
<span className={`px-2 py-1 rounded-full text-xs font-medium ${project.published ? 'bg-green-100 text-green-700' : 'bg-yellow-100 text-yellow-700'}`}>
|
||||
{project.published ? 'Live' : 'Draft'}
|
||||
</span>
|
||||
{project.featured && (
|
||||
<span className="px-2 py-1 bg-purple-500/20 text-purple-400 rounded-full text-xs">Featured</span>
|
||||
<span className="px-2 py-1 bg-stone-200 text-stone-700 rounded-full text-xs font-medium">Featured</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -446,19 +446,19 @@ const ModernAdminDashboard: React.FC<ModernAdminDashboardProps> = ({ isAuthentic
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-sm font-medium text-white/60 uppercase tracking-wider">Messages</h3>
|
||||
<h3 className="text-xs font-bold text-stone-400 uppercase tracking-wider">Messages</h3>
|
||||
<div className="space-y-3">
|
||||
{emails.slice(0, 3).map((email, index) => (
|
||||
<div key={index} className="flex items-center space-x-3 p-3 admin-glass-light rounded-lg hover:scale-[1.02] transition-all duration-200 cursor-pointer" onClick={() => setActiveTab('emails')}>
|
||||
<div className="w-8 h-8 bg-green-500/30 rounded-lg flex items-center justify-center flex-shrink-0">
|
||||
<Mail size={14} className="text-green-400" />
|
||||
<div key={index} className="flex items-center space-x-3 p-3 bg-stone-50 border border-stone-100 rounded-lg hover:shadow-sm transition-all duration-200 cursor-pointer" onClick={() => setActiveTab('emails')}>
|
||||
<div className="w-8 h-8 bg-stone-200 rounded-lg flex items-center justify-center flex-shrink-0">
|
||||
<Mail size={14} className="text-stone-600" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-white font-medium text-sm truncate">From {email.name as string}</p>
|
||||
<p className="text-white/60 text-xs truncate">{(email.subject as string) || 'No subject'}</p>
|
||||
<p className="text-stone-800 font-medium text-sm truncate">From {email.name as string}</p>
|
||||
<p className="text-stone-500 text-xs truncate">{(email.subject as string) || 'No subject'}</p>
|
||||
</div>
|
||||
{!(email.read as boolean) && (
|
||||
<div className="w-2 h-2 bg-red-400 rounded-full flex-shrink-0"></div>
|
||||
<div className="w-2 h-2 bg-red-500 rounded-full flex-shrink-0"></div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
@@ -469,70 +469,70 @@ const ModernAdminDashboard: React.FC<ModernAdminDashboardProps> = ({ isAuthentic
|
||||
|
||||
{/* Quick Actions */}
|
||||
<div className="admin-glass-card p-6 rounded-xl">
|
||||
<h2 className="text-xl font-bold text-white mb-6">Quick Actions</h2>
|
||||
<h2 className="text-xl font-bold text-stone-900 mb-6">Quick Actions</h2>
|
||||
<div className="space-y-4">
|
||||
<button
|
||||
onClick={() => window.location.href = '/editor'}
|
||||
className="w-full flex items-center space-x-3 p-3 admin-glass-light rounded-lg hover:scale-[1.02] transition-all duration-200 text-left group"
|
||||
className="w-full flex items-center space-x-3 p-3 bg-stone-50 border border-stone-100 rounded-lg hover:shadow-sm hover:bg-white transition-all duration-200 text-left group"
|
||||
>
|
||||
<div className="w-10 h-10 bg-green-500/30 rounded-lg flex items-center justify-center group-hover:bg-green-500/40 transition-colors">
|
||||
<Plus size={18} className="text-green-400" />
|
||||
<div className="w-10 h-10 bg-white rounded-lg border border-stone-100 flex items-center justify-center group-hover:border-stone-300 transition-colors">
|
||||
<Plus size={18} className="text-stone-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-white font-medium text-sm">Ghost Editor</p>
|
||||
<p className="text-white/60 text-xs">Professional writing tool</p>
|
||||
<p className="text-stone-800 font-medium text-sm">Ghost Editor</p>
|
||||
<p className="text-stone-500 text-xs">Professional writing tool</p>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setActiveTab('analytics')}
|
||||
className="w-full flex items-center space-x-3 p-3 admin-glass-light rounded-lg hover:scale-[1.02] transition-all duration-200 text-left group"
|
||||
className="w-full flex items-center space-x-3 p-3 bg-stone-50 border border-stone-100 rounded-lg hover:shadow-sm hover:bg-white transition-all duration-200 text-left group"
|
||||
>
|
||||
<div className="w-10 h-10 bg-red-500/30 rounded-lg flex items-center justify-center group-hover:bg-red-500/40 transition-colors">
|
||||
<Activity size={18} className="text-red-400" />
|
||||
<div className="w-10 h-10 bg-white rounded-lg border border-stone-100 flex items-center justify-center group-hover:border-stone-300 transition-colors">
|
||||
<Activity size={18} className="text-stone-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-white font-medium text-sm">Reset Analytics</p>
|
||||
<p className="text-white/60 text-xs">Clear analytics data</p>
|
||||
<p className="text-stone-800 font-medium text-sm">Reset Analytics</p>
|
||||
<p className="text-stone-500 text-xs">Clear analytics data</p>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setActiveTab('emails')}
|
||||
className="w-full flex items-center space-x-3 p-3 admin-glass-light rounded-lg hover:scale-[1.02] transition-all duration-200 text-left group"
|
||||
className="w-full flex items-center space-x-3 p-3 bg-stone-50 border border-stone-100 rounded-lg hover:shadow-sm hover:bg-white transition-all duration-200 text-left group"
|
||||
>
|
||||
<div className="w-10 h-10 bg-green-500/30 rounded-lg flex items-center justify-center group-hover:bg-green-500/40 transition-colors">
|
||||
<Mail size={18} className="text-green-400" />
|
||||
<div className="w-10 h-10 bg-white rounded-lg border border-stone-100 flex items-center justify-center group-hover:border-stone-300 transition-colors">
|
||||
<Mail size={18} className="text-stone-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-white font-medium text-sm">View Messages</p>
|
||||
<p className="text-white/60 text-xs">{stats.unreadEmails} unread messages</p>
|
||||
<p className="text-stone-800 font-medium text-sm">View Messages</p>
|
||||
<p className="text-stone-500 text-xs">{stats.unreadEmails} unread messages</p>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setActiveTab('analytics')}
|
||||
className="w-full flex items-center space-x-3 p-3 admin-glass-light rounded-lg hover:scale-[1.02] transition-all duration-200 text-left group"
|
||||
className="w-full flex items-center space-x-3 p-3 bg-stone-50 border border-stone-100 rounded-lg hover:shadow-sm hover:bg-white transition-all duration-200 text-left group"
|
||||
>
|
||||
<div className="w-10 h-10 bg-purple-500/30 rounded-lg flex items-center justify-center group-hover:bg-purple-500/40 transition-colors">
|
||||
<TrendingUp size={18} className="text-purple-400" />
|
||||
<div className="w-10 h-10 bg-white rounded-lg border border-stone-100 flex items-center justify-center group-hover:border-stone-300 transition-colors">
|
||||
<TrendingUp size={18} className="text-stone-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-white font-medium text-sm">Analytics</p>
|
||||
<p className="text-white/60 text-xs">View detailed statistics</p>
|
||||
<p className="text-stone-800 font-medium text-sm">Analytics</p>
|
||||
<p className="text-stone-500 text-xs">View detailed statistics</p>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setActiveTab('settings')}
|
||||
className="w-full flex items-center space-x-3 p-3 admin-glass-light rounded-lg hover:scale-[1.02] transition-all duration-200 text-left group"
|
||||
className="w-full flex items-center space-x-3 p-3 bg-stone-50 border border-stone-100 rounded-lg hover:shadow-sm hover:bg-white transition-all duration-200 text-left group"
|
||||
>
|
||||
<div className="w-10 h-10 bg-gray-500/30 rounded-lg flex items-center justify-center group-hover:bg-gray-500/40 transition-colors">
|
||||
<Settings size={18} className="text-gray-400" />
|
||||
<div className="w-10 h-10 bg-white rounded-lg border border-stone-100 flex items-center justify-center group-hover:border-stone-300 transition-colors">
|
||||
<Settings size={18} className="text-stone-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-white font-medium text-sm">Settings</p>
|
||||
<p className="text-white/60 text-xs">System configuration</p>
|
||||
<p className="text-stone-800 font-medium text-sm">Settings</p>
|
||||
<p className="text-stone-500 text-xs">System configuration</p>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
@@ -545,8 +545,8 @@ const ModernAdminDashboard: React.FC<ModernAdminDashboardProps> = ({ isAuthentic
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-white">Project Management</h2>
|
||||
<p className="text-white/70 mt-1">Manage your portfolio projects</p>
|
||||
<h2 className="text-2xl font-bold text-stone-900">Project Management</h2>
|
||||
<p className="text-stone-500 mt-1">Manage your portfolio projects</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -565,39 +565,39 @@ const ModernAdminDashboard: React.FC<ModernAdminDashboardProps> = ({ isAuthentic
|
||||
{activeTab === 'settings' && (
|
||||
<div className="space-y-8">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-white">System Settings</h1>
|
||||
<p className="text-white/60">Manage system configuration and preferences</p>
|
||||
<h1 className="text-2xl font-bold text-stone-900">System Settings</h1>
|
||||
<p className="text-stone-500">Manage system configuration and preferences</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<div className="admin-glass-card p-6 rounded-xl">
|
||||
<h2 className="text-xl font-bold text-white mb-4">Import / Export</h2>
|
||||
<p className="text-white/70 mb-4">Backup and restore your portfolio data</p>
|
||||
<h2 className="text-xl font-bold text-stone-900 mb-4">Import / Export</h2>
|
||||
<p className="text-stone-500 mb-4">Backup and restore your portfolio data</p>
|
||||
<ImportExport />
|
||||
</div>
|
||||
|
||||
<div className="admin-glass-card p-6 rounded-xl">
|
||||
<h2 className="text-xl font-bold text-white mb-4">System Status</h2>
|
||||
<h2 className="text-xl font-bold text-stone-900 mb-4">System Status</h2>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between p-3 bg-white/5 rounded-lg">
|
||||
<span className="text-white/80">Database</span>
|
||||
<div className="flex items-center justify-between p-3 bg-stone-50 rounded-lg border border-stone-100">
|
||||
<span className="text-stone-600">Database</span>
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-4 h-4 bg-green-400 rounded-full animate-pulse"></div>
|
||||
<span className="text-green-400 font-medium">Online</span>
|
||||
<div className="w-4 h-4 bg-green-500 rounded-full animate-pulse"></div>
|
||||
<span className="text-green-600 font-medium">Online</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-3 bg-white/5 rounded-lg">
|
||||
<span className="text-white/80">Redis Cache</span>
|
||||
<div className="flex items-center justify-between p-3 bg-stone-50 rounded-lg border border-stone-100">
|
||||
<span className="text-stone-600">Redis Cache</span>
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-4 h-4 bg-green-400 rounded-full animate-pulse"></div>
|
||||
<span className="text-green-400 font-medium">Online</span>
|
||||
<div className="w-4 h-4 bg-green-500 rounded-full animate-pulse"></div>
|
||||
<span className="text-green-600 font-medium">Online</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-3 bg-white/5 rounded-lg">
|
||||
<span className="text-white/80">API Services</span>
|
||||
<div className="flex items-center justify-between p-3 bg-stone-50 rounded-lg border border-stone-100">
|
||||
<span className="text-stone-600">API Services</span>
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-4 h-4 bg-green-400 rounded-full animate-pulse"></div>
|
||||
<span className="text-green-400 font-medium">Online</span>
|
||||
<div className="w-4 h-4 bg-green-500 rounded-full animate-pulse"></div>
|
||||
<span className="text-green-600 font-medium">Online</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -75,7 +75,7 @@ export const PerformanceDashboard: React.FC = () => {
|
||||
setIsVisible(true);
|
||||
trackEvent('dashboard-toggle', { action: 'show' });
|
||||
}}
|
||||
className="fixed bottom-4 right-4 bg-blue-600 text-white px-4 py-2 rounded-lg shadow-lg hover:bg-blue-700 transition-colors z-50"
|
||||
className="fixed bottom-4 right-4 bg-white text-stone-700 border border-stone-200 px-4 py-2 rounded-lg shadow-md hover:bg-stone-50 transition-colors z-50"
|
||||
>
|
||||
📊 Performance
|
||||
</button>
|
||||
|
||||
@@ -52,7 +52,6 @@ export const ProjectManager: React.FC<ProjectManagerProps> = ({
|
||||
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [selectedCategory, setSelectedCategory] = useState<string>('all');
|
||||
// Editor is now a separate page - no modal state needed
|
||||
|
||||
const categories = ['all', 'Web Development', 'Full-Stack', 'Web Application', 'Mobile App', 'Design'];
|
||||
|
||||
@@ -77,10 +76,6 @@ export const ProjectManager: React.FC<ProjectManagerProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
// closeEditor removed - editor is now separate page
|
||||
|
||||
// saveProject removed - editor is now separate page
|
||||
|
||||
const deleteProject = async (projectId: string) => {
|
||||
if (!confirm('Are you sure you want to delete this project?')) return;
|
||||
|
||||
@@ -100,9 +95,9 @@ export const ProjectManager: React.FC<ProjectManagerProps> = ({
|
||||
|
||||
const getStatusColor = (project: Project) => {
|
||||
if (project.published) {
|
||||
return project.featured ? 'text-purple-400 bg-purple-500/20' : 'text-green-400 bg-green-500/20';
|
||||
return project.featured ? 'text-stone-700 bg-stone-200' : 'text-green-700 bg-green-100';
|
||||
}
|
||||
return 'text-yellow-400 bg-yellow-500/20';
|
||||
return 'text-yellow-700 bg-yellow-100';
|
||||
};
|
||||
|
||||
const getStatusText = (project: Project) => {
|
||||
@@ -117,20 +112,20 @@ export const ProjectManager: React.FC<ProjectManagerProps> = ({
|
||||
{/* Header */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-white">Project Management</h1>
|
||||
<p className="text-white/80">{projects.length} projects • {projects.filter(p => p.published).length} published</p>
|
||||
<h1 className="text-3xl font-bold text-stone-900">Project Management</h1>
|
||||
<p className="text-stone-500">{projects.length} projects • {projects.filter(p => p.published).length} published</p>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
<button
|
||||
onClick={onProjectsChange}
|
||||
className="flex items-center space-x-2 px-4 py-2 admin-glass-light rounded-xl hover:scale-105 transition-all duration-200"
|
||||
className="flex items-center space-x-2 px-4 py-2 bg-stone-100 border border-stone-200 rounded-xl hover:bg-stone-200 transition-all duration-200"
|
||||
>
|
||||
<RefreshCw className="w-4 h-4 text-blue-400" />
|
||||
<span className="text-white font-medium">Refresh</span>
|
||||
<RefreshCw className="w-4 h-4 text-stone-600" />
|
||||
<span className="text-stone-700 font-medium">Refresh</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => openEditor()}
|
||||
className="flex items-center space-x-2 px-6 py-2 bg-gradient-to-r from-blue-500 to-purple-500 text-white rounded-xl hover:scale-105 transition-all duration-200 shadow-lg"
|
||||
className="flex items-center space-x-2 px-6 py-2 bg-stone-900 text-white rounded-xl hover:bg-stone-800 transition-all duration-200 shadow-md"
|
||||
>
|
||||
<Plus size={18} />
|
||||
<span className="font-medium">New Project</span>
|
||||
@@ -142,13 +137,13 @@ export const ProjectManager: React.FC<ProjectManagerProps> = ({
|
||||
<div className="flex flex-col sm:flex-row gap-4">
|
||||
{/* Search */}
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-white/60" />
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-stone-400" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search projects..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="w-full pl-10 pr-4 py-3 admin-glass-light border border-white/30 rounded-xl text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
className="w-full pl-10 pr-4 py-3 bg-white border border-stone-200 rounded-xl text-stone-900 placeholder:text-stone-400 focus:outline-none focus:ring-2 focus:ring-stone-400"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -156,23 +151,23 @@ export const ProjectManager: React.FC<ProjectManagerProps> = ({
|
||||
<select
|
||||
value={selectedCategory}
|
||||
onChange={(e) => setSelectedCategory(e.target.value)}
|
||||
className="px-4 py-3 admin-glass-light border border-white/30 rounded-xl text-white focus:outline-none focus:ring-2 focus:ring-blue-500 bg-transparent"
|
||||
className="px-4 py-3 bg-white border border-stone-200 rounded-xl text-stone-900 focus:outline-none focus:ring-2 focus:ring-stone-400"
|
||||
>
|
||||
{categories.map(category => (
|
||||
<option key={category} value={category} className="bg-gray-800">
|
||||
<option key={category} value={category} className="bg-white text-stone-900">
|
||||
{category === 'all' ? 'All Categories' : category}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
{/* View Toggle */}
|
||||
<div className="flex items-center space-x-1 admin-glass-light rounded-xl p-1">
|
||||
<div className="flex items-center space-x-1 bg-white border border-stone-200 rounded-xl p-1">
|
||||
<button
|
||||
onClick={() => setViewMode('grid')}
|
||||
className={`p-2 rounded-lg transition-all duration-200 ${
|
||||
viewMode === 'grid'
|
||||
? 'bg-blue-500/40 text-blue-300'
|
||||
: 'text-white/70 hover:text-white hover:bg-white/10'
|
||||
? 'bg-stone-100 text-stone-900'
|
||||
: 'text-stone-400 hover:text-stone-900 hover:bg-stone-50'
|
||||
}`}
|
||||
>
|
||||
<Grid className="w-4 h-4" />
|
||||
@@ -181,8 +176,8 @@ export const ProjectManager: React.FC<ProjectManagerProps> = ({
|
||||
onClick={() => setViewMode('list')}
|
||||
className={`p-2 rounded-lg transition-all duration-200 ${
|
||||
viewMode === 'list'
|
||||
? 'bg-blue-500/40 text-blue-300'
|
||||
: 'text-white/70 hover:text-white hover:bg-white/10'
|
||||
? 'bg-stone-100 text-stone-900'
|
||||
: 'text-stone-400 hover:text-stone-900 hover:bg-stone-50'
|
||||
}`}
|
||||
>
|
||||
<List className="w-4 h-4" />
|
||||
@@ -198,24 +193,24 @@ export const ProjectManager: React.FC<ProjectManagerProps> = ({
|
||||
key={project.id}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="admin-glass-card p-6 rounded-xl hover:scale-105 transition-all duration-300 group"
|
||||
className="admin-glass-card p-6 rounded-xl hover:shadow-lg transition-all duration-300 group bg-white border border-stone-200"
|
||||
>
|
||||
{/* Project Header */}
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div className="flex-1">
|
||||
<h3 className="text-xl font-bold text-white mb-1">{project.title}</h3>
|
||||
<p className="text-white/70 text-sm">{project.category}</p>
|
||||
<h3 className="text-xl font-bold text-stone-900 mb-1">{project.title}</h3>
|
||||
<p className="text-stone-500 text-sm">{project.category}</p>
|
||||
</div>
|
||||
<div className="flex items-center space-x-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<button
|
||||
onClick={() => openEditor(project)}
|
||||
className="p-2 text-white/70 hover:text-blue-400 hover:bg-white/10 rounded-lg transition-colors"
|
||||
className="p-2 text-stone-500 hover:text-stone-700 hover:bg-stone-100 rounded-lg transition-colors"
|
||||
>
|
||||
<Edit size={16} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => deleteProject(project.id)}
|
||||
className="p-2 text-white/70 hover:text-red-400 hover:bg-white/10 rounded-lg transition-colors"
|
||||
className="p-2 text-stone-500 hover:text-red-600 hover:bg-stone-100 rounded-lg transition-colors"
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
</button>
|
||||
@@ -225,7 +220,7 @@ export const ProjectManager: React.FC<ProjectManagerProps> = ({
|
||||
{/* Project Content */}
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<p className="text-white/70 text-sm line-clamp-2 leading-relaxed">{project.description}</p>
|
||||
<p className="text-stone-600 text-sm line-clamp-2 leading-relaxed">{project.description}</p>
|
||||
</div>
|
||||
|
||||
{/* Tags */}
|
||||
@@ -234,13 +229,13 @@ export const ProjectManager: React.FC<ProjectManagerProps> = ({
|
||||
{project.tags.slice(0, 3).map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="px-2 py-1 bg-white/10 text-white/70 rounded-full text-xs"
|
||||
className="px-2 py-1 bg-stone-100 text-stone-600 border border-stone-200 rounded-full text-xs"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
{project.tags.length > 3 && (
|
||||
<span className="px-2 py-1 bg-white/10 text-white/70 rounded-full text-xs">
|
||||
<span className="px-2 py-1 bg-stone-100 text-stone-600 border border-stone-200 rounded-full text-xs">
|
||||
+{project.tags.length - 3}
|
||||
</span>
|
||||
)}
|
||||
@@ -258,7 +253,7 @@ export const ProjectManager: React.FC<ProjectManagerProps> = ({
|
||||
href={project.github}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="p-1 text-white/60 hover:text-white transition-colors"
|
||||
className="p-1 text-stone-400 hover:text-stone-900 transition-colors"
|
||||
>
|
||||
<Github size={14} />
|
||||
</a>
|
||||
@@ -268,7 +263,7 @@ export const ProjectManager: React.FC<ProjectManagerProps> = ({
|
||||
href={project.live}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="p-1 text-white/60 hover:text-white transition-colors"
|
||||
className="p-1 text-stone-400 hover:text-stone-900 transition-colors"
|
||||
>
|
||||
<Globe size={14} />
|
||||
</a>
|
||||
@@ -277,18 +272,18 @@ export const ProjectManager: React.FC<ProjectManagerProps> = ({
|
||||
</div>
|
||||
|
||||
{/* Analytics */}
|
||||
<div className="grid grid-cols-3 gap-2 pt-3 border-t border-white/10">
|
||||
<div className="grid grid-cols-3 gap-2 pt-3 border-t border-stone-100">
|
||||
<div className="text-center">
|
||||
<p className="text-white font-bold text-sm">{project.analytics?.views || 0}</p>
|
||||
<p className="text-white/60 text-xs">Views</p>
|
||||
<p className="text-stone-900 font-bold text-sm">{project.analytics?.views || 0}</p>
|
||||
<p className="text-stone-500 text-xs">Views</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="text-white font-bold text-sm">{project.analytics?.likes || 0}</p>
|
||||
<p className="text-white/60 text-xs">Likes</p>
|
||||
<p className="text-stone-900 font-bold text-sm">{project.analytics?.likes || 0}</p>
|
||||
<p className="text-stone-500 text-xs">Likes</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="text-white font-bold text-sm">{project.performance?.lighthouse || 90}</p>
|
||||
<p className="text-white/60 text-xs">Score</p>
|
||||
<p className="text-stone-900 font-bold text-sm">{project.performance?.lighthouse || 90}</p>
|
||||
<p className="text-stone-500 text-xs">Score</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -302,13 +297,13 @@ export const ProjectManager: React.FC<ProjectManagerProps> = ({
|
||||
key={project.id}
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
className="admin-glass-card p-6 rounded-xl hover:scale-[1.01] transition-all duration-300 group"
|
||||
className="admin-glass-card p-6 rounded-xl hover:shadow-md transition-all duration-300 group bg-white border border-stone-200"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="flex-1">
|
||||
<h3 className="text-white font-bold text-lg">{project.title}</h3>
|
||||
<p className="text-white/70 text-sm">{project.category}</p>
|
||||
<h3 className="text-stone-900 font-bold text-lg">{project.title}</h3>
|
||||
<p className="text-stone-500 text-sm">{project.category}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -316,7 +311,7 @@ export const ProjectManager: React.FC<ProjectManagerProps> = ({
|
||||
<span className={`px-3 py-1 rounded-full text-xs font-medium ${getStatusColor(project)}`}>
|
||||
{getStatusText(project)}
|
||||
</span>
|
||||
<div className="flex items-center space-x-3 text-white/60 text-sm">
|
||||
<div className="flex items-center space-x-3 text-stone-500 text-sm">
|
||||
<span>{project.analytics?.views || 0} views</span>
|
||||
<span>•</span>
|
||||
<span>{new Date(project.updatedAt).toLocaleDateString()}</span>
|
||||
@@ -324,13 +319,13 @@ export const ProjectManager: React.FC<ProjectManagerProps> = ({
|
||||
<div className="flex items-center space-x-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<button
|
||||
onClick={() => openEditor(project)}
|
||||
className="p-2 text-white/70 hover:text-blue-400 hover:bg-white/10 rounded-lg transition-colors"
|
||||
className="p-2 text-stone-500 hover:text-stone-700 hover:bg-stone-100 rounded-lg transition-colors"
|
||||
>
|
||||
<Edit size={16} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => deleteProject(project.id)}
|
||||
className="p-2 text-white/70 hover:text-red-400 hover:bg-white/10 rounded-lg transition-colors"
|
||||
className="p-2 text-stone-500 hover:text-red-600 hover:bg-stone-100 rounded-lg transition-colors"
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
</button>
|
||||
@@ -341,8 +336,6 @@ export const ProjectManager: React.FC<ProjectManagerProps> = ({
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Editor is now a separate page at /editor */}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -50,9 +50,9 @@ const ToastItem = ({ toast, onRemove }: ToastProps) => {
|
||||
case 'warning':
|
||||
return <AlertTriangle className="w-5 h-5 text-yellow-400" />;
|
||||
case 'info':
|
||||
return <Info className="w-5 h-5 text-blue-400" />;
|
||||
return <Info className="w-5 h-5 text-stone-400" />;
|
||||
default:
|
||||
return <Info className="w-5 h-5 text-blue-400" />;
|
||||
return <Info className="w-5 h-5 text-stone-400" />;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -112,7 +112,7 @@ const ToastItem = ({ toast, onRemove }: ToastProps) => {
|
||||
initial={{ width: '100%' }}
|
||||
animate={{ width: '0%' }}
|
||||
transition={{ duration: (toast.duration || 5000) / 1000, ease: "linear" }}
|
||||
className="absolute bottom-0 left-0 h-1 bg-gradient-to-r from-blue-400 to-green-400 rounded-b-xl"
|
||||
className="absolute bottom-0 left-0 h-1 bg-gradient-to-r from-stone-400 to-stone-600 rounded-b-xl"
|
||||
/>
|
||||
)}
|
||||
</motion.div>
|
||||
|
||||
Reference in New Issue
Block a user