Anish commited on
Commit
5603f49
·
1 Parent(s): a16341c

[UI/UX] Final touches before deployment.

Browse files
backend/app/models/file_model.py CHANGED
@@ -15,7 +15,7 @@ class File(Base):
15
  ip_address = Column(String, nullable=True)
16
 
17
  owner_id = Column(Integer, ForeignKey("users.id"))
18
- created_at = Column(DateTime, default=datetime.now(UTC))
19
 
20
  status = Column(String, default="uploaded")
21
  result = Column(String, nullable=True)
 
15
  ip_address = Column(String, nullable=True)
16
 
17
  owner_id = Column(Integer, ForeignKey("users.id"))
18
+ created_at = Column(DateTime, default=lambda: datetime.now(UTC))
19
 
20
  status = Column(String, default="uploaded")
21
  result = Column(String, nullable=True)
backend/app/models/user_model.py CHANGED
@@ -12,7 +12,7 @@ class User(Base):
12
  password = Column(String, nullable=True)
13
  oauth_provider = Column(String, nullable=True)
14
  oauth_sub_id = Column(String, unique=True, nullable=True)
15
- created_at = Column(DateTime, default=datetime.now(UTC))
16
  reset_token_hash = Column(String, nullable=True)
17
  reset_token_expire_at = Column(DateTime, nullable=True)
18
  is_verified = Column(Boolean, default=False)
 
12
  password = Column(String, nullable=True)
13
  oauth_provider = Column(String, nullable=True)
14
  oauth_sub_id = Column(String, unique=True, nullable=True)
15
+ created_at = Column(DateTime, default=lambda: datetime.now(UTC))
16
  reset_token_hash = Column(String, nullable=True)
17
  reset_token_expire_at = Column(DateTime, nullable=True)
18
  is_verified = Column(Boolean, default=False)
backend/requirements.txt CHANGED
Binary files a/backend/requirements.txt and b/backend/requirements.txt differ
 
frontend/app/dashboard/page.tsx CHANGED
@@ -96,10 +96,28 @@ export default function DashboardPage() {
96
  return confB - confA;
97
  }
98
  });
99
-
100
  return result;
101
  }, [files, searchQuery, sortBy, resultFilter]);
102
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
  // Cursor handled globally (CustomCursor.tsx)
104
  // Local Parallax for Dash Grid
105
  useEffect(() => {
 
96
  return confB - confA;
97
  }
98
  });
 
99
  return result;
100
  }, [files, searchQuery, sortBy, resultFilter]);
101
 
102
+ useEffect(() => {
103
+ const handleDragOver = (e: DragEvent) => {
104
+ e.preventDefault();
105
+ if (!isUploadModalOpen) setIsUploadModalOpen(true);
106
+ };
107
+ const handleEsc = (event: KeyboardEvent) => {
108
+ if (event.key === 'Escape' && isUploadModalOpen) {
109
+ setIsUploadModalOpen(false);
110
+ }
111
+ };
112
+
113
+ window.addEventListener('dragover', handleDragOver);
114
+ document.addEventListener('keydown', handleEsc);
115
+ return () => {
116
+ window.removeEventListener('dragover', handleDragOver);
117
+ document.removeEventListener('keydown', handleEsc);
118
+ }
119
+ }, [isUploadModalOpen]);
120
+
121
  // Cursor handled globally (CustomCursor.tsx)
122
  // Local Parallax for Dash Grid
