diff --git a/apps/web/src/app/(dynamicPages)/community/[community]/[tag]/loading.tsx b/apps/web/src/app/(dynamicPages)/community/[community]/[tag]/loading.tsx index 55e0116f68..d0a15f11e7 100644 --- a/apps/web/src/app/(dynamicPages)/community/[community]/[tag]/loading.tsx +++ b/apps/web/src/app/(dynamicPages)/community/[community]/[tag]/loading.tsx @@ -1,9 +1,5 @@ -import { EntryListLoadingItem } from "@/features/shared/entry-list-loading-item"; +import { ReadingListLoading } from "@/features/shared/reading-layout/reading-list-loading"; export default function Loading() { - return ( -
- -
- ); + return ; } diff --git a/apps/web/src/app/(dynamicPages)/community/[community]/_components/community-menu/index.tsx b/apps/web/src/app/(dynamicPages)/community/[community]/_components/community-menu/index.tsx index 8e14977a07..4d5cafd692 100644 --- a/apps/web/src/app/(dynamicPages)/community/[community]/_components/community-menu/index.tsx +++ b/apps/web/src/app/(dynamicPages)/community/[community]/_components/community-menu/index.tsx @@ -33,7 +33,10 @@ export const CommunityMenu = (props: Props) => { if (filter === EntryFilter.trending) { newLabel = i18next.t("community.posts"); } else if (menuItems.some((item) => item === filter)) { - newLabel = i18next.t(`entry-filter.filter-${filter}`); + newLabel = + filter === EntryFilter.payout + ? i18next.t("g.payout") + : i18next.t(`entry-filter.filter-${filter}`); } else if (label && !newLabel) { newLabel = label; } else { @@ -45,22 +48,23 @@ export const CommunityMenu = (props: Props) => { const isFilterInItems = () => menuItems.some((item) => filter === item); return ( - + {menuItems.map((x) => ( - - {i18next.t(`entry-filter.filter-${x}`)} - + + {x === EntryFilter.payout + ? i18next.t("g.payout") + : i18next.t(`entry-filter.filter-${x}`)} + ))} - + {menuItems .map((x) => ({ - label: i18next.t(`entry-filter.filter-${x}`), + label: + x === EntryFilter.payout + ? i18next.t("g.payout") + : i18next.t(`entry-filter.filter-${x}`), href: `/${x}/${props.community.name}`, selected: filter === x })) diff --git a/apps/web/src/app/(dynamicPages)/community/[community]/community.scss b/apps/web/src/app/(dynamicPages)/community/[community]/community.scss index 25e04ec530..b25e5510e4 100644 --- a/apps/web/src/app/(dynamicPages)/community/[community]/community.scss +++ b/apps/web/src/app/(dynamicPages)/community/[community]/community.scss @@ -22,16 +22,5 @@ .content-side { flex-grow: 1; padding-bottom: 60px; - - @media (min-width: $lg-break) { - .entry-list { - .entry-list-body.grid-view { - align-content: stretch; - display: flex; - flex-wrap: wrap; - justify-content: flex-start; - } - } - } } } diff --git a/apps/web/src/app/(dynamicPages)/community/[community]/layout.tsx b/apps/web/src/app/(dynamicPages)/community/[community]/layout.tsx index e658b1e592..c49e83b31e 100644 --- a/apps/web/src/app/(dynamicPages)/community/[community]/layout.tsx +++ b/apps/web/src/app/(dynamicPages)/community/[community]/layout.tsx @@ -1,3 +1,4 @@ +import "@/features/shared/reading-layout/reading-layout.scss"; import { Feedback } from "@/features/shared/feedback"; import { Navbar } from "@/features/shared/navbar"; import { ScrollToTop } from "@/features/shared/scroll-to-top"; @@ -28,12 +29,12 @@ export default async function CommunityPageLayout({ children, params }: PropsWit ]); return ( - <> +
-
+
{account && communityData && ( @@ -50,10 +51,12 @@ export default async function CommunityPageLayout({ children, params }: PropsWit )}
{communityData && } - {communityData && account && } + {communityData && account && ( + + )} {children}
- +
); } diff --git a/apps/web/src/app/(dynamicPages)/entry/[category]/[author]/[permlink]/layout.tsx b/apps/web/src/app/(dynamicPages)/entry/[category]/[author]/[permlink]/layout.tsx index cc9f1b4a88..810c398da7 100644 --- a/apps/web/src/app/(dynamicPages)/entry/[category]/[author]/[permlink]/layout.tsx +++ b/apps/web/src/app/(dynamicPages)/entry/[category]/[author]/[permlink]/layout.tsx @@ -11,7 +11,7 @@ export default function EntryPageLayout(props: PropsWithChildren) { - + {props.children} ); diff --git a/apps/web/src/app/(dynamicPages)/feed/[...sections]/entry-index.scss b/apps/web/src/app/(dynamicPages)/feed/[...sections]/entry-index.scss index bce6960b75..74ac1e99f6 100644 --- a/apps/web/src/app/(dynamicPages)/feed/[...sections]/entry-index.scss +++ b/apps/web/src/app/(dynamicPages)/feed/[...sections]/entry-index.scss @@ -13,7 +13,6 @@ } @media (min-width: $lg-break) { - } } @@ -33,17 +32,6 @@ border-bottom-color: transparent; } } - - @media (min-width: $lg-break) { - .entry-list { - .entry-list-body.grid-view { - align-content: stretch; - display: flex; - flex-wrap: wrap; - justify-content: flex-start; - } - } - } } .side-menu { @@ -133,4 +121,4 @@ .pt-6 { padding-top: 6rem !important; -} \ No newline at end of file +} diff --git a/apps/web/src/app/(dynamicPages)/feed/[...sections]/loading.tsx b/apps/web/src/app/(dynamicPages)/feed/[...sections]/loading.tsx index 901feea06e..2624ca2ea7 100644 --- a/apps/web/src/app/(dynamicPages)/feed/[...sections]/loading.tsx +++ b/apps/web/src/app/(dynamicPages)/feed/[...sections]/loading.tsx @@ -1,13 +1,5 @@ -import { EntryListLoadingItem } from "@/features/shared/entry-list-loading-item"; -import { LinearProgress } from "@/features/shared/linear-progress"; +import { ReadingListLoading } from "@/features/shared/reading-layout/reading-list-loading"; export default function Loading() { - return ( -
-
- - -
-
- ); + return ; } diff --git a/apps/web/src/app/(dynamicPages)/feed/feed-reading.scss b/apps/web/src/app/(dynamicPages)/feed/feed-reading.scss new file mode 100644 index 0000000000..fdea5f10de --- /dev/null +++ b/apps/web/src/app/(dynamicPages)/feed/feed-reading.scss @@ -0,0 +1,260 @@ +@import "../../../styles/vars_mixins"; + +// Feed-specific column widths and sidebar treatments. +.feed-reading-layout { + max-width: 1440px; + gap: 28px; + padding-inline: 24px; + + .tags-side { + display: none; + width: 180px; + margin-right: 0; + padding-top: 16px; + + @media (min-width: 1024px) { + display: block; + } + } + + .entry-page-content { + width: 100%; + max-width: 840px; + margin-inline: auto; + } + + .side-menu { + width: 280px; + margin-left: 0; + padding-top: 16px; + } + + .feed-sidebar-heading { + color: var(--feed-muted); + font-size: 13px; + font-weight: 600; + line-height: 1.5; + margin-bottom: 16px; + } + + .feed-sidebar-link { + @apply text-blue-dark-sky; + + display: inline-flex; + align-items: center; + min-height: 40px; + margin-top: 12px; + padding: 0; + font-size: 14px; + + &:hover { + text-decoration: underline; + } + } + + .feed-favorites a:not(.feed-sidebar-link) { + @apply text-gray-700 dark:text-gray-300; + } + + .feed-topics { + margin-top: 24px; + padding-top: 24px; + border-top: 1px solid var(--border-color); + } + + .feed-topics:first-child { + margin-top: 0; + padding-top: 0; + border-top: 0; + } + + .feed-topic-list { + display: flex; + flex-direction: column; + gap: 4px; + margin: 0; + padding: 0; + list-style: none; + } + + .feed-topic-row { + display: flex; + align-items: center; + min-width: 0; + gap: 4px; + + > a { + @apply text-gray-700 dark:text-gray-300; + + display: flex; + align-items: center; + gap: 10px; + min-width: 0; + min-height: 36px; + flex: 1; + font-size: 14px; + + &:hover, + &[aria-current="page"] { + @apply text-blue-dark-sky; + } + } + + > [role="button"], + .feed-topic-dismiss { + display: inline-flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + width: 28px; + height: 32px; + margin: 0; + } + } + + .feed-community-heading { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; + + > button { + flex-shrink: 0; + } + } + + .feed-community-name { + @apply text-gray-800 dark:text-gray-200; + + display: flex; + align-items: center; + gap: 8px; + flex: 1; + min-width: 120px; + font-size: 15px; + font-weight: 600; + line-height: 1.4; + overflow-wrap: anywhere; + + .user-avatar { + flex-shrink: 0; + } + &:hover { + @apply text-blue-dark-sky; + } + } + + .feed-community-about { + @apply line-clamp-2; + + color: var(--feed-muted); + margin: 8px 0 4px; + font-size: 14px; + line-height: 1.5; + overflow-wrap: anywhere; + } + + .feed-community-members { + color: var(--feed-muted); + font-size: 12px; + line-height: 1.5; + } + + .feed-community-create { + margin-top: 16px; + border-top: 1px solid var(--border-color); + } + + a:focus-visible, + button:focus-visible, + [tabindex="0"]:focus-visible { + outline: 2px solid #357ce6 !important; + outline-offset: 3px; + } + + @media (max-width: 639px) { + padding-inline: 16px; + } +} + +.feed-navigation { + display: flex; + align-items: flex-start; + justify-content: space-between; + flex-wrap: wrap; + gap: 8px 16px; + min-height: 56px; + padding-block: 4px; + border-bottom: 1px solid var(--border-color); + + .feed-navigation-desktop { + flex: 1; + min-width: 0; + } + .feed-source-tabs, + .feed-sort-tabs { + display: flex; + flex-wrap: wrap; + gap: 0 4px; + margin: 0; + padding: 0; + list-style: none; + li { + min-width: 0; + } + } + .feed-sort-controls, + .feed-navigation-mobile { + align-items: center; + flex-wrap: wrap; + } + .feed-sort-controls { + display: flex; + } + .feed-tab { + border-bottom: 2px solid transparent; + overflow-wrap: anywhere; + } + .feed-tab-selected { + @apply text-blue-dark-sky dark:text-blue-dark-sky; + + border-bottom-color: currentcolor; + font-weight: 600; + } + .feed-reblog-toggle { + color: var(--feed-muted); + display: flex; + align-items: center; + gap: 10px; + min-height: 44px; + padding: 4px 0; + font-size: 13px; + cursor: pointer; + margin-left: auto; + } + .feed-switch-track { + @apply bg-gray-300 dark:bg-gray-600; + + display: inline-flex; + align-items: center; + flex-shrink: 0; + width: 34px; + height: 20px; + padding: 2px; + border-radius: 20px; + } + .feed-switch-thumb { + width: 16px; + height: 16px; + border-radius: 50%; + background: white; + } + [aria-checked="true"] { + .feed-switch-track { + @apply bg-blue-dark-sky; + } + .feed-switch-thumb { + transform: translateX(14px); + } + } +} diff --git a/apps/web/src/app/(dynamicPages)/feed/layout.tsx b/apps/web/src/app/(dynamicPages)/feed/layout.tsx index 545ea64947..f1d07fe5bb 100644 --- a/apps/web/src/app/(dynamicPages)/feed/layout.tsx +++ b/apps/web/src/app/(dynamicPages)/feed/layout.tsx @@ -1,3 +1,4 @@ +import "@/features/shared/reading-layout/reading-layout.scss"; import { TrendingTagsCard } from "@/app/_components/trending-tags-card"; import { EntryIndexMenu } from "@/app/_components/entry-index-menu"; import React, { PropsWithChildren } from "react"; @@ -9,15 +10,16 @@ import { Feedback } from "@/features/shared/feedback"; import { Navbar } from "@/features/shared/navbar"; import { ScrollToTop } from "@/features/shared/scroll-to-top"; import { Theme } from "@/features/shared/theme"; +import "./feed-reading.scss"; export default function FeedLayout({ children }: PropsWithChildren) { return ( - <> +
-
+
@@ -33,6 +35,6 @@ export default function FeedLayout({ children }: PropsWithChildren) {
- +
); } diff --git a/apps/web/src/app/(dynamicPages)/profile/[username]/[section]/loading.tsx b/apps/web/src/app/(dynamicPages)/profile/[username]/[section]/loading.tsx index 55e0116f68..d0a15f11e7 100644 --- a/apps/web/src/app/(dynamicPages)/profile/[username]/[section]/loading.tsx +++ b/apps/web/src/app/(dynamicPages)/profile/[username]/[section]/loading.tsx @@ -1,9 +1,5 @@ -import { EntryListLoadingItem } from "@/features/shared/entry-list-loading-item"; +import { ReadingListLoading } from "@/features/shared/reading-layout/reading-list-loading"; export default function Loading() { - return ( -
- -
- ); + return ; } diff --git a/apps/web/src/app/(dynamicPages)/profile/[username]/_components/profile-info/_index.scss b/apps/web/src/app/(dynamicPages)/profile/[username]/_components/profile-info/_index.scss index cd78a58e78..97bdd36cbc 100644 --- a/apps/web/src/app/(dynamicPages)/profile/[username]/_components/profile-info/_index.scss +++ b/apps/web/src/app/(dynamicPages)/profile/[username]/_components/profile-info/_index.scss @@ -9,7 +9,7 @@ margin-bottom: 6px; display: flex; align-items: center; - flex-flow: row nowrap; + flex-flow: row wrap; svg { @apply text-white; diff --git a/apps/web/src/app/(dynamicPages)/profile/[username]/_components/profile-menu/index.tsx b/apps/web/src/app/(dynamicPages)/profile/[username]/_components/profile-menu/index.tsx index 1081c5cbbf..066aca416f 100644 --- a/apps/web/src/app/(dynamicPages)/profile/[username]/_components/profile-menu/index.tsx +++ b/apps/web/src/app/(dynamicPages)/profile/[username]/_components/profile-menu/index.tsx @@ -77,7 +77,7 @@ export function ProfileMenu({ username }: Props) { const allMenuItems = [...menuItems, ...kebabMenuItemsAll]; return ( - + item.id === section) @@ -87,16 +87,12 @@ export function ProfileMenu({ username }: Props) { isSelected={false} > {dropDownMenuItems.map((item) => ( - - {item.label} - + + {item.label} + ))} - + {menuItems.map((menuItem) => ( {kebabMenuItems.map((item) => ( - - {item.label} - + + {item.label} + ))} diff --git a/apps/web/src/app/(dynamicPages)/profile/[username]/_components/profile-search.tsx b/apps/web/src/app/(dynamicPages)/profile/[username]/_components/profile-search.tsx index 0666e67fdf..433293ae40 100644 --- a/apps/web/src/app/(dynamicPages)/profile/[username]/_components/profile-search.tsx +++ b/apps/web/src/app/(dynamicPages)/profile/[username]/_components/profile-search.tsx @@ -57,7 +57,7 @@ export function ProfileSearch({ username }: Props) { return (
diff --git a/apps/web/src/app/(dynamicPages)/profile/[username]/layout.tsx b/apps/web/src/app/(dynamicPages)/profile/[username]/layout.tsx index bd96be4270..6e6e86aba1 100644 --- a/apps/web/src/app/(dynamicPages)/profile/[username]/layout.tsx +++ b/apps/web/src/app/(dynamicPages)/profile/[username]/layout.tsx @@ -1,3 +1,4 @@ +import "@/features/shared/reading-layout/reading-layout.scss"; import { ProfileCard, ProfileMenu, ProfileSearch } from "./_components"; import { PropsWithChildren } from "react"; import { getAccountFullQueryOptions } from "@ecency/sdk"; @@ -21,17 +22,13 @@ export default async function ProfileLayout({ children, params }: Props) { const account = await prefetchQuery(getAccountFullQueryOptions(username)); return ( - <> +
- -
-
-
-
+ +
+
{account ? : } {account && } @@ -43,6 +40,6 @@ export default async function ProfileLayout({ children, params }: Props) { {children}
- +
); } diff --git a/apps/web/src/app/(dynamicPages)/profile/[username]/loading.tsx b/apps/web/src/app/(dynamicPages)/profile/[username]/loading.tsx index 55e0116f68..d0a15f11e7 100644 --- a/apps/web/src/app/(dynamicPages)/profile/[username]/loading.tsx +++ b/apps/web/src/app/(dynamicPages)/profile/[username]/loading.tsx @@ -1,9 +1,5 @@ -import { EntryListLoadingItem } from "@/features/shared/entry-list-loading-item"; +import { ReadingListLoading } from "@/features/shared/reading-layout/reading-list-loading"; export default function Loading() { - return ( -
- -
- ); + return ; } diff --git a/apps/web/src/app/_components/entry-index-menu/index.tsx b/apps/web/src/app/_components/entry-index-menu/index.tsx index 4a97750028..f238a989f3 100644 --- a/apps/web/src/app/_components/entry-index-menu/index.tsx +++ b/apps/web/src/app/_components/entry-index-menu/index.tsx @@ -4,7 +4,7 @@ import { useActiveAccount } from "@/core/hooks/use-active-account"; import React, { useCallback, useEffect, useMemo } from "react"; import "./_index.scss"; -import { kebabMenuHorizontalSvg, menuDownSvg } from "@ui/svg"; +import { menuDownSvg } from "@ui/svg"; import Link from "next/link"; import i18next from "i18next"; import { Dropdown, DropdownItem, DropdownMenu, DropdownToggle } from "@ui/dropdown"; @@ -18,9 +18,9 @@ import { useFeedMenu } from "@/app/_components/entry-index-menu/use-feed-menu"; -const PILL_CLASS = - "text-gray-steel hover:text-blue-dark-sky rounded-full flex items-center px-3 py-1.5"; -const PILL_SELECTED_CLASS = "bg-blue-dark-sky text-white hover:text-white"; +const TAB_CLASS = + "feed-tab flex items-center px-3 py-3 text-sm text-gray-600 dark:text-gray-400 hover:text-blue-dark-sky"; +const TAB_SELECTED_CLASS = "feed-tab-selected"; export function EntryIndexMenu() { const router = useRouter(); @@ -30,7 +30,7 @@ export function EntryIndexMenu() { const { activeUser } = useActiveAccount(); const prevActiveUser = usePrevious(activeUser); - const { sources, sorts, overflow, isFollowing } = useFeedMenu(); + const { sources, sorts, additionalFilters, isFollowing } = useFeedMenu(); const noReblog = useMemo(() => searchParams?.get("no-reblog") === "true", [searchParams]); @@ -42,17 +42,13 @@ export function EntryIndexMenu() { () => sources.find((s) => s.selected) ?? sources[sources.length - 1], [sources] ); - // Include overflow (Muted/Promoted) so the mobile trigger reflects the active - // filter even when it isn't one of the visible sort tabs. + // Include Muted/Promoted so the mobile trigger reflects the active + // filter when the dropdown is closed. const selectedSort = useMemo( - () => [...sorts, ...overflow].find((s) => s.selected), - [sorts, overflow] + () => [...sorts, ...additionalFilters].find((s) => s.selected), + [sorts, additionalFilters] ); - const reblogLabel = noReblog - ? i18next.t("entry-filter.filter-with-reblog") - : i18next.t("entry-filter.filter-no-reblog"); - // Logged-out users can't view a community ("/my") feed — fall back to global. useEffect(() => { const fallback = globalFeedFallbackPath(pathname); @@ -85,15 +81,15 @@ export function EntryIndexMenu() { router.push(qs ? `${pathname}?${qs}` : pathname); }, [noReblog, pathname, router, searchParams]); - const renderPill = (item: FeedMenuItem) => ( + const renderTab = (item: FeedMenuItem) => (
  • @@ -103,100 +99,71 @@ export function EntryIndexMenu() { ); const reblogToggle = ( - + ); return ( -
    -
    -
    -
    - {/* Desktop: Source group + Sort group (or reblog toggle for Following) */} -
    - {showSources && ( -
      - {sources.map(renderPill)} -
    - )} - {showSources && ( -
    - )} - {isFollowing ? ( - reblogToggle - ) : ( - <> -
      - {sorts.map(renderPill)} -
    -
    - - -
    - - )} -
    - - {/* Mobile/tablet: Source dropdown + Sort dropdown (or reblog toggle) */} -
    - {showSources && ( - - - - - - {sources.map((item, i) => ( - - {item.label} - - ))} - - - )} - {showSources && ( -
    - )} - {isFollowing ? ( - reblogToggle - ) : ( - - - - - - {[...sorts, ...overflow].map((item, i) => ( - - {item.label} - - ))} - - - )} -
    +
    +
    + {showSources && ( +
      + {sources.map(renderTab)} +
    + )} + {!isFollowing && ( +
    +
      + {[...sorts, ...additionalFilters].map(renderTab)} +
    -
    + )} +
    +
    + {showSources && ( + + + + + + {sources.map((item) => ( + + {item.label} + + ))} + + + )} + {!isFollowing && ( + + + + + + {[...sorts, ...additionalFilters].map((item) => ( + + {item.label} + + ))} + + + )}
    + {isFollowing && reblogToggle}
    ); } diff --git a/apps/web/src/app/_components/entry-index-menu/use-feed-menu.ts b/apps/web/src/app/_components/entry-index-menu/use-feed-menu.ts index 7c686b855e..9f3c34972d 100644 --- a/apps/web/src/app/_components/entry-index-menu/use-feed-menu.ts +++ b/apps/web/src/app/_components/entry-index-menu/use-feed-menu.ts @@ -25,8 +25,8 @@ const SORT_FILTERS: EntryFilter[] = [ * Splits the feed filter bar into two orthogonal axes: * - `sources`: where posts come from — Following, Communities, Global * (Following/Communities require an active user; Global is always present) - * - `sorts`: how Communities/Global posts are ranked — Trending, Hot, New, Top - * - `overflow`: niche views surfaced behind a "more" menu — Muted, Promoted + * - `sorts`: how Communities/Global posts are ranked — Trending, Hot, New, Payout + * - `additionalFilters`: additional feed views — Muted, Promoted * * URL scheme is unchanged: Following → /@user/feed (rewritten to the feed * route), Communities → /{sort}/my, Global → /{sort}. @@ -77,7 +77,7 @@ export function useFeedMenu() { const isGlobal = !isFollowing && !isCommunities && !hasTag; // Sort to carry when switching between Communities and Global. Following has no - // sort, and overflow views (muted/promoted) aren't real sorts, so default to Hot. + // sort, and additional views (muted/promoted) aren't real sorts, so default to Hot. const currentSort = SORT_FILTERS.includes(filter as EntryFilter) ? (filter as EntryFilter) : EntryFilter.hot; @@ -128,7 +128,16 @@ export function useFeedMenu() { }); return items; - }, [activeUser, currentSort, hasTag, isCommunities, isFollowing, isGlobal, normalizedTag, router]); + }, [ + activeUser, + currentSort, + hasTag, + isCommunities, + isFollowing, + isGlobal, + normalizedTag, + router + ]); const sorts: FeedMenuItem[] = useMemo(() => { // Preserve the current source context when changing sort: @@ -145,9 +154,7 @@ export function useFeedMenu() { const href = `/${x}${tagSegment ? `/${tagSegment}` : ""}`; return { label: - x === EntryFilter.payout - ? i18next.t("entry-filter.filter-top") - : i18next.t(`entry-filter.filter-${x}`), + x === EntryFilter.payout ? i18next.t("g.payout") : i18next.t(`entry-filter.filter-${x}`), href, selected: (filter as EntryFilter) === x, id: x, @@ -156,7 +163,7 @@ export function useFeedMenu() { }); }, [filter, isCommunities, normalizedTag, router]); - const overflow: FeedMenuItem[] = useMemo( + const additionalFilters: FeedMenuItem[] = useMemo( () => [ { label: i18next.t("entry-filter.filter-muted"), @@ -177,7 +184,7 @@ export function useFeedMenu() { ); return useMemo( - () => ({ sources, sorts, overflow, isFollowing }), - [isFollowing, overflow, sorts, sources] + () => ({ sources, sorts, additionalFilters, isFollowing }), + [isFollowing, additionalFilters, sorts, sources] ); } diff --git a/apps/web/src/app/_components/my-favorites-widget/index.tsx b/apps/web/src/app/_components/my-favorites-widget/index.tsx index 8d003733d3..ed53b0ac31 100644 --- a/apps/web/src/app/_components/my-favorites-widget/index.tsx +++ b/apps/web/src/app/_components/my-favorites-widget/index.tsx @@ -16,14 +16,13 @@ const MAX_VISIBLE = 5; export function MyFavoritesWidget() { const { activeUser } = useActiveAccount(); const username = activeUser?.username; - const accessToken = useMemo( - () => (username ? getAccessToken(username) : undefined), - [username] - ); + const accessToken = useMemo(() => (username ? getAccessToken(username) : undefined), [username]); - const { data: favorites, isLoading, isError } = useQuery( - getFavoritesQueryOptions(username, accessToken) - ); + const { + data: favorites, + isLoading, + isError + } = useQuery(getFavoritesQueryOptions(username, accessToken)); const [showDialog, setShowDialog] = useState(false); @@ -32,8 +31,8 @@ export function MyFavoritesWidget() { } return ( -
    -
    {i18next.t("my-favorites-widget.title")}
    +
    +

    {i18next.t("my-favorites-widget.title")}

    {isLoading && new Array(MAX_VISIBLE).fill(0).map((_, i) => ( @@ -67,10 +66,9 @@ export function MyFavoritesWidget() {
    {!isLoading && favorites && favorites.length > MAX_VISIBLE && ( + + {i18next.t("top-communities.explore")} +
    + + {i18next.t("top-communities.create-button")} + +
    ); }; diff --git a/apps/web/src/app/_components/trending-tags-card/index.tsx b/apps/web/src/app/_components/trending-tags-card/index.tsx index e5eecb7794..b4a37dfe19 100644 --- a/apps/web/src/app/_components/trending-tags-card/index.tsx +++ b/apps/web/src/app/_components/trending-tags-card/index.tsx @@ -3,16 +3,16 @@ import { useActiveAccount } from "@/core/hooks/use-active-account"; import { FollowTagChipToggle, useFollowedTags } from "@/features/shared/follow-tag-btn"; -import { TagLink } from "@/features/shared/tag"; +import { makePathTag } from "@/features/shared/tag"; import { getAccessToken } from "@/utils"; -import { Button } from "@ui/button"; import { getTrendingTagsQueryOptions } from "@ecency/sdk"; import { useInfiniteQuery } from "@tanstack/react-query"; import { UilMultiply } from "@tooni/iconscout-unicons-react"; import i18next from "i18next"; import Link from "next/link"; +import { IntentLink } from "@/features/shared/intent-link"; import { useParams, useRouter } from "next/navigation"; -import { Fragment, useCallback, useMemo } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import "./_index.scss"; export function TrendingTagsCard() { @@ -27,86 +27,95 @@ export function TrendingTagsCard() { const { activeUser } = useActiveAccount(); const username = activeUser?.username; - const accessToken = useMemo( - () => (username ? getAccessToken(username) : undefined), - [username] - ); + const accessToken = useMemo(() => (username ? getAccessToken(username) : undefined), [username]); - const { data: trendingTagsPages } = useInfiniteQuery(getTrendingTagsQueryOptions(250)); + const { + data: trendingTagsPages, + isLoading, + isError + } = useInfiniteQuery(getTrendingTagsQueryOptions(250)); // The user's followed tags come first, then the trending list without them, // so a followed topic is one click away and never listed twice. const { tags: favoriteTags } = useFollowedTags(username, accessToken); const pinnedTags = useMemo(() => favoriteTags?.map((f) => f.tag) ?? [], [favoriteTags]); - const trendingTags = useMemo(() => { - const first = trendingTagsPages?.pages[0]; - if (!first) { - return first; + const firstTrendingPage = trendingTagsPages?.pages[0]; + const [shuffledTags, setShuffledTags] = useState(null); + + // Shuffle after hydration, then keep suggestions steady during local interactions. + useEffect(() => { + const shuffled = [...(firstTrendingPage ?? [])]; + for (let i = shuffled.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; } + setShuffledTags(shuffled); + }, [firstTrendingPage]); + + const trendingTags = useMemo(() => { const pinned = new Set(pinnedTags); - return [...pinnedTags, ...first.filter((t) => !pinned.has(t))]; - }, [pinnedTags, trendingTagsPages?.pages]); + return [ + ...pinnedTags, + ...(shuffledTags ?? firstTrendingPage ?? []).filter((t) => !pinned.has(t)) + ]; + }, [pinnedTags, shuffledTags, firstTrendingPage]); const handleUnselection = useCallback(() => { router.push("/" + filter + ((activeUser && activeUser.username && "/my") || "")); }, [activeUser, filter, router]); - return ( -
    -

    {i18next.t("trending-tags.title")}

    + // Keep the current topic reachable even when it falls outside the short list. + const visibleTags = useMemo(() => { + // Following URLs rewrite to /feed/feed/%40username; that segment is an account, + // while "my" and "global" identify feed sources rather than hashtags. + const selected = + filter !== "feed" && tag && tag !== "my" && tag !== "global" && !/^(@|%40)/i.test(tag) + ? tag + : undefined; + return [...new Set([...(selected ? [selected] : []), ...(trendingTags ?? [])])].slice(0, 6); + }, [filter, tag, trendingTags]); -
    - {trendingTags?.slice(0, 30).map((t) => ( - -
    - - <> - {t} - {activeUser && } - {tag === t && ( -
    { - e.stopPropagation(); - e.preventDefault(); - handleUnselection(); - }} - onKeyDown={(e) => { - if (e.key === "Enter" || e.key === " ") { - e.stopPropagation(); - e.preventDefault(); - handleUnselection(); - } - }} - > - -
    - )} - -
    -
    -
    + return ( +
    +

    {i18next.t("trending-tags.title")}

    +
      + {visibleTags.map((t) => ( +
    • + + + {t} + + {activeUser && } + {tag === t && ( + + )} +
    • ))} - {trendingTags?.length === 0 && - Array.from(new Array(30).keys()).map((i) => ( -
      ( +
      - {trendingTags && trendingTags.length > 0 && ( - - - +
    + {isError && visibleTags.length === 0 && ( +

    {i18next.t("g.server-error")}

    )} + + {i18next.t("trending-tags.explore")} +
    ); } diff --git a/apps/web/src/app/communities/layout.tsx b/apps/web/src/app/communities/layout.tsx index b51c037e0f..9df68b1335 100644 --- a/apps/web/src/app/communities/layout.tsx +++ b/apps/web/src/app/communities/layout.tsx @@ -17,7 +17,7 @@ export default function Layout(props: PropsWithChildren) { <> - +
    diff --git a/apps/web/src/app/discover/layout.tsx b/apps/web/src/app/discover/layout.tsx index 3eb9e78a0e..1022f9dfdc 100644 --- a/apps/web/src/app/discover/layout.tsx +++ b/apps/web/src/app/discover/layout.tsx @@ -23,7 +23,7 @@ export default function Layout( - +
    - +
    {children}
    ); diff --git a/apps/web/src/app/hosting/layout.tsx b/apps/web/src/app/hosting/layout.tsx index e6a47d7696..1f96d00c81 100644 --- a/apps/web/src/app/hosting/layout.tsx +++ b/apps/web/src/app/hosting/layout.tsx @@ -10,7 +10,7 @@ export default function Layout({ children }: PropsWithChildren) { return (
    - +
    {children}
    ); diff --git a/apps/web/src/app/newsletter/layout.tsx b/apps/web/src/app/newsletter/layout.tsx index c383ec9208..fc184e18c6 100644 --- a/apps/web/src/app/newsletter/layout.tsx +++ b/apps/web/src/app/newsletter/layout.tsx @@ -7,7 +7,7 @@ export default function Layout({ children }: PropsWithChildren) { return (
    - +
    {children}
    ); diff --git a/apps/web/src/app/perks/layout.tsx b/apps/web/src/app/perks/layout.tsx index 68a57561fd..8a4bca33cd 100644 --- a/apps/web/src/app/perks/layout.tsx +++ b/apps/web/src/app/perks/layout.tsx @@ -7,7 +7,7 @@ export default function Layout({ children }: PropsWithChildren) { return (
    - +
    {children} diff --git a/apps/web/src/app/publish/layout.tsx b/apps/web/src/app/publish/layout.tsx index b23464a687..8802f5ac57 100644 --- a/apps/web/src/app/publish/layout.tsx +++ b/apps/web/src/app/publish/layout.tsx @@ -14,7 +14,7 @@ export default function Layout({ children }: PropsWithChildren) { - +
    {children}
    diff --git a/apps/web/src/app/signup/_components/signup-layout-client.tsx b/apps/web/src/app/signup/_components/signup-layout-client.tsx index 21655fcef9..bb1a19e1d2 100644 --- a/apps/web/src/app/signup/_components/signup-layout-client.tsx +++ b/apps/web/src/app/signup/_components/signup-layout-client.tsx @@ -31,7 +31,7 @@ export function SignupLayoutClient({ children }: PropsWithChildren) { return (
    - + {/* Hoisted into by React. The hero below must stay lazy: its container is display:none under md, and a lazy image with no box diff --git a/apps/web/src/app/wallet/setup-external/page.tsx b/apps/web/src/app/wallet/setup-external/page.tsx index 8653f42ff3..9fcec79a81 100644 --- a/apps/web/src/app/wallet/setup-external/page.tsx +++ b/apps/web/src/app/wallet/setup-external/page.tsx @@ -50,7 +50,7 @@ export default function WalletSetupExternalPage() { - +
    diff --git a/apps/web/src/app/waves/layout.tsx b/apps/web/src/app/waves/layout.tsx index b53686de46..94c364c041 100644 --- a/apps/web/src/app/waves/layout.tsx +++ b/apps/web/src/app/waves/layout.tsx @@ -64,7 +64,7 @@ export default function WavesLayout(props: PropsWithChildren) {
    - +
    {isWaveDetails && waveAuthor ? ( diff --git a/apps/web/src/features/i18n/locales/en-US.json b/apps/web/src/features/i18n/locales/en-US.json index 51acc044a6..8c78b172e1 100644 --- a/apps/web/src/features/i18n/locales/en-US.json +++ b/apps/web/src/features/i18n/locales/en-US.json @@ -180,6 +180,7 @@ "view-on-x": "View on X" }, "g": { + "dismiss": "Dismiss", "now": "Now", "all": "All", "add": "Add", @@ -347,7 +348,6 @@ "filter-community": "My Community", "source-label": "Feed source", "sort-label": "Sort by", - "more-filters": "More filters", "filter-controversial": "Controversial", "filter-rising": "Rising", "filter-global-part1": "This feed contains ", @@ -362,12 +362,15 @@ "filter-year": "This year", "filter-alltime": "All time", "filter-no-reblog": "Exclude reblog", + "show-reblogs": "Show reblogs", "filter-with-reblog": "Include reblog" }, "navbar": { "discover": "Discover", "decks": "Decks", "post": "Create Post", + "write": "Write", + "perks-streak": "{{label}}: {{streak}}", "day-theme": "Day Theme", "night-theme": "Night Theme", "address-placeholder": "enter url or search query", @@ -668,6 +671,7 @@ }, "trending-tags": { "title": "Topics", + "explore": "Explore topics", "view-more": "View more" }, "sort-trending-tags": { @@ -3186,6 +3190,8 @@ }, "top-communities": { "title": "Top communities", + "discover-title": "Discover communities", + "explore": "Explore communities", "create-button": "Create your community" }, "feedback-modal": { diff --git a/apps/web/src/features/shared/entry-list-item/entry-list-item-muted-content.tsx b/apps/web/src/features/shared/entry-list-item/entry-list-item-muted-content.tsx index b75c3f8889..3159cf8287 100644 --- a/apps/web/src/features/shared/entry-list-item/entry-list-item-muted-content.tsx +++ b/apps/web/src/features/shared/entry-list-item/entry-list-item-muted-content.tsx @@ -81,7 +81,7 @@ export function EntryListItemMutedContent({ entry: entryProp, isThumbLcp }: Prop
    )} -
    +
    {(!nsfw || showNsfw || globalNsfw) && (
    } > -
    +
    diff --git a/apps/web/src/features/shared/navbar/_index.scss b/apps/web/src/features/shared/navbar/_index.scss index 3891c92fb2..2757503c8c 100644 --- a/apps/web/src/features/shared/navbar/_index.scss +++ b/apps/web/src/features/shared/navbar/_index.scss @@ -52,3 +52,60 @@ body.is-inapp-browser .profile-page { padding-bottom: 0; } } + +.ecency-navbar { + .navbar-desktop-shell { + background: #fff; + border-bottom: 1px solid #e5e7eb; + + @include themify(night) { + background: #15171b; + border-bottom-color: #30343b; + } + } + + .ecency-navbar-desktop { + gap: 12px; + } + .navbar-search-slot { + max-width: 320px; + min-width: 120px; + flex: 1; + } + .navbar-actions { + gap: 8px; + margin-left: 0; + flex-shrink: 0; + } + .text-menu { + gap: 8px; + flex-shrink: 0; + } + .text-menu a { + border-radius: 0; + padding: 11px 8px; + border-bottom: 2px solid transparent; + } + .text-menu a[aria-current="page"] { + background: transparent; + border-bottom: 2px solid #357ce6; + } + .navbar-write-button { + white-space: nowrap; + } + + a:focus-visible, + button:focus-visible { + outline: 2px solid #357ce6 !important; + outline-offset: 3px; + } + + @media (max-width: 1279px) { + .ecency-navbar-desktop { + gap: 8px; + } + .navbar-actions { + gap: 4px; + } + } +} diff --git a/apps/web/src/features/shared/navbar/index.tsx b/apps/web/src/features/shared/navbar/index.tsx index 10d7233395..60a72da06e 100644 --- a/apps/web/src/features/shared/navbar/index.tsx +++ b/apps/web/src/features/shared/navbar/index.tsx @@ -8,7 +8,6 @@ import { NavbarMobile } from "./navbar-mobile"; import { NavbarDesktop } from "./navbar-desktop"; import { usePathname, useRouter, useSearchParams } from "next/navigation"; import { Theme } from "@/enums"; -import { classNameObject } from "@ui/util"; import { useClientTheme } from "@/api/queries"; import { useActiveAccount } from "@/core/hooks/use-active-account"; @@ -24,10 +23,9 @@ interface Props { step?: number; setStepOne?: () => void; setStepTwo?: () => void; - experimental?: boolean; // Use this flag for testing something } -export function Navbar({ setStepOne, setStepTwo, step, experimental = false }: Props) { +export function Navbar({ setStepOne, setStepTwo, step }: Props) { const { activeUser } = useActiveAccount(); const [theme, toggleTheme] = useClientTheme(); @@ -102,11 +100,7 @@ export function Navbar({ setStepOne, setStepTwo, step, experimental = false }: P return (
    diff --git a/apps/web/src/features/shared/navbar/navbar-desktop.tsx b/apps/web/src/features/shared/navbar/navbar-desktop.tsx index ef410a9d4d..6beac68ec7 100644 --- a/apps/web/src/features/shared/navbar/navbar-desktop.tsx +++ b/apps/web/src/features/shared/navbar/navbar-desktop.tsx @@ -27,7 +27,6 @@ interface Props { setSmVisible: (v: boolean) => void; mainBarExpanded: boolean; setMainBarExpanded: (v: boolean) => void; - experimental?: boolean; // Use this flag for testing something } export function NavbarDesktop({ @@ -35,8 +34,7 @@ export function NavbarDesktop({ transparentVerify, setStepOne, mainBarExpanded, - setMainBarExpanded, - experimental = false + setMainBarExpanded }: Props) { const { activeUser } = useActiveAccount(); const hydrated = useHydrated(); @@ -60,17 +58,13 @@ export function NavbarDesktop({ return (