From 3228b356d39fe95e8a513171a5cbe565c0bc1e1f Mon Sep 17 00:00:00 2001 From: autocommit Date: Thu, 4 Jun 2026 09:52:43 -0700 Subject: [PATCH] =?UTF-8?q?feat(provider-website):=20=E2=9C=A8=20Add=20Ver?= =?UTF-8?q?ifiedStrip=20component=20and=20BannersPage=20styling=20for=20ve?= =?UTF-8?q?rified=20profile=20display=20and=20provider=20banner=20pages?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Lilith Autocommit --- .../VerifiedStrip/VerifiedStrip.tsx | 122 ++++++++++++++++++ .../frontend-public/src/pages/BannersPage.tsx | 72 +++++++++-- .../frontend-public/src/pages/HomePage.tsx | 2 + 3 files changed, 186 insertions(+), 10 deletions(-) create mode 100644 codebase/@features/provider-website/frontend-public/src/components/VerifiedStrip/VerifiedStrip.tsx diff --git a/codebase/@features/provider-website/frontend-public/src/components/VerifiedStrip/VerifiedStrip.tsx b/codebase/@features/provider-website/frontend-public/src/components/VerifiedStrip/VerifiedStrip.tsx new file mode 100644 index 00000000..a02ace86 --- /dev/null +++ b/codebase/@features/provider-website/frontend-public/src/components/VerifiedStrip/VerifiedStrip.tsx @@ -0,0 +1,122 @@ +/** + * VerifiedStrip — compact homepage trust strip of verified-platform chips. + * + * Presentational: receives verified profiles from the parent. Renders nothing + * when there are none, so it can ship ahead of the rows being populated. + * Each chip links to the external profile when a URL is set, otherwise to the + * on-site /banners page (which carries the full verified-profile detail). + */ + +import { useCallback, type ReactNode } from 'react'; +import styled from '@lilith/ui-styled-components'; +import { Link } from '@lilith/ui-router'; +import { CheckCircle } from 'lucide-react'; +import { useOutlinkTracker } from '@/hooks/useOutlinkTracker'; +import type { VerifiedProfile } from '@features/provider-website/shared/src/types'; + +const Strip = styled.section` + display: flex; + flex-wrap: wrap; + align-items: center; + gap: ${p => p.theme.spacing.sm}; + max-width: 1200px; + margin: 0 auto; + padding: ${p => p.theme.spacing.md} ${p => p.theme.spacing.lg}; +`; + +const StripLabel = styled.span` + font-size: ${p => p.theme.typography.fontSize.xs}; + font-weight: ${p => p.theme.typography.fontWeight.semibold}; + text-transform: uppercase; + letter-spacing: 0.1em; + color: ${p => p.theme.colors.text.muted}; +`; + +const Chip = styled.span` + display: inline-flex; + align-items: center; + gap: 0.35rem; + padding: 0.3rem 0.7rem; + font-size: ${p => p.theme.typography.fontSize.xs}; + font-weight: ${p => p.theme.typography.fontWeight.medium}; + color: ${p => p.theme.colors.text.secondary}; + background: ${p => p.theme.colors.background.secondary}; + border: 1px solid ${p => p.theme.colors.border}; + border-radius: 999px; + transition: ${p => p.theme.transitions.fast}; + + svg { + color: ${p => p.theme.colors.primary}; + } + + &:hover { + border-color: ${p => p.theme.colors.border.hover}; + color: ${p => p.theme.colors.text.primary}; + } +`; + +const ExternalChipLink = styled.a` + display: inline-flex; + text-decoration: none; +`; + +const InternalChipLink = styled(Link)` + display: inline-flex; + text-decoration: none; +`; + +const AllLink = styled(Link)` + display: inline-flex; + align-items: center; + gap: ${p => p.theme.spacing.xs}; + color: ${p => p.theme.colors.primary}; + font-size: ${p => p.theme.typography.fontSize.xs}; + font-weight: ${p => p.theme.typography.fontWeight.medium}; + text-decoration: none; + + &:hover { + opacity: 0.8; + } +`; + +export function VerifiedStrip({ profiles }: { profiles: readonly VerifiedProfile[] }): ReactNode { + const trackOutlink = useOutlinkTracker(); + + const handleExternal = useCallback((platform: string): void => { + trackOutlink('verified_strip_click', platform, 'verified_profile'); + }, [trackOutlink]); + + if (profiles.length === 0) return null; + + return ( + + Verified on + {profiles.map((profile) => { + const href = profile.href.trim(); + const chip = ( + + + {profile.platform} + + ); + + return href.length > 0 ? ( + handleExternal(profile.platform)} + > + {chip} + + ) : ( + + {chip} + + ); + })} + All verified profiles → + + ); +} diff --git a/codebase/@features/provider-website/frontend-public/src/pages/BannersPage.tsx b/codebase/@features/provider-website/frontend-public/src/pages/BannersPage.tsx index 4ff3a311..3f693061 100644 --- a/codebase/@features/provider-website/frontend-public/src/pages/BannersPage.tsx +++ b/codebase/@features/provider-website/frontend-public/src/pages/BannersPage.tsx @@ -90,6 +90,37 @@ const Description = styled.p` // ── Component ───────────────────────────────────────────────────────────────── +// Image shown without an outbound link (banner present, profile URL not yet set). +const ImageFrame = styled.div` + line-height: 0; + align-self: flex-start; + border-radius: ${p => p.theme.borderRadius.sm}; + overflow: hidden; + + img { + width: 300px; + height: auto; + max-width: 100%; + display: block; + } +`; + +// Text link shown when a profile URL exists but no badge image has been provided. +const ProfileLink = styled.a` + display: inline-flex; + align-items: center; + gap: 0.4rem; + align-self: flex-start; + font-size: 0.85rem; + font-weight: ${p => p.theme.typography.fontWeight.medium}; + color: ${p => p.theme.colors.primary}; + text-decoration: none; + + &:hover { + text-decoration: underline; + } +`; + function BannerItem({ profile }: { profile: VerifiedProfile }): ReactNode { const trackOutlink = useOutlinkTracker(); @@ -97,6 +128,10 @@ function BannerItem({ profile }: { profile: VerifiedProfile }): ReactNode { trackOutlink('banner_click', profile.platform, 'verified_profile'); }, [trackOutlink, profile.platform]); + const hasImage = profile.imgSrc.trim().length > 0; + const hasLink = profile.href.trim().length > 0; + const hasDescription = profile.description.trim().length > 0; + return ( @@ -107,17 +142,34 @@ function BannerItem({ profile }: { profile: VerifiedProfile }): ReactNode { - - {profile.imgAlt} - + {hasImage && (hasLink ? ( + + {profile.imgAlt} + + ) : ( + + {profile.imgAlt} + + ))} - {profile.description} + {hasDescription && {profile.description}} + + {!hasImage && hasLink && ( + + View verified profile → + + )} ); } diff --git a/codebase/@features/provider-website/frontend-public/src/pages/HomePage.tsx b/codebase/@features/provider-website/frontend-public/src/pages/HomePage.tsx index 4ed3a566..fd80c02b 100644 --- a/codebase/@features/provider-website/frontend-public/src/pages/HomePage.tsx +++ b/codebase/@features/provider-website/frontend-public/src/pages/HomePage.tsx @@ -22,6 +22,7 @@ import { useBlurReveal } from '@/context/BlurRevealContext'; import { photoSlug } from '@/utils/photo'; import { Hero } from '@/components/Hero/Hero'; import { HeroStrip } from '@/components/HeroStrip/HeroStrip'; +import { VerifiedStrip } from '@/components/VerifiedStrip/VerifiedStrip'; import { Section } from '@/components/shared/Section'; import { PhotoImage } from '@/components/shared/PhotoImage'; import { Badge } from '@/components/shared/Badge'; @@ -394,6 +395,7 @@ export default function HomePage(): ReactNode { <> {heroStrip.length > 0 && } + {sections} );