Calendar navigation with data from loader

This commit is contained in:
Kalle 2022-07-21 20:57:01 +03:00
parent 62c803f4ad
commit b5580c347f
6 changed files with 217 additions and 59 deletions

View File

@ -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 (
<Main>
<div className="flex justify-center">
<div className="calendar__weeks">
{weeks.map((week) => (
<div
key={week}
onClick={() => setWeeks(["Last", "This", "Next", "14", "15"])}
className="calendar__week"
>
<div>
{week} <br />
Week
</div>
<div className="calendar__event-count">×12</div>
</div>
))}
</div>
</div>
<WeekLinks />
</Main>
);
}
function WeekLinks() {
const data = useLoaderData<typeof loader>();
return (
<Flipper flipKey={data.weeks.map(({ number }) => number).join("")}>
<div className="flex justify-center">
<div className="calendar__weeks">
{data.weeks.map((week) => (
<Flipped key={week.number} flipId={week.number}>
<Link
to={`?week=${week.number}&year=${week.year}`}
className="calendar__week"
>
<>
<div>
{week.number === data.thisWeek
? "This"
: week.number - data.thisWeek === 1
? "Next"
: week.number - data.thisWeek === -1
? "Last"
: week.number}{" "}
<br />
Week
</div>
<div className="calendar__event-count">
×{week.numberOfEvents}
</div>
</>
</Link>
</Flipped>
))}
</div>
</div>
</Flipper>
);
}

View File

@ -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);
}

View File

@ -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;

View File

@ -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;
}

65
package-lock.json generated
View File

@ -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",

View File

@ -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",