From 082c36e3af51897cccec6ad6c56629c1c411f5c1 Mon Sep 17 00:00:00 2001 From: Mathieu Bourgon Date: Mon, 18 May 2026 19:55:09 -0400 Subject: [PATCH 1/7] Add delete button to scratches --- .../src/components/ScratchItem.module.scss | 8 ++++ frontend/src/components/ScratchItem.tsx | 41 +++++++++++++++---- frontend/src/components/ScratchList.tsx | 4 +- .../src/components/user/tabs/ScratchesTab.tsx | 4 +- 4 files changed, 47 insertions(+), 10 deletions(-) diff --git a/frontend/src/components/ScratchItem.module.scss b/frontend/src/components/ScratchItem.module.scss index dd7f89cd0..4b689888d 100644 --- a/frontend/src/components/ScratchItem.module.scss +++ b/frontend/src/components/ScratchItem.module.scss @@ -147,3 +147,11 @@ text-overflow: ellipsis; } } + +.red-on-shift { + color: rgb(206, 58, 58); +} + +.red-on-shift:hover { + color: rgb(245, 141, 141) !important; +} \ No newline at end of file diff --git a/frontend/src/components/ScratchItem.tsx b/frontend/src/components/ScratchItem.tsx index bf771139f..457974485 100644 --- a/frontend/src/components/ScratchItem.tsx +++ b/frontend/src/components/ScratchItem.tsx @@ -1,6 +1,6 @@ "use client"; -import type { ReactNode } from "react"; +import { type ReactNode, JSX, useState } from "react"; import Image from "next/image"; import Link from "@/components/Link"; @@ -19,6 +19,8 @@ import PlatformLink from "./PlatformLink"; import { calculateScorePercent, percentToString } from "./ScoreBadge"; import styles from "./ScratchItem.module.scss"; import UserLink from "./user/UserLink"; +import Button from "./Button"; +import { TrashIcon } from "@primer/octicons-react"; type MatchPercentSource = api.TerseScratch | api.BestFork; @@ -83,7 +85,7 @@ function ScratchItemTitle({ href={scratchUrl(scratch)} className={clsx(styles.link, styles.name)} > - {scratch.name} + {scratch.name + ` [${scratch.slug}]`} ); @@ -144,14 +146,36 @@ function ScratchItemRow({ showOwner = true, showPlatform = true, showPresetOrCompiler = true, + showDeleteButton = false, }: { scratch: api.TerseScratch; children?: ReactNode; showOwner?: boolean; showPlatform?: boolean; showPresetOrCompiler?: boolean; + showDeleteButton?: boolean; }) { - return ( + + let [warnDelete, setWarnDelete] = useState(false); + let [showElement, setShowElement] = useState(true); + const deleteScratch = async (scratch: api.TerseScratch) => { + if (!warnDelete && !confirm("Are you sure you want to delete this scratch? This action cannot be undone.")) { + return; + } + + await api.delete_(scratchUrl(scratch), {}); + setShowElement(false); + }; + + document.body.addEventListener("keydown", (evt: KeyboardEvent) => { + setWarnDelete(evt.shiftKey); + }); + + document.body.addEventListener("keyup", (evt: KeyboardEvent) => { + setWarnDelete(evt.shiftKey); + }); + + return <> { showElement &&
  • @@ -168,29 +192,30 @@ function ScratchItemRow({ scratch={scratch} showPresetOrCompiler={showPresetOrCompiler} /> - {(children || showOwner) && ( + {(children || showOwner || showDeleteButton) && (
    {children && (
    {children}
    )} {showOwner && } + {showDeleteButton && }
    )}
  • - ); + } ; } export function ScratchItem({ scratch, children, -}: { scratch: api.TerseScratch; children?: ReactNode }) { +}: { scratch: api.TerseScratch; children?: ReactNode; showDeleteButton?: boolean }) { return {children}; } -export function ScratchItemNoOwner({ scratch }: { scratch: api.TerseScratch }) { - return ; +export function ScratchItemNoOwner({ scratch, showDeleteButton }: { scratch: api.TerseScratch; showDeleteButton?: boolean }) { + return ; } export function ScratchItemPlatformList({ diff --git a/frontend/src/components/ScratchList.tsx b/frontend/src/components/ScratchList.tsx index e49aee2a0..fadf11a9c 100644 --- a/frontend/src/components/ScratchList.tsx +++ b/frontend/src/components/ScratchList.tsx @@ -23,6 +23,7 @@ export interface Props { emptyButtonLabel?: ReactNode; isSortable?: boolean; isPublic?: boolean; + showDeleteButtons?: boolean; } export default function ScratchList({ @@ -33,6 +34,7 @@ export default function ScratchList({ emptyButtonLabel, isSortable, isPublic, + showDeleteButtons }: Props) { const [sortMode, setSortMode] = useState(SortMode.NEWEST_FIRST); const { results, isLoading, hasNext, loadNext } = @@ -62,7 +64,7 @@ export default function ScratchList({ )} > {results.map((scratch) => ( - + ))} {results.length === 0 && emptyButtonLabel && (
  • diff --git a/frontend/src/components/user/tabs/ScratchesTab.tsx b/frontend/src/components/user/tabs/ScratchesTab.tsx index 012e2543c..38eff95e7 100644 --- a/frontend/src/components/user/tabs/ScratchesTab.tsx +++ b/frontend/src/components/user/tabs/ScratchesTab.tsx @@ -1,16 +1,18 @@ import ScratchList from "@/components/ScratchList"; import { ScratchItemNoOwner } from "@/components/ScratchItem"; -import type { User } from "@/lib/api"; +import { type User, useUserIsYou } from "@/lib/api"; import { userUrl } from "@/lib/api/urls"; export default function ScratchesTab({ user }: { user: User }) { + const userIsYou = useUserIsYou(); return (
    ); From b4dc33ce7c9bdf5d9605a45668e6814e5e55cee9 Mon Sep 17 00:00:00 2001 From: Mathieu Bourgon Date: Mon, 18 May 2026 21:08:04 -0400 Subject: [PATCH 2/7] Updated styling for trash can colors --- frontend/src/components/ScratchItem.module.scss | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/ScratchItem.module.scss b/frontend/src/components/ScratchItem.module.scss index 4b689888d..662cab9c7 100644 --- a/frontend/src/components/ScratchItem.module.scss +++ b/frontend/src/components/ScratchItem.module.scss @@ -149,9 +149,13 @@ } .red-on-shift { - color: rgb(206, 58, 58); + --warning-color: rgb(206, 58, 58); + color: var(--warning-color) !important; + border-color: var(--warning-color) !important; } .red-on-shift:hover { - color: rgb(245, 141, 141) !important; + --warning-hover-color: rgb(245, 141, 141); + color: var(--warning-hover-color) !important; + border-color: var(--warning-hover-color) !important; } \ No newline at end of file From 33f15a9f9174de72f633acb976973eef9356e054 Mon Sep 17 00:00:00 2001 From: Mathieu Bourgon Date: Mon, 18 May 2026 21:14:33 -0400 Subject: [PATCH 3/7] Added admin override to delete button --- frontend/src/components/user/tabs/ScratchesTab.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/user/tabs/ScratchesTab.tsx b/frontend/src/components/user/tabs/ScratchesTab.tsx index 38eff95e7..e90248f86 100644 --- a/frontend/src/components/user/tabs/ScratchesTab.tsx +++ b/frontend/src/components/user/tabs/ScratchesTab.tsx @@ -1,18 +1,20 @@ import ScratchList from "@/components/ScratchList"; import { ScratchItemNoOwner } from "@/components/ScratchItem"; -import { type User, useUserIsYou } from "@/lib/api"; +import { type User, useUserIsYou, useThisUserIsAdmin } from "@/lib/api"; import { userUrl } from "@/lib/api/urls"; export default function ScratchesTab({ user }: { user: User }) { const userIsYou = useUserIsYou(); + const isAdmin = useThisUserIsAdmin(); + return (
    ); From 13712a0c40a284cd0a5c569417dcfe3dbd0b3678 Mon Sep 17 00:00:00 2001 From: Mathieu Bourgon Date: Mon, 18 May 2026 21:25:19 -0400 Subject: [PATCH 4/7] Added error checking to deleting scratch --- frontend/src/components/ScratchItem.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/ScratchItem.tsx b/frontend/src/components/ScratchItem.tsx index 457974485..05b762e06 100644 --- a/frontend/src/components/ScratchItem.tsx +++ b/frontend/src/components/ScratchItem.tsx @@ -85,7 +85,7 @@ function ScratchItemTitle({ href={scratchUrl(scratch)} className={clsx(styles.link, styles.name)} > - {scratch.name + ` [${scratch.slug}]`} + {scratch.name} ); @@ -163,8 +163,13 @@ function ScratchItemRow({ return; } - await api.delete_(scratchUrl(scratch), {}); - setShowElement(false); + try { + await api.delete_(scratchUrl(scratch), {}); + setShowElement(false); // Hide deleted element to avoid performing a page refresh, and allow deleting more scratches + } catch (error) { + alert("An error occurred trying to deleting this scratch."); + throw error; + } }; document.body.addEventListener("keydown", (evt: KeyboardEvent) => { From 4c78472fab65135479ec5bf7f943ed130e41444b Mon Sep 17 00:00:00 2001 From: Mathieu Bourgon Date: Mon, 18 May 2026 21:37:01 -0400 Subject: [PATCH 5/7] Updated import order --- frontend/src/components/ScratchItem.tsx | 7 +++---- frontend/src/components/user/tabs/ScratchesTab.tsx | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/ScratchItem.tsx b/frontend/src/components/ScratchItem.tsx index 05b762e06..33e365f55 100644 --- a/frontend/src/components/ScratchItem.tsx +++ b/frontend/src/components/ScratchItem.tsx @@ -1,11 +1,11 @@ "use client"; -import { type ReactNode, JSX, useState } from "react"; +import { type ReactNode, useState } from "react"; import Image from "next/image"; import Link from "@/components/Link"; -import { RepoForkedIcon } from "@primer/octicons-react"; +import { RepoForkedIcon, TrashIcon } from "@primer/octicons-react"; import clsx from "clsx"; import TimeAgo from "@/components/TimeAgo"; @@ -15,12 +15,11 @@ import { presetUrl, scratchUrl, userAvatarUrl } from "@/lib/api/urls"; import getTranslation from "@/lib/i18n/translate"; import AnonymousFrogAvatar from "./Frog/AnonymousFrog"; +import Button from "./Button"; import PlatformLink from "./PlatformLink"; import { calculateScorePercent, percentToString } from "./ScoreBadge"; import styles from "./ScratchItem.module.scss"; import UserLink from "./user/UserLink"; -import Button from "./Button"; -import { TrashIcon } from "@primer/octicons-react"; type MatchPercentSource = api.TerseScratch | api.BestFork; diff --git a/frontend/src/components/user/tabs/ScratchesTab.tsx b/frontend/src/components/user/tabs/ScratchesTab.tsx index e90248f86..7f57d4bdf 100644 --- a/frontend/src/components/user/tabs/ScratchesTab.tsx +++ b/frontend/src/components/user/tabs/ScratchesTab.tsx @@ -1,7 +1,7 @@ import ScratchList from "@/components/ScratchList"; import { ScratchItemNoOwner } from "@/components/ScratchItem"; -import { type User, useUserIsYou, useThisUserIsAdmin } from "@/lib/api"; +import { type User, useThisUserIsAdmin, useUserIsYou } from "@/lib/api"; import { userUrl } from "@/lib/api/urls"; export default function ScratchesTab({ user }: { user: User }) { From e94a6bcc0a82d5f3d093450c2dbeabf87010bc38 Mon Sep 17 00:00:00 2001 From: Mathieu Bourgon Date: Mon, 18 May 2026 21:38:24 -0400 Subject: [PATCH 6/7] Changing warning colors from rgb() to hex --- frontend/src/components/ScratchItem.module.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/ScratchItem.module.scss b/frontend/src/components/ScratchItem.module.scss index 662cab9c7..1a35d62c9 100644 --- a/frontend/src/components/ScratchItem.module.scss +++ b/frontend/src/components/ScratchItem.module.scss @@ -149,13 +149,13 @@ } .red-on-shift { - --warning-color: rgb(206, 58, 58); + --warning-color: #ce3a3a; color: var(--warning-color) !important; border-color: var(--warning-color) !important; } .red-on-shift:hover { - --warning-hover-color: rgb(245, 141, 141); + --warning-hover-color: #f58d8d; color: var(--warning-hover-color) !important; border-color: var(--warning-hover-color) !important; } \ No newline at end of file From b2876bbc9ccd17ecb7ba9572a87c515e26d866bf Mon Sep 17 00:00:00 2001 From: Mathieu Bourgon Date: Tue, 19 May 2026 09:21:52 -0400 Subject: [PATCH 7/7] Linter fixes --- frontend/src/components/ScratchItem.tsx | 109 ++++++++++++++++-------- frontend/src/components/ScratchList.tsx | 8 +- 2 files changed, 80 insertions(+), 37 deletions(-) diff --git a/frontend/src/components/ScratchItem.tsx b/frontend/src/components/ScratchItem.tsx index 33e365f55..29026d344 100644 --- a/frontend/src/components/ScratchItem.tsx +++ b/frontend/src/components/ScratchItem.tsx @@ -154,11 +154,15 @@ function ScratchItemRow({ showPresetOrCompiler?: boolean; showDeleteButton?: boolean; }) { - - let [warnDelete, setWarnDelete] = useState(false); - let [showElement, setShowElement] = useState(true); + const [warnDelete, setWarnDelete] = useState(false); + const [showElement, setShowElement] = useState(true); const deleteScratch = async (scratch: api.TerseScratch) => { - if (!warnDelete && !confirm("Are you sure you want to delete this scratch? This action cannot be undone.")) { + if ( + !warnDelete && + !confirm( + "Are you sure you want to delete this scratch? This action cannot be undone.", + ) + ) { return; } @@ -179,47 +183,82 @@ function ScratchItemRow({ setWarnDelete(evt.shiftKey); }); - return <> { showElement && -
  • -
    -
    - - - - -
    -
    - - {(children || showOwner || showDeleteButton) && ( -
    - {children && ( -
    {children}
    + return ( + <> + {" "} + {showElement && ( +
  • +
    +
    + + + + +
    +
    + + {(children || showOwner || showDeleteButton) && ( +
    + {children && ( +
    + {children} +
    + )} + {showOwner && ( + + )} + {showDeleteButton && ( + + )} +
    )} - {showOwner && } - {showDeleteButton && }
    - )} -
    - -
  • - } ; + + + )}{" "} + + ); } export function ScratchItem({ scratch, children, -}: { scratch: api.TerseScratch; children?: ReactNode; showDeleteButton?: boolean }) { +}: { + scratch: api.TerseScratch; + children?: ReactNode; + showDeleteButton?: boolean; +}) { return {children}; } -export function ScratchItemNoOwner({ scratch, showDeleteButton }: { scratch: api.TerseScratch; showDeleteButton?: boolean }) { - return ; +export function ScratchItemNoOwner({ + scratch, + showDeleteButton, +}: { scratch: api.TerseScratch; showDeleteButton?: boolean }) { + return ( + + ); } export function ScratchItemPlatformList({ diff --git a/frontend/src/components/ScratchList.tsx b/frontend/src/components/ScratchList.tsx index fadf11a9c..6d95202c2 100644 --- a/frontend/src/components/ScratchList.tsx +++ b/frontend/src/components/ScratchList.tsx @@ -34,7 +34,7 @@ export default function ScratchList({ emptyButtonLabel, isSortable, isPublic, - showDeleteButtons + showDeleteButtons, }: Props) { const [sortMode, setSortMode] = useState(SortMode.NEWEST_FIRST); const { results, isLoading, hasNext, loadNext } = @@ -64,7 +64,11 @@ export default function ScratchList({ )} > {results.map((scratch) => ( - + ))} {results.length === 0 && emptyButtonLabel && (