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) && ( + {scratch.owner.username} + ) + ) : ( + + )) + ); +} + +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) && ( - {scratch.owner.username} - ) - ) : ( - - )) - ); -} - -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";