Calendar PT1

This commit is contained in:
Dustin Brett
2023-07-07 20:29:44 -07:00
parent 7f028491e4
commit 16e5c26acb
8 changed files with 377 additions and 21 deletions

View File

@@ -0,0 +1,13 @@
import { memo } from "react";
export const Down = memo(() => (
<svg viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<path d="m30.297 7.297 1.406 1.406L16 24.406.297 8.703l1.406-1.406L16 21.594z" />
</svg>
));
export const Up = memo(() => (
<svg viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<path d="M30.547 23.953 16 9.422 1.453 23.953.047 22.547 16 6.578l15.953 15.969z" />
</svg>
));

View File

@@ -0,0 +1,123 @@
import styled from "styled-components";
import { TASKBAR_HEIGHT } from "utils/constants";
const StyledCalendar = styled.section`
backdrop-filter: ${({ theme }) => `blur(${theme.sizes.taskbar.blur})`};
background-color: ${({ theme }) => theme.colors.taskbar.background};
border: ${({ theme }) => `1px solid ${theme.colors.taskbar.peekBorder}`};
border-bottom: 0;
border-right: 0;
position: absolute;
bottom: ${TASKBAR_HEIGHT}px;
right: 0;
table {
padding: 4px 10px 19px;
td {
display: inline-table;
text-align: center;
width: 46px;
height: 40px;
line-height: 32px;
margin: 0 1px;
color: #fff;
&.prev,
&.next {
color: rgb(125, 125, 125);
}
}
thead {
font-size: 12px;
td[colspan] {
display: table-cell;
padding: 0;
div {
display: flex;
font-size: 15px;
padding: 0 16px 0 12px;
place-content: space-between;
header {
color: rgb(223, 223, 223);
&:hover {
color: #fff;
}
&:active {
color: rgb(165, 156, 156);
}
}
}
}
td:not([colspan]) {
height: auto;
margin-top: -1px;
}
nav {
display: flex;
flex-direction: row;
gap: 32px;
padding-top: 2px;
button {
fill: rgb(223, 223, 223);
&:hover {
fill: #fff;
}
&:active {
fill: rgb(165, 156, 156);
}
svg {
width: 16px;
}
}
}
}
tbody.curr td.today {
background-color: rgb(0, 120, 215);
color: #fff;
position: relative;
&::after,
&::before {
content: "";
position: absolute;
}
&::after {
inset: 0;
}
&::before {
inset: 2px;
border: 2px solid #000;
}
&:hover {
&::after {
border: 2px solid rgb(102, 174, 231);
}
}
&:active {
&::after {
border: 2px solid rgb(153, 201, 239);
}
}
}
}
`;
export default StyledCalendar;

View File

@@ -0,0 +1,75 @@
type DayType = "curr" | "next" | "prev" | "today";
type Day = [number, DayType];
export type Calendar = Day[][];
const DAYS_IN_WEEK = 7;
const GRID_ROW_COUNT = 6;
const FIRST_WEEK: Day[] = [
[1, "curr"],
[2, "curr"],
[3, "curr"],
[4, "curr"],
[5, "curr"],
[6, "curr"],
[7, "curr"],
];
export const createCalendar = (date: Date): Calendar => {
const day = date.getDate();
const month = date.getMonth();
const year = date.getFullYear();
const firstDay = new Date(year, month, 1).getDay();
const firstWeek = FIRST_WEEK.slice(0, DAYS_IN_WEEK - firstDay);
const prevLastRow = Array.from({ length: DAYS_IN_WEEK - firstWeek.length })
.map<Day>((_, index) => [new Date(year, month, -index).getDate(), "prev"])
.reverse();
const firstRow = [...prevLastRow, ...firstWeek];
const daysInMonth = new Date(year, month + 1, 0).getDate();
const remainingDays = Array.from({ length: daysInMonth })
.map((_, index) => new Date(year, month, index + 1).getDate())
.slice(firstRow[firstRow.length - 1][0])
.map<Day>((d) => [d, "curr"]);
const rows = [...firstRow, ...remainingDays].reduce<Calendar>(
(acc, value, index) => {
if (index % DAYS_IN_WEEK === 0) acc.push([]);
const [vDay, vType] = value;
acc[acc.length - 1].push(
vType === "curr" && vDay === day ? [vDay, "today"] : value
);
return acc;
},
[]
);
const lastRow = rows[rows.length - 1];
const lastRowDays = Array.from({
length: DAYS_IN_WEEK - lastRow.length,
}).map<Day>((_, index) => [
new Date(year, month + 1, index + 1).getDate(),
"next",
]);
lastRow.push(...lastRowDays);
if (rows.length < GRID_ROW_COUNT) {
const [lastNumber] = lastRow[lastRow.length - 1];
return [
...rows,
lastNumber > DAYS_IN_WEEK - 1
? FIRST_WEEK.map(([value]) => [value, "next"])
: Array.from({ length: DAYS_IN_WEEK }).map<Day>((_, index) => [
index + 1 + lastNumber,
"next",
]),
...(rows.length === 4
? [FIRST_WEEK.map(([value]) => [value + DAYS_IN_WEEK, "next"])]
: []),
] as Calendar;
}
return rows;
};

