Calendar new page can edit event

This commit is contained in:
Kalle
2022-07-30 22:18:27 +03:00
parent 7d333a5b7f
commit 1da04edc2d
4 changed files with 179 additions and 45 deletions

View File

@@ -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?

View File

@@ -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 (
<div className={clsx("input-container", className)}>
{leftAddon ? <div className="input-addon">{leftAddon}</div> : null}
<input name={name} minLength={minLength} maxLength={maxLength} />
<input
name={name}
minLength={minLength}
maxLength={maxLength}
defaultValue={defaultValue}
/>
</div>
);
}

View File

@@ -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<CreateArgs, "authorId"> & { 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<CalendarEventDate, "startTime" | "eventId"> &
Pick<

View File

@@ -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<UseDataFunctionReturn<typeof loader>>;
// 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<typeof loader>();
return (
<Main halfWidth>
<Form className="stack md items-start" method="post">
{eventToEdit && (
<input
type="hidden"
name="eventToEditId"
value={eventToEdit.eventId}
/>
)}
<NameInput />
<DescriptionTextarea />
<DatesInput />
@@ -146,6 +196,8 @@ export default function CalendarNewEventPage() {
}
function NameInput() {
const { eventToEdit } = useLoaderData<typeof loader>();
return (
<div>
<Label htmlFor="name" required>
@@ -156,17 +208,15 @@ function NameInput() {
required
minLength={CALENDAR_EVENT_NAME_MIN_LENGTH}
maxLength={CALENDAR_EVENT_NAME_MAX_LENGTH}
defaultValue={eventToEdit?.name}
/>
</div>
);
}
function DescriptionTextarea({
initialValue,
}: {
initialValue?: CalendarEvent["description"];
}) {
const [value, setValue] = React.useState(initialValue ?? "");
function DescriptionTextarea() {
const { eventToEdit } = useLoaderData<typeof loader>();
const [value, setValue] = React.useState(eventToEdit?.description ?? "");
return (
<div>
@@ -191,9 +241,15 @@ function DescriptionTextarea({
}
function DatesInput() {
const { eventToEdit } = useLoaderData<typeof loader>();
const { i18n } = useTranslation();
const [dateInputValue, setDateInputValue] = React.useState<string>();
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) => (
<React.Fragment key={id}>
<div
className={clsx("text-lighter", { hidden: dates.length === 1 })}
className={clsx("text-lighter", {
hidden: dates.length === 1,
})}
>
Day {i + 1}
</div>
<div>
{date.toLocaleTimeString(i18n.language, {
hour: "numeric",
minute: "numeric",
year: "numeric",
month: "long",
day: "numeric",
weekday: "long",
})}
<div className={clsx({ invisible: !isMounted })}>
{isMounted
? date.toLocaleTimeString(i18n.language, {
hour: "numeric",
minute: "numeric",
year: "numeric",
month: "long",
day: "numeric",
weekday: "long",
})
: "T"}
</div>
<Button
onClick={() => setDates(dates.filter((date) => date.id !== id))}
className="mr-auto"
className={clsx("mr-auto", { invisible: !isMounted })}
icon={<TrashIcon />}
variant="minimal-destructive"
aria-label="Remove date"
@@ -278,20 +338,9 @@ function DatesInput() {
);
}
function DiscordLinkInput() {
return (
<div className="stack items-start">
<Label htmlFor="discordInviteCode">Discord server invite URL</Label>
<Input
name="discordInviteCode"
leftAddon="https://discord.gg/"
maxLength={CALENDAR_EVENT_DISCORD_INVITE_CODE_MAX_LENGTH}
/>
</div>
);
}
function BracketUrlInput() {
const { eventToEdit } = useLoaderData<typeof loader>();
return (
<div>
<Label htmlFor="bracketUrl" required>
@@ -302,19 +351,39 @@ function BracketUrlInput() {
type="url"
required
maxLength={CALENDAR_EVENT_BRACKET_URL_MAX_LENGTH}
defaultValue={eventToEdit?.bracketUrl}
/>
</div>
);
}
function DiscordLinkInput() {
const { eventToEdit } = useLoaderData<typeof loader>();
return (
<div className="stack items-start">
<Label htmlFor="discordInviteCode">Discord server invite URL</Label>
<Input
name="discordInviteCode"
leftAddon="https://discord.gg/"
maxLength={CALENDAR_EVENT_DISCORD_INVITE_CODE_MAX_LENGTH}
defaultValue={eventToEdit?.discordInviteCode ?? undefined}
/>
</div>
);
}
function TagsAdder() {
const { eventToEdit } = useLoaderData<typeof loader>();
const { t } = useTranslation("calendar");
const [tags, setTags] = React.useState<CalendarEventTag[]>([]);
const [tags, setTags] = React.useState(
(eventToEdit?.tags ?? []) as Array<CalendarEventTag>
);
const id = React.useId();
const tagsForSelect = (
Object.keys(allTags) as Array<CalendarEventTag>
).filter((tag) => !tags.includes(tag));
).filter((tag) => !tags.includes(tag) && tag !== "BADGE");
return (
<div className="stack sm">
@@ -349,8 +418,9 @@ function TagsAdder() {
}
function BadgesAdder() {
const { eventToEdit } = useLoaderData<typeof loader>();
const { managedBadges } = useLoaderData<typeof loader>();
const [badges, setBadges] = React.useState<BadgeType[]>([]);
const [badges, setBadges] = React.useState(eventToEdit?.badges ?? []);
const id = React.useId();
if (managedBadges.length === 0) return null;