mirror of
https://github.com/DustinBrett/daedalOS.git
synced 2026-01-15 12:15:02 +00:00
Calendar PT1
This commit is contained in:
13
components/system/Taskbar/Calendar/Icons.tsx
Normal file
13
components/system/Taskbar/Calendar/Icons.tsx
Normal 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>
|
||||
));
|
||||
123
components/system/Taskbar/Calendar/StyledCalendar.ts
Normal file
123
components/system/Taskbar/Calendar/StyledCalendar.ts
Normal 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;
|
||||
75
components/system/Taskbar/Calendar/functions.ts
Normal file
75
components/system/Taskbar/Calendar/functions.ts
Normal 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;
|
||||
};
|
||||
117
components/system/Taskbar/Calendar/index.tsx
Normal file
117
components/system/Taskbar/Calendar/index.tsx
Normal 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);
|
||||
@@ -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>
|
||||
|
||||
@@ -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]
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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`,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user