From 4e56ff149f8efdbb11202f110bd732ae2fc33155 Mon Sep 17 00:00:00 2001
From: Mark Street <22226349+mkst@users.noreply.github.com>
Date: Wed, 12 Feb 2025 10:35:59 +0000
Subject: [PATCH] Split ScratchItem* from ScratchList (#1458)
---
.../app/(navfooter)/new/NewScratchForm.tsx | 2 +-
frontend/src/app/(navfooter)/page.tsx | 3 +-
.../app/(navfooter)/platform/[id]/page.tsx | 3 +-
.../src/app/(navfooter)/preset/[id]/page.tsx | 3 +-
frontend/src/components/Nav/Search.tsx | 2 +-
.../src/components/ScratchItem.module.scss | 96 +++++++
frontend/src/components/ScratchItem.tsx | 257 ++++++++++++++++++
.../src/components/ScratchList.module.scss | 108 --------
frontend/src/components/ScratchList.tsx | 256 +----------------
.../src/components/user/tabs/ScratchesTab.tsx | 4 +-
10 files changed, 369 insertions(+), 365 deletions(-)
create mode 100644 frontend/src/components/ScratchItem.module.scss
create mode 100644 frontend/src/components/ScratchItem.tsx
diff --git a/frontend/src/app/(navfooter)/new/NewScratchForm.tsx b/frontend/src/app/(navfooter)/new/NewScratchForm.tsx
index d6ef154..627130b 100644
--- a/frontend/src/app/(navfooter)/new/NewScratchForm.tsx
+++ b/frontend/src/app/(navfooter)/new/NewScratchForm.tsx
@@ -19,7 +19,7 @@ import { cpp } from "@/lib/codemirror/cpp";
import getTranslation from "@/lib/i18n/translate";
import { get } from "@/lib/api/request";
import type { TerseScratch } from "@/lib/api/types";
-import { SingleLineScratchItem } from "@/components/ScratchList";
+import { SingleLineScratchItem } from "@/components/ScratchItem";
import { useDebounce } from "use-debounce";
interface FormLabelProps {
diff --git a/frontend/src/app/(navfooter)/page.tsx b/frontend/src/app/(navfooter)/page.tsx
index fab4e4e..dceb87e 100644
--- a/frontend/src/app/(navfooter)/page.tsx
+++ b/frontend/src/app/(navfooter)/page.tsx
@@ -1,6 +1,7 @@
import type { Metadata } from "next";
-import ScratchList, { SingleLineScratchItem } from "@/components/ScratchList";
+import ScratchList from "@/components/ScratchList";
+import { SingleLineScratchItem } from "@/components/ScratchItem";
import YourScratchList from "@/components/YourScratchList";
import WelcomeInfo from "./WelcomeInfo";
diff --git a/frontend/src/app/(navfooter)/platform/[id]/page.tsx b/frontend/src/app/(navfooter)/platform/[id]/page.tsx
index 7f89450..0a5ef69 100644
--- a/frontend/src/app/(navfooter)/platform/[id]/page.tsx
+++ b/frontend/src/app/(navfooter)/platform/[id]/page.tsx
@@ -3,7 +3,8 @@ import type { Metadata } from "next";
import { notFound } from "next/navigation";
import { PlatformIcon } from "@/components/PlatformSelect/PlatformIcon";
-import ScratchList, { ScratchItemPlatformList } from "@/components/ScratchList";
+import ScratchList from "@/components/ScratchList";
+import { ScratchItemPlatformList } from "@/components/ScratchItem";
import { get } from "@/lib/api/request";
import type { PlatformMetadata } from "@/lib/api/types";
diff --git a/frontend/src/app/(navfooter)/preset/[id]/page.tsx b/frontend/src/app/(navfooter)/preset/[id]/page.tsx
index 10f968a..557ddd4 100644
--- a/frontend/src/app/(navfooter)/preset/[id]/page.tsx
+++ b/frontend/src/app/(navfooter)/preset/[id]/page.tsx
@@ -3,7 +3,8 @@ import type { Metadata } from "next";
import { notFound } from "next/navigation";
import { PlatformIcon } from "@/components/PlatformSelect/PlatformIcon";
-import ScratchList, { ScratchItemPresetList } from "@/components/ScratchList";
+import ScratchList from "@/components/ScratchList";
+import { ScratchItemPresetList } from "@/components/ScratchItem";
import { get } from "@/lib/api/request";
import type { Preset } from "@/lib/api/types";
import getTranslation from "@/lib/i18n/translate";
diff --git a/frontend/src/components/Nav/Search.tsx b/frontend/src/components/Nav/Search.tsx
index f35398f..f04281c 100644
--- a/frontend/src/components/Nav/Search.tsx
+++ b/frontend/src/components/Nav/Search.tsx
@@ -14,7 +14,7 @@ import LoadingSpinner from "../loading.svg";
import PlatformLink from "../PlatformLink";
import verticalMenuStyles from "../VerticalMenu.module.scss"; // eslint-disable-line css-modules/no-unused-class
-import { getMatchPercentString, ScratchOwnerAvatar } from "../ScratchList";
+import { getMatchPercentString, ScratchOwnerAvatar } from "../ScratchItem";
import styles from "./Search.module.scss";
diff --git a/frontend/src/components/ScratchItem.module.scss b/frontend/src/components/ScratchItem.module.scss
new file mode 100644
index 0000000..80d5704
--- /dev/null
+++ b/frontend/src/components/ScratchItem.module.scss
@@ -0,0 +1,96 @@
+.item {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ gap: 0.5em;
+
+ overflow: hidden;
+
+ padding: 1em;
+
+ border: 1px solid inherit;
+ border-radius: inherit;
+}
+
+.link {
+ font-weight: 600;
+
+ &:hover {
+ color: var(--link);
+ }
+}
+
+.scratch {
+ line-height: 1.5;
+
+ .header {
+ display: grid;
+ grid-template-columns: auto 1fr auto;
+ align-items: center;
+ gap: 0.5em;
+
+ .name {
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ max-width: 100%;
+ overflow: hidden;
+ }
+
+ /* If two children, align them to the left and right */
+ &:has(> :last-child:nth-child(2)) {
+ grid-template-columns: 1fr auto;
+ }
+ }
+
+ .icon {
+ width: 1.2em;
+ height: 1.2em;
+ }
+
+ .owner {
+ color: var(--g1200);
+ }
+
+ .metadata {
+ display: flex;
+ align-items: flex-end;
+
+ color: var(--g900);
+
+ > span {
+ flex-grow: 1;
+ }
+
+ .actions {
+ padding-top: 0.25em;
+ }
+ }
+}
+
+.singleLine {
+ white-space: nowrap;
+ overflow: hidden;
+
+ display: flex;
+ align-items: center;
+ gap: 0.4em;
+
+ padding: 0.4em 0;
+
+ .icon {
+ flex-shrink: 0;
+ width: 1.2em;
+ height: 1.2em;
+ }
+
+ .name {
+ flex-grow: 1;
+
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ .metadata {
+ color: var(--g1200);
+ }
+}
diff --git a/frontend/src/components/ScratchItem.tsx b/frontend/src/components/ScratchItem.tsx
new file mode 100644
index 0000000..61808ad
--- /dev/null
+++ b/frontend/src/components/ScratchItem.tsx
@@ -0,0 +1,257 @@
+"use client";
+
+import type { ReactNode } from "react";
+
+import Image from "next/image";
+import Link from "next/link";
+
+import classNames from "classnames";
+
+import TimeAgo from "@/components/TimeAgo";
+import * as api from "@/lib/api";
+import { presetUrl, scratchUrl, userAvatarUrl } from "@/lib/api/urls";
+
+import getTranslation from "@/lib/i18n/translate";
+
+import AnonymousFrogAvatar from "./user/AnonymousFrog";
+import PlatformLink from "./PlatformLink";
+import { calculateScorePercent, percentToString } from "./ScoreBadge";
+import styles from "./ScratchItem.module.scss";
+import UserLink from "./user/UserLink";
+
+export function getMatchPercentString(scratch: api.TerseScratch) {
+ if (scratch.score === -1) {
+ return "0%";
+ }
+ if (scratch.match_override) {
+ return "100%";
+ }
+ const matchPercent = calculateScorePercent(
+ scratch.score,
+ scratch.max_score,
+ );
+ const matchPercentString = percentToString(matchPercent);
+
+ return matchPercentString;
+}
+
+export function ScratchItem({
+ scratch,
+ children,
+}: { scratch: api.TerseScratch; children?: ReactNode }) {
+ const compilersTranslation = getTranslation("compilers");
+ const compilerName = compilersTranslation.t(scratch.compiler);
+ const matchPercentString = getMatchPercentString(scratch);
+ const preset = api.usePreset(scratch.preset);
+ const presetName = preset?.name;
+
+ const presetOrCompiler = presetName ? (
+
+ {presetName}
+
+ ) : (
+ {compilerName}
+ );
+
+ return (
+
+
+
+
+
+ {scratch.name}
+
+
+ {scratch.owner ? (
+
+ ) : (
+
No Owner
+ )}
+
+
+
+
+ {presetOrCompiler} • {matchPercentString} matched •{" "}
+
+
+
{children}
+
+
+
+ );
+}
+
+export function ScratchItemNoOwner({ scratch }: { scratch: api.TerseScratch }) {
+ const compilersTranslation = getTranslation("compilers");
+ const compilerName = compilersTranslation.t(scratch.compiler);
+ const matchPercentString = getMatchPercentString(scratch);
+ const preset = api.usePreset(scratch.preset);
+ const presetName = preset?.name;
+
+ const presetOrCompiler = presetName ? (
+
+ {presetName}
+
+ ) : (
+ {compilerName}
+ );
+
+ return (
+
+
+
+
+
+ {scratch.name}
+
+
{/* empty div for alignment */}
+
+
+
+ {presetOrCompiler} • {matchPercentString} matched •{" "}
+
+
+
+
+
+ );
+}
+
+export function ScratchItemPlatformList({
+ scratch,
+}: { scratch: api.TerseScratch }) {
+ const compilersTranslation = getTranslation("compilers");
+ const compilerName = compilersTranslation.t(scratch.compiler);
+ const matchPercentString = getMatchPercentString(scratch);
+ const preset = api.usePreset(scratch.preset);
+ const presetName = preset?.name;
+
+ const presetOrCompiler = presetName ? (
+
+ {presetName}
+
+ ) : (
+ {compilerName}
+ );
+
+ return (
+
+
+
+
+ {scratch.name}
+
+
+ {scratch.owner ? (
+
+ ) : (
+
No Owner
+ )}
+
+
+
+
+ {presetOrCompiler} • {matchPercentString} matched •{" "}
+
+
+
+
+
+ );
+}
+
+export function ScratchItemPresetList({
+ scratch,
+}: { scratch: api.TerseScratch }) {
+ const matchPercentString = getMatchPercentString(scratch);
+
+ return (
+
+
+
+
+ {scratch.name}
+
+
+
+ {matchPercentString} matched •{" "}
+
+
+
+
+ {scratch.owner ? (
+
+ ) : (
+
No Owner
+ )}
+
+
+
+
+ );
+}
+
+export function ScratchOwnerAvatar({ scratch }: { scratch: api.TerseScratch }) {
+ return (
+ scratch.owner &&
+ (!api.isAnonUser(scratch.owner) ? (
+ userAvatarUrl(scratch.owner) && (
+
+ )
+ ) : (
+
+ ))
+ );
+}
+
+export function SingleLineScratchItem({
+ scratch,
+ showOwner = false,
+}: { scratch: api.TerseScratch; showOwner?: boolean }) {
+ const matchPercentString = getMatchPercentString(scratch);
+
+ return (
+
+
+
+ {scratch.name}
+
+ {matchPercentString}
+ {showOwner && }
+
+ );
+}
diff --git a/frontend/src/components/ScratchList.module.scss b/frontend/src/components/ScratchList.module.scss
index 44878cb..a501f63 100644
--- a/frontend/src/components/ScratchList.module.scss
+++ b/frontend/src/components/ScratchList.module.scss
@@ -1,14 +1,3 @@
-.loading {
- display: flex;
- justify-content: center;
- align-items: center;
- gap: 0.5em;
-
- padding: 1em;
-
- opacity: 0.5;
-}
-
.list {
list-style: none;
@@ -17,20 +6,6 @@
gap: 0.5em;
}
-.item {
- display: flex;
- flex-direction: column;
- justify-content: center;
- gap: 0.5em;
-
- overflow: hidden;
-
- padding: 1em;
-
- border: 1px solid inherit;
- border-radius: inherit;
-}
-
.button {
display: flex;
justify-content: center;
@@ -42,86 +17,3 @@
grid-column: span var(--num-columns, 1);
}
-
-.link {
- font-weight: 600;
-
- &:hover {
- color: var(--link);
- }
-}
-
-.scratch {
- line-height: 1.5;
-
- .header {
- display: grid;
- grid-template-columns: auto 1fr auto;
- align-items: center;
- gap: 0.5em;
-
- .name {
- text-overflow: ellipsis;
- white-space: nowrap;
- max-width: 100%;
- overflow: hidden;
- }
-
- /* If two children, align them to the left and right */
- &:has(> :last-child:nth-child(2)) {
- grid-template-columns: 1fr auto;
- }
- }
-
- .icon {
- width: 1.2em;
- height: 1.2em;
- }
-
- .owner {
- color: var(--g1200);
- }
-
- .metadata {
- display: flex;
- align-items: flex-end;
-
- color: var(--g900);
-
- > span {
- flex-grow: 1;
- }
-
- .actions {
- padding-top: 0.25em;
- }
- }
-}
-
-.singleLine {
- white-space: nowrap;
- overflow: hidden;
-
- display: flex;
- align-items: center;
- gap: 0.4em;
-
- padding: 0.4em 0;
-
- .icon {
- flex-shrink: 0;
- width: 1.2em;
- height: 1.2em;
- }
-
- .name {
- flex-grow: 1;
-
- overflow: hidden;
- text-overflow: ellipsis;
- }
-
- .metadata {
- color: var(--g1200);
- }
-}
diff --git a/frontend/src/components/ScratchList.tsx b/frontend/src/components/ScratchList.tsx
index 125ddb2..86b555b 100644
--- a/frontend/src/components/ScratchList.tsx
+++ b/frontend/src/components/ScratchList.tsx
@@ -2,33 +2,24 @@
import { type ReactNode, useState } from "react";
-import Image from "next/image";
import Link from "next/link";
import classNames from "classnames";
-import TimeAgo from "@/components/TimeAgo";
-import * as api from "@/lib/api";
-import { presetUrl, scratchUrl, userAvatarUrl } from "@/lib/api/urls";
-
-import getTranslation from "@/lib/i18n/translate";
-
-import AnonymousFrogAvatar from "./user/AnonymousFrog";
import AsyncButton from "./AsyncButton";
import Button from "./Button";
-import PlatformLink from "./PlatformLink";
-import { calculateScorePercent, percentToString } from "./ScoreBadge";
import styles from "./ScratchList.module.scss";
+import { type TerseScratch, usePaginated } from "@/lib/api";
+import { scratchUrl } from "@/lib/api/urls";
+import { ScratchItem } from "./ScratchItem";
import Sort, { SortMode } from "./SortScratch";
-import UserLink from "./user/UserLink";
-
import { TextSkeleton, SCRATCH_LIST } from "./TextSkeleton";
export interface Props {
title?: string;
url?: string;
className?: string;
- item?: ({ scratch }: { scratch: api.TerseScratch }) => JSX.Element;
+ item?: ({ scratch }: { scratch: TerseScratch }) => JSX.Element;
emptyButtonLabel?: ReactNode;
isSortable?: boolean;
}
@@ -43,7 +34,7 @@ export default function ScratchList({
}: Props) {
const [sortMode, setSortMode] = useState(SortMode.NEWEST_FIRST);
const { results, isLoading, hasNext, loadNext } =
- api.usePaginated(
+ usePaginated(
`${url || "/scratch"}&ordering=${sortMode.toString()}`,
);
@@ -89,240 +80,3 @@ export default function ScratchList({
>
);
}
-
-export function getMatchPercentString(scratch: api.TerseScratch) {
- if (scratch.score === -1) {
- return "0%";
- }
- if (scratch.match_override) {
- return "100%";
- }
- const matchPercent = calculateScorePercent(
- scratch.score,
- scratch.max_score,
- );
- const matchPercentString = percentToString(matchPercent);
-
- return matchPercentString;
-}
-
-export function ScratchItem({
- scratch,
- children,
-}: { scratch: api.TerseScratch; children?: ReactNode }) {
- const compilersTranslation = getTranslation("compilers");
- const compilerName = compilersTranslation.t(scratch.compiler);
- const matchPercentString = getMatchPercentString(scratch);
- const preset = api.usePreset(scratch.preset);
- const presetName = preset?.name;
-
- const presetOrCompiler = presetName ? (
-
- {presetName}
-
- ) : (
- {compilerName}
- );
-
- return (
-
-
-
-
-
- {scratch.name}
-
-
- {scratch.owner ? (
-
- ) : (
-
No Owner
- )}
-
-
-
-
- {presetOrCompiler} • {matchPercentString} matched •{" "}
-
-
-
{children}
-
-
-
- );
-}
-
-export function ScratchItemNoOwner({ scratch }: { scratch: api.TerseScratch }) {
- const compilersTranslation = getTranslation("compilers");
- const compilerName = compilersTranslation.t(scratch.compiler);
- const matchPercentString = getMatchPercentString(scratch);
- const preset = api.usePreset(scratch.preset);
- const presetName = preset?.name;
-
- const presetOrCompiler = presetName ? (
-
- {presetName}
-
- ) : (
- {compilerName}
- );
-
- return (
-
-
-
-
-
- {scratch.name}
-
-
{/* empty div for alignment */}
-
-
-
- {presetOrCompiler} • {matchPercentString} matched •{" "}
-
-
-
-
-
- );
-}
-
-export function ScratchItemPlatformList({
- scratch,
-}: { scratch: api.TerseScratch }) {
- const compilersTranslation = getTranslation("compilers");
- const compilerName = compilersTranslation.t(scratch.compiler);
- const matchPercentString = getMatchPercentString(scratch);
- const preset = api.usePreset(scratch.preset);
- const presetName = preset?.name;
-
- const presetOrCompiler = presetName ? (
-
- {presetName}
-
- ) : (
- {compilerName}
- );
-
- return (
-
-
-
-
- {scratch.name}
-
-
- {scratch.owner ? (
-
- ) : (
-
No Owner
- )}
-
-
-
-
- {presetOrCompiler} • {matchPercentString} matched •{" "}
-
-
-
-
-
- );
-}
-
-export function ScratchItemPresetList({
- scratch,
-}: { scratch: api.TerseScratch }) {
- const matchPercentString = getMatchPercentString(scratch);
-
- return (
-
-
-
-
- {scratch.name}
-
-
-
- {matchPercentString} matched •{" "}
-
-
-
-
- {scratch.owner ? (
-
- ) : (
-
No Owner
- )}
-
-
-
-
- );
-}
-
-export function ScratchOwnerAvatar({ scratch }: { scratch: api.TerseScratch }) {
- return (
- scratch.owner &&
- (!api.isAnonUser(scratch.owner) ? (
- userAvatarUrl(scratch.owner) && (
-
- )
- ) : (
-
- ))
- );
-}
-
-export function SingleLineScratchItem({
- scratch,
- showOwner = false,
-}: { scratch: api.TerseScratch; showOwner?: boolean }) {
- const matchPercentString = getMatchPercentString(scratch);
-
- return (
-
-
-
- {scratch.name}
-
- {matchPercentString}
- {showOwner && }
-
- );
-}
diff --git a/frontend/src/components/user/tabs/ScratchesTab.tsx b/frontend/src/components/user/tabs/ScratchesTab.tsx
index 57dfbd6..012e254 100644
--- a/frontend/src/components/user/tabs/ScratchesTab.tsx
+++ b/frontend/src/components/user/tabs/ScratchesTab.tsx
@@ -1,4 +1,6 @@
-import ScratchList, { ScratchItemNoOwner } from "@/components/ScratchList";
+import ScratchList from "@/components/ScratchList";
+import { ScratchItemNoOwner } from "@/components/ScratchItem";
+
import type { User } from "@/lib/api";
import { userUrl } from "@/lib/api/urls";