Auszeichnungen für die Website
Auszeichnungen, Preise und Siegel, jedes Zeichen im Kit gezeichnet: Lorbeeren, die sich selbst zeichnen, runde Siegel mit Schrift am Rand, eine Medaille am Band, Preise nach Jahr und eine schmale Leiste neben dem Knopf.
Anerkennung in deinem Stil
Eine Section mit Auszeichnungen zeigt, dass andere deine Arbeit schon bewertet haben. Jedes Zeichen hier ist im Kit gezeichnet statt als fremdes Abzeichen eingesetzt, Lorbeeren, Siegel und Medaillen nehmen also deine Farben und Schriften an und wirken nie wie eingeklebte Bilder.
„Lorbeeren“ setzt eine Reihe Kränze mit Rang, Preis und Jahr darin, die Zweige zeichnen sich und die Blätter erscheinen nacheinander, sobald die Reihe in den Blick kommt. „Siegel“ sind runde Siegel mit gezacktem Rand, oben der Aussteller, unten das Jahr, unter dem Zeiger dreht sich der Rand ein Stück. Die „Vitrine“ zeigt den jüngsten Preis als Medaille am Band, über die ein Licht zieht, daneben eine Bewertung mit Sternen und die übrigen Preise nach Jahr. „Nach Jahr“ hat eine Jahresauswahl für die Pfeiltasten, dahinter das Jahr riesig und die Preise dieses Jahres als Karten. Die „Schmale Leiste“ fasst eine Bewertung mit Sternen und kleine Siegel in eine Karte für unter den Hero oder neben den Kaufknopf.
Zeig die Auszeichnungen, die deinen Käufern etwas bedeuten, für einen lokalen Betrieb wiegt ein regionaler Preis oft mehr als ein berühmter. Nenn immer das Jahr, Anerkennung altert. Setz die schmale Leiste nah an die Entscheidung, unter den Hero oder neben den Preis.
Variants
Details
Was Auszeichnungen mitbringt, aus Registry und Quellcode gelesen.
- Kategorie
- Press und Awards
- Bereich
- Marketing
- Variants
- 5
- Version
- 1.0.0 · seit 1.2.0
- Datei
- components/ui/awards.tsx
- npm-Pakete
- Keine
- Sprachen
- Englisch und Deutsch, 9 eigene Texte
- Studio
- Im Code anpassen
- Mit KI bauen
- Per MCP in Claude, Cursor, Copilot und Codex
Props
Die Schnittstelle AwardsProps aus components/ui/awards.tsx. Beschreibungen stammen aus den Kommentaren im Code und sind auf Englisch.
variantoptionalAwardsVariantlaurelssealstrophy-caseby-yearstrip
eyebrowoptionalReact.ReactNodetitleoptionalReact.ReactNodedescriptionoptionalReact.ReactNodeitemsPflichtAwardItem[]ratingoptional{ value: number; count: number; source?: string }strip, trophy-case: an average rating
ctaoptional{ label: string; href: string }
Eingebaute Texte
Diese Texte schreibt Auszeichnungen selbst. Die Sprache folgt dem KitProvider, über die Prop labels ersetzt du einzelne.
Alle 9 Texte anzeigen
| Schlüssel | Englisch | Deutsch |
|---|---|---|
| count | Englisch {n} awards | Deutsch {n} Auszeichnungen |
| count1 | Englisch 1 award | Deutsch 1 Auszeichnung |
| year | Englisch Year | Deutsch Jahr |
| all | Englisch All years | Deutsch Alle Jahre |
| winner | Englisch Winner | Deutsch Gewinner |
| rating | Englisch {v} out of 5 | Deutsch {v} von 5 |
| from | Englisch from {n} reviews | Deutsch aus {n} Bewertungen |
| more | Englisch Read about it | Deutsch Mehr dazu |
| awards | Englisch Awards | Deutsch Auszeichnungen |
Barrierefreiheit
Aus dem Quellcode der Component abgeleitet.
- Auszeichnungen reagiert selbst auf die Tasten Pfeil links, Pfeil rechts und Leertaste.
- Setzt aria-controls, aria-label, aria-labelledby und aria-selected, damit Screenreader Zweck und Zustand ansagen.
- Nutzt die Rollen tab, tablist und tabpanel.
- Ergänzt Text für Screenreader, wo Icons und Grafiken allein stehen.
- Zeigt bei Bedienung mit der Tastatur einen sichtbaren Fokusring.
- Der Server rendert den Startzustand vollständig, JavaScript ergänzt nur die Interaktion.
Bewegung
- Erscheint beim ersten Sichtkontakt mit einem kurzen Auftritt (kit-rise, useInView).
- Bringt eigene Animationen mit, ruhig getaktet und ohne Wackeln.
- Bei reduzierter Bewegung (prefers-reduced-motion) bleibt sie ruhig. 8 Stellen im Code schalten Bewegung dann ab.
Quellcode
Die Datei, die aiw add in dein Projekt legt. Sie gehört danach dir.
Datei anzeigen, 497 Zeilen
'use client';
import { useId, useState } from 'react';
import { cn, round, useInView, useLocale, useTexts } from '@/lib/kit';
import { Button } from '@/components/ui/button';
import { Icon, type IconName } from '@/components/ui/icon';
import { Eyebrow, Section } from '@/components/ui/section';
/*
* Awards, prizes and seals, every mark drawn here: laurels, round seals with
* text on the rim, a medal on a ribbon. No logos of juries or magazines, the
* name of the award stands as text. Five layouts:
*
* laurels a row of wreaths that draw themselves in, the award inside
* seals round seals with the issuer on the rim, the rim turns on hover
* trophy-case one medal set large, the others listed by year beside it
* by-year a year picker and the awards of that year as ribbons
* strip a compact line of seals and a rating, for next to a button
*/
export type AwardsVariant = 'laurels' | 'seals' | 'trophy-case' | 'by-year' | 'strip';
export interface AwardItem {
title: string;
/** Who gave it, written as text */
issuer?: string;
year?: string;
/** Gold, Winner, 1st place */
rank?: string;
category?: string;
description?: string;
icon?: IconName;
href?: string;
}
export interface AwardsProps {
variant?: AwardsVariant;
eyebrow?: React.ReactNode;
title?: React.ReactNode;
description?: React.ReactNode;
items: AwardItem[];
/** strip, trophy-case: an average rating */
rating?: { value: number; count: number; source?: string };
cta?: { label: string; href: string };
labels?: Partial<Texts>;
className?: string;
}
const TEXTS = {
en: { count: '{n} awards', count1: '1 award', year: 'Year', all: 'All years', winner: 'Winner', rating: '{v} out of 5', from: 'from {n} reviews', more: 'Read about it', awards: 'Awards' },
de: { count: '{n} Auszeichnungen', count1: '1 Auszeichnung', year: 'Jahr', all: 'Alle Jahre', winner: 'Gewinner', rating: '{v} von 5', from: 'aus {n} Bewertungen', more: 'Mehr dazu', awards: 'Auszeichnungen' },
};
type Texts = (typeof TEXTS)['en'];
type ViewProps = AwardsProps & { t: Texts };
const fill = (s: string, v: Record<string, string | number>) => s.replace(/\{(\w+)\}/g, (_, k) => String(v[k] ?? ''));
const delay = (ms: number) => ({ '--kit-delay': `${ms}ms` }) as React.CSSProperties;
function Head({ eyebrow, title, description, center, className }: Pick<AwardsProps, 'eyebrow' | 'title' | 'description'> & { center?: boolean; className?: string }) {
if (!title && !eyebrow) return null;
return (
<div className={cn('max-w-[46rem]', center && 'mx-auto max-w-[42rem] text-center', className)}>
{eyebrow && <Eyebrow>{eyebrow}</Eyebrow>}
{title && <h2 className="kit-rise text-balance text-h2 font-semibold text-foreground">{title}</h2>}
{description && (
<p className={cn('kit-rise mt-5 max-w-[36rem] text-pretty text-lead text-muted-foreground', center && 'mx-auto')} style={delay(80)}>
{description}
</p>
)}
</div>
);
}
/* ------------------------------------------------------------------ */
/* Drawn marks */
/* ------------------------------------------------------------------ */
/** One branch of a laurel: leaves along an arc, mirrored for the other side */
function Branch({ flip, drawn, leaves = 9 }: { flip?: boolean; drawn: boolean; leaves?: number }) {
// the left half of a wreath: from the bottom (252 deg) up the side to the top (112 deg)
const cx = 60;
const cy = 56;
const r = 44;
const from = 252;
const to = 112;
const at = (deg: number) => {
const a = (deg * Math.PI) / 180;
return [cx + r * Math.cos(a), cy - r * Math.sin(a)] as const;
};
const stem = Array.from({ length: 25 }, (_, i) => at(from - ((from - to) * i) / 24))
.map(([x, y], i) => `${i ? 'L' : 'M'}${round(x)} ${round(y)}`)
.join(' ');
const items = Array.from({ length: leaves * 2 }, (_, k) => {
const i = Math.floor(k / 2);
const out = k % 2 === 0;
const deg = from - 8 - ((from - to - 14) * i) / (leaves - 1);
const a = (deg * Math.PI) / 180;
// direction of travel along the stem (upward), as the rotation of an upright leaf
const along = (Math.atan2(Math.sin(a), -Math.cos(a)) * 180) / Math.PI;
const sc = 1 - i * 0.05;
const off = (out ? 6 : -5) * sc;
const [px, py] = at(deg);
return { x: round(px + off * Math.cos(a)), y: round(py - off * Math.sin(a)), rot: round(along + (out ? -38 : 38)), s: round(sc, 3), i };
});
return (
<g transform={flip ? 'translate(120 0) scale(-1 1)' : undefined}>
<path d={stem} fill="none" stroke="currentColor" strokeWidth="1.3" strokeLinecap="round" pathLength={1} strokeDasharray="1" strokeDashoffset={drawn ? 0 : 1} className="transition-[stroke-dashoffset] duration-[1200ms] ease-[var(--ease-kit)] motion-reduce:transition-none" />
{items.map((l, k) => (
<ellipse
key={k}
cx={l.x}
cy={l.y}
rx={round(2.6 * l.s)}
ry={round(6.4 * l.s)}
transform={`rotate(${l.rot} ${l.x} ${l.y})`}
fill="currentColor"
className="transition-opacity duration-500 ease-[var(--ease-kit)] motion-reduce:transition-none"
style={{ opacity: drawn ? round(0.95 - l.i * 0.04, 2) : 0, transitionDelay: `${200 + l.i * 80}ms` }}
/>
))}
</g>
);
}
function Laurel({ drawn, className }: { drawn: boolean; className?: string }) {
return (
<svg viewBox="0 0 120 110" aria-hidden className={cn('text-[color-mix(in_oklab,var(--primary)_65%,var(--foreground))]', className)}>
<Branch drawn={drawn} />
<Branch drawn={drawn} flip />
</svg>
);
}
/** A round seal with text on the rim. The rim turns slowly on hover. */
function Seal({ top, bottom, center, icon, size = 168, className }: { top: string; bottom?: string; center?: string; icon?: IconName; size?: number; className?: string }) {
const id = useId().replace(/:/g, '');
const teeth = 36;
const star = Array.from({ length: teeth * 2 }, (_, i) => {
const a = (i * Math.PI) / teeth;
const r = i % 2 ? 46.5 : 49;
return `${round(50 + r * Math.cos(a))},${round(50 + r * Math.sin(a))}`;
}).join(' ');
// the rim text runs over the top on an arc of 240 degrees and shrinks to fit long issuer names
const rimSize = Math.min(6.4, round(145 / (0.85 * Math.max(1, top.length)), 2));
const long = top.length > 18;
return (
<span className={cn('group/seal relative inline-grid place-items-center', className)} style={{ width: size, height: size }}>
<svg viewBox="0 0 100 100" aria-hidden data-clip-ok className="absolute inset-0 size-full">
<defs>
<path id={`${id}-t`} d="M 18.82 68 A 36 36 0 1 1 81.18 68" />
<path id={`${id}-b`} d="M 50 50 m -39.5 0 a 39.5 39.5 0 0 0 79 0" />
</defs>
<polygon points={star} className="fill-[color-mix(in_oklab,var(--primary)_14%,var(--card))] stroke-[color-mix(in_oklab,var(--primary)_40%,var(--border))]" strokeWidth="0.6" />
<g className="origin-center transition-transform duration-[2400ms] ease-[var(--ease-kit)] group-hover/seal:rotate-[24deg] motion-reduce:transition-none">
<circle cx="50" cy="50" r="42" fill="none" className="stroke-[color-mix(in_oklab,var(--primary)_55%,var(--foreground))]" strokeWidth="0.5" />
<circle cx="50" cy="50" r="31" fill="none" className="stroke-[color-mix(in_oklab,var(--primary)_55%,var(--foreground))]" strokeWidth="0.5" />
<text fontSize={rimSize} fontWeight="600" letterSpacing={round(rimSize * 0.17, 2)} className="fill-foreground uppercase">
<textPath href={`#${id}-t`} startOffset="50%" textAnchor="middle">
{top}
</textPath>
</text>
{bottom && (
<text fontSize="5.6" fontWeight="500" letterSpacing="1" className="fill-muted-foreground uppercase">
<textPath href={`#${id}-b`} startOffset="50%" textAnchor="middle" dominantBaseline="hanging">
{bottom}
</textPath>
</text>
)}
{!long && [0, 180].map((a) => (
<circle key={a} cx={round(50 + 36.5 * Math.cos(((a + 0) * Math.PI) / 180))} cy="50" r="0.9" className="fill-[color-mix(in_oklab,var(--primary)_60%,var(--foreground))]" />
))}
</g>
</svg>
<span className="relative flex max-w-[52%] flex-col items-center text-center">
{icon && <Icon name={icon} size={Math.round(size / 7.5)} className="text-[color-mix(in_oklab,var(--primary)_70%,var(--foreground))]" />}
{center && <span className={cn('font-semibold leading-tight tracking-[-0.02em] text-foreground', icon ? 'mt-1' : '')} style={{ fontSize: Math.max(11, Math.round(size / 11)) }}>{center}</span>}
</span>
</span>
);
}
/** A medal on a ribbon in the accent hue, at fixed lightness so the white text reads in both modes and with every accent */
function Medal({ label, sub, className }: { label: string; sub?: string; className?: string }) {
const id = useId().replace(/:/g, '');
return (
<svg viewBox="0 0 200 260" aria-hidden data-clip-ok className={className}>
<defs>
<linearGradient id={`${id}-m`} x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stopColor="oklch(from var(--primary) 0.74 c h)" />
<stop offset="0.5" stopColor="oklch(from var(--primary) 0.6 c h)" />
<stop offset="1" stopColor="oklch(from var(--primary) 0.42 c h)" />
</linearGradient>
<linearGradient id={`${id}-shine`} x1="0" y1="0" x2="1" y2="0">
<stop offset="0" stopColor="white" stopOpacity="0" />
<stop offset="0.5" stopColor="white" stopOpacity="0.45" />
<stop offset="1" stopColor="white" stopOpacity="0" />
</linearGradient>
<clipPath id={`${id}-c`}>
<circle cx="100" cy="170" r="70" />
</clipPath>
</defs>
{/* ribbon */}
<path d="M62 0h34l20 110H82Z" fill="oklch(from var(--primary) 0.45 c h)" />
<path d="M104 0h34l-20 110H84Z" fill="oklch(from var(--primary) 0.66 c h)" />
<path d="M78 96h44v18H78Z" fill="color-mix(in oklab, var(--foreground) 30%, var(--card))" />
{/* medal */}
<circle cx="100" cy="170" r="72" fill="color-mix(in oklab, var(--primary) 40%, var(--foreground))" opacity="0.35" />
<circle cx="100" cy="170" r="70" fill={`url(#${id}-m)`} />
<circle cx="100" cy="170" r="56" fill="none" stroke="rgb(255 255 255 / 0.5)" strokeWidth="1.2" strokeDasharray="2 3" />
<g clipPath={`url(#${id}-c)`}>
<rect x="-60" y="90" width="50" height="170" fill={`url(#${id}-shine)`} transform="skewX(-18)" className="animate-[aw-shine_5s_var(--ease-kit)_1s_infinite] motion-reduce:hidden" />
</g>
<text x="100" y={sub ? 172 : 180} textAnchor="middle" fontSize="26" fontWeight="700" fill="white" letterSpacing="-0.5">
{label}
</text>
{sub && (
<text x="100" y="196" textAnchor="middle" fontSize="11" fontWeight="600" letterSpacing="2" fill="white" opacity="0.85" className="uppercase">
{sub}
</text>
)}
<style>{'@keyframes aw-shine{0%{transform:skewX(-18deg) translateX(0)}40%,100%{transform:skewX(-18deg) translateX(260px)}}'}</style>
</svg>
);
}
function Stars({ value, size = 15 }: { value: number; size?: number }) {
return (
<span className="flex gap-0.5" aria-hidden>
{Array.from({ length: 5 }, (_, i) => {
const f = Math.max(0, Math.min(1, value - i));
return (
<span key={i} className="relative" style={{ width: size, height: size }}>
<Icon name="star" size={size} className="absolute inset-0 text-[color-mix(in_oklab,var(--foreground)_22%,transparent)]" />
<span data-clip-ok className="absolute inset-0 overflow-hidden" style={{ width: `${round(f * 100)}%` }}>
<Icon name="star" size={size} className="fill-current text-[color-mix(in_oklab,var(--warning)_85%,var(--foreground))]" />
</span>
</span>
);
})}
</span>
);
}
/* ------------------------------------------------------------------ */
/* laurels */
/* ------------------------------------------------------------------ */
function Laurels(p: ViewProps) {
const [ref, inView] = useInView<HTMLUListElement>();
const n = p.items.length;
return (
<Section className={p.className}>
<Head {...p} center />
<ul ref={ref} className={cn('mx-auto grid grid-cols-2 gap-x-4 gap-y-10 sm:gap-x-8', p.title && 'mt-14', n >= 4 ? 'lg:grid-cols-4' : n === 3 ? 'md:grid-cols-3' : '', 'max-w-[68rem]')}>
{p.items.map((it, i) => {
const inner = (
<>
<span className="relative grid aspect-[12/11] w-full max-w-[14rem] place-items-center">
<Laurel drawn={inView} className="absolute inset-0 size-full" />
<span className="relative flex max-w-[50%] flex-col items-center pt-2 text-center">
<span className="text-balance text-[13px] font-semibold leading-tight tracking-[-0.01em] text-foreground sm:text-[15px] lg:text-[16px]">{it.title}</span>
{it.year && <span className="mt-1.5 text-[12.5px] tabular-nums text-muted-foreground">{it.year}</span>}
</span>
</span>
{it.rank && <span className="mt-2 text-center text-[11px] font-semibold uppercase tracking-[0.14em] text-[color-mix(in_oklab,var(--primary)_75%,var(--muted-foreground))]">{it.rank}</span>}
{it.issuer && <span className="mt-1 text-center text-[13px] text-muted-foreground">{it.issuer}</span>}
</>
);
return (
<li key={i} className={cn('flex flex-col items-center transition-[opacity,transform] duration-700 ease-[var(--ease-kit)] motion-reduce:transition-none', inView ? 'opacity-100' : 'translate-y-3 opacity-0')} style={{ transitionDelay: `${i * 110}ms` }}>
{it.href ? (
<a href={it.href} className="group/l flex w-full cursor-pointer flex-col items-center rounded-2xl p-2 transition-transform duration-300 ease-[var(--ease-kit)] hover:-translate-y-1 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-solid focus-visible:outline-primary motion-reduce:hover:translate-y-0">
{inner}
</a>
) : (
<div className="flex w-full flex-col items-center p-2">{inner}</div>
)}
</li>
);
})}
</ul>
</Section>
);
}
/* ------------------------------------------------------------------ */
/* seals */
/* ------------------------------------------------------------------ */
function Seals(p: ViewProps) {
return (
<Section background="dots" className={p.className}>
<Head {...p} center />
<ul className={cn('mx-auto grid max-w-[64rem] grid-cols-2 gap-6 sm:gap-10 lg:grid-cols-4', p.title && 'mt-14')}>
{p.items.map((it, i) => (
<li key={i} className="kit-rise flex flex-col items-center text-center" style={delay(i * 90)}>
<Seal top={it.issuer ?? it.title} bottom={it.year} center={it.rank ?? it.title} icon={it.icon ?? 'award'} size={164} className="max-sm:!size-[8.5rem] transition-transform duration-500 ease-[var(--ease-kit)] hover:-rotate-3 motion-reduce:transition-none" />
<p className="mt-4 text-balance text-[14.5px] font-semibold text-foreground">{it.title}</p>
{it.category && <p className="mt-1 text-[13px] text-muted-foreground">{it.category}</p>}
</li>
))}
</ul>
</Section>
);
}
/* ------------------------------------------------------------------ */
/* trophy-case */
/* ------------------------------------------------------------------ */
function TrophyCase(p: ViewProps) {
const { t } = p;
const loc = useLocale() === 'de' ? 'de-DE' : 'en-US';
const [top, ...rest] = p.items;
if (!top) return null;
return (
<Section className={p.className}>
<div className="grid grid-cols-[minmax(0,1fr)] items-center gap-12 lg:grid-cols-[minmax(0,26rem)_minmax(0,1fr)] lg:gap-20">
<div className="kit-rise relative mx-auto w-full max-w-[22rem]">
<div aria-hidden className="absolute inset-x-6 bottom-0 top-1/3 rounded-full bg-[radial-gradient(closest-side,var(--glow),transparent)] blur-2xl" />
<Medal label={top.year ?? ''} sub={top.rank ?? t.winner} className="relative mx-auto w-[62%] animate-kit-float drop-shadow-[0_20px_30px_rgba(0,0,0,0.18)] motion-reduce:animate-none" />
<div className="relative mt-6 text-center">
<p className="text-balance text-[20px] font-semibold leading-snug text-foreground">{top.title}</p>
{top.issuer && <p className="mt-1 text-[14px] text-muted-foreground">{top.issuer}</p>}
{top.description && <p className="mx-auto mt-3 max-w-[22rem] text-pretty text-[14.5px] leading-relaxed text-muted-foreground">{top.description}</p>}
{top.href && (
<a href={top.href} className="mt-3 inline-flex min-h-11 cursor-pointer items-center gap-1.5 text-[14px] font-medium text-foreground underline-offset-4 hover:underline">
{t.more}
<Icon name="arrow-right" size={15} />
</a>
)}
</div>
</div>
<div>
<Head {...p} />
{p.rating && (
<p className="kit-rise mt-6 flex flex-wrap items-center gap-3 text-[14px] text-muted-foreground" style={delay(120)}>
<Stars value={p.rating.value} />
<span>
<strong className="font-semibold text-foreground">{fill(t.rating, { v: new Intl.NumberFormat(loc, { minimumFractionDigits: 1 }).format(p.rating.value) })}</strong> {fill(t.from, { n: new Intl.NumberFormat(loc).format(p.rating.count) })}
</span>
</p>
)}
<ul className="mt-10 border-t border-border">
{rest.map((it, i) => (
<li key={i} className="kit-rise grid grid-cols-[3.5rem_minmax(0,1fr)] items-center gap-4 border-b border-border py-4 sm:grid-cols-[4.5rem_minmax(0,1fr)_auto]" style={delay(160 + i * 60)}>
<span className="text-[15px] font-semibold tabular-nums text-muted-foreground">{it.year}</span>
<span className="min-w-0">
<span className="block text-pretty text-[15px] font-semibold text-foreground">{it.title}</span>
{it.issuer && <span className="block text-[13.5px] text-muted-foreground">{it.issuer}</span>}
</span>
{it.rank && <span className="col-start-2 inline-flex h-7 w-fit items-center gap-1.5 rounded-full border border-border px-3 text-[12.5px] font-medium text-foreground sm:col-start-3"><Icon name="award" size={14} className="text-primary" />{it.rank}</span>}
</li>
))}
</ul>
{p.cta && (
<Button href={p.cta.href} variant="outline" icon="arrow-right" className="mt-8 max-sm:w-full">
{p.cta.label}
</Button>
)}
</div>
</div>
</Section>
);
}
/* ------------------------------------------------------------------ */
/* by-year */
/* ------------------------------------------------------------------ */
function ByYear(p: ViewProps) {
const { t } = p;
const id = useId();
const years = [...new Set(p.items.map((i) => i.year ?? ''))].filter(Boolean).sort((a, b) => b.localeCompare(a));
const [year, setYear] = useState(years[0] ?? '');
const list = p.items.filter((i) => i.year === year);
return (
<Section background="surface" className={p.className}>
<div className="grid grid-cols-[minmax(0,1fr)] gap-10 lg:grid-cols-[minmax(0,1fr)_auto] lg:items-end">
<Head {...p} />
<div
role="tablist"
aria-label={t.year}
className="kit-rise flex gap-1 overflow-x-auto rounded-xl border border-border bg-card p-1 kit-no-scrollbar max-lg:w-fit max-lg:max-w-full"
onKeyDown={(e) => {
const d = e.key === 'ArrowRight' ? 1 : e.key === 'ArrowLeft' ? -1 : 0;
if (!d) return;
e.preventDefault();
const next = (years.indexOf(year) + d + years.length) % years.length;
setYear(years[next]);
(e.currentTarget.querySelectorAll('[role="tab"]')[next] as HTMLElement | undefined)?.focus();
}}
>
{years.map((y) => (
<button
key={y}
type="button"
role="tab"
id={`${id}-${y}`}
aria-selected={year === y}
aria-controls={`${id}-p`}
tabIndex={year === y ? 0 : -1}
onClick={() => setYear(y)}
className={cn('h-11 shrink-0 cursor-pointer rounded-lg px-4 text-[14px] font-semibold tabular-nums transition-[background-color,color] duration-300 focus-visible:outline-2 focus-visible:outline-solid focus-visible:outline-primary sm:h-10', year === y ? 'bg-primary text-primary-foreground' : 'text-muted-foreground hover:bg-surface hover:text-foreground')}
>
{y}
</button>
))}
</div>
</div>
<div id={`${id}-p`} role="tabpanel" aria-labelledby={`${id}-${year}`} className="mt-12 grid grid-cols-[minmax(0,1fr)] gap-6 lg:grid-cols-[minmax(0,14rem)_minmax(0,1fr)] lg:gap-10">
{/* the chosen year, set large, with the count below */}
<div key={year} className="kit-rise flex items-baseline gap-4 lg:block">
<p className="text-[clamp(3.5rem,2.8rem+3vw,6rem)] font-semibold leading-none tracking-[-0.05em] text-foreground tabular-nums">{year}</p>
<p className="text-[14px] text-muted-foreground lg:mt-3">{fill(list.length === 1 ? t.count1 : t.count, { n: list.length })}</p>
</div>
<ul className="relative grid grid-cols-[minmax(0,1fr)] gap-3 md:grid-cols-2">
{list.map((it, i) => (
<li key={`${year}-${i}`} className="kit-rise relative flex gap-4 overflow-hidden rounded-2xl border border-border bg-card p-5 pl-6 shadow-[var(--highlight),var(--shadow-sm)]" style={delay(i * 80)}>
{/* a ribbon tail on the left edge */}
<span aria-hidden className="absolute inset-y-0 left-0 w-1.5 bg-primary" />
<span className="grid size-11 shrink-0 place-items-center rounded-full bg-[color-mix(in_oklab,var(--primary)_12%,var(--card))] text-[color-mix(in_oklab,var(--primary)_80%,var(--foreground))]">
<Icon name={it.icon ?? 'trophy'} size={20} />
</span>
<div className="min-w-0">
{it.rank && <p className="text-[12px] font-semibold uppercase tracking-[0.12em] text-[color-mix(in_oklab,var(--primary)_75%,var(--muted-foreground))]">{it.rank}</p>}
<p className="mt-0.5 text-pretty text-[16px] font-semibold leading-snug text-foreground">{it.title}</p>
<p className="mt-1 text-[13.5px] text-muted-foreground">
{[it.issuer, it.category].filter(Boolean).join(' · ')}
</p>
{it.description && <p className="mt-2 text-pretty text-[14px] leading-relaxed text-muted-foreground">{it.description}</p>}
</div>
</li>
))}
</ul>
</div>
</Section>
);
}
/* ------------------------------------------------------------------ */
/* strip */
/* ------------------------------------------------------------------ */
function Strip(p: ViewProps) {
const { t } = p;
const locale = useLocale();
return (
<div className={cn('py-10', p.className)}>
<div className="kit-container">
<div className="flex flex-col items-center gap-6 rounded-2xl border border-border bg-card px-5 py-6 shadow-[var(--highlight),var(--shadow-sm)] sm:px-8 xl:flex-row xl:justify-between">
{p.rating && (
<div className="kit-rise flex shrink-0 items-center gap-4 xl:border-r xl:border-border xl:pr-8">
<span className="text-[40px] font-semibold leading-none tabular-nums tracking-[-0.04em] text-foreground">{new Intl.NumberFormat(locale === 'de' ? 'de-DE' : 'en-US', { minimumFractionDigits: 1 }).format(p.rating.value)}</span>
<span>
<Stars value={p.rating.value} size={16} />
<span className="mt-1 block text-[13px] text-muted-foreground">
{fill(t.from, { n: new Intl.NumberFormat(locale === 'de' ? 'de-DE' : 'en-US').format(p.rating.count) })}
{p.rating.source && ` · ${p.rating.source}`}
</span>
<span className="sr-only">{fill(t.rating, { v: new Intl.NumberFormat(locale === 'de' ? 'de-DE' : 'en-US', { minimumFractionDigits: 1 }).format(p.rating.value) })}</span>
</span>
</div>
)}
<ul aria-label={t.awards} className="grid w-full grid-cols-2 gap-x-4 gap-y-5 sm:max-w-[40rem] sm:gap-x-8 xl:flex xl:w-auto xl:max-w-none xl:flex-nowrap xl:gap-8">
{p.items.map((it, i) => (
<li key={i} className="kit-rise flex items-center gap-3" style={delay(80 + i * 70)}>
<Seal top={it.issuer ?? ''} center={it.year} size={56} className="shrink-0 [&_text]:hidden" icon={undefined} />
<span className="min-w-0">
<span className="block text-pretty text-[13.5px] font-semibold leading-tight text-foreground">{it.title}</span>
{it.issuer && <span className="mt-0.5 block text-[12px] text-muted-foreground">{it.issuer}</span>}
</span>
</li>
))}
</ul>
</div>
</div>
</div>
);
}
/* ------------------------------------------------------------------ */
const VIEWS: Record<AwardsVariant, (p: ViewProps) => React.ReactNode> = {
laurels: Laurels,
seals: Seals,
'trophy-case': TrophyCase,
'by-year': ByYear,
strip: Strip,
};
export function Awards({ variant = 'laurels', labels, ...rest }: AwardsProps) {
const t = useTexts(TEXTS, labels);
const View = VIEWS[variant] ?? Laurels;
return <View {...rest} variant={variant} t={t} />;
}Häufige Fragen
Wie installiere ich Auszeichnungen?
Mit aiw add @ai-website/awards. Der Befehl legt die Datei components/ui/awards.tsx in dein Projekt, der Code gehört danach dir. Mit dabei sind button, icon und section. Weitere npm-Pakete braucht sie nicht.
Welche Varianten hat Auszeichnungen?
5 Varianten, Lorbeeren, Siegel, Vitrine, Nach Jahr und Schmale Leiste. Du wählst sie mit der Prop variant, zum Beispiel variant="seals".
Kann ich die Texte von Auszeichnungen übersetzen?
Ja. Auszeichnungen bringt 9 eigene Texte auf Deutsch und Englisch mit, etwa „1 Auszeichnung“, „Jahr“ und „Alle Jahre“. Die Sprache folgt dem KitProvider, einzelne Texte ersetzt du über die Prop labels.
Kann ein KI-Assistent Auszeichnungen einbauen?
Ja. Über den MCP-Server der aiw CLI finden Claude, Cursor, GitHub Copilot, Windsurf, Codex und Gemini Auszeichnungen mit aiw_components und installieren sie mit aiw_add @ai-website/awards. Nenn im Prompt die Variante, etwa variant="laurels", dann schreibt der Assistent deine Inhalte direkt in die Props.