View File

@@ -0,0 +1,117 @@
import { Down, Up } from "components/system/Taskbar/Calendar/Icons";
import StyledCalendar from "components/system/Taskbar/Calendar/StyledCalendar";
import type { Calendar as ICalendar } from "components/system/Taskbar/Calendar/functions";
import { createCalendar } from "components/system/Taskbar/Calendar/functions";
import { memo, useEffect, useMemo, useRef, useState } from "react";
import Button from "styles/common/Button";
import { FOCUSABLE_ELEMENT, PREVENT_SCROLL } from "utils/constants";
import { spotlightEffect } from "utils/spotlightEffect";
const DAY_NAMES = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
type CalendarProps = {
toggleCalendar: (showCalendar?: boolean) => void;
};
const Calendar: FC<CalendarProps> = ({ toggleCalendar }) => {
const [date, setDate] = useState(() => new Date());
const [calendar, setCalendar] = useState<ICalendar>(() =>
createCalendar(date)
);
const today = useMemo(() => new Date(), []);
const isCurrentDate = useMemo(
() =>
date.getMonth() === today.getMonth() &&
date.getFullYear() === today.getFullYear(),
[date, today]
);
const changeMonth = (direction: number): void => {
const newDate = new Date(date);
newDate.setMonth(newDate.getMonth() + direction);
setDate(newDate);
setCalendar(createCalendar(newDate));
};
const calendarRef = useRef<HTMLTableElement>(null);
useEffect(() => {
calendarRef.current?.addEventListener("blur", ({ relatedTarget }) => {
if (relatedTarget instanceof HTMLElement) {
if (calendarRef.current?.contains(relatedTarget)) {
calendarRef.current?.focus(PREVENT_SCROLL);
return;
}
const clockElement = document.querySelector("main>nav [role=timer]");
if (
clockElement instanceof HTMLDivElement &&
(clockElement === relatedTarget ||
clockElement.contains(relatedTarget))
) {
return;
}
}
toggleCalendar(false);
});
calendarRef.current?.focus(PREVENT_SCROLL);
}, [toggleCalendar]);
return (
calendar && (
<StyledCalendar ref={calendarRef} {...FOCUSABLE_ELEMENT}>
<table>
<thead>
<tr>
<td colSpan={DAY_NAMES.length}>
<div>
<header>
{`${date.toLocaleString("default", {
month: "long",
})}, ${date.getFullYear()}`}
</header>
<nav>
<Button onClick={() => changeMonth(-1)}>
<Up />
</Button>
<Button onClick={() => changeMonth(1)}>
<Down />
</Button>
</nav>
</div>
</td>
</tr>
<tr>
{DAY_NAMES.map((dayName) => (
<td key={dayName}>{dayName}</td>
))}
</tr>
</thead>
<tbody className={isCurrentDate ? "curr" : undefined}>
{calendar?.map((week) => (
<tr key={week.toString()}>
{week.map(([day, type]) => (
<td
key={`${day}${type}`}
ref={(tdRef: HTMLTableDataCellElement) =>
type === "today"
? undefined
: spotlightEffect(tdRef, false, 2)
}
className={type}
>
{day}
</td>
))}
</tr>
))}
</tbody>
</table>
</StyledCalendar>
)
);
};
export default memo(Calendar);

View File

