v3: src/components sorted into feature folders, the four largest files split, and the component tests mirror the new layout — 48 components sat in one flat folder, ChatModal ran to 1301 lines, DumpCreateModal to 983, and the tests were grouped by theme, so there was no telling which component had none. Components now live in category/, chat/, comment/, dump/ (with dump/create/ for the new-dump wizard), editor/, form/, layout/ (header, page shell, floating buttons), player/, playlist/, profile/, social/ (vote, like, follow, the who-voted popover) and ui/; FeedTabBar and PresenceRow, used by the index page alone, move to pages/index/. ChatModal gives up ChatMessageItem, PendingItem, ChatEditBox and ReplyRef, the message limits and text helpers (chat/message.ts), the day and clock labels (chat/time.ts) and the draft storage (utils/chatDraft.ts, next to dumpDraft.ts). DumpCreateModal gives up StepIndicator, LocalFilePreview, TitleField and DuplicateNotice; CommentThread gives up CommentForm and CommentNode; TextEditor gives up the emoji picker popup and its skin-tone storage; MediaPlayer gives up its icons (which FilePreview imported from it), Waveform, SeekBar and the remembered volume. NewPlaylistForm, a button that opens a modal holding the form, is NewPlaylistButton now, and Thumbnail, ThumbnailPlaceholder, FilePreview and RichContentCard use named exports like everything else. The tests are split one file per component under src/test/components/<folder>/ (and src/test/pages/index/), the same 625 of them. No behaviour changes; the catalogues only move their source references.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,7 +4,7 @@ import { t } from "@lingui/core/macro";
|
||||
import { Trans } from "@lingui/react/macro";
|
||||
import { dumpThumbnailUrl, dumpUrl } from "../utils/urls.ts";
|
||||
import { can } from "../utils/permissions.ts";
|
||||
import { AddToPlaylistModal } from "../components/AddToPlaylistModal.tsx";
|
||||
import { AddToPlaylistModal } from "../components/playlist/AddToPlaylistModal.tsx";
|
||||
|
||||
import { API_URL, VALIDATION } from "../config/api.ts";
|
||||
|
||||
@@ -32,17 +32,17 @@ import { useDocumentTitle } from "../hooks/useDocumentTitle.ts";
|
||||
import { relativeTime } from "../utils/relativeTime.ts";
|
||||
import { useHeightTransition } from "../hooks/useHeightTransition.ts";
|
||||
import { useWS } from "../hooks/useWS.ts";
|
||||
import { Avatar } from "../components/Avatar.tsx";
|
||||
import RichContentCard from "../components/RichContentCard.tsx";
|
||||
import FilePreview from "../components/FilePreview.tsx";
|
||||
import { VoteButton } from "../components/VoteButton.tsx";
|
||||
import { DumpCard } from "../components/DumpCard.tsx";
|
||||
import { PlaylistCard } from "../components/PlaylistCard.tsx";
|
||||
import { PageShell } from "../components/PageShell.tsx";
|
||||
import { PageError } from "../components/PageError.tsx";
|
||||
import { Markdown } from "../components/Markdown.tsx";
|
||||
import { CommentThread } from "../components/CommentThread.tsx";
|
||||
import { Tooltip } from "../components/Tooltip.tsx";
|
||||
import { Avatar } from "../components/ui/Avatar.tsx";
|
||||
import { RichContentCard } from "../components/dump/RichContentCard.tsx";
|
||||
import { FilePreview } from "../components/dump/FilePreview.tsx";
|
||||
import { VoteButton } from "../components/social/VoteButton.tsx";
|
||||
import { DumpCard } from "../components/dump/DumpCard.tsx";
|
||||
import { PlaylistCard } from "../components/playlist/PlaylistCard.tsx";
|
||||
import { PageShell } from "../components/layout/PageShell.tsx";
|
||||
import { PageError } from "../components/layout/PageError.tsx";
|
||||
import { Markdown } from "../components/editor/Markdown.tsx";
|
||||
import { CommentThread } from "../components/comment/CommentThread.tsx";
|
||||
import { Tooltip } from "../components/ui/Tooltip.tsx";
|
||||
import { friendlyFetchError } from "../utils/apiError.ts";
|
||||
|
||||
type DumpState =
|
||||
|
||||
@@ -10,15 +10,15 @@ import { useRequiredAuth } from "../hooks/useAuth.ts";
|
||||
import { useDocumentTitle } from "../hooks/useDocumentTitle.ts";
|
||||
import { formatBytes } from "../utils/format.ts";
|
||||
import { dumpFileUrl, dumpThumbnailUrl, dumpUrl } from "../utils/urls.ts";
|
||||
import { PageShell } from "../components/PageShell.tsx";
|
||||
import { PageError } from "../components/PageError.tsx";
|
||||
import { ErrorCard } from "../components/ErrorCard.tsx";
|
||||
import { PageShell } from "../components/layout/PageShell.tsx";
|
||||
import { PageError } from "../components/layout/PageError.tsx";
|
||||
import { ErrorCard } from "../components/ui/ErrorCard.tsx";
|
||||
import { friendlyFetchError } from "../utils/apiError.ts";
|
||||
import { ConfirmModal } from "../components/ConfirmModal.tsx";
|
||||
import RichContentCard from "../components/RichContentCard.tsx";
|
||||
import FilePreview from "../components/FilePreview.tsx";
|
||||
import { ImagePicker } from "../components/ImagePicker.tsx";
|
||||
import { CategorySelect } from "../components/CategorySelect.tsx";
|
||||
import { ConfirmModal } from "../components/ui/ConfirmModal.tsx";
|
||||
import { RichContentCard } from "../components/dump/RichContentCard.tsx";
|
||||
import { FilePreview } from "../components/dump/FilePreview.tsx";
|
||||
import { ImagePicker } from "../components/ui/ImagePicker.tsx";
|
||||
import { CategorySelect } from "../components/category/CategorySelect.tsx";
|
||||
import {
|
||||
expectOk,
|
||||
FileField,
|
||||
|
||||
@@ -8,9 +8,9 @@ import {
|
||||
} from "react";
|
||||
import { useLocation, useParams } from "react-router";
|
||||
|
||||
import { AppHeader } from "../components/AppHeader.tsx";
|
||||
import { PresenceRow } from "../components/PresenceRow.tsx";
|
||||
import { FeedTabBar } from "../components/FeedTabBar.tsx";
|
||||
import { AppHeader } from "../components/layout/AppHeader.tsx";
|
||||
import { PresenceRow } from "./index/PresenceRow.tsx";
|
||||
import { FeedTabBar } from "./index/FeedTabBar.tsx";
|
||||
import {
|
||||
DEFAULT_FEED_TAB,
|
||||
FEED_TABS,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Link } from "react-router";
|
||||
import { t } from "@lingui/core/macro";
|
||||
import { Trans } from "@lingui/react/macro";
|
||||
import { PageShell } from "../components/PageShell.tsx";
|
||||
import { PageShell } from "../components/layout/PageShell.tsx";
|
||||
import { useDocumentTitle } from "../hooks/useDocumentTitle.ts";
|
||||
|
||||
export function NotFound() {
|
||||
|
||||
@@ -7,8 +7,8 @@ import { relativeTime } from "../utils/relativeTime.ts";
|
||||
import { API_URL, NOTIFICATIONS_PAGE_SIZE } from "../config/api.ts";
|
||||
import { useAuth } from "../hooks/useAuth.ts";
|
||||
import { useDocumentTitle } from "../hooks/useDocumentTitle.ts";
|
||||
import { ErrorCard } from "../components/ErrorCard.tsx";
|
||||
import { Tooltip } from "../components/Tooltip.tsx";
|
||||
import { ErrorCard } from "../components/ui/ErrorCard.tsx";
|
||||
import { Tooltip } from "../components/ui/Tooltip.tsx";
|
||||
import { useWS } from "../hooks/useWS.ts";
|
||||
import type {
|
||||
CommentLikedData,
|
||||
@@ -25,7 +25,7 @@ import type {
|
||||
UserMentionedData,
|
||||
} from "../model.ts";
|
||||
import { deserializeNotification } from "../model.ts";
|
||||
import { PageShell } from "../components/PageShell.tsx";
|
||||
import { PageShell } from "../components/layout/PageShell.tsx";
|
||||
import { friendlyFetchError } from "../utils/apiError.ts";
|
||||
import { expectOk } from "../components/form/index.ts";
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import { Link, useNavigate, useParams } from "react-router";
|
||||
import { t } from "@lingui/core/macro";
|
||||
import { Trans } from "@lingui/react/macro";
|
||||
import { API_URL, VALIDATION } from "../config/api.ts";
|
||||
import { CountedInput } from "../components/CountedInput.tsx";
|
||||
import { CountedInput } from "../components/ui/CountedInput.tsx";
|
||||
import type {
|
||||
Playlist,
|
||||
PlaylistWithDumps,
|
||||
@@ -31,16 +31,16 @@ import { useDocumentTitle } from "../hooks/useDocumentTitle.ts";
|
||||
import { useHeightTransition } from "../hooks/useHeightTransition.ts";
|
||||
import { useWS } from "../hooks/useWS.ts";
|
||||
import { relativeTime } from "../utils/relativeTime.ts";
|
||||
import { DumpCard } from "../components/DumpCard.tsx";
|
||||
import { PageShell } from "../components/PageShell.tsx";
|
||||
import { PageError } from "../components/PageError.tsx";
|
||||
import { ErrorCard } from "../components/ErrorCard.tsx";
|
||||
import { ConfirmModal } from "../components/ConfirmModal.tsx";
|
||||
import { ImagePicker } from "../components/ImagePicker.tsx";
|
||||
import { Markdown } from "../components/Markdown.tsx";
|
||||
import { TextEditor } from "../components/TextEditor.tsx";
|
||||
import { FollowPlaylistButton } from "../components/FollowButton.tsx";
|
||||
import { Tooltip } from "../components/Tooltip.tsx";
|
||||
import { DumpCard } from "../components/dump/DumpCard.tsx";
|
||||
import { PageShell } from "../components/layout/PageShell.tsx";
|
||||
import { PageError } from "../components/layout/PageError.tsx";
|
||||
import { ErrorCard } from "../components/ui/ErrorCard.tsx";
|
||||
import { ConfirmModal } from "../components/ui/ConfirmModal.tsx";
|
||||
import { ImagePicker } from "../components/ui/ImagePicker.tsx";
|
||||
import { Markdown } from "../components/editor/Markdown.tsx";
|
||||
import { TextEditor } from "../components/editor/TextEditor.tsx";
|
||||
import { FollowPlaylistButton } from "../components/social/FollowButton.tsx";
|
||||
import { Tooltip } from "../components/ui/Tooltip.tsx";
|
||||
import { friendlyFetchError } from "../utils/apiError.ts";
|
||||
import { Controller } from "react-hook-form";
|
||||
import {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Trans } from "@lingui/react/macro";
|
||||
|
||||
import { API_URL, VALIDATION } from "../config/api.ts";
|
||||
import { useDocumentTitle } from "../hooks/useDocumentTitle.ts";
|
||||
import { PageShell } from "../components/PageShell.tsx";
|
||||
import { PageShell } from "../components/layout/PageShell.tsx";
|
||||
import {
|
||||
expectOk,
|
||||
FormError,
|
||||
|
||||
@@ -2,11 +2,11 @@ import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { Link, useSearchParams } from "react-router";
|
||||
import { t } from "@lingui/core/macro";
|
||||
import { Trans } from "@lingui/react/macro";
|
||||
import { AppHeader } from "../components/AppHeader.tsx";
|
||||
import { TabBar } from "../components/TabBar.tsx";
|
||||
import { DumpCard } from "../components/DumpCard.tsx";
|
||||
import { PlaylistCard } from "../components/PlaylistCard.tsx";
|
||||
import { ErrorCard } from "../components/ErrorCard.tsx";
|
||||
import { AppHeader } from "../components/layout/AppHeader.tsx";
|
||||
import { TabBar } from "../components/ui/TabBar.tsx";
|
||||
import { DumpCard } from "../components/dump/DumpCard.tsx";
|
||||
import { PlaylistCard } from "../components/playlist/PlaylistCard.tsx";
|
||||
import { ErrorCard } from "../components/ui/ErrorCard.tsx";
|
||||
import { useAuth } from "../hooks/useAuth.ts";
|
||||
import { useDocumentTitle } from "../hooks/useDocumentTitle.ts";
|
||||
import { useWS } from "../hooks/useWS.ts";
|
||||
|
||||
@@ -9,11 +9,11 @@ import { useWS } from "../hooks/useWS.ts";
|
||||
import { useDumpListSync } from "../hooks/useDumpListSync.ts";
|
||||
import { usePositionAwareSync } from "../hooks/usePositionAwareSync.ts";
|
||||
import { useUserDumpFeed } from "../hooks/useUserDumpFeed.ts";
|
||||
import { DumpCard } from "../components/DumpCard.tsx";
|
||||
import { DumpCreateModal } from "../components/DumpCreateModal.tsx";
|
||||
import { ProfileSubpageHeader } from "../components/ProfileSubpageHeader.tsx";
|
||||
import { PageShell } from "../components/PageShell.tsx";
|
||||
import { PageError } from "../components/PageError.tsx";
|
||||
import { DumpCard } from "../components/dump/DumpCard.tsx";
|
||||
import { DumpCreateModal } from "../components/dump/create/DumpCreateModal.tsx";
|
||||
import { ProfileSubpageHeader } from "../components/profile/ProfileSubpageHeader.tsx";
|
||||
import { PageShell } from "../components/layout/PageShell.tsx";
|
||||
import { PageError } from "../components/layout/PageError.tsx";
|
||||
|
||||
export function UserDumps() {
|
||||
const { username } = useParams();
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
import { useAuth } from "../hooks/useAuth.ts";
|
||||
import { useDocumentTitle } from "../hooks/useDocumentTitle.ts";
|
||||
import { useHeightTransition } from "../hooks/useHeightTransition.ts";
|
||||
import { PageShell } from "../components/PageShell.tsx";
|
||||
import { PageShell } from "../components/layout/PageShell.tsx";
|
||||
import {
|
||||
expectOk,
|
||||
FormError,
|
||||
|
||||
@@ -30,13 +30,13 @@ import { usePositionAwareSync } from "../hooks/usePositionAwareSync.ts";
|
||||
import { useInfiniteScroll } from "../hooks/useInfiniteScroll.ts";
|
||||
import { useFeedCache } from "../hooks/useFeedCache.ts";
|
||||
import { useScrollSave } from "../hooks/useScrollSave.ts";
|
||||
import { PlaylistCard } from "../components/PlaylistCard.tsx";
|
||||
import { NewPlaylistForm } from "../components/NewPlaylistForm.tsx";
|
||||
import { ProfileSubpageHeader } from "../components/ProfileSubpageHeader.tsx";
|
||||
import { ConfirmModal } from "../components/ConfirmModal.tsx";
|
||||
import { PageShell } from "../components/PageShell.tsx";
|
||||
import { PageError } from "../components/PageError.tsx";
|
||||
import { ErrorCard } from "../components/ErrorCard.tsx";
|
||||
import { PlaylistCard } from "../components/playlist/PlaylistCard.tsx";
|
||||
import { NewPlaylistButton } from "../components/playlist/NewPlaylistButton.tsx";
|
||||
import { ProfileSubpageHeader } from "../components/profile/ProfileSubpageHeader.tsx";
|
||||
import { ConfirmModal } from "../components/ui/ConfirmModal.tsx";
|
||||
import { PageShell } from "../components/layout/PageShell.tsx";
|
||||
import { PageError } from "../components/layout/PageError.tsx";
|
||||
import { ErrorCard } from "../components/ui/ErrorCard.tsx";
|
||||
import { expectOk } from "../components/form/index.ts";
|
||||
|
||||
interface PlaylistFeed {
|
||||
@@ -381,7 +381,7 @@ export function UserPlaylists() {
|
||||
profileUser={profileUser}
|
||||
title={t`Playlists`}
|
||||
actions={isOwnProfile && (
|
||||
<NewPlaylistForm
|
||||
<NewPlaylistButton
|
||||
toggleClassName="btn-primary"
|
||||
onCreated={(p) =>
|
||||
setState((s) => {
|
||||
|
||||
@@ -30,13 +30,13 @@ import {
|
||||
type RawPublicUser,
|
||||
type UpdateUserRequest,
|
||||
} from "../model.ts";
|
||||
import { Avatar } from "../components/Avatar.tsx";
|
||||
import { DumpCard } from "../components/DumpCard.tsx";
|
||||
import { PlaylistCard } from "../components/PlaylistCard.tsx";
|
||||
import { NewPlaylistForm } from "../components/NewPlaylistForm.tsx";
|
||||
import { PageShell } from "../components/PageShell.tsx";
|
||||
import { PageError } from "../components/PageError.tsx";
|
||||
import { CategoryManager } from "../components/CategoryManager.tsx";
|
||||
import { Avatar } from "../components/ui/Avatar.tsx";
|
||||
import { DumpCard } from "../components/dump/DumpCard.tsx";
|
||||
import { PlaylistCard } from "../components/playlist/PlaylistCard.tsx";
|
||||
import { NewPlaylistButton } from "../components/playlist/NewPlaylistButton.tsx";
|
||||
import { PageShell } from "../components/layout/PageShell.tsx";
|
||||
import { PageError } from "../components/layout/PageError.tsx";
|
||||
import { CategoryManager } from "../components/category/CategoryManager.tsx";
|
||||
import { useAuth } from "../hooks/useAuth.ts";
|
||||
import { useDocumentTitle } from "../hooks/useDocumentTitle.ts";
|
||||
import { useTheme } from "../hooks/useTheme.ts";
|
||||
@@ -52,15 +52,15 @@ import { usePositionAwareSync } from "../hooks/usePositionAwareSync.ts";
|
||||
import type { Playlist, RawPlaylist } from "../model.ts";
|
||||
import { deserializePlaylist } from "../model.ts";
|
||||
import { useFeedCache } from "../hooks/useFeedCache.ts";
|
||||
import { DumpCreateModal } from "../components/DumpCreateModal.tsx";
|
||||
import { FollowUserButton } from "../components/FollowButton.tsx";
|
||||
import { ErrorCard } from "../components/ErrorCard.tsx";
|
||||
import { DumpCreateModal } from "../components/dump/create/DumpCreateModal.tsx";
|
||||
import { FollowUserButton } from "../components/social/FollowButton.tsx";
|
||||
import { ErrorCard } from "../components/ui/ErrorCard.tsx";
|
||||
import { friendlyFetchError } from "../utils/apiError.ts";
|
||||
import { can } from "../utils/permissions.ts";
|
||||
import { TextEditor } from "../components/TextEditor.tsx";
|
||||
import { Markdown } from "../components/Markdown.tsx";
|
||||
import { ChangePasswordModal } from "../components/ChangePasswordModal.tsx";
|
||||
import { TabBar } from "../components/TabBar.tsx";
|
||||
import { TextEditor } from "../components/editor/TextEditor.tsx";
|
||||
import { Markdown } from "../components/editor/Markdown.tsx";
|
||||
import { ChangePasswordModal } from "../components/profile/ChangePasswordModal.tsx";
|
||||
import { TabBar } from "../components/ui/TabBar.tsx";
|
||||
import { useTabParam } from "../hooks/useTabParam.ts";
|
||||
import { Controller } from "react-hook-form";
|
||||
import {
|
||||
@@ -1046,7 +1046,7 @@ export function UserPublicProfile() {
|
||||
</Trans>
|
||||
</h2>
|
||||
{isOwnProfile && (
|
||||
<NewPlaylistForm
|
||||
<NewPlaylistButton
|
||||
onCreated={(p) =>
|
||||
setState((s) => {
|
||||
if (s.status !== "loaded") return s;
|
||||
|
||||
@@ -11,8 +11,8 @@ import {
|
||||
} from "../model.ts";
|
||||
import { useAuth } from "../hooks/useAuth.ts";
|
||||
import { useDocumentTitle } from "../hooks/useDocumentTitle.ts";
|
||||
import { PageShell } from "../components/PageShell.tsx";
|
||||
import { ErrorCard } from "../components/ErrorCard.tsx";
|
||||
import { PageShell } from "../components/layout/PageShell.tsx";
|
||||
import { ErrorCard } from "../components/ui/ErrorCard.tsx";
|
||||
import {
|
||||
expectOk,
|
||||
FormError,
|
||||
|
||||
@@ -12,10 +12,10 @@ import { useWS } from "../hooks/useWS.ts";
|
||||
import { useDumpListSync } from "../hooks/useDumpListSync.ts";
|
||||
import { useFading } from "../hooks/useFading.ts";
|
||||
import { useUserDumpFeed } from "../hooks/useUserDumpFeed.ts";
|
||||
import { DumpCard } from "../components/DumpCard.tsx";
|
||||
import { ProfileSubpageHeader } from "../components/ProfileSubpageHeader.tsx";
|
||||
import { PageShell } from "../components/PageShell.tsx";
|
||||
import { PageError } from "../components/PageError.tsx";
|
||||
import { DumpCard } from "../components/dump/DumpCard.tsx";
|
||||
import { ProfileSubpageHeader } from "../components/profile/ProfileSubpageHeader.tsx";
|
||||
import { PageShell } from "../components/layout/PageShell.tsx";
|
||||
import { PageError } from "../components/layout/PageError.tsx";
|
||||
|
||||
export function UserUpvoted() {
|
||||
const { username } = useParams();
|
||||
|
||||
52
src/pages/index/FeedTabBar.tsx
Normal file
52
src/pages/index/FeedTabBar.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import { useNavigate, useParams } from "react-router";
|
||||
import { Trans } from "@lingui/react/macro";
|
||||
import { useAuth } from "../../hooks/useAuth.ts";
|
||||
import {
|
||||
DEFAULT_FEED_TAB,
|
||||
FEED_TABS,
|
||||
type FeedTab,
|
||||
} from "../../config/feedTabs.ts";
|
||||
import { useDefaultFeedTab } from "../../hooks/useDefaultFeedTab.ts";
|
||||
import { TabBar } from "../../components/ui/TabBar.tsx";
|
||||
import { CategorySwitcher } from "../../components/category/CategorySwitcher.tsx";
|
||||
|
||||
/**
|
||||
* Feed sort tabs (hot/new/journal/followed). Navigation stays within the
|
||||
* current scope: `~` for all categories, or the active category slug — so the
|
||||
* tabs link to `/~/new`, `/music/new`, etc.
|
||||
*/
|
||||
export function FeedTabBar() {
|
||||
const { user } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const { categorySlug, feedTab } = useParams();
|
||||
|
||||
const [preferredTab] = useDefaultFeedTab();
|
||||
const defaultTab: FeedTab = preferredTab === "followed" && !user
|
||||
? DEFAULT_FEED_TAB
|
||||
: preferredTab;
|
||||
const active: FeedTab =
|
||||
(FEED_TABS as readonly string[]).includes(feedTab ?? "") &&
|
||||
(feedTab !== "followed" || !!user)
|
||||
? (feedTab as FeedTab)
|
||||
: defaultTab;
|
||||
|
||||
const scope = categorySlug ?? "~";
|
||||
|
||||
const tabs = [
|
||||
{ key: "hot" as FeedTab, label: <Trans>Hot</Trans> },
|
||||
{ key: "new" as FeedTab, label: <Trans>New</Trans> },
|
||||
{ key: "journal" as FeedTab, label: <Trans>Journal</Trans> },
|
||||
...(user
|
||||
? [{ key: "followed" as FeedTab, label: <Trans>Followed</Trans> }]
|
||||
: []),
|
||||
];
|
||||
|
||||
return (
|
||||
<TabBar
|
||||
tabs={tabs}
|
||||
activeTab={active}
|
||||
onChange={(t) => navigate(`/${scope}/${t}`)}
|
||||
leading={<CategorySwitcher />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { TabBar } from "../../components/TabBar.tsx";
|
||||
import { TabBar } from "../../components/ui/TabBar.tsx";
|
||||
import { t } from "@lingui/core/macro";
|
||||
import { Trans } from "@lingui/react/macro";
|
||||
import { DumpCard } from "../../components/DumpCard.tsx";
|
||||
import { ErrorCard } from "../../components/ErrorCard.tsx";
|
||||
import { DumpCard } from "../../components/dump/DumpCard.tsx";
|
||||
import { ErrorCard } from "../../components/ui/ErrorCard.tsx";
|
||||
import { API_URL, DEFAULT_PAGE_SIZE } from "../../config/api.ts";
|
||||
import { useAuth } from "../../hooks/useAuth.ts";
|
||||
import { useDumpListSync } from "../../hooks/useDumpListSync.ts";
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useMemo } from "react";
|
||||
import { t } from "@lingui/core/macro";
|
||||
import { Trans } from "@lingui/react/macro";
|
||||
import { DumpCard } from "../../components/DumpCard.tsx";
|
||||
import { ErrorCard } from "../../components/ErrorCard.tsx";
|
||||
import { DumpCard } from "../../components/dump/DumpCard.tsx";
|
||||
import { ErrorCard } from "../../components/ui/ErrorCard.tsx";
|
||||
import { hotScore } from "../../utils/hotScore.ts";
|
||||
import type { MainFeedProps } from "./types.ts";
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Fragment, useMemo } from "react";
|
||||
import { t } from "@lingui/core/macro";
|
||||
import { Trans } from "@lingui/react/macro";
|
||||
import { ErrorCard } from "../../components/ErrorCard.tsx";
|
||||
import { JournalCard } from "../../components/JournalCard.tsx";
|
||||
import { ErrorCard } from "../../components/ui/ErrorCard.tsx";
|
||||
import { JournalCard } from "../../components/dump/JournalCard.tsx";
|
||||
import { composeJournal } from "../../utils/journalLayout.ts";
|
||||
import type { MainFeedProps } from "./types.ts";
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useMemo } from "react";
|
||||
import { t } from "@lingui/core/macro";
|
||||
import { Trans } from "@lingui/react/macro";
|
||||
import { DumpCard } from "../../components/DumpCard.tsx";
|
||||
import { ErrorCard } from "../../components/ErrorCard.tsx";
|
||||
import { DumpCard } from "../../components/dump/DumpCard.tsx";
|
||||
import { ErrorCard } from "../../components/ui/ErrorCard.tsx";
|
||||
import type { MainFeedProps } from "./types.ts";
|
||||
|
||||
export function NewFeed(
|
||||
|
||||
74
src/pages/index/PresenceRow.tsx
Normal file
74
src/pages/index/PresenceRow.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import { useState } from "react";
|
||||
import { Link } from "react-router";
|
||||
import { t } from "@lingui/core/macro";
|
||||
import { Avatar } from "../../components/ui/Avatar.tsx";
|
||||
import { useWSPresence } from "../../hooks/useWS.ts";
|
||||
import { useRoomBeside } from "../../hooks/useRoomBeside.ts";
|
||||
|
||||
// Past this many, the row ends in a "+N" counter rather than growing, so a
|
||||
// busy evening doesn't push the feed tabs out of the header.
|
||||
const MAX_SEATS = 6;
|
||||
// However tight the row, it keeps an avatar and the counter — past that, the
|
||||
// tabs beside it scroll instead.
|
||||
const MIN_SEATS = 2;
|
||||
// Each seat (avatar or counter) is 32px wide and overlaps the one before it
|
||||
// by 10px (.index-presence-avatar's negative margin), so adds 22px.
|
||||
const SEAT = 32;
|
||||
const SEAT_STEP = 22;
|
||||
|
||||
/**
|
||||
* Who's online, as a row of overlapping avatars. Given `fitWithin` and
|
||||
* `fitBeside`, it gives up seats to what's beside it — as many avatars as the
|
||||
* room left by the tabs allows, the rest folded into the counter.
|
||||
*/
|
||||
export function PresenceRow(
|
||||
{ fitWithin, fitBeside }: { fitWithin?: string; fitBeside?: string },
|
||||
) {
|
||||
const { onlineUsers } = useWSPresence();
|
||||
const [el, setEl] = useState<HTMLDivElement | null>(null);
|
||||
const room = useRoomBeside(el, { within: fitWithin, beside: fitBeside });
|
||||
|
||||
if (onlineUsers.length === 0) return null;
|
||||
|
||||
const seats = room === null ? MAX_SEATS : Math.min(
|
||||
MAX_SEATS,
|
||||
Math.max(MIN_SEATS, 1 + Math.floor((room - SEAT) / SEAT_STEP)),
|
||||
);
|
||||
// The counter takes the last seat, so the row never runs past `seats`.
|
||||
const shown = onlineUsers.length > seats
|
||||
? onlineUsers.slice(0, seats - 1)
|
||||
: onlineUsers;
|
||||
const rest = onlineUsers.slice(shown.length);
|
||||
const restNames = rest.map((u) => u.username).join(", ");
|
||||
|
||||
return (
|
||||
<div className="index-presence" ref={setEl}>
|
||||
{shown.map((u) => (
|
||||
<Link
|
||||
key={u.userId}
|
||||
to={`/users/${u.username}`}
|
||||
title={u.username}
|
||||
className="index-presence-avatar"
|
||||
>
|
||||
<Avatar
|
||||
userId={u.userId}
|
||||
username={u.username}
|
||||
hasAvatar={u.hasAvatar}
|
||||
size={SEAT}
|
||||
version={u.avatarVersion}
|
||||
/>
|
||||
</Link>
|
||||
))}
|
||||
{rest.length > 0 && (
|
||||
<span
|
||||
className="index-presence-avatar index-presence-more"
|
||||
title={restNames}
|
||||
aria-label={t`${rest.length} more online: ${restNames}`}
|
||||
role="img"
|
||||
>
|
||||
+{rest.length}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user