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
22 changes: 9 additions & 13 deletions packages/shared/src/components/MainFeedLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ import { ClientQuestEventType } from '../graphql/quests';
import { ProfileEmptyScreen } from './profile/ProfileEmptyScreen';
import { Origin } from '../lib/log';
import { ExploreTabs, tabToUrl, urlToTab } from './header';
import { FeedExploreTabs } from './header/FeedExploreTabs';
import { QueryStateKeys, useQueryState } from '../hooks/utils/useQueryState';
import { useSearchResultsLayout } from '../hooks/search/useSearchResultsLayout';
import useCustomDefaultFeed from '../hooks/feed/useCustomDefaultFeed';
Expand All @@ -85,8 +86,6 @@ import { isDevelopment, isProductionAPI, webappUrl } from '../lib/constants';
import { checkIsExtension } from '../lib/func';
import { useTrackQuestClientEvent } from '../hooks/useTrackQuestClientEvent';
import { useLayoutVariant } from '../hooks/layout/useLayoutVariant';
import { ExploreSectionTabs } from './header/ExploreSectionTabs';
import { ExploreSortDropdown } from './header/ExploreSortDropdown';

const FeedExploreHeader = dynamic(
() =>
Expand Down Expand Up @@ -250,7 +249,6 @@ export default function MainFeedLayout({
isPopular,
isAnyExplore,
isExploreLatest,
isDiscussed,
isSortableFeed,
isCustomFeed,
isSearch: isSearchPage,
Expand Down Expand Up @@ -707,14 +705,11 @@ export default function MainFeedLayout({
);
}, [isLaptop, onTabChange, tab]);

// v2 hoists the explore section tabs into the floating card's
// page-header strip (matching the SquadDirectoryLayout pattern). The
// inline FeedExploreComponent is suppressed below to avoid showing
// the same tabs twice.
// The Discussions feed (/discussed) is part of the Explore hub — show the
// same section tabs there so the hub persists. The Sort dropdown is only
// for the actual Explore sorts, so it stays gated on isAnyExplore.
const showExploreV2PageHeader = (isAnyExplore || isDiscussed) && isV2;
// v2 reaches the Explore hub sections (Explore, Tags, Sources, Leaderboard,
// Discussions) from the sidebar's Explore panel, so the page header no longer
// carries a section-tab strip. The header now only hosts the Explore sort
// dropdown, so it's gated on isAnyExplore.
const showExploreV2PageHeader = isAnyExplore && isV2;

// v2 also hoists the regular page-header strip up here, OUTSIDE
// `FeedPageLayoutComponent`, so it can span the full floating-card
Expand Down Expand Up @@ -753,8 +748,9 @@ export default function MainFeedLayout({
<>
{showExploreV2PageHeader && (
<header className={classNames(pageHeaderClassName, '!py-0')}>
<ExploreSectionTabs />
{isAnyExplore && <ExploreSortDropdown />}
{/* Sort options as pill tabs — same navbar as the Tags / Squad
directory pages, not the underlined TabContainer. */}
<FeedExploreTabs />
</header>
)}
{showFeedV2PageHeader && (
Expand Down
25 changes: 19 additions & 6 deletions packages/shared/src/components/header/ExploreHubHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,32 @@
import type { ReactElement, ReactNode } from 'react';
import React from 'react';
import { useRouter } from 'next/router';
import { PageHeader } from '../layout/PageHeader';
import { ExploreSectionTabs } from './ExploreSectionTabs';

// Shared v2 header for the Explore hub's directory pages (Tags, Sources,
// Leaderboard, Best of). Keeps the section-tab strip and its height
// (`!py-0`) consistent in one place. Optional children render as header
// actions (e.g. the "Suggest source" button).
// The Explore hub sections live in the sidebar's Explore panel, so the page
// header is just the standard title strip (same as Analytics / Settings) —
// no breadcrumb, no icon. The title is derived from the route.
const hubTitles: { match: (path: string) => boolean; label: string }[] = [
{ match: (path) => path.startsWith('/sources'), label: 'Sources' },
{ match: (path) => path.startsWith('/users'), label: 'Leaderboard' },
];

// Shared v2 header for the Explore hub's directory pages (Sources, Leaderboard).
// Optional children render as header actions (e.g. "Suggest source").
export function ExploreHubHeader({
children,
}: {
children?: ReactNode;
}): ReactElement {
const router = useRouter();
// asPath-first (the resolved URL) — consistent with FeedExploreTabs and
// correct for dynamic routes where pathname is the template.
const path = (router.asPath || router.pathname || '').split('?')[0];
const title =
hubTitles.find((entry) => entry.match(path))?.label ?? 'Explore';

return (
<PageHeader title={<ExploreSectionTabs />} className="!py-0">
<PageHeader title={title} className="!py-0">
{children}
</PageHeader>
);
Expand Down
70 changes: 0 additions & 70 deletions packages/shared/src/components/header/ExploreSectionTabs.tsx

This file was deleted.

4 changes: 2 additions & 2 deletions packages/shared/src/components/header/FeedExploreHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import React from 'react';
import { useRouter } from 'next/router';
import classNames from 'classnames';
import { BreadCrumbs } from './BreadCrumbs';
import { CalendarIcon, HotIcon } from '../icons';
import { CalendarIcon, CompassIcon } from '../icons';
import { IconSize } from '../Icon';
import TabList from '../tabs/TabList';
import { Tab, TabContainer } from '../tabs/TabContainer';
Expand Down Expand Up @@ -91,7 +91,7 @@ export function FeedExploreHeader({
isListMode && 'tablet:pt-4 laptop:pt-5',
)}
>
<HotIcon size={IconSize.XSmall} secondary /> Explore
<CompassIcon size={IconSize.XSmall} secondary /> Explore
</BreadCrumbs>
)}
<div
Expand Down
Original file line number Diff line number Diff line change
@@ -1,68 +1,67 @@
import type { ReactElement } from 'react';
import React from 'react';
import { useRouter } from 'next/router';
import { ButtonSize, ButtonVariant } from '../buttons/Button';
import {
SquadDirectoryNavbar,
SquadDirectoryNavbarItem,
} from '../squads/layout/SquadDirectoryNavbar';
import { Dropdown } from '../fields/Dropdown';
import { CalendarIcon } from '../icons';
import { IconSize } from '../Icon';
import { ButtonSize, ButtonVariant } from '../buttons/Button';
import { ExploreTabs, tabToUrl, urlToTab } from './FeedExploreHeader';
import { ExploreTabs, urlToTab } from './FeedExploreHeader';
import { QueryStateKeys, useQueryState } from '../../hooks/utils/useQueryState';
import { periodTexts } from '../layout/common';

const sortLabels = Object.values(ExploreTabs);
const sortsWithPeriod: ExploreTabs[] = [
ExploreTabs.MostUpvoted,
ExploreTabs.BestDiscussions,
];

// v2 Explore: switch the feed's ranking via a "Sort" dropdown rather than a
// second row of tabs — sorting a feed isn't navigating to a sibling page, so
// a dropdown reads cleaner (Reddit/GitHub pattern). Each sort is still its
// own route, so selecting one navigates.
export function ExploreSortDropdown(): ReactElement {
// Explore sort tabs rendered with the same pill navbar as the Tags / Squad
// directory pages (SquadDirectoryNavbar), instead of the underlined TabContainer
// — so the look-and-feel matches the rest of the v2 directory headers. The
// date-range filter stays as a compact icon dropdown for the applicable sorts.
export function FeedExploreTabs(): ReactElement {
const router = useRouter();
const currentPath = (router.asPath || router.pathname).split('?')[0];
const activeTab = urlToTab[currentPath] ?? ExploreTabs.Popular;
const selectedIndex = Math.max(0, sortLabels.indexOf(activeTab));
const [period, setPeriod] = useQueryState({
key: [QueryStateKeys.FeedPeriod],
defaultValue: 0,
});

return (
<span className="ml-auto flex items-center gap-2">
<div className="flex w-full min-w-0 items-center gap-2">
<SquadDirectoryNavbar
aria-label="Explore sort"
className="!mx-0 min-w-0 flex-1 !border-0 !px-0"
>
{Object.entries(urlToTab).map(([url, label]) => (
<SquadDirectoryNavbarItem
key={label}
buttonSize={ButtonSize.Small}
isActive={currentPath === url}
label={label}
path={url}
ariaLabel={`Show ${label}`}
/>
))}
</SquadDirectoryNavbar>
{sortsWithPeriod.includes(activeTab) && (
<Dropdown
iconOnly
shouldIndicateSelected
icon={<CalendarIcon size={IconSize.Small} />}
buttonSize={ButtonSize.Small}
buttonVariant={ButtonVariant.Float}
// Render the date filter as a true icon-only square button (the
// shared Dropdown otherwise lays its trigger out as a full-width
// value field). Matches the design system's icon-only Small spec
// (`IconOnlySizeToClassName`) and the v2 layout's compact icon
// buttons: 32px square, rounded-10, no padding.
className={{ button: '!size-8 !rounded-10 !p-0' }}
selectedIndex={period}
options={periodTexts}
onChange={(_, index) => setPeriod(index)}
buttonAriaLabel="Filter by date range"
/>
)}
<Dropdown
selectedIndex={selectedIndex}
options={sortLabels}
buttonSize={ButtonSize.Small}
buttonVariant={ButtonVariant.Float}
buttonAriaLabel="Sort posts"
onChange={(value) => {
const url = tabToUrl[value as ExploreTabs];
if (url) {
router.push(url).catch(() => undefined);
}
}}
/>
</span>
</div>
);
}
8 changes: 8 additions & 0 deletions packages/shared/src/components/icons/Compass/filled.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 12 additions & 0 deletions packages/shared/src/components/icons/Compass/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { ReactElement } from 'react';
import React from 'react';
import type { IconProps } from '../../Icon';
import Icon from '../../Icon';
import OutlinedIcon from './outlined.svg';
import FilledIcon from './filled.svg';

export const CompassIcon = (props: IconProps): ReactElement => (
<Icon {...props} IconPrimary={OutlinedIcon} IconSecondary={FilledIcon} />
);

export default CompassIcon;
5 changes: 5 additions & 0 deletions packages/shared/src/components/icons/Compass/outlined.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions packages/shared/src/components/icons/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export * from './Codeberg';
export * from './CodePen';
export * from './Coin';
export * from './CommunityPicksIcon';
export * from './Compass';
export * from './Cookie';
export * from './Copy';
export * from './Core';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,15 @@ function NotificationsBell({
<a
href={`${webappUrl}notifications`}
aria-label="Notifications"
// It's a tab in the rail tablist; the role makes `aria-selected`
// valid and lets the v2 rail's shared sliding pill track this tab
// like the others (the pill renders the selected background, so here
// we only own the active text color).
role="tab"
aria-selected={atNotificationsPage}
className={classNames(
railTabClass,
atNotificationsPage && 'bg-background-default !text-text-primary',
atNotificationsPage && '!text-text-primary',
)}
onClick={onNavigateNotifications}
>
Expand All @@ -84,7 +90,7 @@ function NotificationsBell({
)}
</span>
{!railHideLabel && (
<span className={railTabLabelClass}>Alerts</span>
<span className={railTabLabelClass}>Activity</span>
)}
</a>
</Link>
Expand Down
21 changes: 14 additions & 7 deletions packages/shared/src/components/notifications/Toast.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,11 @@ const Toast = ({
return;
}

// Auto-dismiss (and the countdown ring) only when the setting is on and the
// toast isn't explicitly persistent; otherwise it stays until dismissed.
const shouldAutoDismiss = autoDismissNotifications && !toast.persistent;
// Auto-dismiss (and the countdown ring) when the setting is on — or a toast
// forces it — and the toast isn't explicitly persistent; otherwise it stays
// until dismissed.
const shouldAutoDismiss =
(autoDismissNotifications || toast.forceAutoDismiss) && !toast.persistent;

if (!toastRef.current) {
toastRef.current = toast;
Expand Down Expand Up @@ -133,9 +135,13 @@ const Toast = ({
return;
}

// No running countdown when auto-dismiss is off or the toast is persistent,
// so clear directly; otherwise let the timed animation play out.
if (!autoDismissNotifications || acted.persistent) {
// No running countdown when auto-dismiss is off (and not forced) or the
// toast is persistent, so clear directly; otherwise let the timed animation
// play out.
if (
(!autoDismissNotifications && !acted.forceAutoDismiss) ||
acted.persistent
) {
toastRef.current = null;
client.setQueryData(TOAST_NOTIF_KEY, null);
return;
Expand Down Expand Up @@ -192,7 +198,8 @@ const Toast = ({
// The dismiss ring is the auto-dismiss countdown made visible, so it shows
// exactly when the toast will auto-dismiss (setting on + not persistent).
// dashoffset drains 0→100 as the remaining time elapses.
const shouldAutoDismiss = autoDismissNotifications && !isPersistentToast;
const shouldAutoDismiss =
(autoDismissNotifications || toast.forceAutoDismiss) && !isPersistentToast;
const showRing = shouldAutoDismiss && toast.timer > 0;
const remaining = toast.timer > 0 ? (timer / toast.timer) * 100 : 0;
const dashoffset = Math.min(100, Math.max(0, 100 - remaining));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -225,11 +225,6 @@ const useAccountPageItems = ({ onClose }: { onClose?: () => void } = {}) => {
icon: EyeIcon,
href: `${settingsUrl}/customization/gamification`,
},
streaks: {
title: 'Streaks',
icon: HotIcon,
href: `${settingsUrl}/customization/streaks`,
},
...(!optOutAchievements && {
achievements: {
title: 'Achievements',
Expand Down
Loading
Loading