Add package summary banner
This commit is contained in:
@@ -2074,9 +2074,25 @@
|
|||||||
"unlimited": "Unbegrenzt",
|
"unlimited": "Unbegrenzt",
|
||||||
"unknown": "Unbekannt",
|
"unknown": "Unbekannt",
|
||||||
"featuresTitle": "Enthaltene Features",
|
"featuresTitle": "Enthaltene Features",
|
||||||
|
"feature": {
|
||||||
|
"priority_support": "Priority Support",
|
||||||
|
"custom_domain": "Eigene Domain",
|
||||||
|
"analytics": "Analytics",
|
||||||
|
"team_management": "Team-Management",
|
||||||
|
"moderation_tools": "Moderations-Tools",
|
||||||
|
"prints": "Print-Uploads",
|
||||||
|
"photo_likes_enabled": "Foto-Likes",
|
||||||
|
"event_checklist": "Event-Checkliste",
|
||||||
|
"advanced_analytics": "Erweiterte Analytics",
|
||||||
|
"branding_allowed": "Branding",
|
||||||
|
"watermark_allowed": "Wasserzeichen"
|
||||||
|
},
|
||||||
"hint": "Im Billing kannst du dein Paket jederzeit prüfen oder upgraden.",
|
"hint": "Im Billing kannst du dein Paket jederzeit prüfen oder upgraden.",
|
||||||
"continue": "Weiter zum Event-Setup",
|
"continue": "Weiter zum Event-Setup",
|
||||||
"dismiss": "Schließen"
|
"dismiss": "Schließen",
|
||||||
|
"bannerTitle": "Deine Paketübersicht",
|
||||||
|
"bannerSubtitle": "{{name}} ist aktiv. Prüfe Limits & Features.",
|
||||||
|
"bannerCta": "Ansehen"
|
||||||
},
|
},
|
||||||
"pickEvent": "Event auswählen",
|
"pickEvent": "Event auswählen",
|
||||||
"status": {
|
"status": {
|
||||||
|
|||||||
@@ -2078,9 +2078,25 @@
|
|||||||
"unlimited": "Unlimited",
|
"unlimited": "Unlimited",
|
||||||
"unknown": "Unknown",
|
"unknown": "Unknown",
|
||||||
"featuresTitle": "Included features",
|
"featuresTitle": "Included features",
|
||||||
|
"feature": {
|
||||||
|
"priority_support": "Priority support",
|
||||||
|
"custom_domain": "Custom domain",
|
||||||
|
"analytics": "Analytics",
|
||||||
|
"team_management": "Team management",
|
||||||
|
"moderation_tools": "Moderation tools",
|
||||||
|
"prints": "Print uploads",
|
||||||
|
"photo_likes_enabled": "Photo likes",
|
||||||
|
"event_checklist": "Event checklist",
|
||||||
|
"advanced_analytics": "Advanced analytics",
|
||||||
|
"branding_allowed": "Branding",
|
||||||
|
"watermark_allowed": "Watermarks"
|
||||||
|
},
|
||||||
"hint": "You can revisit billing any time to review or upgrade your package.",
|
"hint": "You can revisit billing any time to review or upgrade your package.",
|
||||||
"continue": "Continue to event setup",
|
"continue": "Continue to event setup",
|
||||||
"dismiss": "Close"
|
"dismiss": "Close",
|
||||||
|
"bannerTitle": "Your package summary",
|
||||||
|
"bannerSubtitle": "{{name}} is active. Review limits & features.",
|
||||||
|
"bannerCta": "View"
|
||||||
},
|
},
|
||||||
"pickEvent": "Select an event",
|
"pickEvent": "Select an event",
|
||||||
"status": {
|
"status": {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { useAdminPushSubscription } from './hooks/useAdminPushSubscription';
|
|||||||
import { useDevicePermissions } from './hooks/useDevicePermissions';
|
import { useDevicePermissions } from './hooks/useDevicePermissions';
|
||||||
import { useInstallPrompt } from './hooks/useInstallPrompt';
|
import { useInstallPrompt } from './hooks/useInstallPrompt';
|
||||||
import { getTourSeen, resolveTourStepKeys, setTourSeen, type TourStepKey } from './lib/mobileTour';
|
import { getTourSeen, resolveTourStepKeys, setTourSeen, type TourStepKey } from './lib/mobileTour';
|
||||||
|
import { formatPackageLimit, getPackageFeatureLabel } from './lib/packageSummary';
|
||||||
import { trackOnboarding } from '../api';
|
import { trackOnboarding } from '../api';
|
||||||
import { useAuth } from '../auth/context';
|
import { useAuth } from '../auth/context';
|
||||||
import { ADMIN_ACTION_COLORS, ADMIN_MOTION, useAdminTheme } from './theme';
|
import { ADMIN_ACTION_COLORS, ADMIN_MOTION, useAdminTheme } from './theme';
|
||||||
@@ -350,6 +351,11 @@ export default function MobileDashboardPage() {
|
|||||||
locale={locale}
|
locale={locale}
|
||||||
/>
|
/>
|
||||||
) : null;
|
) : null;
|
||||||
|
const showPackageSummaryBanner =
|
||||||
|
Boolean(activePackage && summarySeenPackageId === activePackage.id) &&
|
||||||
|
!summaryOpen &&
|
||||||
|
!packagesLoading &&
|
||||||
|
!packagesError;
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (events.length || isLoading || fallbackLoading || fallbackAttempted) {
|
if (events.length || isLoading || fallbackLoading || fallbackAttempted) {
|
||||||
@@ -387,6 +393,12 @@ export default function MobileDashboardPage() {
|
|||||||
if (!effectiveHasEvents) {
|
if (!effectiveHasEvents) {
|
||||||
return (
|
return (
|
||||||
<MobileShell activeTab="home" title={t('mobileDashboard.title', 'Dashboard')}>
|
<MobileShell activeTab="home" title={t('mobileDashboard.title', 'Dashboard')}>
|
||||||
|
{showPackageSummaryBanner ? (
|
||||||
|
<PackageSummaryBanner
|
||||||
|
packageName={activePackage?.package_name}
|
||||||
|
onOpen={() => setSummaryOpen(true)}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
<OnboardingEmptyState
|
<OnboardingEmptyState
|
||||||
installPrompt={installPrompt}
|
installPrompt={installPrompt}
|
||||||
pushState={pushState}
|
pushState={pushState}
|
||||||
@@ -406,6 +418,12 @@ export default function MobileDashboardPage() {
|
|||||||
title={t('mobileDashboard.title', 'Dashboard')}
|
title={t('mobileDashboard.title', 'Dashboard')}
|
||||||
subtitle={t('mobileDashboard.selectEvent', 'Select an event to continue')}
|
subtitle={t('mobileDashboard.selectEvent', 'Select an event to continue')}
|
||||||
>
|
>
|
||||||
|
{showPackageSummaryBanner ? (
|
||||||
|
<PackageSummaryBanner
|
||||||
|
packageName={activePackage?.package_name}
|
||||||
|
onOpen={() => setSummaryOpen(true)}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
<EventPickerList events={effectiveEvents} locale={locale} text={text} muted={muted} border={border} />
|
<EventPickerList events={effectiveEvents} locale={locale} text={text} muted={muted} border={border} />
|
||||||
{tourSheet}
|
{tourSheet}
|
||||||
{packageSummarySheet}
|
{packageSummarySheet}
|
||||||
@@ -419,6 +437,12 @@ export default function MobileDashboardPage() {
|
|||||||
title={resolveEventDisplayName(activeEvent ?? undefined)}
|
title={resolveEventDisplayName(activeEvent ?? undefined)}
|
||||||
subtitle={formatEventDate(activeEvent?.event_date, locale) ?? undefined}
|
subtitle={formatEventDate(activeEvent?.event_date, locale) ?? undefined}
|
||||||
>
|
>
|
||||||
|
{showPackageSummaryBanner ? (
|
||||||
|
<PackageSummaryBanner
|
||||||
|
packageName={activePackage?.package_name}
|
||||||
|
onOpen={() => setSummaryOpen(true)}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
<DeviceSetupCard
|
<DeviceSetupCard
|
||||||
installPrompt={installPrompt}
|
installPrompt={installPrompt}
|
||||||
pushState={pushState}
|
pushState={pushState}
|
||||||
@@ -486,11 +510,6 @@ function PackageSummarySheet({
|
|||||||
const galleryDays = (limits as Record<string, number | null> | null)?.gallery_days ?? null;
|
const galleryDays = (limits as Record<string, number | null> | null)?.gallery_days ?? null;
|
||||||
const hasFeatures = Array.isArray(features) && features.length > 0;
|
const hasFeatures = Array.isArray(features) && features.length > 0;
|
||||||
|
|
||||||
const formatLimit = (value: number | null) =>
|
|
||||||
value === null || value === undefined
|
|
||||||
? t('mobileDashboard.packageSummary.unlimited', 'Unlimited')
|
|
||||||
: String(value);
|
|
||||||
|
|
||||||
const formatDate = (value?: string | null) => formatEventDate(value, locale) ?? t('mobileDashboard.packageSummary.unknown', 'Unknown');
|
const formatDate = (value?: string | null) => formatEventDate(value, locale) ?? t('mobileDashboard.packageSummary.unknown', 'Unknown');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -506,12 +525,12 @@ function PackageSummarySheet({
|
|||||||
</Text>
|
</Text>
|
||||||
</YStack>
|
</YStack>
|
||||||
<YStack space="$2" marginTop="$2">
|
<YStack space="$2" marginTop="$2">
|
||||||
<SummaryRow label={t('mobileDashboard.packageSummary.limitPhotos', 'Photos')} value={formatLimit(maxPhotos)} />
|
<SummaryRow label={t('mobileDashboard.packageSummary.limitPhotos', 'Photos')} value={formatPackageLimit(maxPhotos, t)} />
|
||||||
<SummaryRow label={t('mobileDashboard.packageSummary.limitGuests', 'Guests')} value={formatLimit(maxGuests)} />
|
<SummaryRow label={t('mobileDashboard.packageSummary.limitGuests', 'Guests')} value={formatPackageLimit(maxGuests, t)} />
|
||||||
<SummaryRow label={t('mobileDashboard.packageSummary.limitDays', 'Gallery days')} value={formatLimit(galleryDays)} />
|
<SummaryRow label={t('mobileDashboard.packageSummary.limitDays', 'Gallery days')} value={formatPackageLimit(galleryDays, t)} />
|
||||||
<SummaryRow
|
<SummaryRow
|
||||||
label={t('mobileDashboard.packageSummary.remaining', 'Remaining events')}
|
label={t('mobileDashboard.packageSummary.remaining', 'Remaining events')}
|
||||||
value={remainingEvents === null || remainingEvents === undefined ? t('mobileDashboard.packageSummary.unlimited', 'Unlimited') : String(remainingEvents)}
|
value={formatPackageLimit(remainingEvents, t)}
|
||||||
/>
|
/>
|
||||||
<SummaryRow label={t('mobileDashboard.packageSummary.purchased', 'Purchased')} value={formatDate(purchasedAt)} />
|
<SummaryRow label={t('mobileDashboard.packageSummary.purchased', 'Purchased')} value={formatDate(purchasedAt)} />
|
||||||
{expiresAt ? (
|
{expiresAt ? (
|
||||||
@@ -520,7 +539,7 @@ function PackageSummarySheet({
|
|||||||
</YStack>
|
</YStack>
|
||||||
</MobileCard>
|
</MobileCard>
|
||||||
|
|
||||||
{hasFeatures ? (
|
{hasFeatures ? (
|
||||||
<MobileCard space="$2" borderColor={border} backgroundColor={surface}>
|
<MobileCard space="$2" borderColor={border} backgroundColor={surface}>
|
||||||
<Text fontSize="$sm" fontWeight="800" color={text}>
|
<Text fontSize="$sm" fontWeight="800" color={text}>
|
||||||
{t('mobileDashboard.packageSummary.featuresTitle', 'Included features')}
|
{t('mobileDashboard.packageSummary.featuresTitle', 'Included features')}
|
||||||
@@ -532,7 +551,7 @@ function PackageSummarySheet({
|
|||||||
<Sparkles size={14} color={primary} />
|
<Sparkles size={14} color={primary} />
|
||||||
</XStack>
|
</XStack>
|
||||||
<Text fontSize="$xs" color={text}>
|
<Text fontSize="$xs" color={text}>
|
||||||
{t(`mobileDashboard.packageSummary.feature.${feature}`, feature)}
|
{getPackageFeatureLabel(feature, t)}
|
||||||
</Text>
|
</Text>
|
||||||
</XStack>
|
</XStack>
|
||||||
))}
|
))}
|
||||||
@@ -569,6 +588,54 @@ function SummaryRow({ label, value }: { label: string; value: string }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function PackageSummaryBanner({
|
||||||
|
packageName,
|
||||||
|
onOpen,
|
||||||
|
}: {
|
||||||
|
packageName?: string | null;
|
||||||
|
onOpen: () => void;
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation('management');
|
||||||
|
const { textStrong, muted, border, surface, accentSoft, primary } = useAdminTheme();
|
||||||
|
const text = textStrong;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MobileCard space="$2" borderColor={border} backgroundColor={surface}>
|
||||||
|
<XStack alignItems="center" justifyContent="space-between" gap="$2">
|
||||||
|
<XStack alignItems="center" space="$2" flex={1}>
|
||||||
|
<XStack
|
||||||
|
width={36}
|
||||||
|
height={36}
|
||||||
|
borderRadius={12}
|
||||||
|
backgroundColor={accentSoft}
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="center"
|
||||||
|
>
|
||||||
|
<Sparkles size={16} color={primary} />
|
||||||
|
</XStack>
|
||||||
|
<YStack space="$0.5" flex={1}>
|
||||||
|
<Text fontSize="$sm" fontWeight="800" color={text}>
|
||||||
|
{t('mobileDashboard.packageSummary.bannerTitle', 'Your package summary')}
|
||||||
|
</Text>
|
||||||
|
<Text fontSize="$xs" color={muted}>
|
||||||
|
{t('mobileDashboard.packageSummary.bannerSubtitle', {
|
||||||
|
name: packageName ?? t('mobileDashboard.packageSummary.fallbackTitle', 'Package summary'),
|
||||||
|
defaultValue: '{{name}} is active. Review limits & features.',
|
||||||
|
})}
|
||||||
|
</Text>
|
||||||
|
</YStack>
|
||||||
|
</XStack>
|
||||||
|
<CTAButton
|
||||||
|
label={t('mobileDashboard.packageSummary.bannerCta', 'View')}
|
||||||
|
tone="ghost"
|
||||||
|
fullWidth={false}
|
||||||
|
onPress={onOpen}
|
||||||
|
/>
|
||||||
|
</XStack>
|
||||||
|
</MobileCard>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function DeviceSetupCard({ installPrompt, pushState, devicePermissions, onOpenSettings }: DeviceSetupProps) {
|
function DeviceSetupCard({ installPrompt, pushState, devicePermissions, onOpenSettings }: DeviceSetupProps) {
|
||||||
const { t } = useTranslation('management');
|
const { t } = useTranslation('management');
|
||||||
const { textStrong, muted, border, primary, accentSoft } = useAdminTheme();
|
const { textStrong, muted, border, primary, accentSoft } = useAdminTheme();
|
||||||
|
|||||||
27
resources/js/admin/mobile/lib/packageSummary.test.ts
Normal file
27
resources/js/admin/mobile/lib/packageSummary.test.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { formatPackageLimit, getPackageFeatureLabel } from './packageSummary';
|
||||||
|
|
||||||
|
const t = (key: string, options?: Record<string, unknown> | string) => {
|
||||||
|
if (typeof options === 'string') {
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
return (options?.defaultValue as string | undefined) ?? key;
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('packageSummary helpers', () => {
|
||||||
|
it('returns translated labels for known features', () => {
|
||||||
|
expect(getPackageFeatureLabel('priority_support', t)).toBe('Priority support');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to raw feature key for unknown features', () => {
|
||||||
|
expect(getPackageFeatureLabel('custom_feature', t)).toBe('custom_feature');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('formats unlimited package limits', () => {
|
||||||
|
expect(formatPackageLimit(null, t)).toBe('Unlimited');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('formats numeric package limits', () => {
|
||||||
|
expect(formatPackageLimit(12, t)).toBe('12');
|
||||||
|
});
|
||||||
|
});
|
||||||
65
resources/js/admin/mobile/lib/packageSummary.ts
Normal file
65
resources/js/admin/mobile/lib/packageSummary.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
type Translate = (key: string, options?: Record<string, unknown> | string) => string;
|
||||||
|
|
||||||
|
const FEATURE_LABELS: Record<string, { key: string; fallback: string }> = {
|
||||||
|
priority_support: {
|
||||||
|
key: 'mobileDashboard.packageSummary.feature.priority_support',
|
||||||
|
fallback: 'Priority support',
|
||||||
|
},
|
||||||
|
custom_domain: {
|
||||||
|
key: 'mobileDashboard.packageSummary.feature.custom_domain',
|
||||||
|
fallback: 'Custom domain',
|
||||||
|
},
|
||||||
|
analytics: {
|
||||||
|
key: 'mobileDashboard.packageSummary.feature.analytics',
|
||||||
|
fallback: 'Analytics',
|
||||||
|
},
|
||||||
|
team_management: {
|
||||||
|
key: 'mobileDashboard.packageSummary.feature.team_management',
|
||||||
|
fallback: 'Team management',
|
||||||
|
},
|
||||||
|
moderation_tools: {
|
||||||
|
key: 'mobileDashboard.packageSummary.feature.moderation_tools',
|
||||||
|
fallback: 'Moderation tools',
|
||||||
|
},
|
||||||
|
prints: {
|
||||||
|
key: 'mobileDashboard.packageSummary.feature.prints',
|
||||||
|
fallback: 'Print uploads',
|
||||||
|
},
|
||||||
|
photo_likes_enabled: {
|
||||||
|
key: 'mobileDashboard.packageSummary.feature.photo_likes_enabled',
|
||||||
|
fallback: 'Photo likes',
|
||||||
|
},
|
||||||
|
event_checklist: {
|
||||||
|
key: 'mobileDashboard.packageSummary.feature.event_checklist',
|
||||||
|
fallback: 'Event checklist',
|
||||||
|
},
|
||||||
|
advanced_analytics: {
|
||||||
|
key: 'mobileDashboard.packageSummary.feature.advanced_analytics',
|
||||||
|
fallback: 'Advanced analytics',
|
||||||
|
},
|
||||||
|
branding_allowed: {
|
||||||
|
key: 'mobileDashboard.packageSummary.feature.branding_allowed',
|
||||||
|
fallback: 'Branding',
|
||||||
|
},
|
||||||
|
watermark_allowed: {
|
||||||
|
key: 'mobileDashboard.packageSummary.feature.watermark_allowed',
|
||||||
|
fallback: 'Watermarks',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getPackageFeatureLabel(feature: string, t: Translate): string {
|
||||||
|
const entry = FEATURE_LABELS[feature];
|
||||||
|
if (entry) {
|
||||||
|
return t(entry.key, entry.fallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
return t(`mobileDashboard.packageSummary.feature.${feature}`, feature);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatPackageLimit(value: number | null | undefined, t: Translate): string {
|
||||||
|
if (value === null || value === undefined) {
|
||||||
|
return t('mobileDashboard.packageSummary.unlimited', 'Unlimited');
|
||||||
|
}
|
||||||
|
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user