diff --git a/README.md b/README.md index 1e7a7305e..09be3e8d7 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,8 @@ sendou.ink/ │ ├── hooks/ -- React hooks │ ├── routes/ -- Routes see: https://remix.run/docs/en/v1/guides/routing │ ├── styles/ -- All .css files of the project for styling -│ └── utils/ -- Random helper functions used in many places +│ ├── utils/ -- Random helper functions used in many places +│ └── permissions.ts / -- What actions are allowed. Separated by frontend and backend as frontend has constraints based on what user sees. ├── migrations/ -- Database migrations ├── cypress/ -- see: https://docs.cypress.io/guides/core-concepts/writing-and-organizing-tests#Folder-structure ├── public/ -- Images, built assets etc. static files to be served as is diff --git a/app/components/Popover.tsx b/app/components/Popover.tsx index 341f873a5..d592cb53d 100644 --- a/app/components/Popover.tsx +++ b/app/components/Popover.tsx @@ -2,6 +2,7 @@ import { Popover as HeadlessPopover } from "@headlessui/react"; import * as React from "react"; import { usePopper } from "react-popper"; +// xxx: gets weird border on click export function Popover({ children, trigger, diff --git a/app/core/plus.ts b/app/core/plus.ts index 26a8b01a7..df9114d06 100644 --- a/app/core/plus.ts +++ b/app/core/plus.ts @@ -27,7 +27,7 @@ export function upcomingVoting(now: Date): MonthYear { } /** Range of first Friday of a month to the following Monday (this range is when voting is active) */ -function monthsVotingRange({ month, year }: MonthYear) { +export function monthsVotingRange({ month, year }: MonthYear) { const startDate = new Date(Date.UTC(year, month, 1, 10)); while (startDate.getDay() !== 5) { diff --git a/app/permissions.ts b/app/permissions.ts new file mode 100644 index 000000000..88ff3e231 --- /dev/null +++ b/app/permissions.ts @@ -0,0 +1,76 @@ +import type * as plusSuggestions from "~/db/models/plusSuggestions.server"; +import type { User } from "./db/types"; +import { allTruthy } from "./utils/arrays"; + +interface CanAddCommentToSuggestionFEArgs { + user?: Pick; + allSuggestions: plusSuggestions.FindResult; + target: Pick; +} +export function canAddCommentToSuggestionFE( + args: CanAddCommentToSuggestionFEArgs +) { + return !alreadyCommentedByUser(args); +} + +interface CanAddCommentToSuggestionBEArgs + extends CanAddCommentToSuggestionFEArgs { + user?: Pick; + targetPlusTier: number; +} +export function canAddCommentToSuggestionBE({ + user, + targetPlusTier, + allSuggestions, + target, +}: CanAddCommentToSuggestionBEArgs) { + return allTruthy([ + canAddCommentToSuggestionFE({ user, allSuggestions, target }), + playerAlreadySuggested({ allSuggestions, target }), + targetPlusTierIsSmallerOrEqual({ user, targetPlusTier }), + ]); +} + +// TODO: needed for new suggestions +// function votingIsActive() { +// const now = new Date(); +// const { endDate, startDate } = monthsVotingRange({ +// month: now.getMonth(), +// year: now.getFullYear(), +// }); + +// return ( +// now.getTime() >= startDate.getTime() && now.getTime() <= endDate.getTime() +// ); +// } + +function alreadyCommentedByUser({ + user, + allSuggestions, + target, +}: CanAddCommentToSuggestionFEArgs) { + return Boolean( + allSuggestions + .find(({ tier }) => tier === target.plusTier) + ?.users.find((u) => u.info.id === target.id) + ?.suggestions.some((s) => s.author.id === user?.id) + ); +} + +function playerAlreadySuggested({ + allSuggestions, + target, +}: Pick) { + return Boolean( + allSuggestions + .find(({ tier }) => tier === target.plusTier) + ?.users.find((u) => u.info.id === target.id) + ); +} + +function targetPlusTierIsSmallerOrEqual({ + user, + targetPlusTier, +}: Pick) { + return user?.plusTier && user.plusTier <= targetPlusTier; +} diff --git a/app/routes/plus/suggestions.tsx b/app/routes/plus/suggestions.tsx index f0898078f..30b39c0a7 100644 --- a/app/routes/plus/suggestions.tsx +++ b/app/routes/plus/suggestions.tsx @@ -14,6 +14,7 @@ import { upcomingVoting } from "~/core/plus"; import { db } from "~/db"; import type * as plusSuggestions from "~/db/models/plusSuggestions.server"; import { useUser } from "~/hooks/useUser"; +import { canAddCommentToSuggestionFE } from "~/permissions"; import styles from "~/styles/plus.css"; import { databaseTimestampToDate } from "~/utils/dates"; import { makeTitle, requireUser } from "~/utils/remix"; @@ -134,7 +135,7 @@ function SuggestedForInfo() { } function SuggestedUser({ - user, + user: suggestedUser, tier, }: { user: Unpacked< @@ -142,36 +143,44 @@ function SuggestedUser({ >; tier: number; }) { - const commentPageUrl = () => - `comment?${new URLSearchParams({ - id: String(user.info.id), - tier: String(tier), - }).toString()}`; + const data = useLoaderData(); + const user = useUser(); + + const commentPageUrl = `comment?${new URLSearchParams({ + id: String(suggestedUser.info.id), + tier: String(tier), + }).toString()}`; return (
-

