mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-09-07 19:55:46 -05:00
Add deleting suggestion of themselves
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { formatDistance } from "date-fns";
|
||||
import type { MonthYear } from "~/modules/plus-server";
|
||||
import { nextNonCompletedVoting } from "~/modules/plus-server";
|
||||
import { atOrError } from "~/utils/arrays";
|
||||
import { databaseTimestampToDate } from "~/utils/dates";
|
||||
import { sql } from "../sql";
|
||||
@@ -191,3 +192,16 @@ const delStm = sql.prepare(`
|
||||
export function del(id: PlusSuggestion["id"]) {
|
||||
delStm.run({ id });
|
||||
}
|
||||
|
||||
const deleteAllStm = sql.prepare(`
|
||||
DELETE FROM "PlusSuggestion"
|
||||
WHERE
|
||||
"suggestedId" = $suggestedId
|
||||
AND tier = $tier
|
||||
AND month = $month
|
||||
AND year = $year
|
||||
`);
|
||||
|
||||
export function deleteAll(args: Pick<PlusSuggestion, "suggestedId" | "tier">) {
|
||||
deleteAllStm.run({ ...args, ...nextNonCompletedVoting(new Date()) });
|
||||
}
|
||||
|
||||
@@ -86,13 +86,24 @@ function fakeUser() {
|
||||
discordAvatar: null,
|
||||
discordDiscriminator: String(faker.random.numeric(4)),
|
||||
discordId: String(faker.random.numeric(17)),
|
||||
discordName: faker.random.word(),
|
||||
discordName: uniqueDiscordName(),
|
||||
twitch: null,
|
||||
twitter: null,
|
||||
youtubeId: null,
|
||||
};
|
||||
}
|
||||
|
||||
const usedNames = new Set<string>();
|
||||
function uniqueDiscordName() {
|
||||
let result = faker.random.word();
|
||||
while (usedNames.has(result)) {
|
||||
result = faker.random.word();
|
||||
}
|
||||
usedNames.add(result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
const idToPlusTier = (id: number) => {
|
||||
if (id < 30) return 1;
|
||||
if (id < 80) return 2;
|
||||
|
||||
@@ -130,6 +130,10 @@ function suggestionHasNoOtherComments({
|
||||
throw new Error(`Invalid suggestion id: ${suggestionId}`);
|
||||
}
|
||||
|
||||
export function canDeleteSuggestionOfThemselves() {
|
||||
return !isVotingActive();
|
||||
}
|
||||
|
||||
interface CanSuggestNewUserFEArgs {
|
||||
user?: Pick<UserWithPlusTier, "id" | "plusTier">;
|
||||
suggestions: plusSuggestions.FindVisibleForUser;
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
canAddCommentToSuggestionFE,
|
||||
canSuggestNewUserFE,
|
||||
canDeleteComment,
|
||||
canDeleteSuggestionOfThemselves,
|
||||
} from "~/permissions";
|
||||
import { makeTitle, parseRequestFormData, validate } from "~/utils/remix";
|
||||
import { discordFullName } from "~/utils/strings";
|
||||
@@ -30,6 +31,8 @@ import { actualNumber } from "~/utils/zod";
|
||||
import { userPage } from "~/utils/urls";
|
||||
import { RelativeTime } from "~/components/RelativeTime";
|
||||
import { databaseTimestampToDate } from "~/utils/dates";
|
||||
import { PLUS_TIERS } from "~/constants";
|
||||
import { assertUnreachable } from "~/utils/types";
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return {
|
||||
@@ -38,9 +41,22 @@ export const meta: MetaFunction = () => {
|
||||
};
|
||||
};
|
||||
|
||||
const suggestionActionSchema = z.object({
|
||||
suggestionId: z.preprocess(actualNumber, z.number()),
|
||||
});
|
||||
const suggestionActionSchema = z.union([
|
||||
z.object({
|
||||
_action: z.literal("DELETE_COMMENT"),
|
||||
suggestionId: z.preprocess(actualNumber, z.number()),
|
||||
}),
|
||||
z.object({
|
||||
_action: z.literal("DELETE_SUGGESTION_OF_THEMSELVES"),
|
||||
tier: z.preprocess(
|
||||
actualNumber,
|
||||
z
|
||||
.number()
|
||||
.min(Math.min(...PLUS_TIERS))
|
||||
.max(Math.max(...PLUS_TIERS))
|
||||
),
|
||||
}),
|
||||
]);
|
||||
|
||||
export const action: ActionFunction = async ({ request }) => {
|
||||
const data = await parseRequestFormData({
|
||||
@@ -49,30 +65,46 @@ export const action: ActionFunction = async ({ request }) => {
|
||||
});
|
||||
const user = await requireUser(request);
|
||||
|
||||
const suggestions = db.plusSuggestions.findVisibleForUser({
|
||||
...nextNonCompletedVoting(new Date()),
|
||||
plusTier: user.plusTier,
|
||||
});
|
||||
switch (data._action) {
|
||||
case "DELETE_COMMENT": {
|
||||
const suggestions = db.plusSuggestions.findVisibleForUser({
|
||||
...nextNonCompletedVoting(new Date()),
|
||||
plusTier: user.plusTier,
|
||||
});
|
||||
|
||||
const targetSuggestion = suggestions
|
||||
? Object.values(suggestions)
|
||||
?.flat()
|
||||
.flatMap((u) => u.suggestions)
|
||||
.find((s) => s.id === data.suggestionId)
|
||||
: undefined;
|
||||
const targetSuggestion = suggestions
|
||||
? Object.values(suggestions)
|
||||
?.flat()
|
||||
.flatMap((u) => u.suggestions)
|
||||
.find((s) => s.id === data.suggestionId)
|
||||
: undefined;
|
||||
|
||||
validate(suggestions);
|
||||
validate(targetSuggestion);
|
||||
validate(
|
||||
canDeleteComment({
|
||||
user,
|
||||
author: targetSuggestion.author,
|
||||
suggestionId: data.suggestionId,
|
||||
suggestions,
|
||||
})
|
||||
);
|
||||
validate(suggestions);
|
||||
validate(targetSuggestion);
|
||||
validate(
|
||||
canDeleteComment({
|
||||
user,
|
||||
author: targetSuggestion.author,
|
||||
suggestionId: data.suggestionId,
|
||||
suggestions,
|
||||
})
|
||||
);
|
||||
|
||||
db.plusSuggestions.del(data.suggestionId);
|
||||
db.plusSuggestions.del(data.suggestionId);
|
||||
|
||||
break;
|
||||
}
|
||||
case "DELETE_SUGGESTION_OF_THEMSELVES": {
|
||||
validate(canDeleteSuggestionOfThemselves());
|
||||
|
||||
db.plusSuggestions.deleteAll({ suggestedId: user.id, tier: data.tier });
|
||||
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
assertUnreachable(data);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
@@ -229,10 +261,30 @@ function SuggestedForInfo() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="plus__suggested-info-text">
|
||||
You are suggested for{" "}
|
||||
{data.suggestedForTiers.map((tier) => `+${tier}`).join(" and ")} this
|
||||
month.
|
||||
<div className="stack md">
|
||||
<div className="plus__suggested-info-text">
|
||||
You are suggested to{" "}
|
||||
{data.suggestedForTiers.map((tier) => `+${tier}`).join(" and ")} this
|
||||
month.
|
||||
</div>
|
||||
{canDeleteSuggestionOfThemselves() ? (
|
||||
<div className="stack vertical md">
|
||||
{data.suggestedForTiers.map((tier) => (
|
||||
<FormWithConfirm
|
||||
key={tier}
|
||||
fields={[
|
||||
["_action", "DELETE_SUGGESTION_OF_THEMSELVES"],
|
||||
["tier", tier],
|
||||
]}
|
||||
dialogHeading={`Delete your suggestion to +${tier}? You won't appear in next voting.`}
|
||||
>
|
||||
<Button key={tier} tiny variant="destructive" type="submit">
|
||||
Delete your +{tier} suggestion
|
||||
</Button>
|
||||
</FormWithConfirm>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -375,7 +427,10 @@ function CommentDeleteButton({
|
||||
}) {
|
||||
return (
|
||||
<FormWithConfirm
|
||||
fields={[["suggestionId", suggestionId]]}
|
||||
fields={[
|
||||
["suggestionId", suggestionId],
|
||||
["_action", "DELETE_COMMENT"],
|
||||
]}
|
||||
dialogHeading={
|
||||
isFirstSuggestion
|
||||
? `Delete your suggestion of ${suggestedDiscordName} to +${tier}?`
|
||||
|
||||
@@ -5,7 +5,7 @@ import { z } from "zod";
|
||||
import { Button, LinkButton } from "~/components/Button";
|
||||
import { Dialog } from "~/components/Dialog";
|
||||
import { Redirect } from "~/components/Redirect";
|
||||
import { PlUS_SUGGESTION_COMMENT_MAX_LENGTH } from "~/constants";
|
||||
import { PlUS_SUGGESTION_COMMENT_MAX_LENGTH, PLUS_TIERS } from "~/constants";
|
||||
import { nextNonCompletedVoting } from "~/modules/plus-server";
|
||||
import { db } from "~/db";
|
||||
import { requireUser, useUser } from "~/modules/auth";
|
||||
@@ -22,7 +22,13 @@ import { CommentTextarea } from "./new";
|
||||
|
||||
const commentActionSchema = z.object({
|
||||
text: z.string().min(1).max(PlUS_SUGGESTION_COMMENT_MAX_LENGTH),
|
||||
tier: z.preprocess(actualNumber, z.number().min(1).max(3)),
|
||||
tier: z.preprocess(
|
||||
actualNumber,
|
||||
z
|
||||
.number()
|
||||
.min(Math.min(...PLUS_TIERS))
|
||||
.max(Math.max(...PLUS_TIERS))
|
||||
),
|
||||
suggestedId: z.preprocess(actualNumber, z.number()),
|
||||
});
|
||||
|
||||
|
||||
@@ -34,7 +34,13 @@ import { atOrError } from "~/utils/arrays";
|
||||
import { requireUser, useUser } from "~/modules/auth";
|
||||
|
||||
const commentActionSchema = z.object({
|
||||
tier: z.preprocess(actualNumber, z.number().min(1).max(3)),
|
||||
tier: z.preprocess(
|
||||
actualNumber,
|
||||
z
|
||||
.number()
|
||||
.min(Math.min(...PLUS_TIERS))
|
||||
.max(Math.max(...PLUS_TIERS))
|
||||
),
|
||||
text: z.string().min(1).max(PlUS_SUGGESTION_FIRST_COMMENT_MAX_LENGTH),
|
||||
"user[value]": z.preprocess(actualNumber, z.number().positive()),
|
||||
});
|
||||
|
||||
@@ -3,8 +3,7 @@ import { defineConfig } from "cypress";
|
||||
export default defineConfig({
|
||||
fixturesFolder: false,
|
||||
e2e: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
setupNodeEvents(on, config) {},
|
||||
// setupNodeEvents(on, config) {},
|
||||
baseUrl: "http://localhost:4455",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -77,5 +77,6 @@ describe("Plus voting results page", () => {
|
||||
cy.contains("Sendou");
|
||||
});
|
||||
|
||||
// xxx: describe Plus Voting
|
||||
// xxx: figure out a good way to do plus server tests with serverside isVotingActive or delete existing tests
|
||||
// -> if way is found then should also add tests for voting
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user