diff --git a/TODO.md b/TODO.md index ba047867b..b68bd4a85 100644 --- a/TODO.md +++ b/TODO.md @@ -9,8 +9,8 @@ Calendar - [x] Implement tags - [x] Add initial tags.json - [x] Add tags to the seed -- [ ] Page to add a new event -- [ ] Edit page +- [x] Page to add a new event +- [x] Edit page - [ ] E2E test form controls (adding/removing date, adding/removing tags) etc. - [ ] E2E test browsing events + event page - [ ] E2E test adding and editing event @@ -27,7 +27,7 @@ Calendar - [x] Calendar new title - [x] Sort tags so that they are always in same order - [x] Maybe make bracket URL mandatory? -- [ ] components render twice on state change? +- [x] components render twice on state change? - [ ] If week has no events show some text + don't show time zone text - [x] flatMap -> React.fragment - [x] Event page layout shift with time not taking space before mount (only if one day) diff --git a/app/permissions.ts b/app/permissions.ts index c42801121..fd53b4e52 100644 --- a/app/permissions.ts +++ b/app/permissions.ts @@ -1,6 +1,11 @@ import type * as plusSuggestions from "~/db/models/plusSuggestions.server"; import { monthsVotingRange } from "./modules/plus-server"; -import type { PlusSuggestion, User, UserWithPlusTier } from "./db/types"; +import type { + CalendarEvent, + PlusSuggestion, + User, + UserWithPlusTier, +} from "./db/types"; import { allTruthy } from "./utils/arrays"; import { ADMIN_DISCORD_ID, LOHI_TOKEN_HEADER_NAME } from "./constants"; import invariant from "tiny-invariant"; @@ -254,3 +259,14 @@ function isBadgeManager({ export function canEditBadgeManagers(user?: IsAdminUser) { return isAdmin(user); } + +interface CanEditCalendarEventArgs { + user?: Pick; + event: Pick; +} +export function canEditCalendarEvent({ + user, + event, +}: CanEditCalendarEventArgs) { + return adminOverride(user)(user?.id === event.authorId); +} diff --git a/app/routes/calendar/$id.tsx b/app/routes/calendar/$id.tsx index 0c904c4d7..0a5b1d5d3 100644 --- a/app/routes/calendar/$id.tsx +++ b/app/routes/calendar/$id.tsx @@ -16,12 +16,14 @@ import { LinkButton } from "~/components/Button"; import { Main } from "~/components/Main"; import { db } from "~/db"; import { useIsMounted } from "~/hooks/useIsMounted"; +import { useUser } from "~/modules/auth"; import { i18next } from "~/modules/i18n"; +import { canEditCalendarEvent } from "~/permissions"; import styles from "~/styles/calendar-event.css"; import { databaseTimestampToDate } from "~/utils/dates"; import { notFoundIfFalsy } from "~/utils/remix"; import { discordFullName, makeTitle } from "~/utils/strings"; -import { resolveBaseUrl } from "~/utils/urls"; +import { calendarEditPage, resolveBaseUrl } from "~/utils/urls"; import { actualNumber, id } from "~/utils/zod"; import { Tags } from "./components/Tags"; import allTags from "./tags.json"; @@ -60,12 +62,13 @@ export const loader = async ({ params, request }: LoaderArgs) => { }; export default function CalendarEventPage() { + const user = useUser(); const { event } = useLoaderData(); const { i18n } = useTranslation(); const isMounted = useIsMounted(); return ( -
+
{event.startTimes.map((startTime, i) => ( @@ -99,7 +102,7 @@ export default function CalendarEventPage() {

{event.name}

-
+
{event.discordUrl ? ( {resolveBaseUrl(event.bracketUrl)} + {canEditCalendarEvent({ user, event }) && ( + + Edit + + )}
diff --git a/app/routes/calendar/components/Tags.tsx b/app/routes/calendar/components/Tags.tsx index 2ed51c42a..f0d19ef66 100644 --- a/app/routes/calendar/components/Tags.tsx +++ b/app/routes/calendar/components/Tags.tsx @@ -1,3 +1,4 @@ +import React from "react"; import { useTranslation } from "react-i18next"; import { Button } from "~/components/Button"; import { CrossIcon } from "~/components/icons/Cross"; @@ -19,8 +20,8 @@ export function Tags({ return (
    {tags.map((tag) => ( - <> -
  • + +
  • {t(`tag.name.${tag}`)} {onDelete && (
  • - + ))}
); diff --git a/app/routes/calendar/new.tsx b/app/routes/calendar/new.tsx index 342016746..30f98b1f1 100644 --- a/app/routes/calendar/new.tsx +++ b/app/routes/calendar/new.tsx @@ -42,7 +42,11 @@ import { removeDuplicates, safeJSONParse, } from "~/utils/zod"; -import { parseRequestFormData } from "~/utils/remix"; +import { + badRequestIfFalsy, + parseRequestFormData, + validate, +} from "~/utils/remix"; import { databaseTimestampToDate, dateToDatabaseTimestamp, @@ -52,6 +56,7 @@ import { calendarEventPage } from "~/utils/urls"; import { makeTitle } from "~/utils/strings"; import { i18next } from "~/modules/i18n"; import type { UseDataFunctionReturn } from "@remix-run/react/dist/components"; +import { canEditCalendarEvent } from "~/permissions"; const MIN_DATE = new Date(Date.UTC(2015, 4, 28)); @@ -137,10 +142,10 @@ export const action: ActionFunction = async ({ request }) => { badges: data.badges ?? [], }; if (data.eventToEditId) { - const eventToEdit = db.calendarEvents.findById(data.eventToEditId); - if (eventToEdit?.authorId !== user.id) { - throw new Response(null, { status: 401 }); - } + const eventToEdit = badRequestIfFalsy( + db.calendarEvents.findById(data.eventToEditId) + ); + validate(canEditCalendarEvent({ user, event: eventToEdit }), 401); db.calendarEvents.update({ eventId: data.eventToEditId, diff --git a/app/styles/utils.css b/app/styles/utils.css index 60c12ccad..727338e8b 100644 --- a/app/styles/utils.css +++ b/app/styles/utils.css @@ -74,6 +74,10 @@ justify-content: center; } +.flex-wrap { + flex-wrap: wrap; +} + .all-unset { all: unset; } diff --git a/app/utils/remix.ts b/app/utils/remix.ts index 9da939bd5..0b6f49ae7 100644 --- a/app/utils/remix.ts +++ b/app/utils/remix.ts @@ -38,10 +38,10 @@ export async function parseRequestFormData({ } } -/** Asserts condition is truthy. Throws a new `Response` with status code 400 and given message if falsy. */ +/** Asserts condition is truthy. Throws a new `Response` with given status code if falsy. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- same format as TS docs: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html#assertion-functions -export function validate(condition: any): asserts condition { +export function validate(condition: any, status = 400): asserts condition { if (condition) return; - throw new Response(null, { status: 400 }); + throw new Response(null, { status }); } diff --git a/app/utils/urls.ts b/app/utils/urls.ts index 29708d849..fbe8389c2 100644 --- a/app/utils/urls.ts +++ b/app/utils/urls.ts @@ -25,6 +25,8 @@ export const badgePage = (badgeId: number) => `${BADGES_PAGE}/${badgeId}`; export const plusSuggestionPage = (tier?: string | number) => `/plus/suggestions${tier ? `?tier=${tier}` : ""}`; export const calendarEventPage = (eventId: number) => `/calendar/${eventId}`; +export const calendarEditPage = (eventId: number) => + `/calendar/new?eventId=${eventId}`; export const badgeUrl = ({ code,