Skip to content

Commit 611f67a

Browse files
committed
Restructure into left sidebar, add n keydown for notes and countdown
1 parent c3de725 commit 611f67a

File tree

7 files changed

+231
-11
lines changed

7 files changed

+231
-11
lines changed

src/App.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,16 @@ function App() {
4949
return dateMatch && regionMatch && typeMatch;
5050
});
5151

52+
useEffect(() => {
53+
const handleKeyDown = (event: KeyboardEvent) => {
54+
if (event.key.toLowerCase() === "n" && selectedEvent) {
55+
setOpenNotes(true);
56+
}
57+
};
58+
window.addEventListener("keydown", handleKeyDown);
59+
return () => window.removeEventListener("keydown", handleKeyDown);
60+
}, [selectedEvent]);
61+
5262
return (
5363
<div className="app-container">
5464
<LeftSidebar
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ import {
1111
ListItemText,
1212
Checkbox,
1313
} from "@mui/material";
14-
import { Challenge } from "../types/challenge";
15-
import rawChallenges from "../../challenges.json";
14+
import { Challenge } from "../../types/challenge";
15+
import rawChallenges from "../../../challenges.json";
1616

1717
interface ChallengeDialogProps {
1818
open: boolean;
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
import { useEffect, useState } from "react";
2+
import {
3+
Dialog,
4+
DialogTitle,
5+
DialogContent,
6+
DialogActions,
7+
Button,
8+
Box,
9+
Typography,
10+
Link,
11+
} from "@mui/material";
12+
import { FutureEvents } from "../../types/futureEvents";
13+
import rawFutureEvents from "../../../futureEvents.json";
14+
15+
interface Props {
16+
open: boolean;
17+
onClose: () => void;
18+
}
19+
20+
const Countdown = ({ open, onClose }: Props) => {
21+
const [futureEvents, setFutureEvents] = useState<FutureEvents[]>([]);
22+
const [timeLeft, setTimeLeft] = useState<ReturnType<
23+
typeof getTimeLeft
24+
> | null>(null);
25+
26+
useEffect(() => {
27+
setFutureEvents(rawFutureEvents);
28+
}, []);
29+
30+
const event = getNextEvent(futureEvents);
31+
32+
useEffect(() => {
33+
if (!event) return;
34+
35+
const updateCountdown = () => {
36+
setTimeLeft(getTimeLeft(event.parsedDate));
37+
};
38+
39+
updateCountdown();
40+
const interval = setInterval(updateCountdown, 1000);
41+
return () => clearInterval(interval);
42+
}, [event]);
43+
44+
return (
45+
<Dialog
46+
open={open}
47+
onClose={onClose}
48+
slotProps={{
49+
paper: {
50+
sx: {
51+
width: "90vw",
52+
height: "90vh",
53+
maxWidth: "1500px",
54+
maxHeight: "90vh",
55+
},
56+
},
57+
}}
58+
>
59+
<DialogTitle
60+
sx={{
61+
fontSize: "2.2rem",
62+
fontWeight: "bold",
63+
textAlign: "center",
64+
}}
65+
>
66+
Next Event
67+
</DialogTitle>
68+
<DialogContent sx={{ p: 2 }}>
69+
<Box sx={{ width: "100%", overflowX: "auto" }}>
70+
{event ? (
71+
<Box
72+
sx={{
73+
display: "flex",
74+
flexDirection: "column",
75+
alignItems: "center",
76+
textAlign: "center",
77+
gap: 2,
78+
mt: 3,
79+
}}
80+
>
81+
<Typography variant="h4" fontWeight="bold">
82+
{event.name}
83+
</Typography>
84+
<Typography variant="subtitle1" color="text.secondary">
85+
{event.location}
86+
</Typography>
87+
<Typography variant="body1">{event.date}</Typography>
88+
{event.link && (
89+
<Link
90+
href={event.link}
91+
target="_blank"
92+
rel="noopener"
93+
underline="hover"
94+
>
95+
Website
96+
</Link>
97+
)}
98+
99+
{timeLeft && (
100+
<Box
101+
sx={{
102+
mt: 4,
103+
mb: 1,
104+
display: "flex",
105+
gap: 3,
106+
flexWrap: "wrap",
107+
}}
108+
>
109+
{[
110+
{ label: "Days", value: timeLeft.days },
111+
{ label: "Hours", value: timeLeft.hours },
112+
{ label: "Minutes", value: timeLeft.minutes },
113+
{ label: "Seconds", value: timeLeft.seconds },
114+
].map((item) => (
115+
<Box
116+
key={item.label}
117+
sx={{
118+
minWidth: 80,
119+
textAlign: "center",
120+
backgroundColor: "#f5f5f5",
121+
borderRadius: 2,
122+
p: 2,
123+
boxShadow: "0px 4px 10px rgba(0, 0, 0, 0.3)",
124+
}}
125+
>
126+
<Typography fontSize="1.8rem" fontWeight="bold">
127+
{item.value}
128+
</Typography>
129+
<Typography variant="caption" color="text.secondary">
130+
{item.label}
131+
</Typography>
132+
</Box>
133+
))}
134+
</Box>
135+
)}
136+
</Box>
137+
) : (
138+
<Typography variant="h6" textAlign="center" mt={2}>
139+
No upcoming events.
140+
</Typography>
141+
)}
142+
</Box>
143+
</DialogContent>
144+
<DialogActions sx={{ justifyContent: "center", p: 2 }}>
145+
<Button onClick={onClose} variant="outlined">
146+
Close
147+
</Button>
148+
</DialogActions>
149+
</Dialog>
150+
);
151+
};
152+
153+
const getTimeLeft = (event: Date) => {
154+
const now = new Date();
155+
const diff = event.getTime() - now.getTime();
156+
157+
if (diff <= 0) return { days: 0, hours: 0, minutes: 0, seconds: 0 };
158+
159+
const seconds = Math.floor(diff / 1000) % 60;
160+
const minutes = Math.floor(diff / 1000 / 60) % 60;
161+
const hours = Math.floor(diff / 1000 / 60 / 60) % 24;
162+
const days = Math.floor(diff / 1000 / 60 / 60 / 24);
163+
164+
return { days, hours, minutes, seconds };
165+
};
166+
167+
const parseDate = (dateStr: string): Date | null => {
168+
const match = dateStr.match(/^(\d{2})\/(\d{2})\/(\d{2})$/);
169+
if (!match) return null;
170+
171+
const [_, day, month, year] = match;
172+
const parsed = new Date(
173+
parseInt(year) + 2000,
174+
parseInt(month) - 1,
175+
parseInt(day)
176+
);
177+
return isNaN(parsed.getTime()) ? null : parsed;
178+
};
179+
180+
export const getNextEvent = (
181+
events: FutureEvents[]
182+
): (FutureEvents & { parsedDate: Date }) | null => {
183+
const today = new Date();
184+
185+
const validEvents = events
186+
.map((event) => {
187+
const parsedDate = parseDate(event.date);
188+
return parsedDate ? { ...event, parsedDate } : null;
189+
})
190+
.filter((e): e is FutureEvents & { parsedDate: Date } => !!e)
191+
.filter((e) => e.parsedDate >= today)
192+
.sort((a, b) => a.parsedDate.getTime() - b.parsedDate.getTime());
193+
194+
return validEvents.length > 0 ? validEvents[0] : null;
195+
};
196+
197+
export default Countdown;

src/components/futureEvents.tsx renamed to src/components/left-sidebar/futureEvents.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ import {
1313
TableBody,
1414
Link,
1515
} from "@mui/material";
16-
import { FutureEvents } from "../types/futureEvents";
17-
import rawFutureEvents from "../../futureEvents.json";
16+
import { FutureEvents } from "../../types/futureEvents";
17+
import rawFutureEvents from "../../../futureEvents.json";
1818

1919
interface Props {
2020
open: boolean;
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
Typography,
1111
Divider,
1212
} from "@mui/material";
13-
import { TileLayerOption } from "../types/tiles";
13+
import { TileLayerOption } from "../../types/tiles";
1414

1515
interface SettingsDialogProps {
1616
open: boolean;
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
TableRow,
1212
Box,
1313
} from "@mui/material";
14-
import { Event } from "../types/event";
14+
import { Event } from "../../types/event";
1515

1616
interface SummaryDialogProps {
1717
open: boolean;

src/components/leftSidebar.tsx

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
1+
import { RefObject, useEffect, useState } from "react";
12
import { Box, IconButton } from "@mui/material";
23
import SettingsIcon from "@mui/icons-material/Settings";
34
import LeaderboardIcon from "@mui/icons-material/Leaderboard";
45
import EmojiEventsIcon from "@mui/icons-material/EmojiEvents";
56
import EventIcon from "@mui/icons-material/Event";
67
import ZoomOutMapIcon from "@mui/icons-material/ZoomOutMap";
7-
import { RefObject, useEffect, useState } from "react";
8-
import SummaryDialog from "./summary";
9-
import ChallengeDialog from "./challenge";
10-
import SettingsDialog from "./settings";
8+
import AlarmIcon from "@mui/icons-material/Alarm";
9+
import SummaryDialog from "./left-sidebar/summary";
10+
import ChallengeDialog from "./left-sidebar/challenge";
11+
import SettingsDialog from "./left-sidebar/settings";
12+
import FutureEventsDialog from "./left-sidebar/futureEvents";
1113
import { TileLayerOption } from "../types/tiles";
1214
import { Event } from "../types/event";
13-
import FutureEventsDialog from "./futureEvents";
15+
import Countdown from "./left-sidebar/countdown";
1416

1517
interface Props {
1618
filteredEvents: Event[];
@@ -31,6 +33,7 @@ const LeftSidebar = ({
3133
const [openSettings, setOpenSettings] = useState(false);
3234
const [openChallenges, setOpenChallenges] = useState(false);
3335
const [openEvents, setOpenEvents] = useState(false);
36+
const [openCountdown, setOpenCountdown] = useState(false);
3437

3538
useEffect(() => {
3639
const handleKeyDown = (event: KeyboardEvent) => {
@@ -86,6 +89,14 @@ const LeftSidebar = ({
8689
<EventIcon fontSize="large" />
8790
</IconButton>
8891

92+
<IconButton
93+
color="inherit"
94+
title="Event Countdown"
95+
onClick={() => setOpenCountdown(true)}
96+
>
97+
<AlarmIcon fontSize="large" />
98+
</IconButton>
99+
89100
<IconButton
90101
color="inherit"
91102
title="Settings"
@@ -116,6 +127,8 @@ const LeftSidebar = ({
116127
onClose={() => setOpenEvents(false)}
117128
/>
118129

130+
<Countdown open={openCountdown} onClose={() => setOpenCountdown(false)} />
131+
119132
<SettingsDialog
120133
open={openSettings}
121134
selectedTile={selectedTile}

0 commit comments

Comments
 (0)