Update partner packages, copy, and demo switcher
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled
tests / ui (push) Has been cancelled

This commit is contained in:
Codex Agent
2026-01-15 17:33:36 +01:00
parent 2f93271d94
commit ad829ae509
50 changed files with 1335 additions and 411 deletions

View File

@@ -28,6 +28,7 @@ interface Package {
price: number;
events: number | null;
features: string[];
included_package_slug?: string | null;
max_events_per_year?: number | null;
limits?: {
max_photos?: number;
@@ -62,9 +63,10 @@ const sortPackagesByPrice = (packages: Package[]): Package[] =>
interface PackageComparisonProps {
packages: Package[];
variant: 'endcustomer' | 'reseller';
serviceTierNames?: Record<string, string>;
}
const buildDisplayFeatures = (pkg: Package): string[] => {
const buildDisplayFeatures = (pkg: Package, variant: 'endcustomer' | 'reseller'): string[] => {
const features = [...pkg.features];
const removeFeature = (key: string) => {
@@ -80,20 +82,22 @@ const buildDisplayFeatures = (pkg: Package): string[] => {
}
};
const watermarkFeature = resolveWatermarkFeatureKey(pkg);
['watermark', 'no_watermark', 'watermark_base', 'watermark_custom'].forEach(removeFeature);
addFeature(watermarkFeature);
if (variant === 'endcustomer') {
const watermarkFeature = resolveWatermarkFeatureKey(pkg);
['watermark', 'no_watermark', 'watermark_base', 'watermark_custom'].forEach(removeFeature);
addFeature(watermarkFeature);
if (pkg.branding_allowed) {
addFeature('custom_branding');
} else {
removeFeature('custom_branding');
if (pkg.branding_allowed) {
addFeature('custom_branding');
} else {
removeFeature('custom_branding');
}
}
return Array.from(new Set(features));
};
function PackageComparison({ packages, variant }: PackageComparisonProps) {
function PackageComparison({ packages, variant, serviceTierNames = {} }: PackageComparisonProps) {
const { t } = useTranslation('marketing');
const { t: tCommon } = useTranslation('common');
@@ -135,12 +139,19 @@ function PackageComparison({ packages, variant }: PackageComparisonProps) {
{
key: 'price',
label: t('packages.price'),
value: (pkg: Package) => `${formatPrice(pkg)} / ${t('packages.billing_per_year')}`,
value: (pkg: Package) => `${formatPrice(pkg)} / ${t('packages.billing_per_kontingent')}`,
},
{
key: 'max_tenants',
label: t('packages.max_tenants'),
value: (pkg: Package) => pkg.limits?.max_tenants?.toLocaleString() ?? tCommon('unlimited'),
key: 'included_package_slug',
label: t('packages.included_package_label', 'Inklusive Event-Level'),
value: (pkg: Package) => {
const slug = pkg.included_package_slug ?? null;
if (!slug) {
return tCommon('unlimited');
}
return serviceTierNames[slug] ?? slug;
},
},
{
key: 'max_events_per_year',
@@ -150,24 +161,51 @@ function PackageComparison({ packages, variant }: PackageComparisonProps) {
},
];
const features = [
{
key: 'watermark',
label: t('packages.watermark_label'),
value: (pkg: Package) => t(`packages.feature_${resolveWatermarkFeatureKey(pkg)}`),
},
{
key: 'branding',
label: t('packages.feature_custom_branding'),
value: (pkg: Package) => (pkg.branding_allowed ? t('packages.available') : t('packages.not_available')),
},
{
key: 'support',
label: t('packages.feature_support'),
value: (pkg: Package) =>
pkg.features.includes('priority_support') ? t('packages.priority_support') : t('packages.standard_support'),
},
];
const features =
variant === 'endcustomer'
? [
{
key: 'watermark',
label: t('packages.watermark_label'),
value: (pkg: Package) => t(`packages.feature_${resolveWatermarkFeatureKey(pkg)}`),
},
{
key: 'branding',
label: t('packages.feature_custom_branding'),
value: (pkg: Package) => (pkg.branding_allowed ? t('packages.available') : t('packages.not_available')),
},
{
key: 'support',
label: t('packages.feature_support'),
value: (pkg: Package) =>
pkg.features.includes('priority_support') ? t('packages.priority_support') : t('packages.standard_support'),
},
]
: [
{
key: 'recommended_usage_window',
label: t('packages.recommended_usage_label', 'Empfehlung'),
value: () => t('packages.recommended_usage_window'),
},
{
key: 'support',
label: t('packages.feature_support'),
value: (pkg: Package) =>
pkg.features.includes('priority_support') ? t('packages.priority_support') : t('packages.standard_support'),
},
{
key: 'dashboard',
label: t('packages.feature_reseller_dashboard'),
value: (pkg: Package) =>
pkg.features.includes('reseller_dashboard') ? t('packages.available') : t('packages.not_available'),
},
{
key: 'reporting',
label: t('packages.feature_advanced_reporting'),
value: (pkg: Package) =>
pkg.features.includes('advanced_reporting') ? t('packages.available') : t('packages.not_available'),
},
];
return (
<div className="space-y-4">
@@ -258,6 +296,15 @@ interface PackagesProps {
const Packages: React.FC<PackagesProps> = ({ endcustomerPackages, resellerPackages }) => {
const [open, setOpen] = useState(false);
const [selectedPackage, setSelectedPackage] = useState<Package | null>(null);
const serviceTierNames = useMemo(() => {
const map: Record<string, string> = {};
endcustomerPackages.forEach((pkg) => {
if (pkg?.slug) {
map[pkg.slug] = pkg.name;
}
});
return map;
}, [endcustomerPackages]);
const [isMobile, setIsMobile] = useState(false);
const dialogScrollRef = useRef<HTMLDivElement | null>(null);
const dialogHeadingRef = useRef<HTMLDivElement | null>(null);
@@ -499,6 +546,26 @@ type PackageMetric = {
value: string;
};
const resolveServiceTierLabel = (slug: string | null | undefined): string => {
if (!slug) {
return '';
}
if (slug === 'starter') {
return 'Starter';
}
if (slug === 'standard') {
return 'Standard';
}
if (slug === 'pro') {
return 'Premium';
}
return slug;
};
const resolvePackageMetrics = (
pkg: Package,
variant: 'endcustomer' | 'reseller',
@@ -508,11 +575,9 @@ const resolvePackageMetrics = (
if (variant === 'reseller') {
return [
{
key: 'max_tenants',
label: t('packages.max_tenants'),
value: pkg.limits?.max_tenants
? pkg.limits.max_tenants.toLocaleString()
: tCommon('unlimited'),
key: 'included_package_slug',
label: t('packages.included_package_label', 'Inklusive Event-Level'),
value: resolveServiceTierLabel(pkg.included_package_slug) || tCommon('unlimited'),
},
{
key: 'max_events_per_year',
@@ -522,9 +587,9 @@ const resolvePackageMetrics = (
: tCommon('unlimited'),
},
{
key: 'branding',
label: t('packages.feature_custom_branding'),
value: pkg.branding_allowed ? tCommon('included') : t('packages.feature_no_branding'),
key: 'recommended_usage_window',
label: t('packages.recommended_usage_label', 'Empfehlung'),
value: t('packages.recommended_usage_window'),
},
];
}
@@ -588,7 +653,7 @@ function PackageCard({
: `${numericPrice.toLocaleString()} ${t('packages.currency.euro')}`;
const cadenceLabel =
variant === 'reseller'
? t('packages.billing_per_year')
? t('packages.billing_per_kontingent')
: t('packages.billing_per_event');
const typeLabel =
variant === 'reseller' ? t('packages.subscription') : t('packages.one_time');
@@ -601,7 +666,7 @@ function PackageCard({
? t('packages.badge_starter')
: null;
const displayFeatures = buildDisplayFeatures(pkg);
const displayFeatures = buildDisplayFeatures(pkg, variant);
const keyFeatures = displayFeatures.slice(0, 3);
const visibleFeatures = compact ? displayFeatures.slice(0, 3) : displayFeatures.slice(0, 5);
const metrics = resolvePackageMetrics(pkg, variant, t, tCommon);
@@ -736,8 +801,8 @@ const PackageDetailGrid: React.FC<PackageDetailGridProps> = ({
}) => {
const metrics = resolvePackageMetrics(packageData, variant, t, tCommon);
const highlightFeatures = useMemo(
() => buildDisplayFeatures(packageData).slice(0, 5),
[packageData],
() => buildDisplayFeatures(packageData, variant).slice(0, 5),
[packageData, variant],
);
return (
@@ -756,7 +821,7 @@ const PackageDetailGrid: React.FC<PackageDetailGridProps> = ({
</p>
{packageData.price > 0 && (
<p className="text-sm text-gray-500 dark:text-gray-300">
/ {variant === 'reseller' ? t('packages.billing_per_year') : t('packages.billing_per_event')}
/ {variant === 'reseller' ? t('packages.billing_per_kontingent') : t('packages.billing_per_event')}
</p>
)}
</div>
@@ -1015,7 +1080,7 @@ const PackageDetailGrid: React.FC<PackageDetailGridProps> = ({
/>
))}
</div>
<PackageComparison packages={orderedResellerPackages} variant="reseller" />
<PackageComparison packages={orderedResellerPackages} variant="reseller" serviceTierNames={serviceTierNames} />
</TabsContent>
</Tabs>
</div>