Skip to content

Commit b0a7573

Browse files
AmirMohammad CheraghaliAmirMohammad Cheraghali
authored andcommitted
feat: Make timezone setting functional — apply IANA timezone across all date/time displays
1 parent 227a1bb commit b0a7573

6 files changed

Lines changed: 141 additions & 29 deletions

File tree

src/components/FavoritesPanel.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { Favorite } from '../hooks/useFavorites';
44
import type { HistoryItem } from '../hooks/useHistory';
55

66
import { type DataSource } from '../utils/pdbUtils';
7+
import { useTimezone, formatDate } from '../lib/timezoneUtils';
78

89
interface FavoritesPanelProps {
910
favorites: Favorite[];
@@ -31,6 +32,7 @@ export const FavoritesPanel: React.FC<FavoritesPanelProps> = ({
3132
showTabs = true,
3233
}) => {
3334
const [activeTab, setActiveTab] = useState<'favorites' | 'history'>(initialTab);
35+
const timezone = useTimezone();
3436

3537
// Sync active tab with prop (for external switching)
3638
React.useEffect(() => {
@@ -137,7 +139,7 @@ export const FavoritesPanel: React.FC<FavoritesPanelProps> = ({
137139
<div className={`text-sm ${subtleText}`}>
138140
{fav.dataSource === 'pdb' ? 'PDB' : fav.dataSource === 'alphafold' ? 'AlphaFold' : 'PubChem'}: {fav.id}
139141
{' • '}
140-
{new Date(fav.addedAt).toLocaleDateString()}
142+
{formatDate(new Date(fav.addedAt), timezone)}
141143
</div>
142144
</div>
143145
</button>
@@ -183,7 +185,7 @@ export const FavoritesPanel: React.FC<FavoritesPanelProps> = ({
183185
<span className={`px-1.5 rounded ${isLightMode ? 'bg-neutral-200' : 'bg-neutral-800'}`}>
184186
{item.dataSource === 'pdb' ? 'PDB' : item.dataSource === 'alphafold' ? 'AF DB' : 'CHEM'}
185187
</span>
186-
{new Date(item.timestamp).toLocaleTimeString()}
188+
{new Date(item.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
187189
</div>
188190
</div>
189191
</button>

src/components/GalleryModal.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
} from 'lucide-react';
1515
import clsx from 'clsx';
1616
import type { Snapshot, Movie } from '../types';
17+
import { useTimezone, formatDate, formatDateTime } from '../lib/timezoneUtils';
1718

1819
interface GalleryModalProps {
1920
isOpen: boolean;
@@ -40,6 +41,7 @@ export const GalleryModal: React.FC<GalleryModalProps> = ({
4041
onDownloadMovie,
4142
isLightMode
4243
}) => {
44+
const timezone = useTimezone();
4345
const [activeTab, setActiveTab] = useState<'all' | 'snapshots' | 'movies'>('all');
4446
const [selectedId, setSelectedId] = useState<string | null>(null);
4547
const [isDeleteConfirmOpen, setIsDeleteConfirmOpen] = useState(false);
@@ -183,7 +185,7 @@ export const GalleryModal: React.FC<GalleryModalProps> = ({
183185
{/* Overlay Info */}
184186
<div className="absolute inset-x-0 bottom-0 p-3 bg-gradient-to-t from-black/80 via-black/40 to-transparent text-white opacity-0 group-hover:opacity-100 transition-opacity">
185187
<p className="text-xs font-bold truncate">{item.pdbId || 'Structure'}</p>
186-
<p className="text-[10px] opacity-80">{new Date(item.timestamp).toLocaleDateString()}</p>
188+
<p className="text-[10px] opacity-80">{formatDate(new Date(item.timestamp), timezone)}</p>
187189
</div>
188190
</div>
189191
))}
@@ -249,7 +251,7 @@ export const GalleryModal: React.FC<GalleryModalProps> = ({
249251
<div className="flex items-center gap-3">
250252
<Calendar className="w-4 h-4 text-purple-500" />
251253
<p className={clsx("text-xs", isLightMode ? "text-neutral-700" : "text-neutral-300")}>
252-
{new Date(selectedItem.timestamp).toLocaleString()}
254+
{formatDateTime(new Date(selectedItem.timestamp), timezone)}
253255
</p>
254256
</div>
255257
{selectedItem.type === 'snapshot' && (

src/components/dashboard/ActivityTimeline.tsx

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
} from 'lucide-react';
66
import { useAuth } from '../../lib/AuthContext';
77
import { getActivityLog, type ActivityLog, type ActivityAction } from '../../lib/structuresService';
8+
import { useTimezone, formatTime } from '../../lib/timezoneUtils';
89

910
// ─── Helpers ──────────────────────────────────────────────────────
1011

@@ -27,19 +28,22 @@ const ACTION_CONFIG: Record<ActivityAction, { label: string; icon: React.Element
2728
duplicate: { label: 'Duplicated', icon: Copy, color: 'text-pink-400', bg: 'bg-pink-500/10 border-pink-500/20' },
2829
};
2930

30-
// Group consecutive logs by date
31-
function groupByDate(logs: ActivityLog[]) {
31+
// Group consecutive logs by date (timezone-aware)
32+
function groupByDate(logs: ActivityLog[], timezone: string) {
3233
const groups: { date: string; items: ActivityLog[] }[] = [];
34+
35+
const todayLabel = new Date().toLocaleDateString(undefined, { timeZone: timezone, year: 'numeric', month: '2-digit', day: '2-digit' });
36+
const yesterdayDate = new Date(Date.now() - 86400000);
37+
const yesterdayLabel = yesterdayDate.toLocaleDateString(undefined, { timeZone: timezone, year: 'numeric', month: '2-digit', day: '2-digit' });
38+
3339
for (const log of logs) {
3440
const d = new Date(log.created_at);
35-
const today = new Date();
36-
const yesterday = new Date(today);
37-
yesterday.setDate(today.getDate() - 1);
41+
const dLabel = d.toLocaleDateString(undefined, { timeZone: timezone, year: 'numeric', month: '2-digit', day: '2-digit' });
3842

3943
let label: string;
40-
if (d.toDateString() === today.toDateString()) label = 'Today';
41-
else if (d.toDateString() === yesterday.toDateString()) label = 'Yesterday';
42-
else label = d.toLocaleDateString(undefined, { weekday: 'short', month: 'short', day: 'numeric' });
44+
if (dLabel === todayLabel) label = 'Today';
45+
else if (dLabel === yesterdayLabel) label = 'Yesterday';
46+
else label = d.toLocaleDateString(undefined, { timeZone: timezone, weekday: 'short', month: 'short', day: 'numeric' });
4347

4448
const last = groups[groups.length - 1];
4549
if (last && last.date === label) last.items.push(log);
@@ -84,6 +88,7 @@ function StatsBar({ logs }: { logs: ActivityLog[] }) {
8488

8589
export const ActivityTimeline = () => {
8690
const { user } = useAuth();
91+
const timezone = useTimezone();
8792
const [logs, setLogs] = useState<ActivityLog[]>([]);
8893
const [loading, setLoading] = useState(true);
8994
const [error, setError] = useState<string | null>(null);
@@ -105,7 +110,7 @@ export const ActivityTimeline = () => {
105110
useEffect(() => { load(); }, [load]);
106111

107112
const visible = filter === 'all' ? logs : logs.filter(l => l.action === filter);
108-
const grouped = groupByDate(visible);
113+
const grouped = groupByDate(visible, timezone);
109114

110115
const FILTERS: { key: ActivityAction | 'all'; label: string }[] = [
111116
{ key: 'all', label: 'All' },
@@ -212,7 +217,10 @@ export const ActivityTimeline = () => {
212217
</div>
213218

214219
{/* Time */}
215-
<span className="text-xs text-[var(--text-muted)] group-hover:text-[var(--text-muted)] transition-colors whitespace-nowrap shrink-0">
220+
<span
221+
className="text-xs text-[var(--text-muted)] group-hover:text-[var(--text-muted)] transition-colors whitespace-nowrap shrink-0"
222+
title={formatTime(log.created_at, timezone)}
223+
>
216224
{timeAgo(log.created_at)}
217225
</span>
218226
</div>

src/components/dashboard/DashboardHome.tsx

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
} from 'lucide-react';
88
import { useAuth } from '../../lib/AuthContext';
99
import { listStructures, getActivityLog, type Structure, type ActivityLog, type ActivityAction } from '../../lib/structuresService';
10+
import { useTimezone, formatTime } from '../../lib/timezoneUtils';
1011

1112
// ─── Helpers ──────────────────────────────────────────────────────
1213

@@ -164,7 +165,7 @@ function TopStructures({ structures }: { structures: Structure[] }) {
164165

165166
// ─── Recent Activity Feed ─────────────────────────────────────────
166167

167-
function RecentFeed({ logs }: { logs: ActivityLog[] }) {
168+
function RecentFeed({ logs, timezone }: { logs: ActivityLog[], timezone: string }) {
168169
const recent = logs.slice(0, 7);
169170
if (recent.length === 0) return null;
170171

@@ -191,7 +192,10 @@ function RecentFeed({ logs }: { logs: ActivityLog[] }) {
191192
<span className="text-xs text-neutral-300 font-medium truncate">{log.structure_name}</span>
192193
)}
193194
</div>
194-
<span className="text-xs text-neutral-700 shrink-0">{timeAgo(log.created_at)}</span>
195+
<span
196+
className="text-xs text-neutral-700 shrink-0"
197+
title={formatTime(log.created_at, timezone)}
198+
>{timeAgo(log.created_at)}</span>
195199
</div>
196200
);
197201
})}
@@ -231,6 +235,7 @@ function StarredRow({ structures }: { structures: Structure[] }) {
231235

232236
export const DashboardHome = () => {
233237
const { user } = useAuth();
238+
const timezone = useTimezone();
234239
const [structures, setStructures] = useState<Structure[]>([]);
235240
const [logs, setLogs] = useState<ActivityLog[]>([]);
236241
const [loading, setLoading] = useState(true);
@@ -254,9 +259,10 @@ export const DashboardHome = () => {
254259
}).length;
255260

256261
const greeting = () => {
257-
const h = new Date().getHours();
258-
if (h < 12) return 'Good morning';
259-
if (h < 18) return 'Good afternoon';
262+
const h = new Date().toLocaleString('en-US', { timeZone: timezone, hour: 'numeric', hour12: false });
263+
const hour = parseInt(h, 10);
264+
if (hour < 12) return 'Good morning';
265+
if (hour < 18) return 'Good afternoon';
260266
return 'Good evening';
261267
};
262268

@@ -327,7 +333,7 @@ export const DashboardHome = () => {
327333
<div className="lg:col-span-2 space-y-4">
328334
<StarredRow structures={structures} />
329335
<TopStructures structures={structures} />
330-
<RecentFeed logs={logs} />
336+
<RecentFeed logs={logs} timezone={timezone} />
331337

332338
{/* Empty state */}
333339
{structures.length === 0 && (

src/components/dashboard/MyStructures.tsx

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
import { useNavigate } from 'react-router-dom';
1010
import { useAuth } from '../../lib/AuthContext';
1111
import { useTranslation } from '../../lib/i18n';
12+
import { timeAgo } from '../../lib/timezoneUtils';
1213
import {
1314
listStructures, uploadStructure, toggleStar, deleteStructure,
1415
renameStructure, updateNotes, updateTags, importFromRCSB,
@@ -56,15 +57,6 @@ function formatBytes(bytes: number | null): string {
5657
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
5758
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
5859
}
59-
function timeAgo(iso: string): string {
60-
const d = Date.now() - new Date(iso).getTime(), m = Math.floor(d / 60000);
61-
if (m < 1) return 'just now';
62-
if (m < 60) return `${m}m ago`;
63-
const h = Math.floor(m / 60);
64-
if (h < 24) return `${h}h ago`;
65-
const dy = Math.floor(h / 24);
66-
return dy < 7 ? `${dy}d ago` : new Date(iso).toLocaleDateString();
67-
}
6860

6961
// ── Tag system ────────────────────────────────────────────────────
7062

src/lib/timezoneUtils.ts

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { useAuth } from './AuthContext';
2+
3+
/** Maps the human-readable timezone labels used in AccountSettings → IANA identifiers */
4+
export const TIMEZONE_MAP: Record<string, string> = {
5+
'Pacific Time (PT)': 'America/Los_Angeles',
6+
'Mountain Time (MT)': 'America/Denver',
7+
'Central Time (CT)': 'America/Chicago',
8+
'Eastern Time (ET)': 'America/New_York',
9+
'Central European Time (CET)': 'Europe/Paris',
10+
'Greenwich Mean Time (GMT)': 'Europe/London',
11+
'Coordinated Universal Time (UTC)': 'UTC',
12+
'Japan Standard Time (JST)': 'Asia/Tokyo',
13+
'China Standard Time (CST)': 'Asia/Shanghai',
14+
'India Standard Time (IST)': 'Asia/Kolkata',
15+
'Australian Eastern Time (AET)': 'Australia/Sydney',
16+
};
17+
18+
/** Resolves a stored timezone preference string to an IANA id */
19+
export function resolveTimezone(preference: string | undefined): string {
20+
if (!preference) return Intl.DateTimeFormat().resolvedOptions().timeZone;
21+
return TIMEZONE_MAP[preference] ?? Intl.DateTimeFormat().resolvedOptions().timeZone;
22+
}
23+
24+
/** Hook that returns the currently authenticated user's IANA timezone */
25+
export function useTimezone(): string {
26+
const { user } = useAuth();
27+
return resolveTimezone(user?.user_metadata?.timezone);
28+
}
29+
30+
/**
31+
* Format an ISO date string (or Date) as a short date: e.g. "Mar 22, 2026"
32+
*/
33+
export function formatDate(
34+
iso: string | Date,
35+
timezone: string,
36+
options?: Intl.DateTimeFormatOptions
37+
): string {
38+
const date = typeof iso === 'string' ? new Date(iso) : iso;
39+
if (isNaN(date.getTime())) return '—';
40+
return date.toLocaleDateString(undefined, {
41+
year: 'numeric',
42+
month: 'short',
43+
day: 'numeric',
44+
timeZone: timezone,
45+
...options,
46+
});
47+
}
48+
49+
/**
50+
* Format an ISO date string (or Date) as a time: e.g. "2:34 PM"
51+
*/
52+
export function formatTime(
53+
iso: string | Date,
54+
timezone: string,
55+
options?: Intl.DateTimeFormatOptions
56+
): string {
57+
const date = typeof iso === 'string' ? new Date(iso) : iso;
58+
if (isNaN(date.getTime())) return '—';
59+
return date.toLocaleTimeString(undefined, {
60+
hour: '2-digit',
61+
minute: '2-digit',
62+
timeZone: timezone,
63+
...options,
64+
});
65+
}
66+
67+
/**
68+
* Format an ISO date string (or Date) as a full datetime: e.g. "Mar 22, 2026, 2:34 PM"
69+
*/
70+
export function formatDateTime(
71+
iso: string | Date,
72+
timezone: string,
73+
options?: Intl.DateTimeFormatOptions
74+
): string {
75+
const date = typeof iso === 'string' ? new Date(iso) : iso;
76+
if (isNaN(date.getTime())) return '—';
77+
return date.toLocaleString(undefined, {
78+
year: 'numeric',
79+
month: 'short',
80+
day: 'numeric',
81+
hour: '2-digit',
82+
minute: '2-digit',
83+
timeZone: timezone,
84+
...options,
85+
});
86+
}
87+
88+
/**
89+
* Relative time string ("just now", "5m ago", "2h ago", "3d ago")
90+
* identical to the existing logic in several components — centralised here.
91+
*/
92+
export function timeAgo(iso: string | Date): string {
93+
const date = typeof iso === 'string' ? new Date(iso) : iso;
94+
const m = Math.floor((Date.now() - date.getTime()) / 60000);
95+
if (m < 1) return 'just now';
96+
if (m < 60) return `${m}m ago`;
97+
const h = Math.floor(m / 60);
98+
if (h < 24) return `${h}h ago`;
99+
const d = Math.floor(h / 24);
100+
if (d < 7) return `${d}d ago`;
101+
return formatDate(date, Intl.DateTimeFormat().resolvedOptions().timeZone);
102+
}

0 commit comments

Comments
 (0)