diff --git a/app/db/models/plusSuggestions.server.ts b/app/db/models/plusSuggestions.server.ts index e5f429f8e..540c3c9ff 100644 --- a/app/db/models/plusSuggestions.server.ts +++ b/app/db/models/plusSuggestions.server.ts @@ -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) { + deleteAllStm.run({ ...args, ...nextNonCompletedVoting(new Date()) }); +} diff --git a/app/db/seed.ts b/app/db/seed.ts index e5d513435..9de50c5c4 100644 --- a/app/db/seed.ts +++ b/app/db/seed.ts @@ -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(); +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; diff --git a/app/permissions.ts b/app/permissions.ts index c87cc63d5..213bc585c 100644 --- a/app/permissions.ts +++ b/app/permissions.ts @@ -130,6 +130,10 @@ function suggestionHasNoOtherComments({ throw new Error(`Invalid suggestion id: ${suggestionId}`); } +export function canDeleteSuggestionOfThemselves() { + return !isVotingActive(); +} + interface CanSuggestNewUserFEArgs { user?: Pick; suggestions: plusSuggestions.FindVisibleForUser; diff --git a/app/routes/plus/suggestions.tsx b/app/routes/plus/suggestions.tsx index c7be03eee..00bd658f6 100644 --- a/app/routes/plus/suggestions.tsx +++ b/app/routes/plus/suggestions.tsx @@ -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 ( -
- You are suggested for{" "} - {data.suggestedForTiers.map((tier) => `+${tier}`).join(" and ")} this - month. +
+
+ You are suggested to{" "} + {data.suggestedForTiers.map((tier) => `+${tier}`).join(" and ")} this + month. +
+ {canDeleteSuggestionOfThemselves() ? ( +
+ {data.suggestedForTiers.map((tier) => ( + + + + ))} +
+ ) : null}
); } @@ -375,7 +427,10 @@ function CommentDeleteButton({ }) { return ( { 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 });