Calendar on stream progress 25.7.

This commit is contained in:
Kalle
2022-07-26 01:53:03 +03:00
parent 29d4f2d157
commit aca0efc746
6 changed files with 224 additions and 112 deletions

View File

@@ -3,6 +3,7 @@ import type { User } from "~/db/types";
import * as React from "react";
const dimensions = {
xs: 36,
sm: 44,
md: 81,
lg: 125,
@@ -16,7 +17,7 @@ export function Avatar({
...rest
}: Pick<User, "discordId" | "discordAvatar"> & {
className?: string;
size: "sm" | "md" | "lg";
size: "xs" | "sm" | "md" | "lg";
} & React.ButtonHTMLAttributes<HTMLImageElement>) {
const [isErrored, setIsErrored] = React.useState(false);
// TODO: just show text... my profile?

View File

@@ -1,4 +1,8 @@
import { dateToDatabaseTimestamp } from "~/utils/dates";
import {
databaseTimestampToDate,
dateToDatabaseTimestamp,
dateToWeekNumber,
} from "~/utils/dates";
import { sql } from "../sql";
import type { CalendarEvent, CalendarEventDate, User } from "../types";
@@ -58,6 +62,39 @@ export function findById(id: CalendarEvent["id"]) {
return findByIdStm.get({ id }) as Nullable<
Pick<CalendarEvent, "name" | "description" | "discordUrl" | "bracketUrl"> &
Pick<CalendarEventDate, "startTime" | "eventId"> &
Pick<User, "discordName" | "discordDiscriminator">
Pick<
User,
"discordName" | "discordDiscriminator" | "discordId" | "discordAvatar"
>
>;
}
const eventsCountPerWeekStm = sql.prepare(`
select
"CalendarEventDate"."startTime"
from "CalendarEventDate"
where "CalendarEventDate"."startTime" between $startTime and $endTime
`);
export function eventsCountPerWeek({
startTime,
endTime,
}: {
startTime: Date;
endTime: Date;
}) {
const startTimes = eventsCountPerWeekStm.all({
startTime: dateToDatabaseTimestamp(startTime),
endTime: dateToDatabaseTimestamp(endTime),
}) as Array<Pick<CalendarEventDate, "startTime">>;
const result = new Map<number, number>();
for (const { startTime } of startTimes) {
const week = dateToWeekNumber(databaseTimestampToDate(startTime));
const previousCount = result.get(week) ?? 0;
result.set(week, previousCount + 1);
}
return result;
}

View File

@@ -11,6 +11,8 @@ import { notFoundIfFalsy } from "~/utils/remix";
import { resolveBaseUrl } from "~/utils/urls";
import { actualNumber, id } from "~/utils/zod";
import styles from "~/styles/calendar-event.css";
import { Avatar } from "~/components/Avatar";
import { discordFullName } from "~/utils/strings";
export const links: LinksFunction = () => {
return [{ rel: "stylesheet", href: styles }];
@@ -32,15 +34,13 @@ export default function CalendarEventPage() {
const { i18n } = useTranslation();
const isMounted = useIsMounted();
// TODO: -> next user info with avatar
// xxx: time doesn't take space, not taking in account many dates
// xxx: not taking in account many dates
return (
<Main className="stack lg">
<section className="stack sm">
<div>
<div className="event__time">
<time
className="event__time"
dateTime={databaseTimestampToDate(event.startTime).toISOString()}
>
{isMounted
@@ -83,7 +83,17 @@ export default function CalendarEventPage() {
</div>
) : null}
</section>
<div>{event.description}</div>
<div className="stack sm">
<div className="event__author">
<Avatar
discordAvatar={event.discordAvatar}
discordId={event.discordId}
size="xs"
/>
{discordFullName(event)}
</div>
<div>{event.description}</div>
</div>
</Main>
);
}

View File

@@ -3,7 +3,7 @@ import { json, type LinksFunction } from "@remix-run/node";
import { Link, useLoaderData } from "@remix-run/react";
import type { UseDataFunctionReturn } from "@remix-run/react/dist/components";
import clsx from "clsx";
import { addDays, subDays } from "date-fns";
import { addDays, addMonths, subDays, subMonths } from "date-fns";
import React from "react";
import { Flipped, Flipper } from "react-flip-toolkit";
import { useTranslation } from "react-i18next";
@@ -35,7 +35,7 @@ export const meta: MetaFunction = (args) => {
return {
title: data.title,
description: `${data.events.length} events happening during week ${
data.thisWeek
data.displayedWeek
} including ${joinListToNaturalString(
data.events.slice(0, 3).map((e) => e.name)
)}`,
@@ -59,23 +59,37 @@ export const loader = async ({ request }: LoaderArgs) => {
});
const now = new Date();
const thisWeek = dateToWeekNumber(now);
const currentWeek = dateToWeekNumber(now);
const weekToFetch = parsedParams.success ? parsedParams.data.week : thisWeek;
const yearToFetch = parsedParams.success
const displayedWeek = parsedParams.success
? parsedParams.data.week
: currentWeek;
const displayedYear = parsedParams.success
? parsedParams.data.year
: now.getFullYear();
const eventCounts = db.calendar.eventsCountPerWeek({
startTime: subMonths(
weekNumberToDate({ week: displayedWeek, year: displayedYear }),
1
),
endTime: addMonths(
weekNumberToDate({ week: displayedWeek, year: displayedYear }),
1
),
});
return json({
thisWeek,
weeks: closeByWeeks({ week: weekToFetch, year: yearToFetch }).map(
currentWeek, // the week that is currently in real life
displayedWeek,
weeks: closeByWeeks({ week: displayedWeek, year: displayedYear }).map(
(week) => ({
...week,
numberOfEvents: 12,
numberOfEvents: eventCounts.get(week.number) ?? 0,
})
),
events: fetchEventsOfWeek({ week: weekToFetch, year: yearToFetch }),
title: makeTitle([`Week ${weekToFetch}`, t("pages.calendar")]),
events: fetchEventsOfWeek({ week: displayedWeek, year: displayedYear }),
title: makeTitle([`Week ${displayedWeek}`, t("pages.calendar")]),
});
};
@@ -107,99 +121,12 @@ function fetchEventsOfWeek(args: { week: number; year: number }) {
}
export default function CalendarPage() {
const data = useLoaderData<typeof loader>();
const { i18n } = useTranslation();
// there is hydration mismatch error if we render
// dates/times on the server
const isMounted = useIsMounted();
// xxx: scrolls down on page load?
// xxx: instead calculate into object
const datesRendered = new Set<string>();
return (
<main>
<WeekLinks />
<div className="calendar__events-container">
{data.events.map((event) => {
const dateString = databaseTimestampToDate(
event.startTime
).toLocaleDateString(i18n.language, {
weekday: "long",
day: "numeric",
month: "long",
});
const renderDateString = !datesRendered.has(dateString);
datesRendered.add(dateString);
return (
<React.Fragment key={event.id}>
<div
className={clsx("calendar__event__date-container", {
invisible: !isMounted || !renderDateString,
})}
>
<div className="calendar__event__date main">
{isMounted ? dateString : null}
</div>
</div>
<section className="calendar__event main">
<div
className={clsx("calendar__event__top-info-container", {
invisible: !isMounted,
})}
>
<time
dateTime={databaseTimestampToDate(
event.startTime
).toISOString()}
className="calendar__event__time"
>
{isMounted
? databaseTimestampToDate(
event.startTime
).toLocaleTimeString(i18n.language, {
hour: "numeric",
minute: "numeric",
})
: null}
</time>
<div className="calendar__event__author">
From {discordFullName(event)}
</div>
</div>
<Link to={String(event.eventId)}>
<h2 className="calendar__event__title">{event.name}</h2>
</Link>
{event.discordUrl || event.bracketUrl ? (
<div className="calendar__event__bottom-info-container">
{event.discordUrl ? (
<LinkButton
to={event.discordUrl}
variant="outlined"
tiny
isExternal
>
Discord
</LinkButton>
) : null}
{event.bracketUrl ? (
<LinkButton
to={event.bracketUrl}
variant="outlined"
tiny
isExternal
>
{resolveBaseUrl(event.bracketUrl)}
</LinkButton>
) : null}
</div>
) : null}
</section>
</React.Fragment>
);
})}
</div>
{isMounted ? <EventsList /> : <div className="calendar__placeholder" />}
</main>
);
}
@@ -223,17 +150,17 @@ function WeekLinks() {
<Flipped key={week.number} flipId={week.number}>
<Link
to={`?week=${week.number}&year=${week.year}`}
className="calendar__week"
className={clsx("calendar__week", { invisible: hidden })}
aria-hidden={hidden}
tabIndex={hidden ? -1 : 0}
>
<>
<div>
{week.number === data.thisWeek
{week.number === data.currentWeek
? "This"
: week.number - data.thisWeek === 1
: week.number - data.currentWeek === 1
? "Next"
: week.number - data.thisWeek === -1
: week.number - data.currentWeek === -1
? "Last"
: week.number}{" "}
<br />
@@ -252,3 +179,124 @@ function WeekLinks() {
</Flipper>
);
}
function EventsList() {
const data = useLoaderData<typeof loader>();
const { i18n } = useTranslation();
// xxx: maybe after all show sunday-monday night events on previous week
// xxx: (Day 2,3,4 etc. in title when multiday event)
return (
<div className="calendar__events-container">
{filterOutNextWeeksEvents(
data.displayedWeek,
eventsGroupedByDay(data.events)
).map(([daysDate, events]) => {
return (
<React.Fragment key={daysDate.getTime()}>
<div className="calendar__event__date-container">
<div className="calendar__event__date main">
{daysDate.toLocaleDateString(i18n.language, {
weekday: "long",
day: "numeric",
month: "long",
})}
</div>
</div>
<div className="stack md">
{events.map((calendarEvent) => {
return (
<section
key={calendarEvent.eventId}
className="calendar__event main"
>
<div className="calendar__event__top-info-container">
<time
dateTime={databaseTimestampToDate(
calendarEvent.startTime
).toISOString()}
className="calendar__event__time"
>
{databaseTimestampToDate(
calendarEvent.startTime
).toLocaleTimeString(i18n.language, {
hour: "numeric",
minute: "numeric",
})}
</time>
<div className="calendar__event__author">
From {discordFullName(calendarEvent)}
</div>
</div>
<Link to={String(calendarEvent.eventId)}>
<h2 className="calendar__event__title">
{calendarEvent.name}
</h2>
</Link>
{calendarEvent.discordUrl || calendarEvent.bracketUrl ? (
<div className="calendar__event__bottom-info-container">
{calendarEvent.discordUrl ? (
<LinkButton
to={calendarEvent.discordUrl}
variant="outlined"
tiny
isExternal
>
Discord
</LinkButton>
) : null}
{calendarEvent.bracketUrl ? (
<LinkButton
to={calendarEvent.bracketUrl}
variant="outlined"
tiny
isExternal
>
{resolveBaseUrl(calendarEvent.bracketUrl)}
</LinkButton>
) : null}
</div>
) : null}
</section>
);
})}
</div>
</React.Fragment>
);
})}
</div>
);
}
function eventsGroupedByDay(
events: UseDataFunctionReturn<typeof loader>["events"]
) {
const result: Array<[Date, UseDataFunctionReturn<typeof loader>["events"]]> =
[];
for (const calendarEvent of events) {
const previousIterationEvents = result[result.length - 1] ?? null;
const eventsDate = databaseTimestampToDate(calendarEvent.startTime);
if (
!previousIterationEvents ||
previousIterationEvents[0].getDay() !== eventsDate.getDay()
) {
result.push([eventsDate, [calendarEvent]]);
} else {
previousIterationEvents[1].push(calendarEvent);
}
}
return result;
}
function filterOutNextWeeksEvents(
displayedWeek: number,
groupedEvents: ReturnType<typeof eventsGroupedByDay>
) {
return groupedEvents.filter(
([date]) => dateToWeekNumber(date) === displayedWeek
);
}

View File

@@ -3,5 +3,14 @@
}
.event__time {
height: 1rem;
font-weight: var(--semi-bold);
}
.event__author {
display: flex;
align-items: center;
color: var(--text-lighter);
font-weight: var(--semi-bold);
gap: var(--s-1);
}

View File

@@ -1,3 +1,7 @@
.calendar__placeholder {
height: 48rem;
}
.calendar__weeks {
display: flex;
align-items: center;
@@ -76,20 +80,23 @@
margin-block-start: var(--s-8);
}
/* xxx: padding for last -> generate grouped events on server? */
.calendar__event {
display: flex;
flex-direction: column;
font-size: var(--fonts-lg);
}
.calendar__event:last-child {
padding-block-end: var(--s-4);
}
.calendar__event__date-container {
background-color: var(--bg);
}
.calendar__event__date {
height: 1.5rem;
font-weight: var(--semi-bold);
padding-block-start: var(--s-6);
}
.calendar__event__time {