Delete comment

This commit is contained in:
Kalle
2022-05-27 21:46:33 +03:00
parent ceb2241ce2
commit 7485aed180
10 changed files with 170 additions and 14 deletions

View File

@@ -9,7 +9,7 @@ export function Dialog({
children: React.ReactNode;
isOpen: boolean;
close?: () => void;
className: string;
className?: string;
}) {
const ref = useDOMSync(isOpen);

View File

@@ -0,0 +1,50 @@
import { Form } from "@remix-run/react";
import React from "react";
import invariant from "tiny-invariant";
import { Button } from "./Button";
import { Dialog } from "./Dialog";
export function FormWithConfirm({
fields,
children,
dialogHeading,
}: {
fields: [name: string, value: string | number][];
children: React.ReactNode;
dialogHeading: string;
}) {
const [dialogOpen, setDialogOpen] = React.useState(false);
const formRef = React.useRef<HTMLFormElement>(null);
const id = React.useId();
const openDialog = () => setDialogOpen(true);
const closeDialog = () => setDialogOpen(false);
invariant(React.isValidElement(children));
return (
<>
<Form id={id} className="hidden" ref={formRef} method="post">
{fields.map(([name, value]) => (
<input type="hidden" key={name} name={name} value={value} />
))}
</Form>
<Dialog isOpen={dialogOpen} close={closeDialog} className="text-center">
<div className="stack md">
<h2 className="text-sm">{dialogHeading}</h2>
<div className="stack vertical md justify-center">
<Button form={id} variant="destructive" type="submit">
Delete
</Button>
<Button variant="outlined" onClick={closeDialog}>
Cancel
</Button>
</div>
</div>
</Dialog>
{React.cloneElement(children, {
onClick: openDialog,
})}
</>
);
}

View File

@@ -53,6 +53,8 @@ WITH gs as (
'suggestions',
json_group_array(
json_object(
'id',
"PlusSuggestion"."id",
'author',
json_object(
'id',
@@ -103,7 +105,7 @@ export type FindResult = {
| "discordDiscriminator"
| "discordAvatar"
>;
suggestions: (Pick<PlusSuggestion, "createdAt" | "text"> & {
suggestions: (Pick<PlusSuggestion, "id" | "createdAt" | "text"> & {
author: Pick<
User,
"id" | "discordId" | "discordName" | "discordDiscriminator"
@@ -141,3 +143,13 @@ const tiersSuggestedForStm = sql.prepare(`
export function tiersSuggestedFor(args: MonthYear & { userId: User["id"] }) {
return JSON.parse(tiersSuggestedForStm.pluck().get(args)) as User["id"][];
}
const delStm = sql.prepare(`
DELETE FROM "PlusSuggestion"
WHERE
"id" = $id
`);
export function del(id: PlusSuggestion["id"]) {
delStm.run({ id });
}

View File

@@ -14,6 +14,7 @@ export interface User {
}
export interface PlusSuggestion {
id: number;
text: string;
authorId: number;
suggestedId: number;

View File

@@ -29,6 +29,14 @@ export function canAddCommentToSuggestionBE({
]);
}
interface CanDeleteCommentArgs {
author: Pick<User, "id">;
user?: Pick<User, "id">;
}
export function canDeleteComment(args: CanDeleteCommentArgs) {
return isOwnComment(args);
}
// TODO: needed for new suggestions
// function votingIsActive() {
// const now = new Date();
@@ -72,3 +80,7 @@ function targetPlusTierIsSmallerOrEqual({
}: Pick<CanAddCommentToSuggestionBEArgs, "user" | "suggested">) {
return user?.plusTier && user.plusTier <= suggested.plusTier;
}
function isOwnComment({ author, user }: CanDeleteCommentArgs) {
return author.id === user?.id;
}

View File

@@ -1,4 +1,5 @@
import type {
ActionFunction,
LinksFunction,
LoaderFunction,
MetaFunction,
@@ -7,19 +8,28 @@ import { json } from "@remix-run/node";
import { Outlet, useLoaderData } from "@remix-run/react";
import * as React from "react";
import invariant from "tiny-invariant";
import { z } from "zod";
import { Avatar } from "~/components/Avatar";
import { LinkButton } from "~/components/Button";
import { Button, LinkButton } from "~/components/Button";
import { Catcher } from "~/components/Catcher";
import { FormWithConfirm } from "~/components/FormWithConfirm";
import { TrashIcon } from "~/components/icons/Trash";
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 { canAddCommentToSuggestionFE, canDeleteComment } from "~/permissions";
import styles from "~/styles/plus.css";
import { databaseTimestampToDate } from "~/utils/dates";
import { makeTitle, requireUser } from "~/utils/remix";
import {
makeTitle,
parseRequestFormData,
requireUser,
validate,
} from "~/utils/remix";
import { discordFullName } from "~/utils/strings";
import type { Unpacked } from "~/utils/types";
import { actualNumber } from "~/utils/zod";
export const links: LinksFunction = () => {
return [{ rel: "stylesheet", href: styles }];
@@ -32,6 +42,35 @@ export const meta: MetaFunction = () => {
};
};
const suggestionActionSchema = z.object({
suggestionId: z.preprocess(actualNumber, z.number()),
});
export const action: ActionFunction = async ({ request }) => {
const data = await parseRequestFormData({
request,
schema: suggestionActionSchema,
});
const user = await requireUser(request);
const suggestions = db.plusSuggestions.find({
...upcomingVoting(new Date()),
plusTier: user.plusTier,
});
const targetSuggestion = suggestions
?.flatMap((s) => s.users)
.flatMap((u) => u.suggestions)
.find((s) => s.id === data.suggestionId);
validate(targetSuggestion);
validate(canDeleteComment({ user, author: targetSuggestion.author }));
db.plusSuggestions.del(data.suggestionId);
return null;
};
export interface PlusSuggestionsLoaderData {
suggestions?: plusSuggestions.FindResult;
suggestedForTiers: number[];
@@ -181,16 +220,29 @@ function SuggestedUser({
</summary>
<div className="stack sm mt-2">
{suggested.suggestions.map((s) => (
<fieldset className="plus__comment" key={s.author.id}>
<fieldset key={s.id} className="plus__comment">
<legend>{discordFullName(s.author)}</legend>
{s.text}
<span className="plus__comment-time">
{" "}
{" "}
<time>
{databaseTimestampToDate(s.createdAt).toLocaleString()}
</time>
</span>
<div className="stack vertical xs items-center">
<span className="plus__comment-time">
<time>
{databaseTimestampToDate(s.createdAt).toLocaleString()}
</time>
</span>
{canDeleteComment({ author: s.author, user }) ? (
<FormWithConfirm
fields={[["suggestionId", s.id]]}
dialogHeading={`Delete your comment to ${suggested.info.discordName}'s +${tier} suggestion?`}
>
<Button
className="plus__delete-button"
icon={<TrashIcon />}
variant="minimal-destructive"
aria-label="Delete comment"
/>
</FormWithConfirm>
) : null}
</div>
</fieldset>
))}
</div>

View File

@@ -71,6 +71,10 @@
flex-direction: column;
}
.stack.xs {
gap: var(--s-1-5);
}
.stack.sm {
gap: var(--s-2);
}
@@ -122,6 +126,14 @@
*
*/
.text-sm {
font-size: var(--fonts-sm);
}
.text-center {
text-align: center;
}
.w-full {
width: 100%;
}
@@ -129,3 +141,15 @@
.mt-2 {
margin-block-start: var(--s-2);
}
.hidden {
display: none;
}
.items-center {
align-items: center;
}
.justify-center {
justify-content: center;
}

View File

@@ -274,11 +274,11 @@ fieldset {
background-color: var(--bg-darker-transparent);
border-radius: var(--rounded);
font-size: var(--fonts-sm);
padding-block-end: var(--s-3);
padding-inline: var(--s-3);
}
legend {
padding: var(--s-1);
background-color: transparent;
border-radius: 2px;
border-radius: var(--rounded-sm);

View File

@@ -58,6 +58,10 @@
font-size: var(--fonts-xxs);
}
.plus__delete-button {
display: inline;
}
.plus__modal {
width: min(90%, 24rem);
}

View File

@@ -14,6 +14,7 @@ CREATE TABLE "User" (
---
CREATE TABLE "PlusSuggestion" (
"id" integer PRIMARY KEY,
"text" text NOT NULL,
"authorId" integer NOT NULL,
"suggestedId" integer NOT NULL,