123
  useEffect(() => {
frontend/app/learn-more/page.tsx CHANGED
@@ -75,7 +75,7 @@ export default function LearnMorePage() {
75
  <div>
76
  <h2 className="text-2xl font-bold mb-4">1. Uploading Media</h2>
77
  <p className="text-[var(--theme-text)]/70 leading-relaxed text-lg">
78
- Click the <strong className="text-[var(--theme-text)]">Analyze Media</strong> button on the top right of the navigation bar, or from the Landing Page. A secure uplink zone will appear. You can drag and drop your images or videos (MP4, WEBM, JPG, PNG) directly into this zone.
79
  </p>
80
  </div>
81
  </div>
 
75
  <div>
76
  <h2 className="text-2xl font-bold mb-4">1. Uploading Media</h2>
77
  <p className="text-[var(--theme-text)]/70 leading-relaxed text-lg">
78
+ <strong className="text-[var(--theme-text)]">Drag and Drop</strong> your <strong className="text-[var(--theme-text)]">Media</strong> anywhere, <strong className="text-[var(--theme-text)]">OR</strong> click the <strong className="text-[var(--theme-text)]">Analyze Media</strong> button on the top right of the navigation bar, or from the Landing Page. An upload zone box will appear. You can drag and drop your images or videos (MP4, JPG, PNG) directly into this zone.
79
  </p>
80
  </div>
81
  </div>
frontend/app/page.tsx CHANGED
@@ -26,6 +26,23 @@ export default function LandingPage() {
26
  }
27
  }, [searchParams]);
28
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
  // --- Footer Copyright and Dynamic Date ---
30
  const [year, setYear] = useState(new Date().getFullYear());
31
 
 
26
  }
27
  }, [searchParams]);
28
 
29
+ useEffect(() => {
30
+ const handleDragOver = (e: DragEvent) => {
31
+ e.preventDefault();
32
+ if (!isUploadModalOpen) setIsUploadModalOpen(true);
33
+ };
34
+ const handleEsc = (e: KeyboardEvent) => {
35
+ if (e.key === 'Escape' && isUploadModalOpen) setIsUploadModalOpen(false);
36
+ };
37
+
38
+ window.addEventListener('dragover', handleDragOver);
39
+ window.addEventListener('keydown', handleEsc);
40
+ return () => {
41
+ window.removeEventListener('dragover', handleDragOver);
42
+ window.removeEventListener('keydown', handleEsc);
43
+ };
44
+ }, [isUploadModalOpen]);
45
+
46
  // --- Footer Copyright and Dynamic Date ---
47
  const [year, setYear] = useState(new Date().getFullYear());
48
 
frontend/app/profile/page.tsx CHANGED
@@ -100,6 +100,17 @@ export default function ProfilePage() {
100
  }
101
  };
102
 
 
 
 
 
 
 
 
 
 
 
 
103
  if (loading) return null;
104
 
105
  return (
 
100
  }
101
  };
102
 
103
+ useEffect(() => {
104
+ const handleEsc = (event: KeyboardEvent) => {
105
+ if (event.key === 'Escape' && isDeleteModalOpen) {
106
+ setIsDeleteModalOpen(false);
107
+ setDeleteInput("");
108
+ }
109
+ };
110
+ document.addEventListener('keydown', handleEsc);
111
+ return () => document.removeEventListener('keydown', handleEsc);
112
+ }, [isDeleteModalOpen]);
113
+
114
  if (loading) return null;
115
 
116
  return (
frontend/components/shared/Navbar.tsx CHANGED
@@ -1,7 +1,7 @@
1
  import React, { useState, useRef, useEffect } from 'react';
2
  import { useRouter, usePathname } from 'next/navigation';
3
  import { useAuth } from '@/contexts/AuthContext';
4
- import { Menu, X, LogOut, Camera, Upload, Trash2, Moon, Sun, MessageSquare, UserCircle, LayoutDashboard } from 'lucide-react';
5
  import { apiLayer } from '@/lib/api';
6
  import FeedbackModal from '@/components/shared/FeedbackModal';
7
 
@@ -15,6 +15,40 @@ export default function Navbar({ onAnalyzeClick }: NavbarProps) {
15
  const { isAuthenticated, user, logout, updateUser } = useAuth();
16
  const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
17
  const [dropdownOpen, setDropdownOpen] = useState(false);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
 
19
  // Avatar logic
20
  const fileInputRef = useRef<HTMLInputElement>(null);
@@ -34,20 +68,26 @@ export default function Navbar({ onAnalyzeClick }: NavbarProps) {
34
  }
35
  }, [user?.avatar_url, user?.google_avatar_url]);
