diff --git a/TODO.md b/TODO.md index 458a54ab9..fe2e46818 100644 --- a/TODO.md +++ b/TODO.md @@ -20,6 +20,8 @@ Calendar ## Other +- [ ] Tags selector can remove +- [ ] Calendar new title - [ ] Sort tags so that they are always in same order - [x] Maybe make bracket URL mandatory? - [ ] components render twice on state change? diff --git a/app/components/Input.tsx b/app/components/Input.tsx index 87faaf30b..d2a7c6a9d 100644 --- a/app/components/Input.tsx +++ b/app/components/Input.tsx @@ -5,18 +5,25 @@ export function Input({ className, minLength, maxLength, + defaultValue, leftAddon, }: { name: string; className?: string; minLength?: number; maxLength?: number; + defaultValue?: string; leftAddon?: string; }) { return (
{leftAddon ?
{leftAddon}
: null} - +
); } diff --git a/app/db/models/calendar.server.ts b/app/db/models/calendar.server.ts index d65d97ad6..db93bd539 100644 --- a/app/db/models/calendar.server.ts +++ b/app/db/models/calendar.server.ts @@ -27,6 +27,16 @@ const createStm = sql.prepare(` ) returning * `); +const updateStm = sql.prepare(` + update "CalendarEvent" set + "name" = $name, + "tags" = $tags, + "description" = $description, + "discordInviteCode" = $discordInviteCode, + "bracketUrl" = $bracketUrl + where "id" = $eventId +`); + const createDateStm = sql.prepare(` insert into "CalendarEventDate" ( "eventId", @@ -38,6 +48,11 @@ const createDateStm = sql.prepare(` ) `); +const deleteDatesByEventIdStm = sql.prepare(` + delete from "CalendarEventDate" + where "eventId" = $eventId +`); + const createBadgeStm = sql.prepare(` insert into "CalendarEventBadge" ( "eventId", @@ -49,6 +64,11 @@ const createBadgeStm = sql.prepare(` ) `); +const deleteBadgesByEventIdStm = sql.prepare(` + delete from "CalendarEventBadge" + where "eventId" = $eventId +`); + export type CreateArgs = Pick< CalendarEvent, | "name" @@ -83,6 +103,33 @@ export const create = sql.transaction( } ); +export const update = sql.transaction( + ({ + startTimes, + badges, + eventId, + ...calendarEventArgs + }: Omit & { eventId: CalendarEvent["id"] }) => { + updateStm.run({ ...calendarEventArgs, eventId }); + + deleteDatesByEventIdStm.run({ eventId }); + for (const startTime of startTimes) { + createDateStm.run({ + eventId, + startTime, + }); + } + + deleteBadgesByEventIdStm.run({ eventId }); + for (const badgeId of badges) { + createBadgeStm.run({ + eventId, + badgeId, + }); + } + } +); + const findAllBetweenTwoTimestampsStm = sql.prepare(` select "CalendarEvent"."name", @@ -150,9 +197,11 @@ const findByIdStm = sql.prepare(` select "CalendarEvent"."name", "CalendarEvent"."description", + "CalendarEvent"."discordInviteCode", "CalendarEvent"."discordUrl", "CalendarEvent"."bracketUrl", "CalendarEvent"."tags", + "User"."id" as "authorId", exists (select 1 from "CalendarEventBadge" where "CalendarEventBadge"."eventId" = "CalendarEventDate"."eventId" @@ -174,7 +223,13 @@ export function findById(id: CalendarEvent["id"]) { const [firstRow, ...rest] = findByIdStm.all({ id }) as Array< Pick< CalendarEvent, - "name" | "description" | "discordUrl" | "bracketUrl" | "tags" + | "name" + | "description" + | "discordUrl" + | "discordInviteCode" + | "bracketUrl" + | "tags" + | "authorId" > & Pick & Pick< diff --git a/app/routes/calendar/new.tsx b/app/routes/calendar/new.tsx index 78ab78be3..22d5b3b0a 100644 --- a/app/routes/calendar/new.tsx +++ b/app/routes/calendar/new.tsx @@ -2,11 +2,7 @@ import { Form, useLoaderData } from "@remix-run/react"; import { Label } from "~/components/Label"; import { Main } from "~/components/Main"; import * as React from "react"; -import type { - Badge as BadgeType, - CalendarEvent, - CalendarEventTag, -} from "~/db/types"; +import type { Badge as BadgeType, CalendarEventTag } from "~/db/types"; import { CALENDAR_EVENT_BRACKET_URL_MAX_LENGTH, CALENDAR_EVENT_DESCRIPTION_MAX_LENGTH, @@ -37,6 +33,7 @@ import { requireUser } from "~/modules/auth"; import { Badge } from "~/components/Badge"; import { z } from "zod"; import { + actualNumber, date, falsyToNull, id, @@ -46,6 +43,7 @@ import { } from "~/utils/zod"; import { parseRequestFormData } from "~/utils/remix"; import { + databaseTimestampToDate, dateToDatabaseTimestamp, dateToYearMonthDayHourMinuteString, } from "~/utils/dates"; @@ -60,7 +58,23 @@ export const links: LinksFunction = () => { return [{ rel: "stylesheet", href: styles }]; }; +// export const meta: MetaFunction = (args) => { +// const data = args.data as Nullable>; + +// if (!data) return {}; + +// return { +// title: data.title, +// description: `${data.events.length} events happening during week ${ +// data.displayedWeek +// } including ${joinListToNaturalString( +// data.events.slice(0, 3).map((e) => e.name) +// )}`, +// }; +// }; + const newCalendarEventActionSchema = z.object({ + eventToEditId: z.preprocess(actualNumber, id.nullish()), name: z .string() .min(CALENDAR_EVENT_NAME_MIN_LENGTH) @@ -100,8 +114,7 @@ export const action: ActionFunction = async ({ request }) => { schema: newCalendarEventActionSchema, }); - const createdEventId = db.calendarEvents.create({ - authorId: user.id, + const commonArgs = { name: data.name, description: data.description, startTimes: data.dates.map((date) => dateToDatabaseTimestamp(date)), @@ -109,9 +122,27 @@ export const action: ActionFunction = async ({ request }) => { discordInviteCode: data.discordInviteCode, tags: data.tags ? data.tags.join(",") : data.tags, badges: data.badges ?? [], - }); + }; + if (data.eventToEditId) { + const eventToEdit = db.calendarEvents.findById(data.eventToEditId); + if (eventToEdit?.authorId !== user.id) { + throw new Response(null, { status: 401 }); + } - return redirect(calendarEventPage(createdEventId)); + db.calendarEvents.update({ + eventId: data.eventToEditId, + ...commonArgs, + }); + + return redirect(calendarEventPage(data.eventToEditId)); + } else { + const createdEventId = db.calendarEvents.create({ + authorId: user.id, + ...commonArgs, + }); + + return redirect(calendarEventPage(createdEventId)); + } }; export const handle = { @@ -120,16 +151,35 @@ export const handle = { export const loader = async ({ request }: LoaderArgs) => { const user = await requireUser(request); + const url = new URL(request.url); + + const eventId = Number(url.searchParams.get("eventId")); + const eventToEdit = Number.isNaN(eventId) + ? undefined + : db.calendarEvents.findById(eventId); return json({ managedBadges: db.badges.managedByUserId(user.id), + eventToEdit: + eventToEdit?.authorId === user.id + ? { ...eventToEdit, badges: db.calendarEvents.findBadgesById(eventId) } + : undefined, }); }; export default function CalendarNewEventPage() { + const { eventToEdit } = useLoaderData(); + return (
+ {eventToEdit && ( + + )} @@ -146,6 +196,8 @@ export default function CalendarNewEventPage() { } function NameInput() { + const { eventToEdit } = useLoaderData(); + return (
); } -function DescriptionTextarea({ - initialValue, -}: { - initialValue?: CalendarEvent["description"]; -}) { - const [value, setValue] = React.useState(initialValue ?? ""); +function DescriptionTextarea() { + const { eventToEdit } = useLoaderData(); + const [value, setValue] = React.useState(eventToEdit?.description ?? ""); return (
@@ -191,9 +241,15 @@ function DescriptionTextarea({ } function DatesInput() { + const { eventToEdit } = useLoaderData(); const { i18n } = useTranslation(); const [dateInputValue, setDateInputValue] = React.useState(); - const [dates, setDates] = React.useState<{ date: Date; id: string }[]>([]); + const [dates, setDates] = React.useState( + (eventToEdit?.startTimes ?? []).map((startTime) => ({ + date: databaseTimestampToDate(startTime), + id: String(Math.random()), + })) + ); const isMounted = useIsMounted(); const usersTimeZone = isMounted @@ -249,23 +305,27 @@ function DatesInput() { {dates.map(({ date, id }, i) => (
Day {i + 1}
-
- {date.toLocaleTimeString(i18n.language, { - hour: "numeric", - minute: "numeric", - year: "numeric", - month: "long", - day: "numeric", - weekday: "long", - })} +
+ {isMounted + ? date.toLocaleTimeString(i18n.language, { + hour: "numeric", + minute: "numeric", + year: "numeric", + month: "long", + day: "numeric", + weekday: "long", + }) + : "T"}