Skip to content

Commit 1d01c3d

Browse files
Change frontend
1 parent 7038dac commit 1d01c3d

12 files changed

Lines changed: 220 additions & 199 deletions

src/app/api/apps/route.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,44 @@ import { successJson } from '@/app/api/utils/jsonResponses';
44
import { dbConnect } from '@/app/api/database';
55

66
export async function GET() {
7+
// Return mock data when running in development or when NEXT_PUBLIC_USE_MOCK is set
8+
if (
9+
process.env.NODE_ENV !== 'production' ||
10+
process.env.NEXT_PUBLIC_USE_MOCK === 'true'
11+
) {
12+
const now = new Date();
13+
const tenMinutesAgo = new Date(
14+
now.getTime() - 10 * 60 * 1000,
15+
).toISOString();
16+
const oneHourAgo = new Date(now.getTime() - 60 * 60 * 1000).toISOString();
17+
18+
const dummyApps = [
19+
{
20+
id: '1',
21+
name: 'Website',
22+
downIntervals: [],
23+
lastUpdated: now.toISOString(),
24+
// imageUrl: '/logo192.png',
25+
},
26+
{
27+
id: '2',
28+
name: 'API',
29+
downIntervals: [
30+
{
31+
severity: 1,
32+
description: 'External provider outage caused errors',
33+
startTime: oneHourAgo,
34+
endTime: tenMinutesAgo,
35+
},
36+
],
37+
lastUpdated: now.toISOString(),
38+
// imageUrl: '/api-icon.png',
39+
},
40+
];
41+
42+
return NextResponse.json(successJson(dummyApps), { status: 200 });
43+
}
44+
745
await dbConnect();
846
try {
947
const apps = await AppController.getApps();

src/app/page.tsx

Lines changed: 53 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -5,78 +5,102 @@ import PageHeader from '@/components/PageHeader';
55
import { ReportBug } from '@/components/ReportBug';
66
import { Subscribe } from '@/components/Subscribe';
77
import { Timeline } from '@/components/Timeline';
8-
import TimelineCard from '@/components/TimelineCard';
98
import { App } from '@/types/App';
109
import { useEffect, useState } from 'react';
1110

1211
interface AppsResponse {
1312
success: boolean;
14-
data: any[];
13+
data: App[];
1514
}
1615

1716
export default function Home() {
1817
const [apps, setApps] = useState<App[]>([]);
1918
const [appNames, setAppNames] = useState<string[]>([]);
20-
const [selectedApp, setSelectedApp] = useState<App | undefined>();
19+
const [lastUpdated, setLastUpdated] = useState<number | null>(null);
20+
const [now, setNow] = useState<number>(Date.now());
2121

22+
// Timer for last updated counter
2223
useEffect(() => {
23-
(async () => {
24+
const interval = setInterval(() => {
25+
setNow(Date.now());
26+
}, 1000);
27+
return () => clearInterval(interval);
28+
}, []);
29+
30+
useEffect(() => {
31+
let mounted = true;
32+
let timeoutId: NodeJS.Timeout;
33+
const controller = new AbortController();
34+
35+
const fetchApps = async () => {
2436
try {
2537
const response = await fetch(`/api/apps/`, {
2638
method: 'GET',
2739
headers: { 'Content-Type': 'application/json' },
40+
signal: controller.signal,
2841
});
2942

3043
if (!response.ok) {
3144
console.error('Failed to fetch apps');
3245
return;
3346
}
3447

35-
const data = ((await response.json()) as AppsResponse).data as App[];
48+
const json = (await response.json()) as AppsResponse;
49+
const data = Array.isArray(json.data) ? json.data : [];
50+
// only update state if still mounted
51+
if (!mounted) return;
3652
setApps(data);
3753
setAppNames(data.map((app) => app.name));
3854
if (data.length > 0) {
39-
setSelectedApp(data[0]);
55+
setLastUpdated(Date.now());
4056
}
41-
} catch (error) {
57+
} catch (error: unknown) {
58+
if ((error as any)?.name === 'AbortError') return; // expected on abort
4259
console.error('Error fetching apps:', error);
60+
} finally {
61+
if (mounted) {
62+
timeoutId = setTimeout(fetchApps, 30_000);
63+
}
4364
}
44-
})();
65+
};
66+
67+
fetchApps();
68+
69+
return () => {
70+
mounted = false;
71+
clearTimeout(timeoutId);
72+
controller.abort();
73+
};
4574
}, []);
4675

4776
return (
4877
<div className="bg-gray-bug overflow-hidden">
4978
<PageHeader />
50-
<div className="flex flex-col mx-auto items-center justify-center px-4 pt-8 gap-4 lg-tablet:px-6 lg-tablet:pt-16 sm-desktop:pt-[124px] sm-desktop:w-[1152px]">
79+
<div className="flex flex-col mx-auto items-center justify-center px-4 pt-8 gap-4 lg-tablet:px-6 lg-tablet:pt-16 sm-desktop:pt-31 sm-desktop:w-6xl">
5180
<div className="flex flex-col mr-auto gap-2 mb-4 sm-tablet:mb-6 lg-tablet:mb-8">
5281
<h1 className="h2 lg-tablet:h1">Platform Status</h1>
5382
<p className="p1 text-gray-04">
5483
Any issues with our applications will be shown below.
5584
</p>
85+
{lastUpdated && (
86+
<p className="p3 sm-tablet:p1 text-gray-06 w-full">
87+
{`Last updated: ${Math.max(0, Math.floor((now - lastUpdated) / 1000))} seconds ago`}
88+
</p>
89+
)}
5690
</div>
57-
<div className="flex flex-col sm-desktop:hidden w-full gap-4">
58-
<Overview
59-
apps={apps}
60-
selectedApp={selectedApp}
61-
setSelectedApp={setSelectedApp}
62-
/>
63-
{selectedApp !== undefined ? (
64-
<TimelineCard key={selectedApp?.id} app={selectedApp} />
65-
) : null}
66-
<Subscribe appNames={appNames} />
67-
<ReportBug appNames={appNames} />
68-
</div>
69-
<div className="flex-row hidden sm-desktop:flex gap-4 w-full">
70-
<div className="flex flex-col gap-4 sm-desktop:w-[540px]">
71-
<Overview
72-
apps={apps}
73-
selectedApp={selectedApp}
74-
setSelectedApp={setSelectedApp}
75-
/>
91+
<div className="flex flex-col sm-desktop:flex-row w-full gap-4">
92+
<div className="flex flex-col gap-4 sm-desktop:w-135">
93+
<Overview apps={apps} />
94+
<div className="hidden sm-desktop:flex flex-col gap-4">
95+
<Subscribe appNames={appNames} />
96+
<ReportBug appNames={appNames} />
97+
</div>
98+
</div>
99+
<Timeline apps={apps} />
100+
<div className="flex sm-desktop:hidden flex-col gap-4">
76101
<Subscribe appNames={appNames} />
77102
<ReportBug appNames={appNames} />
78103
</div>
79-
<Timeline apps={apps} />
80104
</div>
81105
<PageFooter />
82106
</div>
Lines changed: 91 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,113 +1,151 @@
11
'use client';
2-
import { MAX_PREVIEW_HOURS } from '@/constants';
32
import { DownInterval, Severity } from '@/types/DownInterval';
43

4+
const DAYS = 90;
5+
const MS_PER_DAY = 24 * 60 * 60 * 1000;
6+
57
export default function BlocksAndDateTimeline({
68
downIntervals,
79
}: {
810
downIntervals: DownInterval[];
911
}) {
10-
const timeline: (Severity | undefined)[] =
11-
Array(MAX_PREVIEW_HOURS).fill(undefined);
12+
const timeline: (Severity | undefined)[] = Array(DAYS).fill(undefined);
1213

1314
const mapMillisToIndex = (m: number): number => {
14-
const now = new Date().valueOf();
15-
return Math.floor(Math.max(now - m, 0) / (3.6 * Math.pow(10, 6)));
15+
const now = Date.now();
16+
return Math.floor(Math.max(now - m, 0) / MS_PER_DAY);
1617
};
1718

19+
let totalDowntimeMs = 0;
20+
const ninetyDaysAgoMs = Date.now() - DAYS * MS_PER_DAY;
21+
1822
for (const { severity, startTime, endTime } of downIntervals) {
1923
const startMillis = new Date(startTime).valueOf();
20-
const endMillis = endTime
21-
? new Date(endTime).valueOf()
22-
: new Date().valueOf() + 3.6 * Math.pow(10, 6);
23-
const first = Math.min(mapMillisToIndex(startMillis), MAX_PREVIEW_HOURS);
24-
const last = mapMillisToIndex(endMillis);
25-
26-
for (let index = last; index <= first; index++) {
27-
timeline[index] = severity;
24+
const endMillis = endTime ? new Date(endTime).valueOf() : Date.now();
25+
26+
const first = Math.min(mapMillisToIndex(startMillis), DAYS - 1);
27+
const last = Math.max(mapMillisToIndex(endMillis), 0);
28+
29+
if (first >= 0 && last < DAYS) {
30+
for (let index = last; index <= first; index++) {
31+
if (timeline[index] !== Severity.High) {
32+
timeline[index] = severity;
33+
}
34+
}
35+
}
36+
37+
const clampedStart = Math.max(startMillis, ninetyDaysAgoMs);
38+
const clampedEnd = Math.min(endMillis, Date.now());
39+
40+
if (clampedEnd > clampedStart) {
41+
totalDowntimeMs += clampedEnd - clampedStart;
2842
}
2943
}
44+
45+
const totalWindowMs = DAYS * MS_PER_DAY;
46+
const uptimePercentage = Math.max(
47+
0,
48+
((totalWindowMs - totalDowntimeMs) / totalWindowMs) * 100
49+
);
50+
51+
const formattedUptime =
52+
uptimePercentage >= 99.9
53+
? uptimePercentage.toFixed(2)
54+
: uptimePercentage.toFixed(1);
55+
3056
timeline.reverse();
3157

32-
const TimelineBlock = ({ severity }: { severity: Severity | undefined }) => {
58+
// Updated TimelineBlock accepts daysAgo to calculate the exact date
59+
const TimelineBlock = ({
60+
severity,
61+
daysAgo
62+
}: {
63+
severity: Severity | undefined;
64+
daysAgo: number;
65+
}) => {
66+
// Format the date for the tooltip
67+
const blockDate = new Date(Date.now() - daysAgo * MS_PER_DAY);
68+
const dateString = blockDate.toLocaleDateString('en-US', {
69+
month: 'short',
70+
day: 'numeric',
71+
year: 'numeric'
72+
});
73+
74+
// Swap tooltip-info for tooltip-error, tooltip-warning, and tooltip-success
3375
switch (severity) {
3476
case Severity.High: {
3577
return (
3678
<span
37-
className="tooltip tooltip-info rounded-xl h-4 hover:h-5 transition-all ease-in-out bg-failure"
38-
data-tip="Total outage"
79+
className="tooltip tooltip-error rounded-xl h-4 hover:h-5 transition-all ease-in-out bg-failure"
80+
data-tip={`${dateString}: Total Outage`}
3981
/>
4082
);
4183
}
4284
case Severity.Medium: {
4385
return (
44-
// <div className="tooltip" data-tip="hello">
4586
<span
46-
className="tooltip tooltip-info rounded-xl h-8 hover:h-9 transition-all ease-in-out bg-warning"
47-
data-tip="Partial Outage"
87+
className="tooltip tooltip-warning rounded-xl h-8 hover:h-9 transition-all ease-in-out bg-warning"
88+
data-tip={`${dateString}: Partial Outage`}
4889
/>
49-
// </div>
5090
);
5191
}
5292
default: {
5393
return (
54-
// <div className="tooltip" data-tip="hello">
5594
<span
56-
className="tooltip tooltip-info rounded-xl h-12 hover:h-13 transition-all ease-in-out bg-success"
57-
data-tip="Operational"
95+
className="tooltip tooltip-success rounded-xl h-12 hover:h-13 transition-all ease-in-out bg-success"
96+
data-tip={`${dateString}: Operational`}
5897
/>
59-
// </div>
6098
);
6199
}
62100
}
63101
};
64102

65103
return (
66104
<>
67-
{/* Blocks and Date Mobile */}
68-
<div className="sm-tablet:hidden">
69-
<p className="text-gray-06 p">Timeline</p>
70-
<div className="h-2" />
105+
<div className="flex flex-row justify-between items-end mb-2">
106+
<p className="text-gray-06 p1">Timeline</p>
107+
<p className="p1 text-gray-06 font-medium">{formattedUptime}% uptime</p>
108+
</div>
109+
110+
{/* Mobile: 30 Days */}
111+
<div className="sm-tablet:hidden flex flex-col w-full">
71112
<div className="grid grid-flow-col gap-1 justify-stretch h-13 items-end">
72-
{timeline.slice(timeline.length - 24).map((elt, i) => (
73-
<TimelineBlock severity={elt} key={i} />
113+
{timeline.slice(timeline.length - 30).map((elt, i) => (
114+
// Calculate daysAgo: (total days in slice - 1) - current index
115+
<TimelineBlock severity={elt} daysAgo={29 - i} key={i} />
74116
))}
75117
</div>
76-
<div className="h-2" />
77-
78-
<div className="flex flex-row justify-end">
79-
<p className="p3 text-gray-04">Last 24 hours</p>
118+
<div className="flex flex-row justify-between items-center text-sm mt-3">
119+
<p className="p3 text-gray-04">30 days ago</p>
120+
<p className="p3 text-gray-04">Today</p>
80121
</div>
81122
</div>
82-
{/* Blocks and Date small tablet */}
83-
<div className="hidden sm-tablet:block md-desktop:hidden">
84-
<p className="text-gray-06 p1">Timeline</p>
85-
<div className="h-2" />
123+
124+
{/* Tablet: 60 Days */}
125+
<div className="hidden sm-tablet:flex md-desktop:hidden flex-col w-full">
86126
<div className="grid grid-flow-col gap-1 justify-stretch h-13 items-end">
87-
{timeline.slice(timeline.length - 48).map((elt, i) => (
88-
<TimelineBlock severity={elt} key={i} />
127+
{timeline.slice(timeline.length - 60).map((elt, i) => (
128+
<TimelineBlock severity={elt} daysAgo={59 - i} key={i} />
89129
))}
90130
</div>
91-
<div className="h-2" />
92-
93-
<div className="flex flex-row justify-end">
94-
<p className="p1 text-gray-04">Last 48 hours</p>
131+
<div className="flex flex-row justify-between items-center text-sm mt-3">
132+
<p className="p1 text-gray-04">60 days ago</p>
133+
<p className="p1 text-gray-04">Today</p>
95134
</div>
96135
</div>
97-
{/* Blocks and date large */}
98-
<div className="hidden md-desktop:flex flex-col">
99-
<p className="text-gray-06 p1">Timeline</p>
100-
<div className="h-2" />
136+
137+
{/* Desktop: 90 Days */}
138+
<div className="hidden md-desktop:flex flex-col w-full">
101139
<div className="grid grid-flow-col gap-1 justify-stretch h-13 items-end">
102140
{timeline.map((elt, i) => (
103-
<TimelineBlock severity={elt} key={i} />
141+
<TimelineBlock severity={elt} daysAgo={89 - i} key={i} />
104142
))}
105143
</div>
106-
<div className="h-2" />
107-
<div className="flex flex-row justify-end">
108-
<p className="p1 text-gray-04">Last 72 hours</p>
144+
<div className="flex flex-row justify-between items-center text-sm mt-3">
145+
<p className="p1 text-gray-04">90 days ago</p>
146+
<p className="p1 text-gray-04">Today</p>
109147
</div>
110148
</div>
111149
</>
112150
);
113-
}
151+
}

0 commit comments

Comments
 (0)