feat(provider-website): Add VerifiedStrip component and BannersPage styling for verified profile display and provider banner pages

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
autocommit 2026-06-04 09:52:43 -07:00
parent ef51645b2e
commit 3228b356d3
3 changed files with 186 additions and 10 deletions

View file

@ -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 (
<Strip aria-label="Verified profiles">
<StripLabel>Verified on</StripLabel>
{profiles.map((profile) => {
const href = profile.href.trim();
const chip = (
<Chip>
<CheckCircle size={13} />
{profile.platform}
</Chip>
);
return href.length > 0 ? (
<ExternalChipLink
key={profile.platform}
href={href}
rel="noopener"
target="_blank"
onClick={() => handleExternal(profile.platform)}
>
{chip}
</ExternalChipLink>
) : (
<InternalChipLink key={profile.platform} to="/banners">
{chip}
</InternalChipLink>
);
})}
<AllLink to="/banners">All verified profiles </AllLink>
</Strip>
);
}

View file

@ -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 (
<BannerCard>
<PlatformRow>
@ -107,17 +142,34 @@ function BannerItem({ profile }: { profile: VerifiedProfile }): ReactNode {
</VerifiedBadge>
</PlatformRow>
<BannerPreview
href={profile.href}
rel="noopener"
target="_blank"
title={profile.imgAlt}
onClick={handleBannerClick}
>
<img src={profile.imgSrc} alt={profile.imgAlt} />
</BannerPreview>
{hasImage && (hasLink ? (
<BannerPreview
href={profile.href}
rel="noopener"
target="_blank"
title={profile.imgAlt}
onClick={handleBannerClick}
>
<img src={profile.imgSrc} alt={profile.imgAlt} />
</BannerPreview>
) : (
<ImageFrame>
<img src={profile.imgSrc} alt={profile.imgAlt} />
</ImageFrame>
))}
<Description>{profile.description}</Description>
{hasDescription && <Description>{profile.description}</Description>}
{!hasImage && hasLink && (
<ProfileLink
href={profile.href}
rel="noopener"
target="_blank"
onClick={handleBannerClick}
>
View verified profile
</ProfileLink>
)}
</BannerCard>
);
}

View file

@ -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 {
<>
<Hero />
{heroStrip.length > 0 && <HeroStrip items={heroStrip} />}
<VerifiedStrip profiles={data.verifiedProfiles ?? []} />
{sections}
</>
);