Coach Credibility
Coach-themed link-in-bio with trust-building stack: hero photo + circle avatar overlap, credentials line in teal small caps,
Preview
Source
tsx
"use client";
import { useState } from "react";
import {
Instagram,
Twitter,
Youtube,
Mail,
Globe,
ShoppingBag,
PlayCircle,
Linkedin,
Music2,
ArrowRight,
ArrowUpRight,
Star,
Calendar,
Check,
Quote,
CheckCircle2,
type LucideIcon,
} from "lucide-react";
import type { LinkIcon, LinksInBioData, RichBlock } from "../types";
import { defaultData } from "../defaultData";
const ICONS: Record<LinkIcon, LucideIcon> = {
instagram: Instagram,
twitter: Twitter,
tiktok: Music2,
youtube: Youtube,
spotify: Music2,
apple: Music2,
linkedin: Linkedin,
email: Mail,
globe: Globe,
shop: ShoppingBag,
play: PlayCircle,
};
const PAPER = "#fbf8f4";
const CARD = "#ffffff";
const INK = "#1a1c2c";
const ACCENT = "#0e6e64";
const ACCENT_SOFT = "#dff1ee";
const RULE = "rgba(26,28,44,0.10)";
function StarRow({ rating = 5 }: { rating?: number }) {
return (
<div className="flex gap-0.5" style={{ color: "#f4b400" }}>
{Array.from({ length: 5 }).map((_, i) => (
<Star
key={i}
className="h-3.5 w-3.5"
fill={i < rating ? "#f4b400" : "transparent"}
strokeWidth={1.6}
/>
))}
</div>
);
}
function TestimonialCard({
block,
}: {
block: Extract<RichBlock, { kind: "testimonial" }>;
}) {
return (
<li
className="flex w-72 shrink-0 snap-center flex-col rounded-2xl p-5"
style={{ background: CARD, border: `1px solid ${RULE}` }}
>
{block.rating && <StarRow rating={block.rating} />}
<Quote className="mt-2 h-4 w-4" style={{ color: ACCENT, opacity: 0.5 }} />
<p className="mt-2 text-sm leading-relaxed" style={{ color: INK }}>
{block.quote}
</p>
<div className="mt-auto flex items-center gap-3 pt-4">
{block.avatar && (
<img
src={block.avatar}
alt={block.author}
width={40}
height={40}
loading="lazy"
className="h-10 w-10 rounded-full object-cover"
/>
)}
<div className="min-w-0">
<p className="truncate text-sm font-semibold" style={{ color: INK }}>
{block.author}
</p>
{block.role && (
<p className="truncate text-xs" style={{ color: "rgba(26,28,44,0.55)" }}>
{block.role}
</p>
)}
</div>
</div>
</li>
);
}
export interface LinksInBioCoachCredibilityProps {
data?: LinksInBioData;
}
export default function LinksInBioCoachCredibility({
data = defaultData,
}: LinksInBioCoachCredibilityProps) {
const heroImage = data.heroImage ?? data.avatar;
const program = data.richBlocks?.find(
(b): b is Extract<RichBlock, { kind: "group-program" }> => b.kind === "group-program"
);
const freebie = data.richBlocks?.find(
(b): b is Extract<RichBlock, { kind: "freebie" }> => b.kind === "freebie"
);
const testimonials = data.richBlocks?.filter(
(b): b is Extract<RichBlock, { kind: "testimonial" }> => b.kind === "testimonial"
) ?? [];
const press = data.pressLogos ?? [];
const [email, setEmail] = useState("");
const [submitted, setSubmitted] = useState(false);
return (
<>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Fraunces:ital,wght@0,400..900;1,400..900&family=Inter:wght@400;500;600;700&display=swap"
/>
<section
className="min-h-screen w-full pb-12"
style={{ background: PAPER, color: INK, fontFamily: "'Inter', sans-serif" }}
>
{/* Hero */}
<div className="relative h-64 w-full overflow-hidden sm:h-72">
<img
src={heroImage}
alt=""
width={1600}
height={800}
loading="eager"
className="h-full w-full object-cover"
/>
<div
aria-hidden
className="absolute inset-0"
style={{
background: `linear-gradient(180deg, rgba(14,110,100,0.15) 0%, ${PAPER} 100%)`,
}}
/>
</div>
{/* Avatar overlap + identity */}
<div className="relative mx-auto -mt-14 w-full max-w-[520px] px-5">
<div className="flex flex-col items-center text-center">
<img
src={data.avatar}
alt={data.name}
width={128}
height={128}
loading="eager"
className="h-28 w-28 rounded-full object-cover"
style={{
border: `4px solid ${PAPER}`,
boxShadow: "0 16px 36px rgba(14,110,100,0.18)",
}}
/>
<h1
className="mt-4 leading-[1.05]"
style={{
fontFamily: "'Fraunces', serif",
fontWeight: 700,
fontSize: "clamp(2rem, 6vw, 2.6rem)",
letterSpacing: "-0.01em",
color: INK,
}}
>
{data.name}
{data.verified && (
<span className="ml-2 align-middle text-base" style={{ color: ACCENT }}>
✓
</span>
)}
</h1>
{data.credentials && (
<p
className="mt-2 text-[12px] uppercase tracking-[0.28em]"
style={{ color: ACCENT }}
>
{data.credentials}
</p>
)}
{data.bio && (
<p
className="mt-3 max-w-[420px] text-[15px] leading-relaxed"
style={{ color: "rgba(26,28,44,0.7)" }}
>
{data.bio}
</p>
)}
</div>
{/* Press strip */}
{press.length > 0 && (
<div className="mt-7">
<p
className="text-center text-[10px] font-semibold uppercase tracking-[0.32em]"
style={{ color: "rgba(26,28,44,0.45)" }}
>
As featured in
</p>
<div className="mt-3 flex flex-wrap items-center justify-center gap-x-6 gap-y-2">
{press.slice(0, 5).map((p) => (
<span
key={p.name}
className="text-sm font-semibold"
style={{
fontFamily: "'Fraunces', serif",
color: "rgba(26,28,44,0.65)",
fontStyle: "italic",
}}
>
{p.name}
</span>
))}
</div>
</div>
)}
{/* Stat strip */}
<div
className="mt-7 grid grid-cols-3 overflow-hidden rounded-2xl"
style={{ background: CARD, border: `1px solid ${RULE}` }}
>
{[
{ num: "240+", label: "clients coached" },
{ num: "94%", label: "stay 6+ months" },
{ num: "12 yrs", label: "as a coach" },
].map((s) => (
<div
key={s.label}
className="border-r p-4 text-center last:border-r-0"
style={{ borderColor: RULE }}
>
<p
className="text-xl font-bold tracking-tight"
style={{ color: ACCENT, fontFamily: "'Fraunces', serif" }}
>
{s.num}
</p>
<p
className="mt-1 text-[10px] uppercase tracking-[0.18em]"
style={{ color: "rgba(26,28,44,0.55)" }}
>
{s.label}
</p>
</div>
))}
</div>
{/* Testimonials carousel */}
{testimonials.length > 0 && (
<div className="mt-8">
<p
className="text-[10px] font-semibold uppercase tracking-[0.32em]"
style={{ color: ACCENT }}
>
Client wins
</p>
<ul
className="mt-3 flex gap-3 overflow-x-auto pb-3 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
style={{ scrollSnapType: "x mandatory" }}
>
{testimonials.map((t, i) => (
<TestimonialCard key={i} block={t} />
))}
</ul>
</div>
)}
{/* Group program */}
{program && (
<div
className="mt-8 overflow-hidden rounded-3xl p-5"
style={{ background: ACCENT_SOFT, border: `1px solid ${RULE}` }}
>
<span
className="inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.2em]"
style={{ background: ACCENT, color: PAPER }}
>
<Calendar className="h-3 w-3" /> Group program
</span>
<h3
className="mt-3 text-xl leading-snug"
style={{ fontFamily: "'Fraunces', serif", fontWeight: 700, color: INK }}
>
{program.title}
</h3>
<p className="mt-1 text-xs" style={{ color: "rgba(26,28,44,0.7)" }}>
Cohort opens {program.startDate}
{program.slotsTotal && (
<>
{" · "}
{Math.max(0, program.slotsTotal - (program.slotsFilled ?? 0))} of{" "}
{program.slotsTotal} seats left
</>
)}
</p>
{program.features && (
<ul className="mt-3 grid gap-1.5">
{program.features.slice(0, 4).map((f) => (
<li key={f} className="flex items-start gap-2 text-sm" style={{ color: INK }}>
<Check className="mt-0.5 h-4 w-4 shrink-0" style={{ color: ACCENT }} />
<span>{f}</span>
</li>
))}
</ul>
)}
<a
href={program.href}
className="mt-4 inline-flex items-center gap-1 rounded-full px-5 py-2.5 text-sm font-semibold"
style={{ background: ACCENT, color: PAPER }}
>
{program.ctaLabel ?? "Apply"} <ArrowRight className="h-3.5 w-3.5" />
</a>
</div>
)}
{/* Freebie capture */}
{freebie && (
<div
className="mt-6 rounded-3xl p-5"
style={{ background: CARD, border: `1px solid ${RULE}` }}
>
<p
className="text-[10px] font-semibold uppercase tracking-[0.22em]"
style={{ color: ACCENT }}
>
Free to start
</p>
<p
className="mt-1 text-lg leading-snug"
style={{ fontFamily: "'Fraunces', serif", fontWeight: 600, color: INK }}
>
{freebie.title}
</p>
{freebie.description && (
<p className="mt-1.5 text-sm" style={{ color: "rgba(26,28,44,0.7)" }}>
{freebie.description}
</p>
)}
{submitted ? (
<div
className="mt-3 flex items-center gap-2 rounded-xl px-3 py-2.5 text-sm"
style={{ background: ACCENT_SOFT, color: INK }}
>
<CheckCircle2 className="h-4 w-4" style={{ color: ACCENT }} />
Sent — check your inbox.
</div>
) : (
<form
onSubmit={(e) => {
e.preventDefault();
if (email) setSubmitted(true);
}}
className="mt-3 flex flex-col gap-2 sm:flex-row"
>
<input
type="email"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder={freebie.placeholder ?? "[email protected]"}
className="min-w-0 flex-1 rounded-full border px-4 py-2.5 text-sm focus:border-[color:var(--accent)] focus:outline-none"
style={{ borderColor: RULE, ["--accent" as never]: ACCENT, color: INK }}
/>
<button
type="submit"
className="shrink-0 rounded-full px-5 py-2.5 text-sm font-semibold"
style={{ background: ACCENT, color: PAPER }}
>
{freebie.cta ?? "Get it"}
</button>
</form>
)}
{freebie.socialProof && !submitted && (
<p
className="mt-2 text-[11px]"
style={{ color: "rgba(26,28,44,0.5)" }}
>
{freebie.socialProof}
</p>
)}
</div>
)}
{/* Plain links */}
<ul className="mt-7 space-y-2">
{data.links.slice(0, 4).map((link) => {
const Icon = link.icon ? ICONS[link.icon] : ArrowUpRight;
return (
<li key={link.label}>
<a
href={link.href}
className="group flex items-center gap-3 rounded-2xl border bg-white px-4 py-3.5 transition-all hover:-translate-y-0.5"
style={{ borderColor: RULE, color: INK }}
>
<span
className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full"
style={{ background: ACCENT_SOFT, color: ACCENT }}
>
<Icon className="h-4 w-4" strokeWidth={1.6} />
</span>
<span className="flex-1 truncate text-sm font-semibold">
{link.label}
</span>
{link.badge && (
<span
className="rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wider"
style={{ background: ACCENT, color: PAPER }}
>
{link.badge}
</span>
)}
<ArrowUpRight
className="h-4 w-4 shrink-0 transition-transform group-hover:-translate-y-0.5 group-hover:translate-x-0.5"
style={{ color: "rgba(26,28,44,0.3)" }}
/>
</a>
</li>
);
})}
</ul>
{/* Socials */}
{data.socials && data.socials.length > 0 && (
<div className="mt-8 flex items-center justify-center gap-3">
{data.socials.map((s, i) => {
const Icon = ICONS[s.type] ?? Globe;
return (
<a
key={i}
href={s.href}
aria-label={s.type}
className="flex h-10 w-10 items-center justify-center rounded-full bg-white transition-colors hover:text-[color:var(--accent)]"
style={{
border: `1px solid ${RULE}`,
color: INK,
["--accent" as never]: ACCENT,
}}
>
<Icon className="h-4 w-4" strokeWidth={1.6} />
</a>
);
})}
</div>
)}
</div>
</section>
</>
);
} Claude Code Instructions
CLI Install
npx innovations add coach-credibilityWhere to use it
A coach / consultant link-in-bio page built around credibility signals. Loads Fraunces and Inter from Google Fonts.
Pass a typed 'data' prop (LinksInBioData from src/registry/links-in-bio/types.ts). Provide:
- credentials — short line beneath the name (e.g. 'Certified life coach · NYT bestseller')
- pressLogos — array of { name } rendered as italic Fraunces text in the press strip
- richBlocks — opt into testimonial / group-program / freebie blocks. Multiple testimonial blocks become a swipeable carousel.
The 3-up stat strip is hardcoded sample copy ('240+ clients coached', '94% stay 6+ months', '12 yrs as a coach') — edit the array in the component to swap with your real numbers before going live.
In Astro:
import LinksInBioCoachCredibility from '../components/innovations/links-in-bio/coach-credibility';
<LinksInBioCoachCredibility client:load data={myProfile} />
In Next.js:
import LinksInBioCoachCredibility from '@/components/innovations/links-in-bio/coach-credibility';
Best for: coaches, consultants, therapists, fractional execs — any service-business creator who closes on trust.