From b5580c347fb0c3e9e09086ed0be7caaefed00741 Mon Sep 17 00:00:00 2001 From: Kalle <38327916+Sendouc@users.noreply.github.com> Date: Thu, 21 Jul 2022 20:57:01 +0300 Subject: [PATCH] Calendar navigation with data from loader --- app/routes/calendar.tsx | 127 +++++++++++++++++++++++++++++++--------- app/styles/calendar.css | 60 +++++++++++-------- app/styles/global.css | 2 + app/utils/dates.ts | 21 +++++++ package-lock.json | 65 ++++++++++++++++++-- package.json | 1 + 6 files changed, 217 insertions(+), 59 deletions(-) diff --git a/app/routes/calendar.tsx b/app/routes/calendar.tsx index a905a3072..f34b0ac7f 100644 --- a/app/routes/calendar.tsx +++ b/app/routes/calendar.tsx @@ -1,41 +1,112 @@ -import { type LinksFunction } from "@remix-run/node"; -import { Link } from "@remix-run/react"; -import * as React from "react"; +import type { LoaderArgs } from "@remix-run/node"; +import { json, type LinksFunction } from "@remix-run/node"; +import { Link, useLoaderData } from "@remix-run/react"; +import { addDays, subDays } from "date-fns"; +import { Flipped, Flipper } from "react-flip-toolkit"; +import { z } from "zod"; import { Main } from "~/components/Main"; import styles from "~/styles/calendar.css"; +import { dateToWeekNumber, weekNumberToDate } from "~/utils/dates"; +import { actualNumber } from "~/utils/zod"; export const links: LinksFunction = () => { return [{ rel: "stylesheet", href: styles }]; }; -export default function CalendarPage() { - const [weeks, setWeeks] = React.useState([ - "10", - "Last", - "This", - "Next", - "14", - ]); +const loaderSearchParamsSchema = z.object({ + week: z.preprocess(actualNumber, z.number().int().min(1).max(53)), + year: z.preprocess( + actualNumber, + z.number().int().min(2022).max(new Date().getFullYear()) + ), +}); +export const loader = ({ request }: LoaderArgs) => { + const url = new URL(request.url); + const parsedParams = loaderSearchParamsSchema.safeParse({ + year: url.searchParams.get("year"), + week: url.searchParams.get("week"), + }); + + const now = new Date(); + const thisWeek = dateToWeekNumber(now); + + const weekToFetch = parsedParams.success ? parsedParams.data.week : thisWeek; + const yearToFetch = parsedParams.success + ? parsedParams.data.year + : now.getFullYear(); + + console.log({ weekToFetch, yearToFetch, thisWeek }); // TODO: db query to get events of this week + + return json({ + thisWeek, + weeks: closeByWeeks({ week: weekToFetch, year: yearToFetch }).map( + (week) => ({ + ...week, + numberOfEvents: 12, + }) + ), + }); +}; + +function closeByWeeks(args: { week: number; year: number }) { + const dateFromWeekNumber = weekNumberToDate(args); + + return [-4, -3, -2, -1, 0, 1, 2, 3, 4].map((week) => { + const date = + week < 0 + ? subDays(dateFromWeekNumber, Math.abs(week) * 7) + : addDays(dateFromWeekNumber, week * 7); + + return { + number: dateToWeekNumber(date), + year: date.getFullYear(), + }; + }); +} + +export default function CalendarPage() { return (
-
-
- {weeks.map((week) => ( -
setWeeks(["Last", "This", "Next", "14", "15"])} - className="calendar__week" - > -
- {week}
- Week -
-
×12
-
- ))} -
-
+
); } + +function WeekLinks() { + const data = useLoaderData(); + + return ( + number).join("")}> +
+
+ {data.weeks.map((week) => ( + + + <> +
+ {week.number === data.thisWeek + ? "This" + : week.number - data.thisWeek === 1 + ? "Next" + : week.number - data.thisWeek === -1 + ? "Last" + : week.number}{" "} +
+ Week +
+
+ ×{week.numberOfEvents} +
+ + +
+ ))} +
+
+
+ ); +} diff --git a/app/styles/calendar.css b/app/styles/calendar.css index 14e79d179..7391711b8 100644 --- a/app/styles/calendar.css +++ b/app/styles/calendar.css @@ -1,57 +1,67 @@ .calendar__weeks { display: flex; - max-width: 24rem; align-items: center; - justify-content: space-between; + justify-content: center; gap: var(--s-2); + overflow-x: hidden; padding-inline: var(--s-2-5); --full-size-week-height: 6.75rem; - - /* xxx: this should be behind a media query */ - --full-size-week-width: 6rem; + --full-size-week-width: 5rem; } .calendar__week { display: flex; - width: var(--full-size-week-width); - height: var(--full-size-week-height); + width: calc(var(--full-size-week-width) - 2rem); + height: calc(var(--full-size-week-height) - 2rem); flex-direction: column; justify-content: space-between; padding: var(--s-2); - background-color: var(--theme-transparent); + background-color: var(--theme-very-transparent); border-radius: var(--rounded); - color: var(--theme); - font-size: var(--fonts-sm); + cursor: pointer; + font-size: var(--fonts-xxs); font-weight: var(--bold); text-align: center; } +.calendar__week:hover { + background-color: var(--theme-transparent); +} + .calendar__event-count { - font-size: var(--fonts-xs); + font-size: var(--fonts-xxxs); font-weight: var(--body); } .calendar__week:nth-child(1), -.calendar__week:nth-child(5) { - width: calc(var(--full-size-week-width) - 2rem); - height: calc(var(--full-size-week-height) - 2rem); - font-size: var(--fonts-xxs); -} - -.calendar__week:nth-child(1) > .calendar__event-count, -.calendar__week:nth-child(5) > .calendar__event-count { - font-size: var(--fonts-xxxs); -} - .calendar__week:nth-child(2), -.calendar__week:nth-child(4) { +.calendar__week:nth-child(8), +.calendar__week:nth-child(9) { + width: calc(var(--full-size-week-width) - 2.5rem); + height: calc(var(--full-size-week-height) - 2.5rem); + font-size: var(--fonts-xxxxs); + opacity: 0; +} + +.calendar__week:nth-child(5) { + width: var(--full-size-week-width); + height: var(--full-size-week-height); + font-size: var(--fonts-sm); +} + +.calendar__week:nth-child(5) > .calendar__event-count { + font-size: var(--fonts-sm); +} + +.calendar__week:nth-child(4), +.calendar__week:nth-child(6) { width: calc(var(--full-size-week-width) - 1rem); height: calc(var(--full-size-week-height) - 1rem); font-size: var(--fonts-xs); } -.calendar__week:nth-child(2) > .calendar__event-count, -.calendar__week:nth-child(4) > .calendar__event-count { +.calendar__week:nth-child(4) > .calendar__event-count, +.calendar__week:nth-child(6) > .calendar__event-count { font-size: var(--fonts-xxs); } diff --git a/app/styles/global.css b/app/styles/global.css index 4ab97e46b..c3bb87c03 100644 --- a/app/styles/global.css +++ b/app/styles/global.css @@ -20,6 +20,7 @@ --theme: hsl(255deg 64% 63%); --theme-vibrant: hsl(255deg 100% 81%); --theme-transparent: hsl(255deg 66.7% 75% / 40%); + --theme-very-transparent: hsl(255deg 66.7% 75% / 30%); --theme-transparent-vibrant: hsl(255deg 100% 81% / 54%); --theme-semi-transparent-vibrant: hsl(255deg 100% 81% / 75%); --theme-secondary: hsl(85deg 66.7% 55.3%); @@ -32,6 +33,7 @@ --fonts-xs: 0.8rem; --fonts-xxs: 0.7rem; --fonts-xxxs: 0.6rem; + --fonts-xxxxs: 0.5rem; --extra-bold: 700; --bold: 600; --semi-bold: 500; diff --git a/app/utils/dates.ts b/app/utils/dates.ts index 46afbbfd0..f323d8cb9 100644 --- a/app/utils/dates.ts +++ b/app/utils/dates.ts @@ -1,3 +1,5 @@ +import { getWeek } from "date-fns"; + export function databaseTimestampToDate(timestamp: number) { return new Date(timestamp * 1000); } @@ -5,3 +7,22 @@ export function databaseTimestampToDate(timestamp: number) { export function dateToDatabaseTimestamp(date: Date) { return Math.floor(date.getTime() / 1000); } + +export function dateToWeekNumber(date: Date) { + return getWeek(date, { weekStartsOn: 1, firstWeekContainsDate: 4 }); +} + +// https://stackoverflow.com/a/71336659 +export function weekNumberToDate({ + week, + year, +}: { + week: number; + year: number; +}) { + const result = new Date(year, 0, 4); + result.setDate( + result.getDate() - (result.getDay() || 7) + 1 + 7 * (week - 1) + ); + return result; +} diff --git a/package-lock.json b/package-lock.json index aaed18590..3dc4e58a9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "node-cron": "3.0.0", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-flip-toolkit": "^7.0.14", "react-i18next": "^11.18.1", "react-popper": "^2.3.0", "remix-auth": "^3.2.2", @@ -7648,6 +7649,18 @@ "integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==", "dev": true }, + "node_modules/flip-toolkit": { + "version": "7.0.14", + "resolved": "https://registry.npmjs.org/flip-toolkit/-/flip-toolkit-7.0.14.tgz", + "integrity": "sha512-mV3fwJuFxXU0tSiUL79NEfykwjEABV7I4gdx7S83GrQ0eJC/TIS1R8TW0u2qdDAtd5yhTcrm9IVfqyjJdGqrqQ==", + "dependencies": { + "rematrix": "0.2.2" + }, + "engines": { + "node": ">=8", + "npm": ">=5" + } + }, "node_modules/flow-parser": { "version": "0.177.0", "resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.177.0.tgz", @@ -11995,7 +12008,6 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -12005,8 +12017,7 @@ "node_modules/prop-types/node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, "node_modules/property-information": { "version": "6.1.1", @@ -12195,6 +12206,23 @@ "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz", "integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==" }, + "node_modules/react-flip-toolkit": { + "version": "7.0.14", + "resolved": "https://registry.npmjs.org/react-flip-toolkit/-/react-flip-toolkit-7.0.14.tgz", + "integrity": "sha512-4ezk9g9yPMDbePCTW81ZIG1dMXa3qCIlYhGnWjJb0fIJJOazSOwV2TXp0lB/kn4R8QK5N3AdwYKPwFQQY8TW3A==", + "dependencies": { + "flip-toolkit": "7.0.14", + "prop-types": "^15.5.7" + }, + "engines": { + "node": ">=8", + "npm": ">=5" + }, + "peerDependencies": { + "react": ">= 16.x", + "react-dom": ">= 16.x" + } + }, "node_modules/react-i18next": { "version": "11.18.1", "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-11.18.1.tgz", @@ -12603,6 +12631,11 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/rematrix": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/rematrix/-/rematrix-0.2.2.tgz", + "integrity": "sha512-agFFS3RzrLXJl5LY5xg/xYyXvUuVAnkhgKO7RaO9J1Ssth6yvbO+PIiV67V59MB5NCdAK2flvGvNT4mdKVniFA==" + }, "node_modules/remix-auth": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/remix-auth/-/remix-auth-3.2.2.tgz", @@ -20999,6 +21032,14 @@ "integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==", "dev": true }, + "flip-toolkit": { + "version": "7.0.14", + "resolved": "https://registry.npmjs.org/flip-toolkit/-/flip-toolkit-7.0.14.tgz", + "integrity": "sha512-mV3fwJuFxXU0tSiUL79NEfykwjEABV7I4gdx7S83GrQ0eJC/TIS1R8TW0u2qdDAtd5yhTcrm9IVfqyjJdGqrqQ==", + "requires": { + "rematrix": "0.2.2" + } + }, "flow-parser": { "version": "0.177.0", "resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.177.0.tgz", @@ -24136,7 +24177,6 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, "requires": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -24146,8 +24186,7 @@ "react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" } } }, @@ -24288,6 +24327,15 @@ "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz", "integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==" }, + "react-flip-toolkit": { + "version": "7.0.14", + "resolved": "https://registry.npmjs.org/react-flip-toolkit/-/react-flip-toolkit-7.0.14.tgz", + "integrity": "sha512-4ezk9g9yPMDbePCTW81ZIG1dMXa3qCIlYhGnWjJb0fIJJOazSOwV2TXp0lB/kn4R8QK5N3AdwYKPwFQQY8TW3A==", + "requires": { + "flip-toolkit": "7.0.14", + "prop-types": "^15.5.7" + } + }, "react-i18next": { "version": "11.18.1", "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-11.18.1.tgz", @@ -24601,6 +24649,11 @@ "unified": "^10.0.0" } }, + "rematrix": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/rematrix/-/rematrix-0.2.2.tgz", + "integrity": "sha512-agFFS3RzrLXJl5LY5xg/xYyXvUuVAnkhgKO7RaO9J1Ssth6yvbO+PIiV67V59MB5NCdAK2flvGvNT4mdKVniFA==" + }, "remix-auth": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/remix-auth/-/remix-auth-3.2.2.tgz", diff --git a/package.json b/package.json index 54721e5b0..8c5a13943 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "node-cron": "3.0.0", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-flip-toolkit": "^7.0.14", "react-i18next": "^11.18.1", "react-popper": "^2.3.0", "remix-auth": "^3.2.2",