@@ -7,6 +7,7 @@ import useWorker from "hooks/useWorker";
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
BASE_CLOCK_WIDTH,
FOCUSABLE_ELEMENT,
ONE_TIME_PASSIVE_EVENT,
TASKBAR_HEIGHT,
} from "utils/constants";
@@ -39,16 +40,12 @@ const easterEggOnClick: React.MouseEventHandler<HTMLElement> = async ({
triggerEasterEggCountdown === EASTER_EGG_CLICK_COUNT &&
target instanceof HTMLElement
) {
target.setAttribute("tabIndex", "-1");
["blur", "mouseleave"].forEach((type) => {
target.removeEventListener(type, resetEasterEggCountdown);
target.addEventListener(
type,
resetEasterEggCountdown,
ONE_TIME_PASSIVE_EVENT
);
});
target.removeEventListener("mouseleave", resetEasterEggCountdown);
target.addEventListener(
"mouseleave",
resetEasterEggCountdown,
ONE_TIME_PASSIVE_EVENT
);
}
triggerEasterEggCountdown -= 1;
@@ -57,11 +54,16 @@ const easterEggOnClick: React.MouseEventHandler<HTMLElement> = async ({
const { default: spawnSheep } = await import("utils/spawnSheep");
spawnSheep();
triggerEasterEggCountdown = EASTER_EGG_CLICK_COUNT;
}
};
const Clock: FC = () => {
type ClockProps = {
toggleCalendar: () => void;
};
const Clock: FC<ClockProps> = ({ toggleCalendar }) => {
const [now, setNow] = useState<LocaleTimeDate>(
Object.create(null) as LocaleTimeDate
);
@@ -97,7 +99,7 @@ const Clock: FC = () => {
},
[clockSource]
);
const clockContextMenu = useClockContextMenu();
const clockContextMenu = useClockContextMenu(toggleCalendar);
const currentWorker = useWorker<ClockWorkerResponse>(
clockWorkerInit,
updateTime
@@ -130,6 +132,13 @@ const Clock: FC = () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
[currentWorker, now]
);
const onClockClick = useCallback(
(event: React.MouseEvent<HTMLElement>) => {
easterEggOnClick(event);
toggleCalendar();
},
[toggleCalendar]
);
useEffect(() => {
offScreenClockCanvas.current = undefined;
@@ -163,11 +172,12 @@ const Clock: FC = () => {
<StyledClock
ref={supportsOffscreenCanvas ? clockCallbackRef : undefined}
aria-label="Clock"
onClick={easterEggOnClick}
onClick={onClockClick}
role="timer"
title={date}
suppressHydrationWarning
{...clockContextMenu}
{...FOCUSABLE_ELEMENT}
>
{supportsOffscreenCanvas ? undefined : time}
</StyledClock>

View File

@@ -3,13 +3,17 @@ import type { ContextMenuCapture } from "contexts/menu/useMenuContextState";
import { useSession } from "contexts/session";
import { useMemo } from "react";
const useClockContextMenu = (): ContextMenuCapture => {
const useClockContextMenu = (
toggleCalendar: (showCalendar?: boolean) => void
): ContextMenuCapture => {
const { contextMenu } = useMenu();
const { clockSource, setClockSource } = useSession();
return useMemo(
() =>
contextMenu?.(() => {
toggleCalendar(false);
const isLocal = clockSource === "local";
return [
@@ -25,7 +29,7 @@ const useClockContextMenu = (): ContextMenuCapture => {
},
];
}),
[clockSource, contextMenu, setClockSource]
[clockSource, contextMenu, setClockSource, toggleCalendar]
);
};

View File

@@ -4,15 +4,27 @@ import StyledTaskbar from "components/system/Taskbar/StyledTaskbar";
import TaskbarEntries from "components/system/Taskbar/TaskbarEntries";
import useTaskbarContextMenu from "components/system/Taskbar/useTaskbarContextMenu";
import dynamic from "next/dynamic";
import { memo, useState } from "react";
import { memo, useCallback, useState } from "react";
import { FOCUSABLE_ELEMENT } from "utils/constants";
const Calendar = dynamic(() => import("components/system/Taskbar/Calendar"));
const StartMenu = dynamic(() => import("components/system/StartMenu"));
const Taskbar: FC = () => {
const [startMenuVisible, setStartMenuVisible] = useState(false);
const toggleStartMenu = (showMenu?: boolean): void =>
setStartMenuVisible((currentMenuState) => showMenu ?? !currentMenuState);
const [calendarVisible, setCalendarVisible] = useState(false);
const toggleStartMenu = useCallback(
(showMenu?: boolean): void =>
setStartMenuVisible((currentMenuState) => showMenu ?? !currentMenuState),
[]
);
const toggleCalendar = useCallback(
(showCalendar?: boolean): void =>
setCalendarVisible(
(currentCalendarState) => showCalendar ?? !currentCalendarState
),
[]
);
return (
<>
@@ -23,8 +35,9 @@ const Taskbar: FC = () => {
toggleStartMenu={toggleStartMenu}
/>
<TaskbarEntries />
<Clock />
<Clock toggleCalendar={toggleCalendar} />
</StyledTaskbar>
{calendarVisible && <Calendar toggleCalendar={toggleCalendar} />}
</>
);
};

View File

@@ -1,6 +1,7 @@
export const spotlightEffect = (
element: HTMLElement | null,
onlyBorder = false
onlyBorder = false,
border = 1
): void => {
if (!element) return;
@@ -22,7 +23,7 @@ export const spotlightEffect = (
background: onlyBorder
? undefined
: `radial-gradient(circle at ${x}px ${y}px, rgba(255, 255, 255, 0.3), rgba(255, 255, 255, 0))`,
borderImage: `radial-gradient(20% 75% at ${x}px ${y}px, rgba(255, 255, 255, 0.6), rgba(255, 255, 255, 0.1)) 1 / 1px / 0px stretch`,
borderImage: `radial-gradient(20% 75% at ${x}px ${y}px, rgba(255, 255, 255, 0.6), rgba(255, 255, 255, 0.1)) 1 / ${border}px / 0px stretch`,
});
}
},