{user.info.discordName}

- - Comment - +

{suggestedUser.info.discordName}

+ {canAddCommentToSuggestionFE({ + user, + allSuggestions: data.suggestions!, + target: { id: suggestedUser.info.id, plusTier: tier }, + }) ? ( + + Comment + + ) : null}
- Comments ({user.suggestions.length}) + Comments ({suggestedUser.suggestions.length})
- {user.suggestions.map((s) => ( + {suggestedUser.suggestions.map((s) => ( // xxx: white-space: pre-wrap?
{discordFullName(s.author)} diff --git a/app/routes/plus/suggestions/comment.tsx b/app/routes/plus/suggestions/comment.tsx index 8e935a78b..e733d4897 100644 --- a/app/routes/plus/suggestions/comment.tsx +++ b/app/routes/plus/suggestions/comment.tsx @@ -12,8 +12,28 @@ import { PLUS_SUGGESTIONS_PAGE } from "~/utils/urls"; import type { PlusSuggestionsLoaderData } from "../suggestions"; import * as React from "react"; import { PlUS_SUGGESTION_COMMENT_MAX_LENGTH } from "~/constants"; +import type { ActionFunction } from "@remix-run/node"; +import { z } from "zod"; +import { parseRequestFormData, requireUser } from "~/utils/remix"; +import { canAddCommentToSuggestionFE } from "~/permissions"; +import { useUser } from "~/hooks/useUser"; + +const commentActionSchema = z.object({ + comment: z.string().max(PlUS_SUGGESTION_COMMENT_MAX_LENGTH), +}); + +export const action: ActionFunction = async ({ request }) => { + const data = await parseRequestFormData({ + request, + schema: commentActionSchema, + }); + const user = await requireUser(request); + + return null; +}; export default function PlusCommentModalPage() { + const user = useUser(); const matches = useMatches(); const navigate = useNavigate(); const [searchParams] = useSearchParams(); @@ -26,7 +46,14 @@ export default function PlusCommentModalPage() { ?.find(({ tier }) => tier === tierSuggestedTo) ?.users.find((u) => u.info.id === userBeingCommentedId); - if (!userBeingCommented) { + if ( + !userBeingCommented || + !canAddCommentToSuggestionFE({ + user, + allSuggestions: data.suggestions!, + target: { id: userBeingCommentedId, plusTier: tierSuggestedTo }, + }) + ) { return ; } diff --git a/app/utils/arrays.ts b/app/utils/arrays.ts new file mode 100644 index 000000000..fd025a626 --- /dev/null +++ b/app/utils/arrays.ts @@ -0,0 +1,3 @@ +export function allTruthy(arr: unknown[]) { + return arr.every(Boolean); +}