diff --git a/components/system/Taskbar/Calendar/Icons.tsx b/components/system/Taskbar/Calendar/Icons.tsx new file mode 100644 index 00000000..03686451 --- /dev/null +++ b/components/system/Taskbar/Calendar/Icons.tsx @@ -0,0 +1,13 @@ +import { memo } from "react"; + +export const Down = memo(() => ( + + + +)); + +export const Up = memo(() => ( + + + +)); diff --git a/components/system/Taskbar/Calendar/StyledCalendar.ts b/components/system/Taskbar/Calendar/StyledCalendar.ts new file mode 100644 index 00000000..338fa7fa --- /dev/null +++ b/components/system/Taskbar/Calendar/StyledCalendar.ts @@ -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; diff --git a/components/system/Taskbar/Calendar/functions.ts b/components/system/Taskbar/Calendar/functions.ts new file mode 100644 index 00000000..c4e8ecfa --- /dev/null +++ b/components/system/Taskbar/Calendar/functions.ts @@ -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((_, 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((d) => [d, "curr"]); + const rows = [...firstRow, ...remainingDays].reduce( + (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((_, 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((_, index) => [ + index + 1 + lastNumber, + "next", + ]), + ...(rows.length === 4 + ? [FIRST_WEEK.map(([value]) => [value + DAYS_IN_WEEK, "next"])] + : []), + ] as Calendar; + } + + return rows; +}; diff --git a/components/system/Taskbar/Calendar/index.tsx b/components/system/Taskbar/Calendar/index.tsx new file mode 100644 index 00000000..ff17dfbb --- /dev/null +++ b/components/system/Taskbar/Calendar/index.tsx @@ -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 = ({ toggleCalendar }) => { + const [date, setDate] = useState(() => new Date()); + const [calendar, setCalendar] = useState(() => + 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(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 && ( + + + + + + + + {DAY_NAMES.map((dayName) => ( + + ))} + + + + {calendar?.map((week) => ( + + {week.map(([day, type]) => ( + + ))} + + ))} + +
+
+
+ {`${date.toLocaleString("default", { + month: "long", + })}, ${date.getFullYear()}`} +
+ +
+
{dayName}
+ type === "today" + ? undefined + : spotlightEffect(tdRef, false, 2) + } + className={type} + > + {day} +
+
+ ) + ); +}; + +export default memo(Calendar); diff --git a/components/system/Taskbar/Clock/index.tsx b/components/system/Taskbar/Clock/index.tsx index 9f930f42..99da2f5f 100644 --- a/components/system/Taskbar/Clock/index.tsx +++ b/components/system/Taskbar/Clock/index.tsx @@ -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 = 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 = async ({ const { default: spawnSheep } = await import("utils/spawnSheep"); spawnSheep(); + triggerEasterEggCountdown = EASTER_EGG_CLICK_COUNT; } }; -const Clock: FC = () => { +type ClockProps = { + toggleCalendar: () => void; +}; + +const Clock: FC = ({ toggleCalendar }) => { const [now, setNow] = useState( Object.create(null) as LocaleTimeDate ); @@ -97,7 +99,7 @@ const Clock: FC = () => { }, [clockSource] ); - const clockContextMenu = useClockContextMenu(); + const clockContextMenu = useClockContextMenu(toggleCalendar); const currentWorker = useWorker( 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) => { + easterEggOnClick(event); + toggleCalendar(); + }, + [toggleCalendar] + ); useEffect(() => { offScreenClockCanvas.current = undefined; @@ -163,11 +172,12 @@ const Clock: FC = () => { {supportsOffscreenCanvas ? undefined : time} diff --git a/components/system/Taskbar/Clock/useClockContextMenu.ts b/components/system/Taskbar/Clock/useClockContextMenu.ts index 1a6a32f2..95247f2f 100644 --- a/components/system/Taskbar/Clock/useClockContextMenu.ts +++ b/components/system/Taskbar/Clock/useClockContextMenu.ts @@ -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] ); }; diff --git a/components/system/Taskbar/index.tsx b/components/system/Taskbar/index.tsx index e93e635c..80b2ff36 100644 --- a/components/system/Taskbar/index.tsx +++ b/components/system/Taskbar/index.tsx @@ -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} /> - + + {calendarVisible && } ); }; diff --git a/utils/spotlightEffect.ts b/utils/spotlightEffect.ts index 6278529f..78dad0c7 100644 --- a/utils/spotlightEffect.ts +++ b/utils/spotlightEffect.ts @@ -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`, }); } },