Split ScratchItem* from ScratchList (#1458)

This commit is contained in:
Mark Street
2025-02-12 10:35:59 +00:00
committed by GitHub
parent 04c6ae436c
commit 4e56ff149f
10 changed files with 369 additions and 365 deletions
@@ -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 {
+2 -1
View File
@@ -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";
@@ -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";
@@ -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";
+1 -1
View File
@@ -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";
@@ -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);
}
}
+257
View File
@@ -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 ? (
<Link href={presetUrl(preset)} className={styles.link}>
{presetName}
</Link>
) : (
<span>{compilerName}</span>
);
return (
<li className={styles.item}>
<div className={styles.scratch}>
<div className={styles.header}>
<PlatformLink
size={16}
scratch={scratch}
className={styles.icon}
/>
<Link
href={scratchUrl(scratch)}
className={classNames(styles.link, styles.name)}
>
{scratch.name}
</Link>
<div className={styles.owner}>
{scratch.owner ? (
<UserLink user={scratch.owner} />
) : (
<div>No Owner</div>
)}
</div>
</div>
<div className={styles.metadata}>
<span>
{presetOrCompiler} {matchPercentString} matched {" "}
<TimeAgo date={scratch.last_updated} />
</span>
<div className={styles.actions}>{children}</div>
</div>
</div>
</li>
);
}
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 ? (
<Link href={presetUrl(preset)} className={styles.link}>
{presetName}
</Link>
) : (
<span>{compilerName}</span>
);
return (
<li className={styles.item}>
<div className={styles.scratch}>
<div className={styles.header}>
<PlatformLink
size={16}
scratch={scratch}
className={styles.icon}
/>
<Link
href={scratchUrl(scratch)}
className={classNames(styles.link, styles.name)}
>
{scratch.name}
</Link>
<div>{/* empty div for alignment */}</div>
</div>
<div className={styles.metadata}>
<span>
{presetOrCompiler} {matchPercentString} matched {" "}
<TimeAgo date={scratch.last_updated} />
</span>
</div>
</div>
</li>
);
}
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 ? (
<Link href={presetUrl(preset)} className={styles.link}>
{presetName}
</Link>
) : (
<span>{compilerName}</span>
);
return (
<li className={styles.item}>
<div className={styles.scratch}>
<div className={styles.header}>
<Link
href={scratchUrl(scratch)}
className={classNames(styles.link, styles.name)}
>
{scratch.name}
</Link>
<div className={styles.owner}>
{scratch.owner ? (
<UserLink user={scratch.owner} />
) : (
<div>No Owner</div>
)}
</div>
</div>
<div className={styles.metadata}>
<span>
{presetOrCompiler} {matchPercentString} matched {" "}
<TimeAgo date={scratch.last_updated} />
</span>
</div>
</div>
</li>
);
}
export function ScratchItemPresetList({
scratch,
}: { scratch: api.TerseScratch }) {
const matchPercentString = getMatchPercentString(scratch);
return (
<li className={styles.item}>
<div className={styles.scratch}>
<div className={styles.header}>
<Link
href={scratchUrl(scratch)}
className={classNames(styles.link, styles.name)}
>
{scratch.name}
</Link>
<div className={styles.metadata}>
<span>
{matchPercentString} matched {" "}
<TimeAgo date={scratch.last_updated} />
</span>
</div>
<div className={styles.owner}>
{scratch.owner ? (
<UserLink user={scratch.owner} />
) : (
<div>No Owner</div>
)}
</div>
</div>
</div>
</li>
);
}
export function ScratchOwnerAvatar({ scratch }: { scratch: api.TerseScratch }) {
return (
scratch.owner &&
(!api.isAnonUser(scratch.owner) ? (
userAvatarUrl(scratch.owner) && (
<Image
src={userAvatarUrl(scratch.owner)}
alt={scratch.owner.username}
width={16}
height={16}
className={styles.scratchOwnerAvatar}
/>
)
) : (
<AnonymousFrogAvatar
user={scratch.owner}
width={16}
height={16}
className={styles.scratchOwnerAvatar}
/>
))
);
}
export function SingleLineScratchItem({
scratch,
showOwner = false,
}: { scratch: api.TerseScratch; showOwner?: boolean }) {
const matchPercentString = getMatchPercentString(scratch);
return (
<li className={styles.singleLine}>
<PlatformLink size={16} scratch={scratch} className={styles.icon} />
<Link
href={scratchUrl(scratch)}
className={classNames(styles.link, styles.name)}
>
{scratch.name}
</Link>
<div className={styles.metadata}>{matchPercentString}</div>
{showOwner && <ScratchOwnerAvatar scratch={scratch} />}
</li>
);
}
@@ -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);
}
}
+5 -251
View File
@@ -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<api.TerseScratch>(
usePaginated<TerseScratch>(
`${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 ? (
<Link href={presetUrl(preset)} className={styles.link}>
{presetName}
</Link>
) : (
<span>{compilerName}</span>
);
return (
<li className={styles.item}>
<div className={styles.scratch}>
<div className={styles.header}>
<PlatformLink
size={16}
scratch={scratch}
className={styles.icon}
/>
<Link
href={scratchUrl(scratch)}
className={classNames(styles.link, styles.name)}
>
{scratch.name}
</Link>
<div className={styles.owner}>
{scratch.owner ? (
<UserLink user={scratch.owner} />
) : (
<div>No Owner</div>
)}
</div>
</div>
<div className={styles.metadata}>
<span>
{presetOrCompiler} {matchPercentString} matched {" "}
<TimeAgo date={scratch.last_updated} />
</span>
<div className={styles.actions}>{children}</div>
</div>
</div>
</li>
);
}
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 ? (
<Link href={presetUrl(preset)} className={styles.link}>
{presetName}
</Link>
) : (
<span>{compilerName}</span>
);
return (
<li className={styles.item}>
<div className={styles.scratch}>
<div className={styles.header}>
<PlatformLink
size={16}
scratch={scratch}
className={styles.icon}
/>
<Link
href={scratchUrl(scratch)}
className={classNames(styles.link, styles.name)}
>
{scratch.name}
</Link>
<div>{/* empty div for alignment */}</div>
</div>
<div className={styles.metadata}>
<span>
{presetOrCompiler} {matchPercentString} matched {" "}
<TimeAgo date={scratch.last_updated} />
</span>
</div>
</div>
</li>
);
}
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 ? (
<Link href={presetUrl(preset)} className={styles.link}>
{presetName}
</Link>
) : (
<span>{compilerName}</span>
);
return (
<li className={styles.item}>
<div className={styles.scratch}>
<div className={styles.header}>
<Link
href={scratchUrl(scratch)}
className={classNames(styles.link, styles.name)}
>
{scratch.name}
</Link>
<div className={styles.owner}>
{scratch.owner ? (
<UserLink user={scratch.owner} />
) : (
<div>No Owner</div>
)}
</div>
</div>
<div className={styles.metadata}>
<span>
{presetOrCompiler} {matchPercentString} matched {" "}
<TimeAgo date={scratch.last_updated} />
</span>
</div>
</div>
</li>
);
}
export function ScratchItemPresetList({
scratch,
}: { scratch: api.TerseScratch }) {
const matchPercentString = getMatchPercentString(scratch);
return (
<li className={styles.item}>
<div className={styles.scratch}>
<div className={styles.header}>
<Link
href={scratchUrl(scratch)}
className={classNames(styles.link, styles.name)}
>
{scratch.name}
</Link>
<div className={styles.metadata}>
<span>
{matchPercentString} matched {" "}
<TimeAgo date={scratch.last_updated} />
</span>
</div>
<div className={styles.owner}>
{scratch.owner ? (
<UserLink user={scratch.owner} />
) : (
<div>No Owner</div>
)}
</div>
</div>
</div>
</li>
);
}
export function ScratchOwnerAvatar({ scratch }: { scratch: api.TerseScratch }) {
return (
scratch.owner &&
(!api.isAnonUser(scratch.owner) ? (
userAvatarUrl(scratch.owner) && (
<Image
src={userAvatarUrl(scratch.owner)}
alt={scratch.owner.username}
width={16}
height={16}
className={styles.scratchOwnerAvatar}
/>
)
) : (
<AnonymousFrogAvatar
user={scratch.owner}
width={16}
height={16}
className={styles.scratchOwnerAvatar}
/>
))
);
}
export function SingleLineScratchItem({
scratch,
showOwner = false,
}: { scratch: api.TerseScratch; showOwner?: boolean }) {
const matchPercentString = getMatchPercentString(scratch);
return (
<li className={styles.singleLine}>
<PlatformLink size={16} scratch={scratch} className={styles.icon} />
<Link
href={scratchUrl(scratch)}
className={classNames(styles.link, styles.name)}
>
{scratch.name}
</Link>
<div className={styles.metadata}>{matchPercentString}</div>
{showOwner && <ScratchOwnerAvatar scratch={scratch} />}
</li>
);
}
@@ -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";