Skip to content
Merged
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
29 changes: 20 additions & 9 deletions src/app/api/indexer/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -330,19 +330,23 @@ ${ACTIVITY_NODE_SELECTION}
}
`,

// Every endorsement-typed badge award across the network — backs the
// /endorsement-graph page. Returns the directed edge
// (issuer `did` → `subject` DID) plus the issuer's denormalised actor
// Every badge award of one `badgeType` across the network — backs the
// /endorsement-graph page, which scans once per type ("endorsement" and
// "award"; the proxy allowlists the value). Returns the directed edge
// (issuer `did` → `subject`) plus the issuer's denormalised actor
// profile (same `issuer { ... }` join as ReceivedEndorsements, drops a
// per-issuer resolve fan-out). Subjects that never issued an award have
// no inline profile here, so the client resolves those DIDs via
// per-issuer resolve fan-out). The subject union carries the DID for
// account-targeted awards and the strong-ref `uri` for record-targeted
// ones (award-typed badges usually point at records; the at:// authority
// identifies the owning account). Subjects that never issued an award
// have no inline profile here, so the client resolves those DIDs via
// NetworkActorsByDids. `excludeAuthorLabels` hides awards authored by
// likely-test accounts, mirroring AwardCount so the graph matches the
// public counters. Paginated; the client unions pages up to a cap.
AllEndorsements: `
query AllEndorsements($first: Int!, $after: String) {
query AllEndorsements($badgeType: String!, $first: Int!, $after: String) {
appCertifiedBadgeAward(
where: { badgeType: { eq: "endorsement" } }
where: { badgeType: { eq: $badgeType } }
excludeAuthorLabels: ${JSON.stringify([...DEFAULT_HIDDEN_ORG_LABELS])}
first: $first
after: $after
Expand All @@ -357,6 +361,7 @@ ${ACTIVITY_NODE_SELECTION}
subject {
__typename
... on AppCertifiedDefsDid { did }
... on ComAtprotoRepoStrongRef { uri }
}
issuer { did handle displayName avatarCid pds }
response { state }
Expand Down Expand Up @@ -1605,9 +1610,15 @@ function buildVariables(
return { viewer, degree: rawDegree }
}
case "AllEndorsements": {
// Zero required vars — paginated network-wide scan. Same clamp
// shape as the other 100-per-page reads (ReceivedEndorsements).
// Paginated network-wide scan, one badge type per pass. Strict
// allowlist on `badgeType` (defaults to "endorsement" for older
// clients) — anything else 400s here rather than fanning an
// arbitrary string out to the indexer. Same clamp shape as the
// other 100-per-page reads (ReceivedEndorsements).
const badgeType = vars.badgeType === undefined ? "endorsement" : vars.badgeType
if (badgeType !== "endorsement" && badgeType !== "award") return null
return {
badgeType,
first: clampFirst(vars.first, MAX_FIRST, 100),
after: readString(vars.after, MAX_AFTER_LEN),
}
Expand Down
15 changes: 15 additions & 0 deletions src/app/help/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,21 @@ export default function HelpPage() {
</p>
</section>

<section>
<h2 className="font-headline text-h2 text-[var(--fg-primary)] mb-4">
What is certified.app?
</h2>
<p>
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&apos;t live in this app: each account
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.
</p>
</section>

<section>
<h2 className="font-headline text-h2 text-[var(--fg-primary)] mb-4">
Take the walk-through
Expand Down
34 changes: 33 additions & 1 deletion src/app/styles/visualization.css
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,17 @@
min-width: 200px;
}

/* Endorsement/award checkboxes float over the canvas, so give the row the
same card chrome as the legend for readability. */
.viz__kind-toggles {
gap: var(--space-3, 12px);
padding: var(--space-2, 8px) var(--space-3, 12px);
background: var(--bg-elevated);
border: 1px solid var(--border-default);
border-radius: var(--radius);
box-shadow: var(--shadow-sm);
}

/* Layout + scope switchers fill the control panel so their segments read
as one clear toggle rather than floating short. */
.viz__layout-toggle,
Expand Down Expand Up @@ -263,6 +274,10 @@
border-top-width: 3px;
}

.viz__legend-swatch--award {
border-top-color: var(--color-warning);
}

/* ---- Hover / selected node info panel ---- */
.viz__panel {
position: absolute;
Expand Down Expand Up @@ -446,6 +461,13 @@
gap: var(--space-3, 12px);
}

/* With an odd cell count (5 since the Awards stat) the last cell spans the
2-column grid instead of orphaning at half width. The tablet range
overrides this — there the five cells share one row. */
.viz__stats-grid > .viz__stat:last-child:nth-child(odd) {
grid-column: 1 / -1;
}

.viz__stat {
display: flex;
flex-direction: column;
Expand Down Expand Up @@ -560,7 +582,11 @@
}

.viz__stats-grid {
grid-template-columns: repeat(4, minmax(0, 1fr));
grid-template-columns: repeat(5, minmax(0, 1fr));
}

.viz__stats-grid > .viz__stat:last-child:nth-child(odd) {
grid-column: auto;
}
}

Expand All @@ -573,6 +599,12 @@
grid-template-columns: repeat(2, minmax(0, 1fr));
}

/* Back to 2 columns here, so the odd-count span applies again (the
tablet block above also matches this range and would otherwise win). */
.viz__stats-grid > .viz__stat:last-child:nth-child(odd) {
grid-column: 1 / -1;
}

.viz__panel {
width: 220px;
}
Expand Down
22 changes: 6 additions & 16 deletions src/components/layout/bottom-nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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
// <FeedbackTrigger> 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/");

Expand Down
16 changes: 15 additions & 1 deletion src/components/layout/feedback-trigger.tsx
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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(() => {
Expand Down Expand Up @@ -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 (
<button
Expand Down
Loading
Loading