Skip to content

Commit 0762304

Browse files
committed
fixed: ui discrepancy on home page
1 parent 1eb7350 commit 0762304

File tree

5 files changed

+349
-11
lines changed

5 files changed

+349
-11
lines changed
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
'use client';
2+
3+
import { AlertCircle, X, Info } from 'lucide-react';
4+
import { Alert, AlertDescription } from '@/components/ui/alert';
5+
import { Button } from '@/components/ui/button';
6+
import {
7+
Tooltip,
8+
TooltipContent,
9+
TooltipTrigger,
10+
} from '@/components/ui/tooltip';
11+
import { useEffect, useState } from 'react';
12+
13+
interface MaintenanceBannerProps {
14+
startTime: string; // ISO string
15+
endTime: string; // ISO string
16+
}
17+
18+
export function MaintenanceBanner({
19+
startTime,
20+
endTime,
21+
}: MaintenanceBannerProps) {
22+
const [timeDisplay, setTimeDisplay] = useState<string>('');
23+
const [isMaintenanceActive, setIsMaintenanceActive] = useState(false);
24+
const [isDismissed, setIsDismissed] = useState(false);
25+
const [isMounted, setIsMounted] = useState(false);
26+
27+
// Create a unique key for this maintenance window
28+
const maintenanceKey = `maintenance-dismissed-${startTime}-${endTime}`;
29+
30+
useEffect(() => {
31+
setIsMounted(true);
32+
// Check if this maintenance has been dismissed
33+
const dismissed = localStorage.getItem(maintenanceKey);
34+
if (dismissed === 'true') {
35+
setIsDismissed(true);
36+
}
37+
}, [maintenanceKey]);
38+
39+
useEffect(() => {
40+
const updateCountdown = () => {
41+
const now = new Date();
42+
const start = new Date(startTime);
43+
const end = new Date(endTime);
44+
45+
// Check if maintenance is currently active
46+
if (now >= start && now <= end) {
47+
setIsMaintenanceActive(true);
48+
const diffToEnd = end.getTime() - now.getTime();
49+
50+
if (diffToEnd <= 0) {
51+
setTimeDisplay('Maintenance completed');
52+
return;
53+
}
54+
55+
const hours = Math.floor(diffToEnd / (1000 * 60 * 60));
56+
const minutes = Math.floor(
57+
(diffToEnd % (1000 * 60 * 60)) / (1000 * 60),
58+
);
59+
60+
if (hours > 0) {
61+
setTimeDisplay(`${hours}h ${minutes}m remaining`);
62+
} else {
63+
setTimeDisplay(`${minutes}m remaining`);
64+
}
65+
} else if (now < start) {
66+
// Maintenance hasn't started yet
67+
setIsMaintenanceActive(false);
68+
const diffToStart = start.getTime() - now.getTime();
69+
70+
if (diffToStart <= 0) {
71+
setTimeDisplay('starting now');
72+
return;
73+
}
74+
75+
const days = Math.floor(diffToStart / (1000 * 60 * 60 * 24));
76+
const hours = Math.floor(
77+
(diffToStart % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60),
78+
);
79+
const minutes = Math.floor(
80+
(diffToStart % (1000 * 60 * 60)) / (1000 * 60),
81+
);
82+
83+
if (days > 0) {
84+
setTimeDisplay(`starting in ${days}d ${hours}h`);
85+
} else if (hours > 0) {
86+
setTimeDisplay(`starting in ${hours}h ${minutes}m`);
87+
} else {
88+
setTimeDisplay(`starting in ${minutes}m`);
89+
}
90+
} else {
91+
// Maintenance is over
92+
setTimeDisplay('');
93+
}
94+
};
95+
96+
updateCountdown();
97+
const interval = setInterval(updateCountdown, 60000); // Update every minute
98+
99+
return () => clearInterval(interval);
100+
}, [startTime, endTime]);
101+
102+
const handleDismiss = () => {
103+
setIsDismissed(true);
104+
localStorage.setItem(maintenanceKey, 'true');
105+
};
106+
107+
const formatDateTime = (isoString: string) => {
108+
const date = new Date(isoString);
109+
return date.toLocaleString(undefined, {
110+
weekday: 'short',
111+
month: 'short',
112+
day: 'numeric',
113+
hour: 'numeric',
114+
minute: '2-digit',
115+
timeZoneName: 'short',
116+
});
117+
};
118+
119+
const getDuration = () => {
120+
const start = new Date(startTime);
121+
const end = new Date(endTime);
122+
const diff = end.getTime() - start.getTime();
123+
const hours = Math.floor(diff / (1000 * 60 * 60));
124+
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
125+
126+
if (hours > 0) {
127+
return `${hours}h ${minutes}m`;
128+
} else {
129+
return `${minutes}m`;
130+
}
131+
};
132+
133+
// Don't show banner if maintenance is over or dismissed
134+
const now = new Date();
135+
const end = new Date(endTime);
136+
137+
// Don't render until mounted (SSR safety)
138+
if (!isMounted) {
139+
return null;
140+
}
141+
142+
if (now > end || isDismissed) {
143+
return null;
144+
}
145+
146+
return (
147+
<Alert
148+
className={`py-2 ${
149+
isMaintenanceActive
150+
? 'border-orange-200 bg-orange-50 dark:border-orange-800 dark:bg-orange-950'
151+
: 'border-yellow-200 bg-yellow-50 dark:border-yellow-800 dark:bg-yellow-950'
152+
}`}
153+
>
154+
<AlertCircle
155+
className={`h-4 w-4 ${
156+
isMaintenanceActive
157+
? 'text-orange-600 dark:text-orange-400'
158+
: 'text-yellow-600 dark:text-yellow-400'
159+
}`}
160+
/>
161+
<AlertDescription
162+
className={`flex items-center gap-2 ${
163+
isMaintenanceActive
164+
? 'text-orange-700 dark:text-orange-300'
165+
: 'text-yellow-700 dark:text-yellow-300'
166+
}`}
167+
>
168+
<span className="font-medium">
169+
{isMaintenanceActive
170+
? 'Scheduled maintenance in progress'
171+
: 'Scheduled maintenance'}
172+
</span>
173+
{timeDisplay && (
174+
<>
175+
<span></span>
176+
<span>{timeDisplay}</span>
177+
</>
178+
)}
179+
180+
{/* Info icon with tooltip */}
181+
<Tooltip>
182+
<TooltipTrigger asChild>
183+
<Info className="h-3 w-3 cursor-help opacity-60 hover:opacity-100 transition-opacity" />
184+
</TooltipTrigger>
185+
<TooltipContent side="top" className="max-w-xs">
186+
<div className="space-y-1 text-xs">
187+
{!isMaintenanceActive && (
188+
<div>Starts: {formatDateTime(startTime)}</div>
189+
)}
190+
<div>
191+
{isMaintenanceActive ? 'Expected completion' : 'Ends'}:{' '}
192+
{formatDateTime(endTime)}
193+
</div>
194+
<div>Duration: {getDuration()}</div>
195+
</div>
196+
</TooltipContent>
197+
</Tooltip>
198+
</AlertDescription>
199+
200+
{/* Dismiss button */}
201+
<Button
202+
variant="ghost"
203+
size="sm"
204+
className={`absolute right-2 top-1/2 -translate-y-1/2 h-6 w-6 p-0 hover:bg-transparent ${
205+
isMaintenanceActive
206+
? 'text-orange-600 hover:text-orange-800 dark:text-orange-400 dark:hover:text-orange-200'
207+
: 'text-yellow-600 hover:text-yellow-800 dark:text-yellow-400 dark:hover:text-yellow-200'
208+
}`}
209+
onClick={handleDismiss}
210+
aria-label="Dismiss maintenance notice"
211+
>
212+
<X className="h-3 w-3" />
213+
</Button>
214+
</Alert>
215+
);
216+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
'use client';
2+
3+
import { Clock, AlertTriangle, Server } from 'lucide-react';
4+
import { useEffect, useState } from 'react';
5+
6+
interface MaintenanceNoticeProps {
7+
endTime: string; // ISO string
8+
}
9+
10+
export function MaintenanceNotice({ endTime }: MaintenanceNoticeProps) {
11+
const [timeRemaining, setTimeRemaining] = useState<string>('');
12+
13+
useEffect(() => {
14+
const updateTimeRemaining = () => {
15+
const now = new Date();
16+
const end = new Date(endTime);
17+
const diff = end.getTime() - now.getTime();
18+
19+
if (diff <= 0) {
20+
setTimeRemaining('Almost done!');
21+
return;
22+
}
23+
24+
const hours = Math.floor(diff / (1000 * 60 * 60));
25+
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
26+
27+
if (hours > 0) {
28+
setTimeRemaining(`${hours}h ${minutes}m remaining`);
29+
} else {
30+
setTimeRemaining(`${minutes}m remaining`);
31+
}
32+
};
33+
34+
updateTimeRemaining();
35+
const interval = setInterval(updateTimeRemaining, 60000); // Update every minute
36+
37+
return () => clearInterval(interval);
38+
}, [endTime]);
39+
40+
const formatEndTime = (isoString: string) => {
41+
const date = new Date(isoString);
42+
return date.toLocaleString(undefined, {
43+
weekday: 'short',
44+
month: 'short',
45+
day: 'numeric',
46+
hour: 'numeric',
47+
minute: '2-digit',
48+
timeZoneName: 'short',
49+
});
50+
};
51+
52+
return (
53+
<div className="w-full max-w-4xl mx-auto p-4">
54+
<div className="bg-background border border-border rounded-lg p-6 shadow-sm">
55+
<div className="flex items-start gap-4">
56+
<div className="flex-shrink-0">
57+
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-amber-100 dark:bg-amber-900/20">
58+
<Server className="h-6 w-6 text-amber-600 dark:text-amber-500 animate-pulse" />
59+
</div>
60+
</div>
61+
62+
<div className="flex-1 space-y-4">
63+
<div>
64+
<h3 className="text-lg font-semibold text-foreground">
65+
Scheduled Maintenance
66+
</h3>
67+
<p className="text-muted-foreground mt-1">
68+
We're performing scheduled maintenance to improve our systems.
69+
Some features may be temporarily unavailable.
70+
</p>
71+
</div>
72+
73+
<div className="space-y-3">
74+
<div className="flex items-center gap-2 text-sm text-muted-foreground">
75+
<Clock className="h-4 w-4 flex-shrink-0" />
76+
<span>Expected completion: {formatEndTime(endTime)}</span>
77+
</div>
78+
79+
{timeRemaining && (
80+
<div className="inline-flex items-center gap-2 px-3 py-2 bg-amber-50 dark:bg-amber-900/10 border border-amber-200 dark:border-amber-800/30 rounded-md">
81+
<AlertTriangle className="h-4 w-4 text-amber-600 dark:text-amber-500" />
82+
<span className="text-sm font-medium text-amber-800 dark:text-amber-200">
83+
{timeRemaining}
84+
</span>
85+
</div>
86+
)}
87+
</div>
88+
</div>
89+
</div>
90+
</div>
91+
</div>
92+
);
93+
}

frontend/src/app/(home)/layout.tsx

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,19 @@ import { useAccounts } from '@/hooks/use-accounts';
1313
import { Loader2 } from 'lucide-react';
1414
import { MaintenancePage } from '@/components/maintenance/maintenance-page';
1515
import { StatusOverlay } from '@/components/ui/status-overlay';
16+
import type { IMaintenanceNotice } from '@/lib/edge-flags';
17+
import { MaintenanceNotice } from './_components/maintenance-notice';
18+
import { MaintenanceBanner } from './_components/maintenance-banner';
1619
import { useApiHealth } from '@/hooks/react-query/usage/use-health';
1720

1821
interface HomeLayoutProps {
1922
children: React.ReactNode;
23+
maintenanceNotice?: IMaintenanceNotice;
2024
}
2125

2226
export default function HomeLayout({
2327
children,
28+
maintenanceNotice,
2429
}: HomeLayoutProps) {
2530
const { user, isLoaded } = useUser();
2631
const [sidebarOpen, setSidebarOpen] = useState(false);
@@ -30,6 +35,8 @@ export default function HomeLayout({
3035
// Enhanced features from layout-content.tsx
3136
const [showPricingAlert, setShowPricingAlert] = useState(false);
3237
const [showMaintenanceAlert, setShowMaintenanceAlert] = useState(false);
38+
// TODO: Implement maintenance notice fetching from API or server component
39+
const [currentMaintenanceNotice] = useState<IMaintenanceNotice>({ enabled: false });
3340
const { data: accounts } = useAccounts();
3441
const personalAccount = accounts?.find((account) => account.personal_account);
3542

@@ -63,7 +70,34 @@ export default function HomeLayout({
6370
return () => window.removeEventListener('toggleHomeSidebar', handleToggleSidebar);
6471
}, []);
6572

73+
// Enhanced: Smart Maintenance System
74+
if (currentMaintenanceNotice.enabled) {
75+
const now = new Date();
76+
const startTime = currentMaintenanceNotice.startTime;
77+
const endTime = currentMaintenanceNotice.endTime;
78+
79+
// If maintenance period has started, show maintenance page
80+
if (now > startTime) {
81+
return (
82+
<div className="w-screen h-screen flex items-center justify-center">
83+
<div className="max-w-xl">
84+
<MaintenanceNotice endTime={endTime.toISOString()} />
85+
</div>
86+
</div>
87+
);
88+
}
89+
}
6690

91+
// Enhanced: Maintenance banner for upcoming maintenance
92+
let maintenanceBanner: React.ReactNode | null = null;
93+
if (currentMaintenanceNotice.enabled) {
94+
maintenanceBanner = (
95+
<MaintenanceBanner
96+
startTime={currentMaintenanceNotice.startTime.toISOString()}
97+
endTime={currentMaintenanceNotice.endTime.toISOString()}
98+
/>
99+
);
100+
}
67101

68102
// Enhanced: Show loading state while checking auth or health
69103
if (!isClient || !isLoaded || isCheckingHealth) {
@@ -112,6 +146,9 @@ export default function HomeLayout({
112146
<SidebarProvider open={sidebarOpen} onOpenChange={setSidebarOpen}>
113147
<SidebarLeft />
114148
<SidebarInset>
149+
{/* Enhanced: Maintenance banner */}
150+
{maintenanceBanner}
151+
115152
<div
116153
className="w-full relative min-h-screen"
117154
style={!isThreadPage ? {

frontend/src/components/home/sections/navbar.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ export function Navbar({ sidebarOpen = false }: { sidebarOpen?: boolean }) {
248248
}
249249

250250
return (
251-
<header className="relative z-50 mt-4">
251+
<header className="relative z-50">
252252
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
253253
<div className="flex h-[56px] items-center justify-between">
254254
{!sidebarOpen && (

0 commit comments

Comments
 (0)