Spaces:
Running
Running
Anish commited on
Commit ·
825fa02
1
Parent(s): 67264dd
[Minor changes to Frontend]
Browse files- backend/app/api/auth_routes.py +2 -1
- frontend/app/dashboard/page.tsx +14 -1
- frontend/app/globals.css +1 -1
- frontend/app/layout.tsx +2 -2
- frontend/app/login/page.tsx +2 -2
- frontend/app/not-found.tsx +7 -34
- frontend/app/page.tsx +11 -1
- frontend/app/profile/page.tsx +3 -1
- frontend/app/result/[id]/page.tsx +27 -14
- frontend/components/dashboard/AnalyticsSection.tsx +3 -3
- frontend/components/dashboard/HistoryGrid.tsx +2 -2
- frontend/components/shared/FeedbackModal.tsx +1 -1
- frontend/components/shared/Navbar.tsx +4 -2
- frontend/components/upload/UploadZone.tsx +1 -2
- frontend/contexts/AuthContext.tsx +9 -2
backend/app/api/auth_routes.py
CHANGED
|
@@ -134,5 +134,6 @@ def get_current_active_user(
|
|
| 134 |
"name": getattr(current_user, 'username', getattr(current_user, 'email', 'User')),
|
| 135 |
"is_admin": getattr(current_user, 'is_admin', False),
|
| 136 |
"avatar_url": getattr(current_user, 'avatar_url', None),
|
| 137 |
-
"google_avatar_url": getattr(current_user, 'google_avatar_url', None)
|
|
|
|
| 138 |
}
|
|
|
|
| 134 |
"name": getattr(current_user, 'username', getattr(current_user, 'email', 'User')),
|
| 135 |
"is_admin": getattr(current_user, 'is_admin', False),
|
| 136 |
"avatar_url": getattr(current_user, 'avatar_url', None),
|
| 137 |
+
"google_avatar_url": getattr(current_user, 'google_avatar_url', None),
|
| 138 |
+
"created_at": current_user.created_at.isoformat() if getattr(current_user, 'created_at', None) else None
|
| 139 |
}
|
frontend/app/dashboard/page.tsx
CHANGED
|
@@ -11,7 +11,7 @@ import { motion, AnimatePresence } from "framer-motion";
|
|
| 11 |
import { useRouter } from "next/navigation";
|
| 12 |
|
| 13 |
export default function DashboardPage() {
|
| 14 |
-
const { isAuthenticated, loading
|
| 15 |
const router = useRouter();
|
| 16 |
|
| 17 |
const [files, setFiles] = useState<any[]>([]);
|
|
@@ -34,6 +34,16 @@ export default function DashboardPage() {
|
|
| 34 |
}
|
| 35 |
}, [isAuthenticated, loading, router]);
|
| 36 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
const handleDeleteFile = async (id: string, filename: string) => {
|
| 38 |
try {
|
| 39 |
await apiLayer.deleteFile(id);
|
|
@@ -196,6 +206,9 @@ export default function DashboardPage() {
|
|
| 196 |
animate={{ opacity: 1, backdropFilter: 'blur(20px)' }}
|
| 197 |
exit={{ opacity: 0, backdropFilter: 'blur(0px)' }}
|
| 198 |
className="fixed inset-0 z-[100] bg-black/60 flex justify-center items-center p-4"
|
|
|
|
|
|
|
|
|
|
| 199 |
>
|
| 200 |
<motion.div
|
| 201 |
initial={{ scale: 0.95, y: 20 }}
|
|
|
|
| 11 |
import { useRouter } from "next/navigation";
|
| 12 |
|
| 13 |
export default function DashboardPage() {
|
| 14 |
+
const { isAuthenticated, loading } = useAuth();
|
| 15 |
const router = useRouter();
|
| 16 |
|
| 17 |
const [files, setFiles] = useState<any[]>([]);
|
|
|
|
| 34 |
}
|
| 35 |
}, [isAuthenticated, loading, router]);
|
| 36 |
|
| 37 |
+
// Scroll Lock for Modal
|
| 38 |
+
useEffect(() => {
|
| 39 |
+
if (isUploadModalOpen) {
|
| 40 |
+
document.body.style.overflow = 'hidden';
|
| 41 |
+
} else {
|
| 42 |
+
document.body.style.overflow = '';
|
| 43 |
+
}
|
| 44 |
+
return () => { document.body.style.overflow = ''; };
|
| 45 |
+
}, [isUploadModalOpen]);
|
| 46 |
+
|
| 47 |
const handleDeleteFile = async (id: string, filename: string) => {
|
| 48 |
try {
|
| 49 |
await apiLayer.deleteFile(id);
|
|
|
|
| 206 |
animate={{ opacity: 1, backdropFilter: 'blur(20px)' }}
|
| 207 |
exit={{ opacity: 0, backdropFilter: 'blur(0px)' }}
|
| 208 |
className="fixed inset-0 z-[100] bg-black/60 flex justify-center items-center p-4"
|
| 209 |
+
onClick={(e) => {
|
| 210 |
+
if (e.target === e.currentTarget) setIsUploadModalOpen(false);
|
| 211 |
+
}}
|
| 212 |
>
|
| 213 |
<motion.div
|
| 214 |
initial={{ scale: 0.95, y: 20 }}
|
frontend/app/globals.css
CHANGED
|
@@ -56,7 +56,7 @@
|
|
| 56 |
:root {
|
| 57 |
--theme-bg-val: #080c14;
|
| 58 |
--theme-text-val: #fde8d6;
|
| 59 |
-
--theme-border-val:
|
| 60 |
--theme-glass-val: rgba(8, 12, 20, 0.8);
|
| 61 |
--theme-glass-border-val: rgba(255, 255, 255, 0.1);
|
| 62 |
--theme-bg: var(--theme-bg-val);
|
|
|
|
| 56 |
:root {
|
| 57 |
--theme-bg-val: #080c14;
|
| 58 |
--theme-text-val: #fde8d6;
|
| 59 |
+
--theme-border-val: #ffffff0d;
|
| 60 |
--theme-glass-val: rgba(8, 12, 20, 0.8);
|
| 61 |
--theme-glass-border-val: rgba(255, 255, 255, 0.1);
|
| 62 |
--theme-bg: var(--theme-bg-val);
|
frontend/app/layout.tsx
CHANGED
|
@@ -18,8 +18,8 @@ const geistMono = Geist_Mono({
|
|
| 18 |
});
|
| 19 |
|
| 20 |
export const metadata: Metadata = {
|
| 21 |
-
title: "
|
| 22 |
-
description: "
|
| 23 |
};
|
| 24 |
|
| 25 |
import { AuthProvider } from "@/contexts/AuthContext";
|
|
|
|
| 18 |
});
|
| 19 |
|
| 20 |
export const metadata: Metadata = {
|
| 21 |
+
title: "Spotix",
|
| 22 |
+
description: "Detect AI generated Image/Video",
|
| 23 |
};
|
| 24 |
|
| 25 |
import { AuthProvider } from "@/contexts/AuthContext";
|
frontend/app/login/page.tsx
CHANGED
|
@@ -94,7 +94,7 @@ export default function LoginPage() {
|
|
| 94 |
};
|
| 95 |
|
| 96 |
return (
|
| 97 |
-
<div id="login-section" className="min-h-[100vh] bg-[var(--theme-bg)] text-[var(--theme-text)] font-sans relative flex justify-center pt-[
|
| 98 |
<style dangerouslySetInnerHTML={{
|
| 99 |
__html: `
|
| 100 |
html, body, a, button, [role="button"], input, select, textarea, .cursor-pointer { cursor: none !important; }
|
|
@@ -137,7 +137,7 @@ export default function LoginPage() {
|
|
| 137 |
<div className="absolute -top-40 -right-40 w-80 h-80 bg-theme-text/5 rounded-full blur-[80px] pointer-events-none"></div>
|
| 138 |
|
| 139 |
<h1 className="text-3xl tracking-widest font-bold mb-2 font-sans uppercase">
|
| 140 |
-
Neural<span className="text-[var(--theme-
|
| 141 |
</h1>
|
| 142 |
<p className="text-[#d0c4bb]/60 text-sm mb-6">Authenticate to seamlessly vault and analyze.</p>
|
| 143 |
|
|
|
|
| 94 |
};
|
| 95 |
|
| 96 |
return (
|
| 97 |
+
<div id="login-section" className="min-h-[100vh] bg-[var(--theme-bg)] text-[var(--theme-text)] font-sans relative flex justify-center pt-[10vh] px-4 overflow-y-auto overflow-x-hidden">
|
| 98 |
<style dangerouslySetInnerHTML={{
|
| 99 |
__html: `
|
| 100 |
html, body, a, button, [role="button"], input, select, textarea, .cursor-pointer { cursor: none !important; }
|
|
|
|
| 137 |
<div className="absolute -top-40 -right-40 w-80 h-80 bg-theme-text/5 rounded-full blur-[80px] pointer-events-none"></div>
|
| 138 |
|
| 139 |
<h1 className="text-3xl tracking-widest font-bold mb-2 font-sans uppercase">
|
| 140 |
+
Neural<span className="text-[var(--theme-text)]/50"> Vault</span> Access
|
| 141 |
</h1>
|
| 142 |
<p className="text-[#d0c4bb]/60 text-sm mb-6">Authenticate to seamlessly vault and analyze.</p>
|
| 143 |
|
frontend/app/not-found.tsx
CHANGED
|
@@ -28,6 +28,7 @@ export default function NotFoundPage() {
|
|
| 28 |
|
| 29 |
.glitch-text {
|
| 30 |
position: relative;
|
|
|
|
| 31 |
}
|
| 32 |
.glitch-text::before, .glitch-text::after {
|
| 33 |
content: attr(data-text);
|
|
@@ -38,26 +39,6 @@ export default function NotFoundPage() {
|
|
| 38 |
height: 100%;
|
| 39 |
opacity: 0.8;
|
| 40 |
}
|
| 41 |
-
.glitch-text::before {
|
| 42 |
-
left: 2px;
|
| 43 |
-
text-shadow: -2px 0 red;
|
| 44 |
-
clip: rect(24px, 550px, 90px, 0);
|
| 45 |
-
animation: glitch-anim 3s infinite linear alternate-reverse;
|
| 46 |
-
}
|
| 47 |
-
.glitch-text::after {
|
| 48 |
-
left: -2px;
|
| 49 |
-
text-shadow: -2px 0 blue;
|
| 50 |
-
clip: rect(85px, 550px, 140px, 0);
|
| 51 |
-
animation: glitch-anim 2.5s infinite linear alternate-reverse;
|
| 52 |
-
}
|
| 53 |
-
@keyframes glitch-anim {
|
| 54 |
-
0% { clip: rect(23px, 9999px, 85px, 0); }
|
| 55 |
-
20% { clip: rect(98px, 9999px, 14px, 0); }
|
| 56 |
-
40% { clip: rect(43px, 9999px, 65px, 0); }
|
| 57 |
-
60% { clip: rect(12px, 9999px, 92px, 0); }
|
| 58 |
-
80% { clip: rect(76px, 9999px, 34px, 0); }
|
| 59 |
-
100% { clip: rect(54px, 9999px, 12px, 0); }
|
| 60 |
-
}
|
| 61 |
`
|
| 62 |
}} />
|
| 63 |
|
|
@@ -69,16 +50,8 @@ export default function NotFoundPage() {
|
|
| 69 |
backgroundSize: '40px 40px'
|
| 70 |
}}></div>
|
| 71 |
|
| 72 |
-
{/* Dynamic Spotlight */}
|
| 73 |
-
<div
|
| 74 |
-
className="absolute inset-0 pointer-events-none z-0 mix-blend-screen opacity-20 transition-opacity duration-300"
|
| 75 |
-
style={{
|
| 76 |
-
background: `radial-gradient(circle 400px at ${mousePos.x}px ${mousePos.y}px, var(--theme-text), transparent 80%)`
|
| 77 |
-
}}
|
| 78 |
-
/>
|
| 79 |
-
|
| 80 |
<div className="flex-1 flex flex-col items-center justify-center relative z-10 px-6 text-center">
|
| 81 |
-
<div className="
|
| 82 |
<AlertTriangle className="w-16 h-16 text-red-500/80 animate-pulse !cursor-none" />
|
| 83 |
</div>
|
| 84 |
|
|
@@ -87,19 +60,19 @@ export default function NotFoundPage() {
|
|
| 87 |
</h1>
|
| 88 |
|
| 89 |
<div className="bg-red-500/10 border border-red-500/20 text-red-400 font-mono uppercase tracking-[0.3em] text-xs px-4 py-2 rounded-full mb-8 backdrop-blur-md !cursor-none">
|
| 90 |
-
|
| 91 |
</div>
|
| 92 |
|
| 93 |
<p className="text-[#d0c4bb] max-w-lg mb-12 text-lg font-light leading-relaxed !cursor-none">
|
| 94 |
-
The requested temporal coordinate does not exist within the
|
| 95 |
</p>
|
| 96 |
|
| 97 |
<button
|
| 98 |
onClick={() => router.push('/')}
|
| 99 |
-
className="group relative overflow-hidden rounded-full bg-[var(--theme-text)] text-[var(--theme-bg)] px-10 py-4 font-bold tracking-widest text-sm uppercase transition-all duration-
|
| 100 |
>
|
| 101 |
-
<ArrowLeft className="w-5 h-5 transition-transform duration-
|
| 102 |
-
<span>Return to
|
| 103 |
<div className="absolute inset-0 bg-white/20 translate-y-[100%] group-hover:translate-y-[0%] transition-transform duration-500 ease-[cubic-bezier(0.23,1,0.32,1)] rounded-full !cursor-none"></div>
|
| 104 |
</button>
|
| 105 |
</div>
|
|
|
|
| 28 |
|
| 29 |
.glitch-text {
|
| 30 |
position: relative;
|
| 31 |
+
font-size: 200px;
|
| 32 |
}
|
| 33 |
.glitch-text::before, .glitch-text::after {
|
| 34 |
content: attr(data-text);
|
|
|
|
| 39 |
height: 100%;
|
| 40 |
opacity: 0.8;
|
| 41 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
`
|
| 43 |
}} />
|
| 44 |
|
|
|
|
| 50 |
backgroundSize: '40px 40px'
|
| 51 |
}}></div>
|
| 52 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
<div className="flex-1 flex flex-col items-center justify-center relative z-10 px-6 text-center">
|
| 54 |
+
<div className="mt-20 relative dash-border rounded-full p-6 bg-[var(--theme-text)]/5 shadow-[0_0_80px_rgba(253,232,214,0.1)]">
|
| 55 |
<AlertTriangle className="w-16 h-16 text-red-500/80 animate-pulse !cursor-none" />
|
| 56 |
</div>
|
| 57 |
|
|
|
|
| 60 |
</h1>
|
| 61 |
|
| 62 |
<div className="bg-red-500/10 border border-red-500/20 text-red-400 font-mono uppercase tracking-[0.3em] text-xs px-4 py-2 rounded-full mb-8 backdrop-blur-md !cursor-none">
|
| 63 |
+
ERROR // ENDPOINT NOT FOUND //
|
| 64 |
</div>
|
| 65 |
|
| 66 |
<p className="text-[#d0c4bb] max-w-lg mb-12 text-lg font-light leading-relaxed !cursor-none">
|
| 67 |
+
The requested temporal coordinate does not exist within the Spotix network.
|
| 68 |
</p>
|
| 69 |
|
| 70 |
<button
|
| 71 |
onClick={() => router.push('/')}
|
| 72 |
+
className="group relative overflow-hidden rounded-full bg-[var(--theme-text)] text-[var(--theme-bg)] px-10 py-4 font-bold tracking-widest text-sm uppercase transition-all duration-100 hover:shadow-[0_0_40px_rgba(253,232,214,0.1)] hover:scale-105 !cursor-none flex items-center gap-3"
|
| 73 |
>
|
| 74 |
+
<ArrowLeft className="w-5 h-5 transition-transform duration-100 group-hover:-translate-x-1 !cursor-none" />
|
| 75 |
+
<span>Return to Spotix</span>
|
| 76 |
<div className="absolute inset-0 bg-white/20 translate-y-[100%] group-hover:translate-y-[0%] transition-transform duration-500 ease-[cubic-bezier(0.23,1,0.32,1)] rounded-full !cursor-none"></div>
|
| 77 |
</button>
|
| 78 |
</div>
|
frontend/app/page.tsx
CHANGED
|
@@ -58,6 +58,16 @@ export default function LandingPage() {
|
|
| 58 |
return () => window.removeEventListener('keydown', handleKeyDown);
|
| 59 |
}, []);
|
| 60 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
// Global Cursor & Interactions
|
| 62 |
// Note: The global custom cursor is now managed by components/shared/CustomCursor.tsx
|
| 63 |
|
|
@@ -273,7 +283,7 @@ export default function LandingPage() {
|
|
| 273 |
<div className="loader-panel top"></div>
|
| 274 |
<div className="loader-panel bottom"></div>
|
| 275 |
<div className="loader-content flex flex-col items-center">
|
| 276 |
-
<div className="text-[10px] font-mono tracking-[0.5em] text-[var(--theme-text)] uppercase mb-4">Initializing
|
| 277 |
<div id="progress-bar-container">
|
| 278 |
<div id="progress-bar-fill" style={{ width: `${loadProgress}%` }}></div>
|
| 279 |
</div>
|
|
|
|
| 58 |
return () => window.removeEventListener('keydown', handleKeyDown);
|
| 59 |
}, []);
|
| 60 |
|
| 61 |
+
// Scroll Lock for Modal
|
| 62 |
+
useEffect(() => {
|
| 63 |
+
if (isUploadModalOpen) {
|
| 64 |
+
document.body.style.overflow = 'hidden';
|
| 65 |
+
} else {
|
| 66 |
+
document.body.style.overflow = '';
|
| 67 |
+
}
|
| 68 |
+
return () => { document.body.style.overflow = ''; };
|
| 69 |
+
}, [isUploadModalOpen]);
|
| 70 |
+
|
| 71 |
// Global Cursor & Interactions
|
| 72 |
// Note: The global custom cursor is now managed by components/shared/CustomCursor.tsx
|
| 73 |
|
|
|
|
| 283 |
<div className="loader-panel top"></div>
|
| 284 |
<div className="loader-panel bottom"></div>
|
| 285 |
<div className="loader-content flex flex-col items-center">
|
| 286 |
+
<div className="text-[10px] font-mono tracking-[0.5em] text-[var(--theme-text)] uppercase mb-4">Initializing Spotix Engine</div>
|
| 287 |
<div id="progress-bar-container">
|
| 288 |
<div id="progress-bar-fill" style={{ width: `${loadProgress}%` }}></div>
|
| 289 |
</div>
|
frontend/app/profile/page.tsx
CHANGED
|
@@ -7,7 +7,7 @@ import { apiLayer } from "@/lib/api";
|
|
| 7 |
import { Camera, Mail, Shield, CheckCircle, Activity, Calendar, Trash2 } from "lucide-react";
|
| 8 |
|
| 9 |
export default function ProfilePage() {
|
| 10 |
-
const { user, isAuthenticated, loading } = useAuth();
|
| 11 |
const [stats, setStats] = useState({ totalAnalyses: 0 });
|
| 12 |
|
| 13 |
// Edit States
|
|
@@ -46,6 +46,7 @@ export default function ProfilePage() {
|
|
| 46 |
|
| 47 |
try {
|
| 48 |
await apiLayer.updateUsername(usernameInput);
|
|
|
|
| 49 |
setStatusMsg({ type: "success", text: "Username updated successfully." });
|
| 50 |
setIsEditingUsername(false);
|
| 51 |
setTimeout(() => setStatusMsg(null), 3000);
|
|
@@ -69,6 +70,7 @@ export default function ProfilePage() {
|
|
| 69 |
setLocalAvatar(base64String);
|
| 70 |
try {
|
| 71 |
await apiLayer.updateAvatar(base64String);
|
|
|
|
| 72 |
setStatusMsg({ type: "success", text: "Avatar updated successfully." });
|
| 73 |
setTimeout(() => setStatusMsg(null), 3000);
|
| 74 |
} catch (err) {
|
|
|
|
| 7 |
import { Camera, Mail, Shield, CheckCircle, Activity, Calendar, Trash2 } from "lucide-react";
|
| 8 |
|
| 9 |
export default function ProfilePage() {
|
| 10 |
+
const { user, isAuthenticated, loading, updateUser } = useAuth();
|
| 11 |
const [stats, setStats] = useState({ totalAnalyses: 0 });
|
| 12 |
|
| 13 |
// Edit States
|
|
|
|
| 46 |
|
| 47 |
try {
|
| 48 |
await apiLayer.updateUsername(usernameInput);
|
| 49 |
+
updateUser({ name: usernameInput });
|
| 50 |
setStatusMsg({ type: "success", text: "Username updated successfully." });
|
| 51 |
setIsEditingUsername(false);
|
| 52 |
setTimeout(() => setStatusMsg(null), 3000);
|
|
|
|
| 70 |
setLocalAvatar(base64String);
|
| 71 |
try {
|
| 72 |
await apiLayer.updateAvatar(base64String);
|
| 73 |
+
updateUser({ avatar_url: base64String });
|
| 74 |
setStatusMsg({ type: "success", text: "Avatar updated successfully." });
|
| 75 |
setTimeout(() => setStatusMsg(null), 3000);
|
| 76 |
} catch (err) {
|
frontend/app/result/[id]/page.tsx
CHANGED
|
@@ -40,12 +40,25 @@ export default function ResultPage() {
|
|
| 40 |
|
| 41 |
useEffect(() => {
|
| 42 |
const handleKeyDown = (e: KeyboardEvent) => {
|
| 43 |
-
if (e.key === 'Escape')
|
|
|
|
|
|
|
|
|
|
| 44 |
};
|
| 45 |
window.addEventListener('keydown', handleKeyDown);
|
| 46 |
return () => window.removeEventListener('keydown', handleKeyDown);
|
| 47 |
}, []);
|
| 48 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
useEffect(() => {
|
| 50 |
let dragTimer: NodeJS.Timeout;
|
| 51 |
const handleDragOver = (e: DragEvent) => {
|
|
@@ -211,7 +224,7 @@ export default function ResultPage() {
|
|
| 211 |
<div className="absolute inset-0 flex flex-col items-center justify-center z-50">
|
| 212 |
<div className="font-mono text-sm uppercase tracking-widest text-[#d0c4bb] mb-6">Error Establishing Uplink</div>
|
| 213 |
<button onClick={() => router.push("/")} className="bg-[var(--theme-text)]/10 px-8 py-3 rounded-full hover:bg-[var(--theme-text)]/20 transition-all text-sm font-bold border border-theme-border !cursor-none group">
|
| 214 |
-
|
| 215 |
</button>
|
| 216 |
</div>
|
| 217 |
) : (
|
|
@@ -239,7 +252,7 @@ export default function ResultPage() {
|
|
| 239 |
{/* Left Side: Media Preview */}
|
| 240 |
<div className="lg:w-1/2 flex flex-col gap-6">
|
| 241 |
<div className="flex justify-between items-end mb-2 relative">
|
| 242 |
-
<h2 className="text-sm font-mono tracking-widest text-[var(--theme-text)]/90 uppercase">
|
| 243 |
{heatmapUrl && (
|
| 244 |
<div className="text-xs font-semibold uppercase tracking-wider text-[var(--theme-text)] bg-theme-text/5 px-4 py-1.5 rounded-full border border-theme-border">
|
| 245 |
DRAG TO REVEAL HEATMAP
|
|
@@ -318,14 +331,14 @@ export default function ResultPage() {
|
|
| 318 |
verdictStatus === 'SUSPICIOUS' ? 'text-amber-400' :
|
| 319 |
'text-emerald-400'
|
| 320 |
}`}>
|
| 321 |
-
|
| 322 |
</span>
|
| 323 |
-
<h1 className="text-
|
| 324 |
{label}
|
| 325 |
</h1>
|
| 326 |
{confidenceVal !== null && (
|
| 327 |
-
<div className="text-3xl font-light text-[var(--theme-
|
| 328 |
-
{confidenceVal.toFixed(1)}% <span className="text-lg opacity-
|
| 329 |
</div>
|
| 330 |
)}
|
| 331 |
{nsfwScore !== null && (
|
|
@@ -339,9 +352,9 @@ export default function ResultPage() {
|
|
| 339 |
{/* Diagnostic Explanation Box */}
|
| 340 |
<div className="upload-glass p-8 rounded-3xl relative overflow-hidden group">
|
| 341 |
<div className="absolute top-0 left-0 w-full h-[1px] bg-gradient-to-r from-transparent via-[var(--theme-border)]/20 to-transparent transform -translate-x-full group-hover:translate-x-full transition-transform duration-1000"></div>
|
| 342 |
-
<h3 className="text-sm font-semibold uppercase tracking-widest text-[#d0c4bb] mb-4">
|
| 343 |
<p className="text-sm text-theme-text/80 leading-relaxed font-light">
|
| 344 |
-
{fileData?.ai_explanation ? fileData.ai_explanation : 'The
|
| 345 |
</p>
|
| 346 |
</div>
|
| 347 |
|
|
@@ -349,10 +362,10 @@ export default function ResultPage() {
|
|
| 349 |
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
|
| 350 |
<div className="upload-glass p-6 rounded-2xl flex flex-col gap-4 !cursor-none relative overflow-hidden group">
|
| 351 |
<div className="absolute top-0 left-0 w-full h-[1px] bg-gradient-to-r from-transparent via-[var(--theme-border)]/30 to-transparent transform -translate-x-full group-hover:translate-x-full transition-transform duration-1000"></div>
|
| 352 |
-
<Activity className="w-6 h-6 text-[var(--theme-
|
| 353 |
<div>
|
| 354 |
<h4 className="text-[var(--theme-text)] font-bold text-lg mb-1">Neural Fingerprint</h4>
|
| 355 |
-
<p className="text-[#d0c4bb] text-xs leading-relaxed opacity-80 mb-
|
| 356 |
{freqScore !== null && (
|
| 357 |
<div className="flex items-end gap-2">
|
| 358 |
<span className="text-2xl font-mono text-emerald-400/90">{freqScore}</span>
|
|
@@ -364,7 +377,7 @@ export default function ResultPage() {
|
|
| 364 |
|
| 365 |
<div className="upload-glass p-6 rounded-2xl flex flex-col gap-4 !cursor-none relative overflow-hidden group">
|
| 366 |
<div className="absolute top-0 left-0 w-full h-[1px] bg-gradient-to-r from-transparent via-[var(--theme-border)]/30 to-transparent transform -translate-x-full group-hover:translate-x-full transition-transform duration-1000 delay-100"></div>
|
| 367 |
-
<Layers className="w-6 h-6 text-[var(--theme-
|
| 368 |
<div>
|
| 369 |
<h4 className="text-[var(--theme-text)] font-bold text-lg mb-1">Noise Pattern</h4>
|
| 370 |
<p className="text-[#d0c4bb] text-xs leading-relaxed opacity-80 mb-3">Evaluating PRNU map distribution. Sensor consistency is within normal threshold.</p>
|
|
@@ -379,9 +392,9 @@ export default function ResultPage() {
|
|
| 379 |
|
| 380 |
<div className="upload-glass p-6 rounded-2xl flex flex-col gap-4 !cursor-none md:col-span-2 relative overflow-hidden group">
|
| 381 |
<div className="absolute top-0 left-0 w-full h-[1px] bg-gradient-to-r from-transparent via-[var(--theme-border)]/30 to-transparent transform -translate-x-full group-hover:translate-x-full transition-transform duration-1000 delay-200"></div>
|
| 382 |
-
<Hash className="w-6 h-6 text-[var(--theme-
|
| 383 |
<div>
|
| 384 |
-
<h4 className="text-[var(--theme-text)] font-bold text-lg mb-
|
| 385 |
<p className="text-[#d0c4bb] text-xs leading-relaxed opacity-80 flex flex-wrap gap-2">
|
| 386 |
{displayTags.map(tag => (
|
| 387 |
<span key={tag} className="inline-block px-3 py-1 bg-theme-text/5 rounded-md border border-theme-border font-mono capitalize shadow-sm">
|
|
|
|
| 40 |
|
| 41 |
useEffect(() => {
|
| 42 |
const handleKeyDown = (e: KeyboardEvent) => {
|
| 43 |
+
if (e.key === 'Escape') {
|
| 44 |
+
setShowAuthModal(false);
|
| 45 |
+
setIsUploadModalOpen(false);
|
| 46 |
+
}
|
| 47 |
};
|
| 48 |
window.addEventListener('keydown', handleKeyDown);
|
| 49 |
return () => window.removeEventListener('keydown', handleKeyDown);
|
| 50 |
}, []);
|
| 51 |
|
| 52 |
+
// Scroll Lock for Modal
|
| 53 |
+
useEffect(() => {
|
| 54 |
+
if (isUploadModalOpen) {
|
| 55 |
+
document.body.style.overflow = 'hidden';
|
| 56 |
+
} else {
|
| 57 |
+
document.body.style.overflow = '';
|
| 58 |
+
}
|
| 59 |
+
return () => { document.body.style.overflow = ''; };
|
| 60 |
+
}, [isUploadModalOpen]);
|
| 61 |
+
|
| 62 |
useEffect(() => {
|
| 63 |
let dragTimer: NodeJS.Timeout;
|
| 64 |
const handleDragOver = (e: DragEvent) => {
|
|
|
|
| 224 |
<div className="absolute inset-0 flex flex-col items-center justify-center z-50">
|
| 225 |
<div className="font-mono text-sm uppercase tracking-widest text-[#d0c4bb] mb-6">Error Establishing Uplink</div>
|
| 226 |
<button onClick={() => router.push("/")} className="bg-[var(--theme-text)]/10 px-8 py-3 rounded-full hover:bg-[var(--theme-text)]/20 transition-all text-sm font-bold border border-theme-border !cursor-none group">
|
| 227 |
+
RETURN TO PLATFORM
|
| 228 |
</button>
|
| 229 |
</div>
|
| 230 |
) : (
|
|
|
|
| 252 |
{/* Left Side: Media Preview */}
|
| 253 |
<div className="lg:w-1/2 flex flex-col gap-6">
|
| 254 |
<div className="flex justify-between items-end mb-2 relative">
|
| 255 |
+
<h2 className="text-sm font-mono tracking-widest text-[var(--theme-text)]/90 uppercase">Analyzed File</h2>
|
| 256 |
{heatmapUrl && (
|
| 257 |
<div className="text-xs font-semibold uppercase tracking-wider text-[var(--theme-text)] bg-theme-text/5 px-4 py-1.5 rounded-full border border-theme-border">
|
| 258 |
DRAG TO REVEAL HEATMAP
|
|
|
|
| 331 |
verdictStatus === 'SUSPICIOUS' ? 'text-amber-400' :
|
| 332 |
'text-emerald-400'
|
| 333 |
}`}>
|
| 334 |
+
Analysis Verdict
|
| 335 |
</span>
|
| 336 |
+
<h1 className="text-5xs md:text-6xl font-black tracking-tight text-[var(--theme-text)] my-2">
|
| 337 |
{label}
|
| 338 |
</h1>
|
| 339 |
{confidenceVal !== null && (
|
| 340 |
+
<div className="text-3xl font-light text-[var(--theme-text)]">
|
| 341 |
+
{confidenceVal.toFixed(1)}% <span className="text-lg opacity-100">Confidence</span>
|
| 342 |
</div>
|
| 343 |
)}
|
| 344 |
{nsfwScore !== null && (
|
|
|
|
| 352 |
{/* Diagnostic Explanation Box */}
|
| 353 |
<div className="upload-glass p-8 rounded-3xl relative overflow-hidden group">
|
| 354 |
<div className="absolute top-0 left-0 w-full h-[1px] bg-gradient-to-r from-transparent via-[var(--theme-border)]/20 to-transparent transform -translate-x-full group-hover:translate-x-full transition-transform duration-1000"></div>
|
| 355 |
+
<h3 className="text-sm font-semibold uppercase tracking-widest text-[#d0c4bb] mb-4">AI Explanation</h3>
|
| 356 |
<p className="text-sm text-theme-text/80 leading-relaxed font-light">
|
| 357 |
+
{fileData?.ai_explanation ? fileData.ai_explanation : 'The Spotix engine has evaluated structural frequencies and edge patterns. No anomalies flagged in standard distribution.'}
|
| 358 |
</p>
|
| 359 |
</div>
|
| 360 |
|
|
|
|
| 362 |
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
|
| 363 |
<div className="upload-glass p-6 rounded-2xl flex flex-col gap-4 !cursor-none relative overflow-hidden group">
|
| 364 |
<div className="absolute top-0 left-0 w-full h-[1px] bg-gradient-to-r from-transparent via-[var(--theme-border)]/30 to-transparent transform -translate-x-full group-hover:translate-x-full transition-transform duration-1000"></div>
|
| 365 |
+
<Activity className="w-6 h-6 text-[var(--theme-text)] opacity-80" />
|
| 366 |
<div>
|
| 367 |
<h4 className="text-[var(--theme-text)] font-bold text-lg mb-1">Neural Fingerprint</h4>
|
| 368 |
+
<p className="text-[#d0c4bb] text-xs leading-relaxed opacity-80 mb-8">Frequency bounds evaluated across 512 discrete localized convolutions.</p>
|
| 369 |
{freqScore !== null && (
|
| 370 |
<div className="flex items-end gap-2">
|
| 371 |
<span className="text-2xl font-mono text-emerald-400/90">{freqScore}</span>
|
|
|
|
| 377 |
|
| 378 |
<div className="upload-glass p-6 rounded-2xl flex flex-col gap-4 !cursor-none relative overflow-hidden group">
|
| 379 |
<div className="absolute top-0 left-0 w-full h-[1px] bg-gradient-to-r from-transparent via-[var(--theme-border)]/30 to-transparent transform -translate-x-full group-hover:translate-x-full transition-transform duration-1000 delay-100"></div>
|
| 380 |
+
<Layers className="w-6 h-6 text-[var(--theme-text)] opacity-80" />
|
| 381 |
<div>
|
| 382 |
<h4 className="text-[var(--theme-text)] font-bold text-lg mb-1">Noise Pattern</h4>
|
| 383 |
<p className="text-[#d0c4bb] text-xs leading-relaxed opacity-80 mb-3">Evaluating PRNU map distribution. Sensor consistency is within normal threshold.</p>
|
|
|
|
| 392 |
|
| 393 |
<div className="upload-glass p-6 rounded-2xl flex flex-col gap-4 !cursor-none md:col-span-2 relative overflow-hidden group">
|
| 394 |
<div className="absolute top-0 left-0 w-full h-[1px] bg-gradient-to-r from-transparent via-[var(--theme-border)]/30 to-transparent transform -translate-x-full group-hover:translate-x-full transition-transform duration-1000 delay-200"></div>
|
| 395 |
+
<Hash className="w-6 h-6 text-[var(--theme-text)] opacity-80" />
|
| 396 |
<div>
|
| 397 |
+
<h4 className="text-[var(--theme-text)] font-bold text-lg mb-4">Detection Tags</h4>
|
| 398 |
<p className="text-[#d0c4bb] text-xs leading-relaxed opacity-80 flex flex-wrap gap-2">
|
| 399 |
{displayTags.map(tag => (
|
| 400 |
<span key={tag} className="inline-block px-3 py-1 bg-theme-text/5 rounded-md border border-theme-border font-mono capitalize shadow-sm">
|
frontend/components/dashboard/AnalyticsSection.tsx
CHANGED
|
@@ -52,16 +52,16 @@ export default function AnalyticsSection({ files }: { files: any[] }) {
|
|
| 52 |
{/* Quick Stats Column */}
|
| 53 |
<div className="flex flex-col gap-6 lg:col-span-1">
|
| 54 |
<div className="p-6 bg-[var(--theme-text)]/5 border border-[var(--theme-border)] rounded-2xl dash-border hover:bg-[var(--theme-text)]/10 transition backdrop-blur-sm">
|
| 55 |
-
<h3 className="text-[var(--theme-text)]/
|
| 56 |
<p className="text-4xl font-bold font-mono tracking-tighter text-[var(--theme-text)]">{stats.summary.total}</p>
|
| 57 |
</div>
|
| 58 |
<div className="grid grid-cols-2 gap-6">
|
| 59 |
<div className="p-6 bg-[var(--theme-text)]/5 border border-[var(--theme-border)] rounded-2xl hover:border-red-500/50 transition backdrop-blur-sm group">
|
| 60 |
-
<h3 className="text-[var(--theme-text)]/40 uppercase tracking-widest text-[
|
| 61 |
<p className="text-2xl font-bold font-mono text-red-400 group-hover:text-red-300 transition">{stats.summary.ai}%</p>
|
| 62 |
</div>
|
| 63 |
<div className="p-6 bg-[var(--theme-text)]/5 border border-[var(--theme-border)] rounded-2xl hover:border-green-500/50 transition backdrop-blur-sm group">
|
| 64 |
-
<h3 className="text-[var(--theme-text)]/40 uppercase tracking-widest text-[
|
| 65 |
<p className="text-2xl font-bold font-mono text-green-400 group-hover:text-green-300 transition">{stats.summary.real}%</p>
|
| 66 |
</div>
|
| 67 |
</div>
|
|
|
|
| 52 |
{/* Quick Stats Column */}
|
| 53 |
<div className="flex flex-col gap-6 lg:col-span-1">
|
| 54 |
<div className="p-6 bg-[var(--theme-text)]/5 border border-[var(--theme-border)] rounded-2xl dash-border hover:bg-[var(--theme-text)]/10 transition backdrop-blur-sm">
|
| 55 |
+
<h3 className="text-[var(--theme-text)]/60 uppercase tracking-widest text-[14px] mb-4">Total Analyses</h3>
|
| 56 |
<p className="text-4xl font-bold font-mono tracking-tighter text-[var(--theme-text)]">{stats.summary.total}</p>
|
| 57 |
</div>
|
| 58 |
<div className="grid grid-cols-2 gap-6">
|
| 59 |
<div className="p-6 bg-[var(--theme-text)]/5 border border-[var(--theme-border)] rounded-2xl hover:border-red-500/50 transition backdrop-blur-sm group">
|
| 60 |
+
<h3 className="text-[var(--theme-text)]/40 uppercase tracking-widest text-[14px] mb-6.5">Synthetic</h3>
|
| 61 |
<p className="text-2xl font-bold font-mono text-red-400 group-hover:text-red-300 transition">{stats.summary.ai}%</p>
|
| 62 |
</div>
|
| 63 |
<div className="p-6 bg-[var(--theme-text)]/5 border border-[var(--theme-border)] rounded-2xl hover:border-green-500/50 transition backdrop-blur-sm group">
|
| 64 |
+
<h3 className="text-[var(--theme-text)]/40 uppercase tracking-widest text-[14px] mb-6.5">Authentic</h3>
|
| 65 |
<p className="text-2xl font-bold font-mono text-green-400 group-hover:text-green-300 transition">{stats.summary.real}%</p>
|
| 66 |
</div>
|
| 67 |
</div>
|
frontend/components/dashboard/HistoryGrid.tsx
CHANGED
|
@@ -42,7 +42,7 @@ export default function HistoryGrid({ files, onDeleteFile }: { files: any[], onD
|
|
| 42 |
<Clock className="w-6 h-6 text-[var(--theme-text)]/20 !cursor-none" />
|
| 43 |
</div>
|
| 44 |
<p className="text-[var(--theme-text)]/40 mb-2 font-mono text-sm tracking-widest uppercase !cursor-none">No analyses yet</p>
|
| 45 |
-
<p className="text-[var(--theme-text)]/30 text-xs text-center max-w-sm !cursor-none">Upload media using the button above to begin generating your detection history.</p>
|
| 46 |
</div>
|
| 47 |
);
|
| 48 |
}
|
|
@@ -149,7 +149,7 @@ export default function HistoryGrid({ files, onDeleteFile }: { files: any[], onD
|
|
| 149 |
</div>
|
| 150 |
<div className="p-6 !cursor-none">
|
| 151 |
<p className="text-[var(--theme-text)]/70 text-sm mb-6 !cursor-none">
|
| 152 |
-
Are you sure you want to delete <strong className="text-[var(--theme-text)] font-mono">{fileToDelete.filename}</strong>? This action cannot be undone and will permanently remove the item from
|
| 153 |
</p>
|
| 154 |
<div className="flex gap-3 !cursor-none">
|
| 155 |
<button
|
|
|
|
| 42 |
<Clock className="w-6 h-6 text-[var(--theme-text)]/20 !cursor-none" />
|
| 43 |
</div>
|
| 44 |
<p className="text-[var(--theme-text)]/40 mb-2 font-mono text-sm tracking-widest uppercase !cursor-none">No analyses yet</p>
|
| 45 |
+
<p className="text-[var(--theme-text)]/30 text-xs text-center max-w-sm !cursor-none">Upload media using the [Analyze New Media] button above to begin generating your detection history.</p>
|
| 46 |
</div>
|
| 47 |
);
|
| 48 |
}
|
|
|
|
| 149 |
</div>
|
| 150 |
<div className="p-6 !cursor-none">
|
| 151 |
<p className="text-[var(--theme-text)]/70 text-sm mb-6 !cursor-none">
|
| 152 |
+
Are you sure you want to delete <strong className="text-[var(--theme-text)] font-mono">{fileToDelete.filename}</strong>? This action cannot be undone and will permanently remove the item from Spotix.
|
| 153 |
</p>
|
| 154 |
<div className="flex gap-3 !cursor-none">
|
| 155 |
<button
|
frontend/components/shared/FeedbackModal.tsx
CHANGED
|
@@ -11,7 +11,7 @@ export default function FeedbackModal({ isOpen, onClose }: { isOpen: boolean, on
|
|
| 11 |
const [status, setStatus] = useState<"idle" | "loading" | "success" | "error">("idle");
|
| 12 |
const [errorMsg, setErrorMsg] = useState("");
|
| 13 |
|
| 14 |
-
const handleSubmit = async (e: React.
|
| 15 |
e.preventDefault();
|
| 16 |
if (message.trim().length < 5) {
|
| 17 |
setErrorMsg("Message must be at least 5 characters.");
|
|
|
|
| 11 |
const [status, setStatus] = useState<"idle" | "loading" | "success" | "error">("idle");
|
| 12 |
const [errorMsg, setErrorMsg] = useState("");
|
| 13 |
|
| 14 |
+
const handleSubmit = async (e: React.SubmitEvent) => {
|
| 15 |
e.preventDefault();
|
| 16 |
if (message.trim().length < 5) {
|
| 17 |
setErrorMsg("Message must be at least 5 characters.");
|
frontend/components/shared/Navbar.tsx
CHANGED
|
@@ -12,7 +12,7 @@ interface NavbarProps {
|
|
| 12 |
export default function Navbar({ onAnalyzeClick }: NavbarProps) {
|
| 13 |
const router = useRouter();
|
| 14 |
const pathname = usePathname();
|
| 15 |
-
const { isAuthenticated, user, logout } = useAuth();
|
| 16 |
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
| 17 |
const [dropdownOpen, setDropdownOpen] = useState(false);
|
| 18 |
|
|
@@ -69,6 +69,7 @@ export default function Navbar({ onAnalyzeClick }: NavbarProps) {
|
|
| 69 |
setLocalAvatar(base64String);
|
| 70 |
try {
|
| 71 |
await apiLayer.updateAvatar(base64String);
|
|
|
|
| 72 |
} catch (err) {
|
| 73 |
console.error("Failed to update avatar:", err);
|
| 74 |
}
|
|
@@ -84,6 +85,7 @@ export default function Navbar({ onAnalyzeClick }: NavbarProps) {
|
|
| 84 |
}
|
| 85 |
try {
|
| 86 |
await apiLayer.updateAvatar("");
|
|
|
|
| 87 |
} catch (err) {
|
| 88 |
console.error("Failed to remove avatar:", err);
|
| 89 |
}
|
|
@@ -94,7 +96,7 @@ export default function Navbar({ onAnalyzeClick }: NavbarProps) {
|
|
| 94 |
<>
|
| 95 |
<nav className="fixed top-6 left-1/2 -translate-x-1/2 z-[60] w-[92%] max-w-5xl">
|
| 96 |
<div className="bg-[var(--theme-bg)]/60 backdrop-blur-xl border border-[#4d453e]/15 rounded-full px-6 md:px-8 py-3 md:py-4 flex justify-between items-center shadow-[0_20px_50px_rgba(0,0,0,0.4)] transition-all duration-500">
|
| 97 |
-
<div onClick={() => router.push('/')} className="cursor-pointer text-base md:text-lg font-black tracking-tighter text-[var(--theme-text)] uppercase hover:opacity-80 transition-opacity">
|
| 98 |
|
| 99 |
<div className="hidden lg:flex items-center gap-10">
|
| 100 |
<a className={`font-['Inter'] tracking-tight font-medium text-[11px] uppercase transition-colors nav-link ${pathname === '/' ? 'text-[var(--theme-text)]' : 'text-[#d0c4bb] hover:text-[var(--theme-text)]'}`} href="/">Platform</a>
|
|
|
|
| 12 |
export default function Navbar({ onAnalyzeClick }: NavbarProps) {
|
| 13 |
const router = useRouter();
|
| 14 |
const pathname = usePathname();
|
| 15 |
+
const { isAuthenticated, user, logout, updateUser } = useAuth();
|
| 16 |
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
| 17 |
const [dropdownOpen, setDropdownOpen] = useState(false);
|
| 18 |
|
|
|
|
| 69 |
setLocalAvatar(base64String);
|
| 70 |
try {
|
| 71 |
await apiLayer.updateAvatar(base64String);
|
| 72 |
+
updateUser({ avatar_url: base64String });
|
| 73 |
} catch (err) {
|
| 74 |
console.error("Failed to update avatar:", err);
|
| 75 |
}
|
|
|
|
| 85 |
}
|
| 86 |
try {
|
| 87 |
await apiLayer.updateAvatar("");
|
| 88 |
+
updateUser({ avatar_url: null });
|
| 89 |
} catch (err) {
|
| 90 |
console.error("Failed to remove avatar:", err);
|
| 91 |
}
|
|
|
|
| 96 |
<>
|
| 97 |
<nav className="fixed top-6 left-1/2 -translate-x-1/2 z-[60] w-[92%] max-w-5xl">
|
| 98 |
<div className="bg-[var(--theme-bg)]/60 backdrop-blur-xl border border-[#4d453e]/15 rounded-full px-6 md:px-8 py-3 md:py-4 flex justify-between items-center shadow-[0_20px_50px_rgba(0,0,0,0.4)] transition-all duration-500">
|
| 99 |
+
<div onClick={() => router.push('/')} className="cursor-pointer text-base md:text-lg font-black tracking-tighter text-[var(--theme-text)] uppercase hover:opacity-80 transition-opacity">SPOTIX</div>
|
| 100 |
|
| 101 |
<div className="hidden lg:flex items-center gap-10">
|
| 102 |
<a className={`font-['Inter'] tracking-tight font-medium text-[11px] uppercase transition-colors nav-link ${pathname === '/' ? 'text-[var(--theme-text)]' : 'text-[#d0c4bb] hover:text-[var(--theme-text)]'}`} href="/">Platform</a>
|
frontend/components/upload/UploadZone.tsx
CHANGED
|
@@ -149,8 +149,7 @@ export default function UploadZone({ autoAnalyze = false }: { autoAnalyze?: bool
|
|
| 149 |
</defs>
|
| 150 |
</svg>
|
| 151 |
|
| 152 |
-
<div
|
| 153 |
-
className={`rounded-[2.5rem] p-1 md:p-2 bg-[var(--theme-bg)] backdrop-blur-3xl border-2 border-dashed border-[var(--theme-text)]/40 relative overflow-hidden group/upload transition-all duration-700 shadow-[0_40px_100px_rgba(0,0,0,0.5)] hover:border-[var(--theme-text)] hover:shadow-[0_0_40px_var(--theme-text)] ${isDragging ? 'border-[var(--theme-text)] shadow-[0_0_60px_var(--theme-text)]' : ''}`}
|
| 154 |
onDragOver={onDragOver}
|
| 155 |
onDragLeave={onDragLeave}
|
| 156 |
onDrop={onDrop}
|
|
|
|
| 149 |
</defs>
|
| 150 |
</svg>
|
| 151 |
|
| 152 |
+
<div className={`rounded-[2.5rem] p-1 md:p-2 bg-[var(--theme-bg)] backdrop-blur-3xl border-2 border-dashed border-[var(--theme-text)]/40 relative overflow-hidden group/upload transition-all duration-700 hover:border-[var(--theme-text)] ${isDragging ? 'border-[var(--theme-text)]' : ''}`}
|
|
|
|
| 153 |
onDragOver={onDragOver}
|
| 154 |
onDragLeave={onDragLeave}
|
| 155 |
onDrop={onDrop}
|
frontend/contexts/AuthContext.tsx
CHANGED
|
@@ -9,6 +9,8 @@ type UserData = {
|
|
| 9 |
name: string;
|
| 10 |
is_admin: boolean;
|
| 11 |
avatar_url?: string | null;
|
|
|
|
|
|
|
| 12 |
};
|
| 13 |
|
| 14 |
type AuthContextType = {
|
|
@@ -16,9 +18,10 @@ type AuthContextType = {
|
|
| 16 |
user: UserData | null;
|
| 17 |
loading: boolean;
|
| 18 |
logout: () => void;
|
|
|
|
| 19 |
};
|
| 20 |
|
| 21 |
-
const AuthContext = createContext<AuthContextType>({ isAuthenticated: false, user: null, loading: true, logout: () => {} });
|
| 22 |
|
| 23 |
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
| 24 |
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
|
@@ -58,8 +61,12 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|
| 58 |
router.push("/login");
|
| 59 |
};
|
| 60 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
return (
|
| 62 |
-
<AuthContext.Provider value={{ isAuthenticated, user, loading, logout }}>
|
| 63 |
{children}
|
| 64 |
</AuthContext.Provider>
|
| 65 |
);
|
|
|
|
| 9 |
name: string;
|
| 10 |
is_admin: boolean;
|
| 11 |
avatar_url?: string | null;
|
| 12 |
+
google_avatar_url?: string | null;
|
| 13 |
+
created_at?: string | null;
|
| 14 |
};
|
| 15 |
|
| 16 |
type AuthContextType = {
|
|
|
|
| 18 |
user: UserData | null;
|
| 19 |
loading: boolean;
|
| 20 |
logout: () => void;
|
| 21 |
+
updateUser: (data: Partial<UserData>) => void;
|
| 22 |
};
|
| 23 |
|
| 24 |
+
const AuthContext = createContext<AuthContextType>({ isAuthenticated: false, user: null, loading: true, logout: () => {}, updateUser: () => {} });
|
| 25 |
|
| 26 |
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
| 27 |
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
|
|
|
| 61 |
router.push("/login");
|
| 62 |
};
|
| 63 |
|
| 64 |
+
const updateUser = (data: Partial<UserData>) => {
|
| 65 |
+
setUser(prev => prev ? { ...prev, ...data } : null);
|
| 66 |
+
};
|
| 67 |
+
|
| 68 |
return (
|
| 69 |
+
<AuthContext.Provider value={{ isAuthenticated, user, loading, logout, updateUser }}>
|
| 70 |
{children}
|
| 71 |
</AuthContext.Provider>
|
| 72 |
);
|