Add package summary banner
This commit is contained in:
@@ -2074,9 +2074,25 @@
|
||||
"unlimited": "Unbegrenzt",
|
||||
"unknown": "Unbekannt",
|
||||
"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.",
|
||||
"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",
|
||||
"status": {
|
||||
|
||||
@@ -2078,9 +2078,25 @@
|
||||
"unlimited": "Unlimited",
|
||||
"unknown": "Unknown",
|
||||
"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.",
|
||||
"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",
|
||||
"status": {
|
||||
|
||||
@@ -17,6 +17,7 @@ import { useAdminPushSubscription } from './hooks/useAdminPushSubscription';
|
||||
import { useDevicePermissions } from './hooks/useDevicePermissions';
|
||||
import { useInstallPrompt } from './hooks/useInstallPrompt';
|
||||
import { getTourSeen, resolveTourStepKeys, setTourSeen, type TourStepKey } from './lib/mobileTour';
|
||||
import { formatPackageLimit, getPackageFeatureLabel } from './lib/packageSummary';
|
||||
import { trackOnboarding } from '../api';
|
||||
import { useAuth } from '../auth/context';
|
||||
import { ADMIN_ACTION_COLORS, ADMIN_MOTION, useAdminTheme } from './theme';
|
||||
@@ -350,6 +351,11 @@ export default function MobileDashboardPage() {
|
||||
locale={locale}
|
||||
/>
|
||||
) : null;
|
||||
const showPackageSummaryBanner =
|
||||
Boolean(activePackage && summarySeenPackageId === activePackage.id) &&
|
||||
!summaryOpen &&
|
||||
!packagesLoading &&
|
||||
!packagesError;
|
||||
|
||||
React.useEffect(() => {
|
||||
if (events.length || isLoading || fallbackLoading || fallbackAttempted) {
|
||||
@@ -387,6 +393,12 @@ export default function MobileDashboardPage() {
|
||||
if (!effectiveHasEvents) {
|
||||
return (
|
||||
<MobileShell activeTab="home" title={t('mobileDashboard.title', 'Dashboard')}>
|
||||
{showPackageSummaryBanner ? (
|
||||
<PackageSummaryBanner
|
||||
packageName={activePackage?.package_name}
|
||||
onOpen={() => setSummaryOpen(true)}
|
||||
/>
|
||||
) : null}
|
||||
<OnboardingEmptyState
|
||||
installPrompt={installPrompt}
|
||||
pushState={pushState}
|
||||
@@ -406,6 +418,12 @@ export default function MobileDashboardPage() {
|
||||
title={t('mobileDashboard.title', 'Dashboard')}
|
||||
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} />
|
||||
{tourSheet}
|
||||
{packageSummarySheet}
|
||||
@@ -419,6 +437,12 @@ export default function MobileDashboardPage() {
|
||||
title={resolveEventDisplayName(activeEvent ?? undefined)}
|
||||
subtitle={formatEventDate(activeEvent?.event_date, locale) ?? undefined}
|
||||
>
|
||||
{showPackageSummaryBanner ? (
|
||||
<PackageSummaryBanner
|
||||
packageName={activePackage?.package_name}
|
||||
onOpen={() => setSummaryOpen(true)}
|
||||
/>
|
||||
) : null}
|
||||
<DeviceSetupCard
|
||||
installPrompt={installPrompt}
|
||||
pushState={pushState}
|
||||
@@ -486,11 +510,6 @@ function PackageSummarySheet({
|
||||
const galleryDays = (limits as Record<string, number | null> | null)?.gallery_days ?? null;
|
||||
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');
|
||||
|
||||
return (
|
||||
@@ -506,12 +525,12 @@ function PackageSummarySheet({
|
||||
</Text>
|
||||
</YStack>
|
||||
<YStack space="$2" marginTop="$2">
|
||||
<SummaryRow label={t('mobileDashboard.packageSummary.limitPhotos', 'Photos')} value={formatLimit(maxPhotos)} />
|
||||
<SummaryRow label={t('mobileDashboard.packageSummary.limitGuests', 'Guests')} value={formatLimit(maxGuests)} />
|
||||
<SummaryRow label={t('mobileDashboard.packageSummary.limitDays', 'Gallery days')} value={formatLimit(galleryDays)} />
|
||||
<SummaryRow label={t('mobileDashboard.packageSummary.limitPhotos', 'Photos')} value={formatPackageLimit(maxPhotos, t)} />
|
||||
<SummaryRow label={t('mobileDashboard.packageSummary.limitGuests', 'Guests')} value={formatPackageLimit(maxGuests, t)} />
|
||||
<SummaryRow label={t('mobileDashboard.packageSummary.limitDays', 'Gallery days')} value={formatPackageLimit(galleryDays, t)} />
|
||||
<SummaryRow
|
||||
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)} />
|
||||
{expiresAt ? (
|
||||
@@ -520,7 +539,7 @@ function PackageSummarySheet({
|
||||
</YStack>
|
||||
</MobileCard>
|
||||
|
||||
{hasFeatures ? (
|
||||
{hasFeatures ? (
|
||||
<MobileCard space="$2" borderColor={border} backgroundColor={surface}>
|
||||
<Text fontSize="$sm" fontWeight="800" color={text}>
|
||||
{t('mobileDashboard.packageSummary.featuresTitle', 'Included features')}
|
||||
@@ -532,7 +551,7 @@ function PackageSummarySheet({
|
||||
<Sparkles size={14} color={primary} />
|
||||
</XStack>
|
||||
<Text fontSize="$xs" color={text}>
|
||||
{t(`mobileDashboard.packageSummary.feature.${feature}`, feature)}
|
||||
{getPackageFeatureLabel(feature, t)}
|
||||
</Text>
|
||||
</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) {
|
||||
const { t } = useTranslation('management');
|
||||
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