mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-09-09 12:46:10 -05:00
Calendar on stream progress 25.7.
This commit is contained in:
@@ -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?
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user