mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-03-21 18:04:39 -05:00
Calendar navigation with data from loader
This commit is contained in:
parent
62c803f4ad
commit
b5580c347f
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
65
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user