mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-06-13 12:42:36 -05:00
Edit event full flow
This commit is contained in:
parent
beb8e73436
commit
7d7f771c19
6
TODO.md
6
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)
|
||||
|
|
|
|||
|
|
@ -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<User, "id" | "discordId">;
|
||||
event: Pick<CalendarEvent, "authorId">;
|
||||
}
|
||||
export function canEditCalendarEvent({
|
||||
user,
|
||||
event,
|
||||
}: CanEditCalendarEventArgs) {
|
||||
return adminOverride(user)(user?.id === event.authorId);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<typeof loader>();
|
||||
const { i18n } = useTranslation();
|
||||
const isMounted = useIsMounted();
|
||||
|
||||
return (
|
||||
<Main className="stack lg">
|
||||
<Main className="stack md">
|
||||
<section className="stack sm">
|
||||
<div className="event__times">
|
||||
{event.startTimes.map((startTime, i) => (
|
||||
|
|
@ -99,7 +102,7 @@ export default function CalendarEventPage() {
|
|||
<h2>{event.name}</h2>
|
||||
<Tags tags={event.tags} />
|
||||
</div>
|
||||
<div className="stack horizontal sm">
|
||||
<div className="stack horizontal sm flex-wrap">
|
||||
{event.discordUrl ? (
|
||||
<LinkButton
|
||||
to={event.discordUrl}
|
||||
|
|
@ -118,6 +121,11 @@ export default function CalendarEventPage() {
|
|||
>
|
||||
{resolveBaseUrl(event.bracketUrl)}
|
||||
</LinkButton>
|
||||
{canEditCalendarEvent({ user, event }) && (
|
||||
<LinkButton tiny to={calendarEditPage(event.eventId)}>
|
||||
Edit
|
||||
</LinkButton>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<ul className="calendar__event__tags">
|
||||
{tags.map((tag) => (
|
||||
<>
|
||||
<li key={tag} style={{ backgroundColor: allTags[tag].color }}>
|
||||
<React.Fragment key={tag}>
|
||||
<li style={{ backgroundColor: allTags[tag].color }}>
|
||||
{t(`tag.name.${tag}`)}
|
||||
{onDelete && (
|
||||
<Button
|
||||
|
|
@ -33,7 +34,7 @@ export function Tags({
|
|||
/>
|
||||
)}
|
||||
</li>
|
||||
</>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -74,6 +74,10 @@
|
|||
justify-content: center;
|
||||
}
|
||||
|
||||
.flex-wrap {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.all-unset {
|
||||
all: unset;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,10 +38,10 @@ export async function parseRequestFormData<T extends z.ZodTypeAny>({
|
|||
}
|
||||
}
|
||||
|
||||
/** 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 });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user