Add a tier number and category label to package cards

Inspired by a reference mockup showing a numbered, categorized tier
lineup (Einstieg/Upgrade/.../Referenz). Each card now shows its price
rank (01, 02...) plus an auto-derived category eyebrow label, computed
the same way as the material-tier color system so it needs no extra
admin config and scales to any package count. Kept the existing
responsive grid rather than switching to a fixed horizontal row, since
the fixed layout looks broken for models with only 1-2 packages.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Maaxxs 2026-07-08 09:48:59 +02:00
parent dea2a936a1
commit bac6d114a2
3 changed files with 12 additions and 1 deletions

View file

@ -155,6 +155,7 @@ export default {
productLoading: 'Produkt wird geladen…',
requestContact: 'Kontakt anfragen',
featuredBadge: 'Empfohlen',
tierLabels: ['Einstieg', 'Upgrade', 'Performance', 'Premium', 'High-End', 'Signature', 'Referenz'],
},
leistungen: {
metaTitle: 'Leistungen',

View file

@ -155,6 +155,7 @@ export default {
productLoading: 'Loading product…',
requestContact: 'Request contact',
featuredBadge: 'Recommended',
tierLabels: ['Entry', 'Upgrade', 'Performance', 'Premium', 'High-End', 'Signature', 'Reference'],
},
leistungen: {
metaTitle: 'Services',

View file

@ -111,6 +111,7 @@ export default function ModelPage() {
// oben. Das braucht keine zusaetzliche Admin-Einstellung und passt sich automatisch
// an jede Paketanzahl an.
const materials = theme === 'dark' ? MATERIALS_DARK : MATERIALS_LIGHT;
const tierLabels = t('modelPage.tierLabels');
const rankById = new Map(
[...packages].sort((a, b) => a.total_price - b.total_price).map((p, i) => [p.id, i])
);
@ -118,6 +119,8 @@ export default function ModelPage() {
const n = packages.length;
const rank = rankById.get(pkg.id) ?? 0;
const tierT = n <= 1 ? 0 : rank / (n - 1);
const tierNumber = String(rank + 1).padStart(2, '0');
const tierLabel = tierLabels[Math.min(Math.floor(tierT * tierLabels.length), tierLabels.length - 1)];
const bgRgb = materialRgbAt(materials, tierT, 'bg').map(Math.round);
const accentRgbFloat = materialRgbAt(materials, tierT, 'accent');
@ -151,6 +154,8 @@ export default function ModelPage() {
return {
isDarkCard,
mutedColor,
tierNumber,
tierLabel,
priceColor: `rgb(${priceRgb.join(', ')})`,
iconChipStyle: {
backgroundColor: `rgba(${accentRgb.join(', ')}, 0.16)`,
@ -198,7 +203,7 @@ export default function ModelPage() {
<div className="grid gap-6 sm:grid-cols-2 xl:grid-cols-3">
{packages.map((pkg) => {
const { isDarkCard, style, priceColor, mutedColor, iconChipStyle } = styleOf(pkg);
const { isDarkCard, style, priceColor, mutedColor, iconChipStyle, tierNumber, tierLabel } = styleOf(pkg);
return (
<div key={pkg.id} style={style} className="relative flex flex-col rounded-xl border p-6">
@ -208,6 +213,10 @@ export default function ModelPage() {
</span>
)}
<p style={{ color: mutedColor }} className="mb-2 text-xs font-semibold uppercase tracking-wider">
{tierNumber} · {tierLabel}
</p>
{pkg.icon_name && (
<div style={iconChipStyle} className="mb-3 flex h-11 w-11 items-center justify-center rounded-full">
<DynamicIcon name={pkg.icon_name} className="h-6 w-6" />