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:
parent
ef51645b2e
commit
3228b356d3
3 changed files with 186 additions and 10 deletions
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue