From 3e88733f59f98f6cd6ce492b48738a1379e79cbc Mon Sep 17 00:00:00 2001 From: Holke Brammer <103380539+holkexyz@users.noreply.github.com> Date: Wed, 1 Jul 2026 11:29:14 +0200 Subject: [PATCH 1/4] fix(feedback): hide the floating button where it's redundant MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The floating feedback button is the app's primary door into the feedback modal, but it shouldn't appear in two places: - On /welcome, which has its own contact CTA and minimal landing chrome. - Wherever the mobile bottom nav is on screen — that bar already carries a Feedback entry, so the floating button duplicated it and overlapped the bar. Centralize the bottom nav's show/hide rule in a new isBottomNavVisible helper (not desktop + standalone + not /embed + not /welcome) so and derive visibility from one predicate and can't drift. Both resolve on the same hydration tick, so they hand off with no window where the bar and the floating button both show. In a regular mobile browser tab (no bottom bar) the floating button still shows, as there's nothing to duplicate. Co-Authored-By: Claude Opus 4.8 --- src/components/layout/bottom-nav.tsx | 22 ++++---------- src/components/layout/feedback-trigger.tsx | 16 +++++++++- src/lib/layout/bottom-nav-visibility.ts | 34 ++++++++++++++++++++++ 3 files changed, 55 insertions(+), 17 deletions(-) create mode 100644 src/lib/layout/bottom-nav-visibility.ts diff --git a/src/components/layout/bottom-nav.tsx b/src/components/layout/bottom-nav.tsx index 63a8c00e..14c4a185 100644 --- a/src/components/layout/bottom-nav.tsx +++ b/src/components/layout/bottom-nav.tsx @@ -6,6 +6,7 @@ import { useFeedback } from "@/lib/feedback-context"; import { useOrg } from "@/lib/groups/org-context"; import { isRouteVisibleToActor } from "@/lib/groups/personal-only"; import { useLayoutBreakpoints } from "@/hooks/use-layout-breakpoints"; +import { isBottomNavVisible } from "@/lib/layout/bottom-nav-visibility"; import Tooltip from "@/components/ui/tooltip"; export default function BottomNav() { @@ -15,22 +16,11 @@ export default function BottomNav() { const { activeOrg } = useOrg(); const { isDesktop, isStandalone } = useLayoutBreakpoints(); - // Unmount at ≥800px — the left rail is the primary nav on desktop and - // bottom-nav's focusable buttons would compete for tab order. - if (isDesktop) return null; - - // Only show in installed / standalone mode (PWA or A2HS web clip). - // In a regular browser tab the address bar and OS gestures provide - // navigation; the bar also occludes content on /welcome for guests. - if (!isStandalone) return null; - - // Embeds render bare (board-only) inside a third-party iframe. - if (pathname.startsWith("/embed")) return null; - - // /welcome uses the minimal landing chrome (wordmark + sign-in) at every - // width — no bottom nav. Matches the mobile navbar + desktop-top-bar, - // which both return null on /welcome. - if (pathname === "/welcome") return null; + // Visibility is centralized in isBottomNavVisible so the floating + // can hide itself under the exact same conditions + // (the bar already carries a Feedback entry). See the helper for the + // full per-condition rationale. + if (!isBottomNavVisible({ pathname, isDesktop, isStandalone })) return null; const isHome = pathname === "/home" || pathname.startsWith("/home/"); diff --git a/src/components/layout/feedback-trigger.tsx b/src/components/layout/feedback-trigger.tsx index c0abb446..4bc289ba 100644 --- a/src/components/layout/feedback-trigger.tsx +++ b/src/components/layout/feedback-trigger.tsx @@ -1,8 +1,11 @@ "use client" import { useCallback, useEffect, useState } from "react" +import { usePathname } from "next/navigation" import { MessageSquare } from "lucide-react" import { useFeedback } from "@/lib/feedback-context" +import { useLayoutBreakpoints } from "@/hooks/use-layout-breakpoints" +import { isBottomNavVisible } from "@/lib/layout/bottom-nav-visibility" /** * Floating "Share Feedback" button, pinned bottom-right. It's the app's @@ -15,6 +18,8 @@ import { useFeedback } from "@/lib/feedback-context" */ export default function FeedbackTrigger() { const { isOpen, openFeedback } = useFeedback() + const pathname = usePathname() + const { isDesktop, isStandalone } = useLayoutBreakpoints() const [bottomOffset, setBottomOffset] = useState(20) const updatePosition = useCallback(() => { @@ -62,7 +67,16 @@ export default function FeedbackTrigger() { } }, [updatePosition]) - if (isOpen) return null + // Hidden on the marketing landing page (/welcome has its own contact + // CTA), and wherever the mobile bottom nav is on screen — that bar + // already carries a Feedback entry, so the floating button would + // duplicate it and overlap the bar. + if ( + isOpen || + pathname === "/welcome" || + isBottomNavVisible({ pathname, isDesktop, isStandalone }) + ) + return null return ( + {/* Badge-kind toggles. The last checked box is disabled so at + least one kind is always visible. */} +
+ setShowEndorsements(e.target.checked)} + /> + setShowAwards(e.target.checked)} + /> +
@@ -953,40 +1037,32 @@ export default function EndorsementGraph({ nodes, links, focusReq, truncated = f - {neighbourList.endorsed.length > 0 && ( -
-
Endorsed ({neighbourList.endorsed.length})
-
- {neighbourList.endorsed.slice(0, 12).map((n) => ( - - - - ))} -
-
- )} - - {neighbourList.endorsedBy.length > 0 && ( -
-
- Endorsed by ({neighbourList.endorsedBy.length}) -
-
- {neighbourList.endorsedBy.slice(0, 12).map((n) => ( - - - - ))} + {( + [ + ["Endorsed", neighbourList.endorsed], + ["Endorsed by", neighbourList.endorsedBy], + ["Awarded", neighbourList.awarded], + ["Awarded by", neighbourList.awardedBy], + ] as const + ).map(([title, list]) => + list.length > 0 ? ( +
+
+ {title} ({list.length}) +
+
+ {list.slice(0, 12).map((n) => ( + + + + ))} +
-
+ ) : null, )} { const participants = nodes.length @@ -96,6 +96,10 @@ export default function EndorsementStats({ graph, onFocus }: EndorsementStatsPro {totalEndorsements.toLocaleString()} Endorsements
+
+ {totalAwards.toLocaleString()} + Awards +
{stats.participants.toLocaleString()} People diff --git a/src/hooks/use-endorsement-graph.ts b/src/hooks/use-endorsement-graph.ts index 67f75e1d8ae16b1878943575572468fb1b67f4b7..e76f8da20ac9ca1781cd546c1ad9ae476bb7dcf8 100644 GIT binary patch delta 3962 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 U(>(llsX14iKx+8owfE=#2eGa-mH+?% delta 772 zcmZ8fO=}ZT6lIcV(}}?-k~UPrZ6=wdrkP50Q6jM)8c|CtL~B<zRB+*bs%}MpgG+JKh2Td2fH;#$wKy>I-rRdQ_uRwz@ksjBe)fI$XJ!mK zgv)RSQ&JJ{NU7uA9jK+-jMFZ)1U``RD?s%&)u8HqIc#+(qdrP>C@^}TdPpE9w}EaO zTyP+aLdzuWHr1it?TbD!>!^pSvjrbKILuxWgC5m=vfF7Zd zPluIS(1Rvn%WO8KE;i;Ma;!AEH`W>ZVvz4x5+n>yE$2lEH!P53vaMx>y(!RMm`II^{plmo>m7c ze$^1rqN3SZHqB+<30??@@#(p1?vXTx62T6oi@e{sOPeBtiRs(8Gp$N8zRCc8$}D17 zIk_eWeh_Vh162jzZ@UG5YnpE8P^nZP!v!;}b_N#zYnH+4o2HBxl(X1Xs<@{t~`*V8K-B@@Y{R5?B{|^8F From 1b24c9bb21ca96d21ec8c8faef361ceab3a929a4 Mon Sep 17 00:00:00 2001 From: Holke Brammer <103380539+holkexyz@users.noreply.github.com> Date: Thu, 2 Jul 2026 10:31:18 +0200 Subject: [PATCH 3/4] docs(help): explain what certified.app is MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New section between the intro and the walk-through: certified.app is a view into the whole Certified network — every account, the activities and impact data they publish, and the connections between them — with the data living in each account's AT Protocol repo rather than in the app. Links to Explore and the endorsement graph as starting points. Co-Authored-By: Claude Fable 5 --- src/app/help/page.tsx | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/app/help/page.tsx b/src/app/help/page.tsx index a3e9afc0..f52d89ee 100644 --- a/src/app/help/page.tsx +++ b/src/app/help/page.tsx @@ -1,4 +1,5 @@ import type { Metadata } from "next"; +import Link from "next/link"; import PageTitle from "@/components/layout/page-title"; import HelpFeedbackLink from "@/components/help/help-feedback-link"; import HelpTourButton from "@/components/help/help-tour-button"; @@ -47,6 +48,38 @@ export default function HelpPage() {

+
+

+ What is certified.app? +

+

+ certified.app is a view into the whole Certified network. It brings together every + account on the network, the activities and impact data they publish, and the + connections between them — who endorses whom, who follows whom, and who works + together in which group. The data itself doesn't live in this app: each account + keeps its own records on the AT Protocol, and certified.app reads across all of them + to show the network in one place. Other compatible apps can present the same network + in their own way. +

+

+ A good place to start browsing is{" "} + + Explore + + , and the{" "} + + endorsement graph + {" "} + draws the network's connections as an interactive map. +

+
+

Take the walk-through From 7f9c14add7538899a9b1da02a9bbab5a59c7819d Mon Sep 17 00:00:00 2001 From: Holke Brammer <103380539+holkexyz@users.noreply.github.com> Date: Thu, 2 Jul 2026 10:40:14 +0200 Subject: [PATCH 4/4] docs(help): tighten the certified.app section Drop the Explore / endorsement-graph pointer paragraph and say "on AT Protocol" without the article. Co-Authored-By: Claude Fable 5 --- src/app/help/page.tsx | 22 ++-------------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/src/app/help/page.tsx b/src/app/help/page.tsx index f52d89ee..a0b0df98 100644 --- a/src/app/help/page.tsx +++ b/src/app/help/page.tsx @@ -1,5 +1,4 @@ import type { Metadata } from "next"; -import Link from "next/link"; import PageTitle from "@/components/layout/page-title"; import HelpFeedbackLink from "@/components/help/help-feedback-link"; import HelpTourButton from "@/components/help/help-tour-button"; @@ -52,32 +51,15 @@ export default function HelpPage() {

What is certified.app?

-

+

certified.app is a view into the whole Certified network. It brings together every account on the network, the activities and impact data they publish, and the connections between them — who endorses whom, who follows whom, and who works together in which group. The data itself doesn't live in this app: each account - keeps its own records on the AT Protocol, and certified.app reads across all of them + keeps its own records on AT Protocol, and certified.app reads across all of them to show the network in one place. Other compatible apps can present the same network in their own way.

-

- A good place to start browsing is{" "} - - Explore - - , and the{" "} - - endorsement graph - {" "} - draws the network's connections as an interactive map. -