Skip to content

Commit 8ddc2b0

Browse files
refactor(): recreate component
Co-Authored-By: Cristian C. Giraldo <52677987+imchriistian@users.noreply.github.com>
1 parent 8548504 commit 8ddc2b0

8 files changed

Lines changed: 138 additions & 59 deletions

File tree

bun.lock

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"prisma:migrate": "prisma migrate dev"
1212
},
1313
"dependencies": {
14+
"@js-temporal/polyfill": "^0.5.1",
1415
"@prisma/client": "^6.17.1",
1516
"next": "15.5.6",
1617
"prisma": "^6.17.1",

src/app/page.tsx

Lines changed: 13 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
"use client";
1+
'use client';
22

3-
import { useState } from "react";
4-
import { IncidentForm } from "@/components/IncidentForm";
5-
import { IncidentCard } from "@/components/IncidentCard";
6-
import { mockIncidents } from "@/lib/mockData";
7-
import { Incident } from "@/types/incident";
8-
import { Counter } from "@/components/Counter";
3+
import { useState } from 'react';
4+
import { IncidentForm } from '@/components/IncidentForm';
5+
import { IncidentCard } from '@/components/IncidentCard';
6+
import { mockIncidents } from '@/lib/mockData';
7+
import { Incident } from '@/types/incident';
8+
import { Counter } from '@/components/Counter';
9+
import { Temporal } from '@js-temporal/polyfill';
910

1011
export default function HomePage() {
1112
const [incidents, setIncidents] = useState<Incident[]>(mockIncidents);
@@ -15,21 +16,15 @@ export default function HomePage() {
1516
};
1617

1718
return (
18-
<div className="max-w-2xl mx-auto">
19-
<div className="flex justify-center my-6">
20-
<Counter />
21-
</div>
22-
19+
<div className='max-w-2xl mx-auto'>
2320
<IncidentForm onAddIncident={handleAddIncident} />
24-
<h2 className="text-lg font-semibold mt-8 mb-4 text-white">
25-
Incident History
26-
</h2>
27-
<div className="space-y-3">
21+
<h2 className='text-lg font-semibold mt-8 mb-4 text-white'>Incident History</h2>
22+
<div className='space-y-3'>
2823
{incidents.length === 0 ? (
29-
<p className="text-gray-500">No incidents recorded.</p>
24+
<p className='text-gray-500'>No incidents recorded.</p>
3025
) : (
3126
incidents.map((incident) => (
32-
<IncidentCard key={incident.id} incident={incident} />
27+
<Counter key={incident.id} title={incident.title} lastIncidentDate={Temporal.Instant.from(incident.date)} />
3328
))
3429
)}
3530
</div>

src/components/Counter.tsx

Lines changed: 56 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,66 @@
1-
"use client";
1+
import React, { useEffect, useState } from 'react';
2+
import { Temporal } from '@js-temporal/polyfill';
3+
import { timeAgo } from '@/utils/timeAgo';
24

3-
import { useEffect, useState } from "react";
4-
5-
export function Counter() {
6-
const [daysWithoutIncidents, setDaysWithoutIncidents] = useState<number>(0);
5+
export function Counter({ title, lastIncidentDate }: { title: string; lastIncidentDate: Temporal.Instant }) {
6+
const [now, setNow] = useState(() => Temporal.Now.instant());
77

88
useEffect(() => {
9-
// Only using this to save the date of the last incident in localStorage
10-
const lastIncident = localStorage.getItem("lastIncidentDate");
11-
if (lastIncident) {
12-
const diff =
13-
(Date.now() - new Date(lastIncident).getTime()) / (1000 * 60 * 60 * 24);
14-
setDaysWithoutIncidents(Math.floor(diff));
15-
} else {
16-
setDaysWithoutIncidents(0);
17-
}
9+
const interval = setInterval(() => {
10+
setNow(Temporal.Now.instant());
11+
}, 1000);
12+
return () => clearInterval(interval);
1813
}, []);
1914

20-
const variantClass =
21-
daysWithoutIncidents === 0
22-
? "bg-red-600 text-white"
23-
: daysWithoutIncidents > 10
24-
? "bg-green-600 text-white"
25-
: daysWithoutIncidents > 5
26-
? "bg-yellow-400 text-black"
27-
: "bg-blue-600 text-white";
15+
const relative = timeAgo({ pastDate: lastIncidentDate, targetDate: now });
16+
const lastIncidentPlainDate = Temporal.PlainDate.from(lastIncidentDate.toString().slice(0, 10));
17+
const todayPlainDate = Temporal.PlainDate.from(now.toString().slice(0, 10));
18+
const days = lastIncidentPlainDate.until(todayPlainDate).days;
19+
const daysString = Math.max(0, days).toString().padStart(8, '0');
2820

2921
return (
30-
<div className={`${variantClass} px-6 py-4 rounded-lg shadow-md text-center w-full max-w-sm`}>
31-
<span className="text-xs block">Days without incidents</span>
32-
<span className="text-3xl font-bold">{daysWithoutIncidents}</span>
22+
<div className='bg-black text-white rounded-lg max-w-3xl w-full shadow-xl divide-y-2 divide-gray-900'>
23+
<div className='relative bg-black text-center rounded-t-lg py-3'>
24+
<div className='absolute left-4 top-1/2 -translate-y-1/2'>
25+
<span className='block w-3.5 h-3.5 rounded-full bg-yellow-400 animate-ping opacity-40 absolute'></span>
26+
<span className='block w-3.5 h-3.5 rounded-full bg-yellow-400 relative'></span>
27+
</div>
28+
29+
<span className='opacity-60 mr-2'>Days since</span>
30+
<span className='font-bold text-xl'>{title}</span>
31+
32+
<div className='absolute right-4 top-1/2 -translate-y-1/2'>
33+
<span className='block w-3.5 h-3.5 rounded-full bg-yellow-400 animate-ping opacity-40 absolute'></span>
34+
<span className='block w-3.5 h-3.5 rounded-full bg-yellow-400 relative'></span>
35+
</div>
36+
</div>
37+
38+
<div className='bg-white flex justify-center'>
39+
<div className='flex w-full divide-x-2 divide-gray-900'>
40+
{daysString.split('').map((digit, index) => (
41+
<span
42+
key={index}
43+
className='bg-white text-black text-6xl font-[DigitalDisplay] leading-none flex items-center justify-center w-16 h-24 flex-1'
44+
>
45+
{digit}
46+
</span>
47+
))}
48+
</div>
49+
</div>
50+
51+
<div className='bg-yellow-400 rounded-b-lg text-black text-center py-3 px-4 text-base flex'>
52+
<div className='flex-3 flex-grow-[3] flex items-center justify-start'>
53+
<span className='opacity-60 mr-1'>Last:</span>
54+
<span>{relative}</span>
55+
</div>
56+
<div className='flex-1 flex-grow flex items-center justify-end gap-2'>
57+
<button className='bg-black text-yellow-400 px-3 py-1 rounded hover:bg-gray-800 transition'>🗑️</button>
58+
<button className='bg-black text-yellow-400 px-3 py-1 rounded hover:bg-gray-800 transition'>🔄</button>
59+
<button className='bg-black text-yellow-400 px-3 py-1 rounded hover:bg-gray-800 transition'>↕️</button>
60+
</div>
61+
</div>
3362
</div>
3463
);
3564
}
65+
66+
export default Counter;

src/components/IncidentCard.tsx

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,19 @@
1-
import { Incident } from "@/types/incident";
2-
1+
import { Incident } from '@/types/incident';
32
interface Props {
43
incident: Incident;
54
}
65

76
export function IncidentCard({ incident }: Props) {
87
// Compute a deterministic date in DD-MM-YYYY using the ISO string to avoid hydration differences
9-
const isoDate = new Date(incident.date).toISOString().split("T")[0];
10-
const [year, month, day] = isoDate.split("-");
8+
const isoDate = incident.date.toString().split('T')[0];
9+
const [year, month, day] = isoDate.split('-');
1110
const formattedDate = `${day}-${month}-${year}`;
1211

1312
return (
14-
<div className="bg-white p-4 rounded-lg shadow-sm border">
15-
<h3 className="font-semibold text-black">{incident.title}</h3>
16-
<p className="text-sm text-gray-600 mt-1">{incident.description}</p>
17-
<span className="text-xs text-red-600 mt-2 block">{formattedDate}</span>
13+
<div className='bg-white p-4 rounded-lg shadow-sm border'>
14+
<h3 className='font-semibold text-black'>{incident.title}</h3>
15+
<p className='text-sm text-gray-600 mt-1'>{incident.description}</p>
16+
<span className='text-xs text-red-600 mt-2 block'>{formattedDate}</span>
1817
</div>
1918
);
2019
}

src/lib/mockData.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
1-
import { Incident } from "@/types/incident";
1+
import { Incident } from '@/types/incident';
2+
import { Temporal } from '@js-temporal/polyfill';
23

34
export const mockIncidents: Incident[] = [
45
{
56
id: 1,
6-
title: "Main Server Failure",
7-
description: "Server stopped responding for 10 minutes.",
8-
date: new Date("2025-10-15"),
7+
title: 'Main Server Failure',
8+
description: 'Server stopped responding for 10 minutes.',
9+
date: Temporal.Instant.from('2025-10-24T05:35:00Z'),
910
},
1011
{
1112
id: 2,
12-
title: "Form Validation Error",
13-
description: "Email field was not accepting certain valid domains.",
14-
date: new Date("2025-10-20"),
13+
title: 'Form Validation Error',
14+
description: 'Email field was not accepting certain valid domains.',
15+
date: Temporal.Instant.from('2025-10-20T00:00:00Z'),
1516
},
1617
];

src/types/incident.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
import { Temporal } from '@js-temporal/polyfill';
2+
13
export interface Incident {
24
id: number;
35
title: string;
46
description: string;
5-
date: Date;
7+
date: Temporal.Instant;
68
}

src/utils/timeAgo.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { Temporal } from '@js-temporal/polyfill';
2+
3+
export function timeAgo({ pastDate, targetDate }: { pastDate: Temporal.Instant; targetDate: Temporal.Instant }) {
4+
targetDate ??= Temporal.Now.instant();
5+
const dateDiff = targetDate.since(pastDate, { largestUnit: 'auto' });
6+
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
7+
const locale = Intl.DateTimeFormat().resolvedOptions().locale;
8+
9+
if (dateDiff.months >= 1 || dateDiff.years >= 1) {
10+
const date = pastDate.toZonedDateTimeISO(timeZone);
11+
return date.toLocaleString(locale, {
12+
month: 'short',
13+
day: 'numeric',
14+
year: 'numeric',
15+
weekday: undefined,
16+
});
17+
}
18+
19+
const relativeTimeFormat = new Intl.RelativeTimeFormat(locale, {
20+
numeric: 'auto',
21+
});
22+
const units: [Intl.RelativeTimeFormatUnit, number][] = [
23+
['second', 60],
24+
['minute', 60],
25+
['hour', 24],
26+
['day', 30],
27+
];
28+
let delta = (targetDate.epochMilliseconds - pastDate.epochMilliseconds) / 1000;
29+
30+
for (const [unit, limit] of units) {
31+
if (Math.abs(delta) < limit) {
32+
return relativeTimeFormat.format(-Math.round(delta), unit);
33+
}
34+
delta /= limit;
35+
}
36+
37+
const date = pastDate.toZonedDateTimeISO('America/Mexico_City');
38+
return date.toLocaleString(locale, {
39+
month: 'short',
40+
day: 'numeric',
41+
year: 'numeric',
42+
});
43+
}
44+
45+
export default timeAgo;

0 commit comments

Comments
 (0)