36
 
37
- // Close dropdown on outside click or ESC
38
  useEffect(() => {
39
  const handleClickOutside = (event: MouseEvent) => {
40
  if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
41
  setDropdownOpen(false);
 
42
  }
43
  };
 
44
  const handleEsc = (event: KeyboardEvent) => {
45
- if (event.key === 'Escape') setDropdownOpen(false);
 
 
 
46
  };
 
47
  if (dropdownOpen) {
48
  document.addEventListener('mousedown', handleClickOutside);
49
  document.addEventListener('keydown', handleEsc);
50
  }
 
51
  return () => {
52
  document.removeEventListener('mousedown', handleClickOutside);
53
  document.removeEventListener('keydown', handleEsc);
@@ -121,10 +161,19 @@ export default function Navbar({ onAnalyzeClick }: NavbarProps) {
121
  </button>
122
  </>
123
  ) : (
124
- <div className="flex items-center gap-3 relative group" ref={dropdownRef}>
 
 
 
 
 
125
  <div
126
  className="flex items-center gap-3 cursor-pointer hover:opacity-80 transition-opacity"
127
- onClick={() => setDropdownOpen(!dropdownOpen)}
 
 
 
 
128
  >
129
  <span className="hidden sm:block text-[var(--theme-text)] opacity-80 text-sm font-mono tracking-tight hover-scale">{user?.name || "User"}</span>
130
  <div className="w-10 h-10 rounded-full border border-[var(--theme-border)] overflow-hidden bg-[var(--theme-text)]/5 flex items-center justify-center dash-border hover-scale">
@@ -137,7 +186,8 @@ export default function Navbar({ onAnalyzeClick }: NavbarProps) {
137
  </div>
138
 
139
  {dropdownOpen && (
140
- <div className="absolute right-0 top-full mt-3 w-56 bg-[var(--theme-bg)] border border-[var(--theme-border)] rounded-xl py-2 shadow-2xl flex flex-col z-[100] !cursor-none">
 
141
 
142
  <button onClick={() => { setDropdownOpen(false); router.push('/dashboard'); }} className="px-4 py-3 flex items-center gap-3 text-sm text-[var(--theme-text)]/70 hover:text-[var(--theme-text)] hover:bg-[var(--theme-text)]/5 transition-colors text-left !cursor-none">
143
  <LayoutDashboard className="w-4 h-4 !cursor-none" /> Dashboard
@@ -147,9 +197,6 @@ export default function Navbar({ onAnalyzeClick }: NavbarProps) {
147
  <UserCircle className="w-4 h-4 !cursor-none" /> My Profile
148
  </button>
149
 
150
- <div className="h-px w-full bg-[var(--theme-border)] my-1"></div>
151
-
152
-
153
  {localAvatar && (
154
  <button
155
  className="px-4 py-3 flex items-center gap-3 text-sm text-yellow-500/80 hover:text-yellow-500 hover:bg-[var(--theme-text)]/5 transition-colors text-left !cursor-none"
@@ -161,6 +208,20 @@ export default function Navbar({ onAnalyzeClick }: NavbarProps) {
161
 
162
  <div className="h-px w-full bg-[var(--theme-border)] my-1"></div>
163
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
164
 
165
  <button
166
  className="px-4 py-3 flex items-center gap-3 text-sm text-[var(--theme-text)]/70 hover:text-[var(--theme-text)] hover:bg-[var(--theme-text)]/5 transition-colors text-left !cursor-none"
@@ -183,6 +244,7 @@ export default function Navbar({ onAnalyzeClick }: NavbarProps) {
183
  >
184
  <LogOut className="w-4 h-4 !cursor-none" /> Logout
185
  </button>
 
186
  </div>
187
  )}
188
  </div>
 
1
  import React, { useState, useRef, useEffect } from 'react';
2
  import { useRouter, usePathname } from 'next/navigation';
3
  import { useAuth } from '@/contexts/AuthContext';
4
+ import { Menu, X, LogOut, Camera, Upload, Trash2, Moon, Sun, MessageSquare, UserCircle, LayoutDashboard, Volume2, VolumeX } from 'lucide-react';
5
  import { apiLayer } from '@/lib/api';
6
  import FeedbackModal from '@/components/shared/FeedbackModal';
7
 
 
15
  const { isAuthenticated, user, logout, updateUser } = useAuth();
16
  const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
17
  const [dropdownOpen, setDropdownOpen] = useState(false);
18
+ const [dropdownPinned, setDropdownPinned] = useState(false);
19
+ const [soundEnabled, setSoundEnabled] = useState(true);
20
+ const closeTimeoutRef = useRef<NodeJS.Timeout | null>(null);
21
+
22
+ const handleMouseEnter = () => {
23
+ if (!dropdownPinned) {
24
+ if (closeTimeoutRef.current) clearTimeout(closeTimeoutRef.current);
25
+ setDropdownOpen(true);
26
+ }
27
+ };
28
+
29
+ const handleMouseLeave = () => {
30
+ if (!dropdownPinned) {
31
+ closeTimeoutRef.current = setTimeout(() => {
32
+ setDropdownOpen(false);
33
+ }, 300); // 300ms grace period
34
+ }
35
+ };
36
+
37
+ useEffect(() => {
38
+ if (typeof window !== 'undefined') {
39
+ const saved = localStorage.getItem('spotix_sound');
40
+ if (saved !== null) {
41
+ setSoundEnabled(saved === 'true');
42
+ }
43
+ }
44
+ }, []);
45
+
46
+ const toggleSound = (e: React.MouseEvent) => {
47
+ e.stopPropagation();
48
+ const newState = !soundEnabled;
49
+ setSoundEnabled(newState);
50
+ localStorage.setItem('spotix_sound', String(newState));
51
+ };
52
 
53
  // Avatar logic
54
  const fileInputRef = useRef<HTMLInputElement>(null);
 
68
  }
69
  }, [user?.avatar_url, user?.google_avatar_url]);
70
 
 
71
  useEffect(() => {
72
  const handleClickOutside = (event: MouseEvent) => {
73
  if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
74
  setDropdownOpen(false);
75
+ setDropdownPinned(false);
76
  }
77
  };
78
+
79
  const handleEsc = (event: KeyboardEvent) => {
80
+ if (event.key === 'Escape') {
81
+ setDropdownOpen(false);
82
+ setDropdownPinned(false);
83
+ }
84
  };
85
+
86
  if (dropdownOpen) {
87
  document.addEventListener('mousedown', handleClickOutside);
88
  document.addEventListener('keydown', handleEsc);
89
  }
90
+
91
  return () => {
92
  document.removeEventListener('mousedown', handleClickOutside);
93
  document.removeEventListener('keydown', handleEsc);
 
161
  </button>
162
  </>
163
  ) : (
164
+ <div
165
+ className="flex items-center gap-3 relative group"
166
+ ref={dropdownRef}
167
+ onMouseEnter={handleMouseEnter}
168
+ onMouseLeave={handleMouseLeave}
169
+ >
170
  <div
171
  className="flex items-center gap-3 cursor-pointer hover:opacity-80 transition-opacity"
172
+ onClick={() => {
173
+ const newPinned = !dropdownPinned;
174
+ setDropdownPinned(newPinned);
175
+ setDropdownOpen(newPinned);
176
+ }}
177
  >
178
  <span className="hidden sm:block text-[var(--theme-text)] opacity-80 text-sm font-mono tracking-tight hover-scale">{user?.name || "User"}</span>
179
  <div className="w-10 h-10 rounded-full border border-[var(--theme-border)] overflow-hidden bg-[var(--theme-text)]/5 flex items-center justify-center dash-border hover-scale">
 
186
  </div>
187
 
188
  {dropdownOpen && (
189
+ <div className="absolute right-0 top-full pt-3 w-56 flex flex-col z-[100] !cursor-none">
190
+ <div className="bg-[var(--theme-bg)] border border-[var(--theme-border)] rounded-xl py-2 shadow-2xl flex flex-col w-full h-full">
191
 
192
  <button onClick={() => { setDropdownOpen(false); router.push('/dashboard'); }} className="px-4 py-3 flex items-center gap-3 text-sm text-[var(--theme-text)]/70 hover:text-[var(--theme-text)] hover:bg-[var(--theme-text)]/5 transition-colors text-left !cursor-none">
193
  <LayoutDashboard className="w-4 h-4 !cursor-none" /> Dashboard
 
197
  <UserCircle className="w-4 h-4 !cursor-none" /> My Profile
198
  </button>
199
 
 
 
 
200
  {localAvatar && (
201
  <button
202
  className="px-4 py-3 flex items-center gap-3 text-sm text-yellow-500/80 hover:text-yellow-500 hover:bg-[var(--theme-text)]/5 transition-colors text-left !cursor-none"
 
208
 
209
  <div className="h-px w-full bg-[var(--theme-border)] my-1"></div>
210
 
211
+ <button
212
+ onClick={toggleSound}
213
+ className="px-4 py-3 flex items-center gap-3 text-sm text-[var(--theme-text)]/70 hover:text-[var(--theme-text)] hover:bg-[var(--theme-text)]/5 transition-colors text-left justify-between !cursor-none"
214
+ >
215
+ <div className="flex items-center gap-3">
216
+ {soundEnabled ? <Volume2 className="w-4 h-4 !cursor-none text-green-400" /> : <VolumeX className="w-4 h-4 !cursor-none text-red-400" />}
217
+ Alerts Sound
218
+ </div>
219
+ <span className={`text-[10px] font-bold px-2 py-0.5 rounded-full uppercase tracking-wider ${soundEnabled ? 'bg-green-500/20 text-green-400' : 'bg-red-500/20 text-red-400'}`}>
220
+ {soundEnabled ? 'ON' : 'OFF'}
221
+ </span>
222
+ </button>
223
+
224
+ <div className="h-px w-full bg-[var(--theme-border)] my-1"></div>
225
 
226
  <button
227
  className="px-4 py-3 flex items-center gap-3 text-sm text-[var(--theme-text)]/70 hover:text-[var(--theme-text)] hover:bg-[var(--theme-text)]/5 transition-colors text-left !cursor-none"
 
244
  >
245
  <LogOut className="w-4 h-4 !cursor-none" /> Logout
246
  </button>
247
+ </div>
248
  </div>
249
  )}
250
  </div>
frontend/components/upload/UploadZone.tsx CHANGED
@@ -99,6 +99,13 @@ export default function UploadZone({ autoAnalyze = false }: { autoAnalyze?: bool
99
  conf: fileData.confidence ?? null
100
  });
101
  setInteractionState('result');
 
 
 
 
 
 
 
102
  }, 400);
103
  } else if (fileData.status === 'Failed' || fileData.status === 'error' || fileData.status === 'FAILED') {
104
  polling = false;
 
99
  conf: fileData.confidence ?? null
100
  });
101
  setInteractionState('result');
102
+
103
+ const soundSetting = localStorage.getItem('spotix_sound');
104
+ if (soundSetting !== 'false') {
105
+ const audio = new Audio('/sounds/notif.wav');
106
+ audio.volume = 1.0;
107
+ audio.play().catch(e => console.error("Audio play failed:", e));
108
+ }
109
  }, 400);
110
  } else if (fileData.status === 'Failed' || fileData.status === 'error' || fileData.status === 'FAILED') {
111
  polling = false;