Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions frontend/src/components/ScratchItem.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -147,3 +147,15 @@
text-overflow: ellipsis;
}
}

.red-on-shift {
--warning-color: #ce3a3a;
color: var(--warning-color) !important;
border-color: var(--warning-color) !important;
}

.red-on-shift:hover {
--warning-hover-color: #f58d8d;
color: var(--warning-hover-color) !important;
border-color: var(--warning-hover-color) !important;
}
128 changes: 98 additions & 30 deletions frontend/src/components/ScratchItem.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
"use client";

import type { ReactNode } 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";
Expand All @@ -15,6 +15,7 @@ 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";
Expand Down Expand Up @@ -144,53 +145,120 @@ function ScratchItemRow({
showOwner = true,
showPlatform = true,
showPresetOrCompiler = true,
showDeleteButton = false,
}: {
scratch: api.TerseScratch;
children?: ReactNode;
showOwner?: boolean;
showPlatform?: boolean;
showPresetOrCompiler?: boolean;
showDeleteButton?: boolean;
}) {
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.",
)
) {
return;
}

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) => {
setWarnDelete(evt.shiftKey);
});

document.body.addEventListener("keyup", (evt: KeyboardEvent) => {
setWarnDelete(evt.shiftKey);
});

return (
<li className={styles.item}>
<div className={styles.scratch}>
<div className={styles.header}>
<ScratchItemTitle
scratch={scratch}
showPlatform={showPlatform}
/>
<span className={styles.improvementSlot}>
<Improvement improvement={scratch.best_fork} />
</span>
</div>
<div className={styles.metadata}>
<ScratchMetadata
scratch={scratch}
showPresetOrCompiler={showPresetOrCompiler}
/>
{(children || showOwner) && (
<div className={styles.metadataAside}>
{children && (
<div className={styles.actions}>{children}</div>
<>
{" "}
{showElement && (
<li className={styles.item}>
<div className={styles.scratch}>
<div className={styles.header}>
<ScratchItemTitle
scratch={scratch}
showPlatform={showPlatform}
/>
<span className={styles.improvementSlot}>
<Improvement improvement={scratch.best_fork} />
</span>
</div>
<div className={styles.metadata}>
<ScratchMetadata
scratch={scratch}
showPresetOrCompiler={showPresetOrCompiler}
/>
{(children || showOwner || showDeleteButton) && (
<div className={styles.metadataAside}>
{children && (
<div className={styles.actions}>
{children}
</div>
)}
{showOwner && (
<ScratchOwner scratch={scratch} />
)}
{showDeleteButton && (
<Button
onClick={() =>
deleteScratch(scratch)
}
className={
warnDelete
? styles["red-on-shift"]
: ""
}
>
<TrashIcon />
</Button>
)}
</div>
)}
{showOwner && <ScratchOwner scratch={scratch} />}
</div>
)}
</div>
</div>
</li>
</div>
</li>
)}{" "}
</>
);
}

export function ScratchItem({
scratch,
children,
}: { scratch: api.TerseScratch; children?: ReactNode }) {
}: {
scratch: api.TerseScratch;
children?: ReactNode;
showDeleteButton?: boolean;
}) {
return <ScratchItemRow scratch={scratch}>{children}</ScratchItemRow>;
}

export function ScratchItemNoOwner({ scratch }: { scratch: api.TerseScratch }) {
return <ScratchItemRow scratch={scratch} showOwner={false} />;
export function ScratchItemNoOwner({
scratch,
showDeleteButton,
}: { scratch: api.TerseScratch; showDeleteButton?: boolean }) {
return (
<ScratchItemRow
scratch={scratch}
showOwner={false}
showDeleteButton={showDeleteButton}
/>
);
}

export function ScratchItemPlatformList({
Expand Down
8 changes: 7 additions & 1 deletion frontend/src/components/ScratchList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export interface Props {
emptyButtonLabel?: ReactNode;
isSortable?: boolean;
isPublic?: boolean;
showDeleteButtons?: boolean;
}

export default function ScratchList({
Expand All @@ -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 } =
Expand Down Expand Up @@ -62,7 +64,11 @@ export default function ScratchList({
)}
>
{results.map((scratch) => (
<Item key={scratchUrl(scratch)} scratch={scratch} />
<Item
key={scratchUrl(scratch)}
scratch={scratch}
showDeleteButton={showDeleteButtons}
/>
))}
{results.length === 0 && emptyButtonLabel && (
<li className={styles.button}>
Expand Down
6 changes: 5 additions & 1 deletion frontend/src/components/user/tabs/ScratchesTab.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
import ScratchList from "@/components/ScratchList";
import { ScratchItemNoOwner } from "@/components/ScratchItem";

import type { User } from "@/lib/api";
import { type User, useThisUserIsAdmin, useUserIsYou } from "@/lib/api";
import { userUrl } from "@/lib/api/urls";

export default function ScratchesTab({ user }: { user: User }) {
const userIsYou = useUserIsYou();
const isAdmin = useThisUserIsAdmin();

return (
<section className="mt-4">
<ScratchList
url={`${userUrl(user)}/scratches?page_size=20`}
item={ScratchItemNoOwner}
isSortable={true}
showDeleteButtons={userIsYou(user) || isAdmin}
/>
</section>
);
Expand Down