Add comment to database

This commit is contained in:
Kalle 2022-05-26 21:49:48 +03:00
parent a4b839d55a
commit 44e745692d
6 changed files with 93 additions and 44 deletions

View File

@ -49,6 +49,7 @@ function useDOMSync(isOpen: boolean) {
if (isOpen) {
dialog.showModal();
// TODO: can be replaced with https://twitter.com/argyleink/status/1529869352660439048 once gets control
html.classList.add("lock-scroll");
} else {
dialog.close();

View File

@ -4,8 +4,8 @@ import { allTruthy } from "./utils/arrays";
interface CanAddCommentToSuggestionFEArgs {
user?: Pick<User, "id">;
allSuggestions: plusSuggestions.FindResult;
target: Pick<User, "id" | "plusTier">;
suggestions: plusSuggestions.FindResult;
suggested: { id: User["id"]; plusTier: NonNullable<User["plusTier"]> };
}
export function canAddCommentToSuggestionFE(
args: CanAddCommentToSuggestionFEArgs
@ -16,18 +16,16 @@ export function canAddCommentToSuggestionFE(
interface CanAddCommentToSuggestionBEArgs
extends CanAddCommentToSuggestionFEArgs {
user?: Pick<User, "id" | "plusTier">;
targetPlusTier: number;
}
export function canAddCommentToSuggestionBE({
user,
targetPlusTier,
allSuggestions,
target,
suggestions,
suggested,
}: CanAddCommentToSuggestionBEArgs) {
return allTruthy([
canAddCommentToSuggestionFE({ user, allSuggestions, target }),
playerAlreadySuggested({ allSuggestions, target }),
targetPlusTierIsSmallerOrEqual({ user, targetPlusTier }),
canAddCommentToSuggestionFE({ user, suggestions, suggested }),
playerAlreadySuggested({ suggestions, suggested }),
targetPlusTierIsSmallerOrEqual({ user, suggested }),
]);
}
@ -46,31 +44,31 @@ export function canAddCommentToSuggestionBE({
function alreadyCommentedByUser({
user,
allSuggestions,
target,
suggestions,
suggested,
}: CanAddCommentToSuggestionFEArgs) {
return Boolean(
allSuggestions
.find(({ tier }) => tier === target.plusTier)
?.users.find((u) => u.info.id === target.id)
suggestions
.find(({ tier }) => tier === suggested.plusTier)
?.users.find((u) => u.info.id === suggested.id)
?.suggestions.some((s) => s.author.id === user?.id)
);
}
function playerAlreadySuggested({
allSuggestions,
target,
}: Pick<CanAddCommentToSuggestionBEArgs, "allSuggestions" | "target">) {
suggestions,
suggested,
}: Pick<CanAddCommentToSuggestionBEArgs, "suggestions" | "suggested">) {
return Boolean(
allSuggestions
.find(({ tier }) => tier === target.plusTier)
?.users.find((u) => u.info.id === target.id)
suggestions
.find(({ tier }) => tier === suggested.plusTier)
?.users.find((u) => u.info.id === suggested.id)
);
}
function targetPlusTierIsSmallerOrEqual({
user,
targetPlusTier,
}: Pick<CanAddCommentToSuggestionBEArgs, "user" | "targetPlusTier">) {
return user?.plusTier && user.plusTier <= targetPlusTier;
suggested,
}: Pick<CanAddCommentToSuggestionBEArgs, "user" | "suggested">) {
return user?.plusTier && user.plusTier <= suggested.plusTier;
}

View File

@ -97,7 +97,7 @@ export default function PlusSuggestionsPage() {
{visibleSuggestions.users.map((u) => (
<SuggestedUser
key={`${u.info.id}-${tierVisible}`}
user={u}
suggested={u}
tier={tierVisible}
/>
))}
@ -135,10 +135,10 @@ function SuggestedForInfo() {
}
function SuggestedUser({
user: suggestedUser,
suggested,
tier,
}: {
user: Unpacked<
suggested: Unpacked<
Unpacked<NonNullable<PlusSuggestionsLoaderData["suggestions"]>>["users"]
>;
tier: number;
@ -147,23 +147,25 @@ function SuggestedUser({
const user = useUser();
const commentPageUrl = `comment?${new URLSearchParams({
id: String(suggestedUser.info.id),
id: String(suggested.info.id),
tier: String(tier),
}).toString()}`;
invariant(data.suggestions);
return (
<div className="stack md">
<div className="plus__suggested-user-info">
<Avatar
discordAvatar={suggestedUser.info.discordAvatar}
discordId={suggestedUser.info.discordId}
discordAvatar={suggested.info.discordAvatar}
discordId={suggested.info.discordId}
size="md"
/>
<h2>{suggestedUser.info.discordName}</h2>
<h2>{suggested.info.discordName}</h2>
{canAddCommentToSuggestionFE({
user,
allSuggestions: data.suggestions!,
target: { id: suggestedUser.info.id, plusTier: tier },
suggestions: data.suggestions,
suggested: { id: suggested.info.id, plusTier: tier },
}) ? (
<LinkButton
className="plus__comment-button"
@ -177,10 +179,10 @@ function SuggestedUser({
</div>
<details>
<summary className="plus__view-comments-action">
Comments ({suggestedUser.suggestions.length})
Comments ({suggested.suggestions.length})
</summary>
<div className="stack sm mt-2">
{suggestedUser.suggestions.map((s) => (
{suggested.suggestions.map((s) => (
// xxx: white-space: pre-wrap?
<fieldset key={s.author.id}>
<legend>{discordFullName(s.author)}</legend>

View File

@ -14,12 +14,21 @@ 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 { parseRequestFormData, requireUser, validate } from "~/utils/remix";
import {
canAddCommentToSuggestionBE,
canAddCommentToSuggestionFE,
} from "~/permissions";
import { useUser } from "~/hooks/useUser";
import { upcomingVoting } from "~/core/plus";
import { db } from "~/db";
import { actualNumber } from "~/utils/zod";
import invariant from "tiny-invariant";
const commentActionSchema = z.object({
comment: z.string().max(PlUS_SUGGESTION_COMMENT_MAX_LENGTH),
text: z.string().min(1).max(PlUS_SUGGESTION_COMMENT_MAX_LENGTH),
tier: z.preprocess(actualNumber, z.number().min(1).max(3)),
suggestedId: z.preprocess(actualNumber, z.number()),
});
export const action: ActionFunction = async ({ request }) => {
@ -29,9 +38,31 @@ export const action: ActionFunction = async ({ request }) => {
});
const user = await requireUser(request);
const suggestions = db.plusSuggestions.find({
...upcomingVoting(new Date()),
plusTier: user.plusTier,
});
validate(suggestions);
validate(
canAddCommentToSuggestionBE({
suggestions,
user,
suggested: { id: data.suggestedId, plusTier: data.tier },
})
);
db.plusSuggestions.create({
authorId: user.id,
...data,
...upcomingVoting(new Date()),
});
return null;
};
// xxx: making unnecessary call to loader + when going back to modal and back keeps making http calls?
// xxx: modal closes when submitting without redirect?
export default function PlusCommentModalPage() {
const user = useUser();
const matches = useMatches();
@ -39,19 +70,20 @@ export default function PlusCommentModalPage() {
const [searchParams] = useSearchParams();
const data = matches.at(-2)!.data as PlusSuggestionsLoaderData;
const userBeingCommentedId = Number(searchParams.get("id"));
const targetUserId = Number(searchParams.get("id"));
const tierSuggestedTo = Number(searchParams.get("tier"));
const userBeingCommented = data.suggestions
?.find(({ tier }) => tier === tierSuggestedTo)
?.users.find((u) => u.info.id === userBeingCommentedId);
?.users.find((u) => u.info.id === targetUserId);
invariant(data.suggestions);
if (
!userBeingCommented ||
!canAddCommentToSuggestionFE({
user,
allSuggestions: data.suggestions!,
target: { id: userBeingCommentedId, plusTier: tierSuggestedTo },
suggestions: data.suggestions,
suggested: { id: targetUserId, plusTier: tierSuggestedTo },
})
) {
return <Redirect to={PLUS_SUGGESTIONS_PAGE} />;
@ -60,6 +92,8 @@ export default function PlusCommentModalPage() {
return (
<Dialog className="plus__modal" isOpen>
<Form method="post" className="stack md">
<input type="hidden" name="tier" value={tierSuggestedTo} />
<input type="hidden" name="suggestedId" value={targetUserId} />
<h2 className="plus__modal-title">
{userBeingCommented.info.discordName}&apos;s +{tierSuggestedTo}{" "}
suggestion
@ -85,7 +119,7 @@ function CommentTextarea() {
return (
<div>
<Label
htmlFor="comment"
htmlFor="text"
valueLimits={{
current: value.length,
max: PlUS_SUGGESTION_COMMENT_MAX_LENGTH,
@ -94,8 +128,8 @@ function CommentTextarea() {
Your comment
</Label>
<textarea
id="comment"
name="comment"
id="text"
name="text"
className="plus__modal-textarea"
rows={4}
value={value}

View File

@ -65,6 +65,14 @@ export async function getUser(request: Request) {
return db.users.findByIdentifier(userId);
}
/** Asserts condition is truthy. Throws a new `Response` with status code 400 and given message 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 {
if (condition) return;
throw new Response(null, { status: 400 });
}
export function makeTitle(title: string) {
return `${title} | sendou.ink`;
}

View File

@ -15,3 +15,9 @@ export function falsyToNull(value: unknown): unknown {
return null;
}
export function actualNumber(value: unknown) {
const parsed = Number(value);
return Number.isNaN(parsed) ? undefined : parsed;
}