mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-07-18 16:43:56 -05:00
Add comment to database
This commit is contained in:
parent
a4b839d55a
commit
44e745692d
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}'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}
|
||||
|
|
|
|||
|
|
@ -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`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user