Update partner packages, copy, and demo switcher
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user