diff --git a/app/components/Avatar.tsx b/app/components/Avatar.tsx index 79a4d5e23..637693160 100644 --- a/app/components/Avatar.tsx +++ b/app/components/Avatar.tsx @@ -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 & { className?: string; - size: "sm" | "md" | "lg"; + size: "xs" | "sm" | "md" | "lg"; } & React.ButtonHTMLAttributes) { const [isErrored, setIsErrored] = React.useState(false); // TODO: just show text... my profile? diff --git a/app/db/models/calendar.server.ts b/app/db/models/calendar.server.ts index f56fb2e79..82fd00f02 100644 --- a/app/db/models/calendar.server.ts +++ b/app/db/models/calendar.server.ts @@ -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 & Pick & - Pick + 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>; + + const result = new Map(); + + for (const { startTime } of startTimes) { + const week = dateToWeekNumber(databaseTimestampToDate(startTime)); + const previousCount = result.get(week) ?? 0; + result.set(week, previousCount + 1); + } + + return result; +} diff --git a/app/routes/calendar/$id.tsx b/app/routes/calendar/$id.tsx index d546c70a0..aa747ca5b 100644 --- a/app/routes/calendar/$id.tsx +++ b/app/routes/calendar/$id.tsx @@ -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 (
-
+
) : null}
-
{event.description}
+
+
+ + {discordFullName(event)} +
+
{event.description}
+
); } diff --git a/app/routes/calendar/index.tsx b/app/routes/calendar/index.tsx index b3306fc0d..3bb7b7f9d 100644 --- a/app/routes/calendar/index.tsx +++ b/app/routes/calendar/index.tsx @@ -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(); - 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(); return (
-
- {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 ( - -
-
- {isMounted ? dateString : null} -
-
-
-
- -
- From {discordFullName(event)} -
-
- -

{event.name}

- - {event.discordUrl || event.bracketUrl ? ( -
- {event.discordUrl ? ( - - Discord - - ) : null} - {event.bracketUrl ? ( - - {resolveBaseUrl(event.bracketUrl)} - - ) : null} -
- ) : null} -
-
- ); - })} -
+ {isMounted ? :
}
); } @@ -223,17 +150,17 @@ function WeekLinks() { <>
- {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}{" "}
@@ -252,3 +179,124 @@ function WeekLinks() { ); } + +function EventsList() { + const data = useLoaderData(); + 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 ( +
+ {filterOutNextWeeksEvents( + data.displayedWeek, + eventsGroupedByDay(data.events) + ).map(([daysDate, events]) => { + return ( + +
+
+ {daysDate.toLocaleDateString(i18n.language, { + weekday: "long", + day: "numeric", + month: "long", + })} +
+
+
+ {events.map((calendarEvent) => { + return ( +
+
+ +
+ From {discordFullName(calendarEvent)} +
+
+ +

+ {calendarEvent.name} +

+ + {calendarEvent.discordUrl || calendarEvent.bracketUrl ? ( +
+ {calendarEvent.discordUrl ? ( + + Discord + + ) : null} + {calendarEvent.bracketUrl ? ( + + {resolveBaseUrl(calendarEvent.bracketUrl)} + + ) : null} +
+ ) : null} +
+ ); + })} +
+
+ ); + })} +
+ ); +} + +function eventsGroupedByDay( + events: UseDataFunctionReturn["events"] +) { + const result: Array<[Date, UseDataFunctionReturn["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 +) { + return groupedEvents.filter( + ([date]) => dateToWeekNumber(date) === displayedWeek + ); +} diff --git a/app/styles/calendar-event.css b/app/styles/calendar-event.css index ac515e833..6bf4052cf 100644 --- a/app/styles/calendar-event.css +++ b/app/styles/calendar-event.css @@ -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); +} diff --git a/app/styles/calendar.css b/app/styles/calendar.css index 5992fcb16..e5c454edd 100644 --- a/app/styles/calendar.css +++ b/app/styles/calendar.css @@ -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 {