Refresh mobile dashboard and header
This commit is contained in:
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import { useLocation, useNavigate, useParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Bell, CalendarDays, Camera, CheckCircle2, ChevronDown, Download, Image as ImageIcon, Layout, ListTodo, MapPin, Megaphone, MessageCircle, Pencil, QrCode, Settings, ShieldCheck, Smartphone, Sparkles, TrendingUp, Tv, Users } from 'lucide-react';
|
||||
import { Activity, Bell, CalendarDays, Camera, CheckCircle2, ChevronDown, Download, Image as ImageIcon, Layout, ListTodo, MapPin, Megaphone, MessageCircle, Pencil, QrCode, Settings, ShieldCheck, Smartphone, Sparkles, TrendingUp, Tv, Users } from 'lucide-react';
|
||||
import { Card } from '@tamagui/card';
|
||||
import { Progress } from '@tamagui/progress';
|
||||
import { XGroup, YGroup } from '@tamagui/group';
|
||||
@@ -26,6 +26,8 @@ import { useAuth } from '../auth/context';
|
||||
import toast from 'react-hot-toast';
|
||||
import { ADMIN_ACTION_COLORS, ADMIN_MOTION, useAdminTheme } from './theme';
|
||||
import { isPastEvent } from './eventDate';
|
||||
import { buildLimitWarnings } from '../lib/limitWarnings';
|
||||
import { withAlpha } from './components/colors';
|
||||
|
||||
type DeviceSetupProps = {
|
||||
installPrompt: ReturnType<typeof useInstallPrompt>;
|
||||
@@ -552,29 +554,23 @@ export default function MobileDashboardPage() {
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<MobileShell
|
||||
activeTab="home"
|
||||
title={t('mobileDashboard.title', 'Dashboard')}
|
||||
>
|
||||
{showPackageSummaryBanner && activePackage ? (
|
||||
<PackageSummaryBanner
|
||||
activePackage={activePackage}
|
||||
onOpen={() => setSummaryOpen(true)}
|
||||
return (
|
||||
<MobileShell
|
||||
activeTab="home"
|
||||
title={t('mobileDashboard.title', 'Dashboard')}
|
||||
>
|
||||
<EventHeaderCard
|
||||
event={activeEvent}
|
||||
locale={locale}
|
||||
canSwitch={effectiveMultiple}
|
||||
onSwitch={() => setEventSwitcherOpen(true)}
|
||||
canEdit={canManageEvents}
|
||||
onEdit={
|
||||
canManageEvents
|
||||
? () => activeEvent?.slug && navigate(adminPath(`/mobile/events/${activeEvent.slug}/edit`))
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
<EventHeaderCard
|
||||
event={activeEvent}
|
||||
locale={locale}
|
||||
canSwitch={effectiveMultiple}
|
||||
onSwitch={() => setEventSwitcherOpen(true)}
|
||||
canEdit={canManageEvents}
|
||||
onEdit={
|
||||
canManageEvents
|
||||
? () => activeEvent?.slug && navigate(adminPath(`/mobile/events/${activeEvent.slug}/edit`))
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
<EventManagementGrid
|
||||
event={activeEvent}
|
||||
tasksEnabled={tasksEnabled}
|
||||
@@ -582,6 +578,13 @@ export default function MobileDashboardPage() {
|
||||
permissions={memberPermissions}
|
||||
onNavigate={(path) => navigate(path)}
|
||||
/>
|
||||
{showPackageSummaryBanner && activePackage ? (
|
||||
<PackageSummaryBanner
|
||||
activePackage={activePackage}
|
||||
onOpen={() => setSummaryOpen(true)}
|
||||
/>
|
||||
) : null}
|
||||
<TodayStrip event={activeEvent} stats={stats} tasksEnabled={tasksEnabled} />
|
||||
<KpiStrip
|
||||
event={activeEvent}
|
||||
stats={stats}
|
||||
@@ -591,24 +594,25 @@ export default function MobileDashboardPage() {
|
||||
/>
|
||||
|
||||
<AlertsAndHints event={activeEvent} stats={stats} tasksEnabled={tasksEnabled} />
|
||||
<RecentActivity event={activeEvent} stats={stats} />
|
||||
<DeviceSetupCard
|
||||
installPrompt={installPrompt}
|
||||
pushState={pushState}
|
||||
devicePermissions={devicePermissions}
|
||||
onOpenSettings={() => navigate(adminPath('/mobile/settings'))}
|
||||
/>
|
||||
{tourSheet}
|
||||
{packageSummarySheet}
|
||||
{taskDecisionSheet}
|
||||
<EventSwitcherSheet
|
||||
open={eventSwitcherOpen}
|
||||
onClose={() => setEventSwitcherOpen(false)}
|
||||
events={effectiveEvents}
|
||||
locale={locale}
|
||||
/>
|
||||
</MobileShell>
|
||||
);
|
||||
}
|
||||
{tourSheet}
|
||||
{packageSummarySheet}
|
||||
{taskDecisionSheet}
|
||||
<EventSwitcherSheet
|
||||
open={eventSwitcherOpen}
|
||||
onClose={() => setEventSwitcherOpen(false)}
|
||||
events={effectiveEvents}
|
||||
locale={locale}
|
||||
/>
|
||||
</MobileShell>
|
||||
);
|
||||
}
|
||||
|
||||
function PackageSummarySheet({
|
||||
open,
|
||||
@@ -1369,6 +1373,36 @@ function resolveLocation(event: TenantEvent | null, t: (key: string, fallback: s
|
||||
return t('events.detail.locationPlaceholder', 'Location');
|
||||
}
|
||||
|
||||
function resolveHeroGradient(
|
||||
event: TenantEvent,
|
||||
colors: { primary: string; accent: string }
|
||||
): string {
|
||||
const descriptor = `${event.event_type?.name ?? ''} ${event.event_type?.slug ?? ''} ${resolveEventDisplayName(event)}`.toLowerCase();
|
||||
const palette = [
|
||||
{
|
||||
keys: ['wedding', 'hochzeit', 'wedding day', 'marriage'],
|
||||
stops: ['#FCE4EC', '#F8D7DA', '#EDE7F6'],
|
||||
},
|
||||
{
|
||||
keys: ['birthday', 'geburtstag'],
|
||||
stops: ['#FFE8D6', '#FFD6E8', '#E0F2FE'],
|
||||
},
|
||||
{
|
||||
keys: ['corporate', 'business', 'company', 'konferenz', 'conference'],
|
||||
stops: ['#E0E7FF', '#DBEAFE', '#E2E8F0'],
|
||||
},
|
||||
{
|
||||
keys: ['festival', 'party', 'fest', 'celebration'],
|
||||
stops: ['#FFE4B5', '#FFEDD5', '#EDE9FE'],
|
||||
},
|
||||
];
|
||||
|
||||
const match = palette.find((entry) => entry.keys.some((key) => descriptor.includes(key)));
|
||||
const stops = match?.stops ?? [colors.primary, colors.accent];
|
||||
|
||||
return `linear-gradient(135deg, ${withAlpha(stops[0], 0.75)}, ${withAlpha(stops[1], 0.65)}, ${withAlpha(stops[2], 0.55)})`;
|
||||
}
|
||||
|
||||
function EventHeaderCard({
|
||||
event,
|
||||
locale,
|
||||
@@ -1385,7 +1419,7 @@ function EventHeaderCard({
|
||||
onEdit?: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation('management');
|
||||
const { textStrong, muted, border, surface, accentSoft, primary, shadow } = useAdminTheme();
|
||||
const { textStrong, muted, border, surface, accentSoft, primary, accent, shadow } = useAdminTheme();
|
||||
|
||||
if (!event) {
|
||||
return null;
|
||||
@@ -1393,6 +1427,16 @@ function EventHeaderCard({
|
||||
|
||||
const dateLabel = formatEventDate(event.event_date, locale) ?? t('events.detail.dateTbd', 'Date tbd');
|
||||
const locationLabel = resolveLocation(event, t);
|
||||
const eventTypeLabel = event.event_type?.name ?? t('events.detail.typeDefault', 'Event');
|
||||
const accentColor = accent || primary;
|
||||
const heroGradient = resolveHeroGradient(event, { primary, accent: accentColor });
|
||||
const statusLabel =
|
||||
event.status === 'published'
|
||||
? t('events.status.published', 'Live')
|
||||
: event.status === 'archived'
|
||||
? t('events.status.archived', 'Archived')
|
||||
: t('events.status.draft', 'Draft');
|
||||
const statusTone = event.status === 'published' ? 'success' : event.status === 'archived' ? 'muted' : 'warning';
|
||||
|
||||
return (
|
||||
<Card
|
||||
@@ -1406,39 +1450,54 @@ function EventHeaderCard({
|
||||
shadowOpacity={0.16}
|
||||
shadowRadius={16}
|
||||
shadowOffset={{ width: 0, height: 10 }}
|
||||
overflow="hidden"
|
||||
>
|
||||
<YStack space="$2.5">
|
||||
<YStack
|
||||
position="absolute"
|
||||
inset={0}
|
||||
backgroundColor="transparent"
|
||||
opacity={0.92}
|
||||
style={{ background: heroGradient }}
|
||||
/>
|
||||
<YStack position="absolute" top={-30} right={-40} opacity={0.22}>
|
||||
<Sparkles size={120} color={primary} />
|
||||
</YStack>
|
||||
<YStack space="$2.5" position="relative">
|
||||
<XStack alignItems="center" justifyContent="space-between" space="$2">
|
||||
{canSwitch ? (
|
||||
<Pressable onPress={onSwitch} aria-label={t('mobileDashboard.pickEvent', 'Select an event')}>
|
||||
<XStack alignItems="center" space="$2">
|
||||
<Text fontSize="$lg" fontWeight="800" color={textStrong}>
|
||||
<Text fontSize="$lg" fontWeight="900" color={textStrong}>
|
||||
{resolveEventDisplayName(event)}
|
||||
</Text>
|
||||
<ChevronDown size={16} color={muted} />
|
||||
</XStack>
|
||||
</Pressable>
|
||||
) : (
|
||||
<Text fontSize="$lg" fontWeight="800" color={textStrong}>
|
||||
<Text fontSize="$lg" fontWeight="900" color={textStrong}>
|
||||
{resolveEventDisplayName(event)}
|
||||
</Text>
|
||||
)}
|
||||
<PillBadge tone={event.status === 'published' ? 'success' : 'warning'}>
|
||||
{event.status === 'published'
|
||||
? t('events.status.published', 'Live')
|
||||
: t('events.status.draft', 'Draft')}
|
||||
<PillBadge tone={statusTone}>
|
||||
{statusLabel}
|
||||
</PillBadge>
|
||||
</XStack>
|
||||
|
||||
<XStack alignItems="center" space="$2">
|
||||
<CalendarDays size={16} color={muted} />
|
||||
<Text fontSize="$sm" color={muted}>
|
||||
{dateLabel}
|
||||
</Text>
|
||||
<MapPin size={16} color={muted} />
|
||||
<Text fontSize="$sm" color={muted}>
|
||||
{locationLabel}
|
||||
</Text>
|
||||
<Text fontSize="$xs" color={muted} fontWeight="700">
|
||||
{eventTypeLabel}
|
||||
</Text>
|
||||
<XStack alignItems="center" space="$2" flexWrap="wrap">
|
||||
<XStack alignItems="center" space="$1.5">
|
||||
<CalendarDays size={16} color={muted} />
|
||||
<Text fontSize="$sm" color={muted}>
|
||||
{dateLabel}
|
||||
</Text>
|
||||
</XStack>
|
||||
<XStack alignItems="center" space="$1.5">
|
||||
<MapPin size={16} color={muted} />
|
||||
<Text fontSize="$sm" color={muted}>
|
||||
{locationLabel}
|
||||
</Text>
|
||||
</XStack>
|
||||
</XStack>
|
||||
</YStack>
|
||||
|
||||
@@ -1643,7 +1702,7 @@ function EventManagementGrid({
|
||||
backgroundColor={surfaceMuted}
|
||||
>
|
||||
<Text fontSize="$xs" fontWeight="800" color={textStrong}>
|
||||
{t('events.detail.managementTitle', 'Event management')}
|
||||
{t('mobileDashboard.quickActionsTitle', 'Quick actions')}
|
||||
</Text>
|
||||
</XStack>
|
||||
</XStack>
|
||||
@@ -1693,17 +1752,31 @@ function KpiStrip({
|
||||
const { textStrong, muted, border, surface, surfaceMuted, shadow } = useAdminTheme();
|
||||
const text = textStrong;
|
||||
if (!event) return null;
|
||||
const photoLimit = event.limits?.photos ?? null;
|
||||
const guestLimit = event.limits?.guests ?? null;
|
||||
const resolveLimitNote = (limit: typeof photoLimit, label: string) => {
|
||||
if (!limit) return null;
|
||||
if (limit.state === 'unlimited') {
|
||||
return t('mobileDashboard.kpiUnlimited', '{{label}} unlimited', { label });
|
||||
}
|
||||
if (typeof limit.remaining === 'number') {
|
||||
return t('mobileDashboard.kpiRemaining', '{{remaining}} remaining', { remaining: limit.remaining });
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const kpis = [
|
||||
{
|
||||
label: t('mobileDashboard.kpiPhotos', 'Photos'),
|
||||
value: stats?.uploads_total ?? event.photo_count ?? '—',
|
||||
icon: ImageIcon,
|
||||
note: resolveLimitNote(photoLimit, t('mobileDashboard.kpiPhotos', 'Photos')),
|
||||
},
|
||||
{
|
||||
label: t('mobileDashboard.kpiGuests', 'Guests'),
|
||||
value: event.active_invites_count ?? event.total_invites_count ?? '—',
|
||||
icon: Users,
|
||||
note: resolveLimitNote(guestLimit, t('mobileDashboard.kpiGuests', 'Guests')),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1712,6 +1785,7 @@ function KpiStrip({
|
||||
label: t('mobileDashboard.kpiTasks', 'Open tasks'),
|
||||
value: event.tasks_count ?? '—',
|
||||
icon: ListTodo,
|
||||
note: tasksEnabled ? t('mobileDashboard.kpiTasksNote', 'Active') : t('mobileDashboard.kpiTasksDisabled', 'Disabled'),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1755,7 +1829,7 @@ function KpiStrip({
|
||||
) : (
|
||||
<XStack space="$2" flexWrap="wrap">
|
||||
{kpis.map((kpi) => (
|
||||
<KpiTile key={kpi.label} icon={kpi.icon} label={kpi.label} value={kpi.value ?? '—'} />
|
||||
<KpiTile key={kpi.label} icon={kpi.icon} label={kpi.label} value={kpi.value ?? '—'} note={kpi.note ?? undefined} />
|
||||
))}
|
||||
</XStack>
|
||||
)}
|
||||
@@ -1764,19 +1838,48 @@ function KpiStrip({
|
||||
);
|
||||
}
|
||||
|
||||
function translateLimits(t: (key: string, defaultValue?: string, options?: Record<string, unknown>) => string) {
|
||||
const defaults: Record<string, string> = {
|
||||
photosBlocked: 'Upload limit reached. Buy more photos to continue.',
|
||||
photosWarning: '{{remaining}} of {{limit}} photos remaining.',
|
||||
guestsBlocked: 'Guest limit reached.',
|
||||
guestsWarning: '{{remaining}} of {{limit}} guests remaining.',
|
||||
galleryExpired: 'Gallery expired. Extend to keep it online.',
|
||||
galleryWarningHour: 'Gallery expires in {{hours}} hour.',
|
||||
galleryWarningHours: 'Gallery expires in {{hours}} hours.',
|
||||
galleryWarningDay: 'Gallery expires in {{days}} day.',
|
||||
galleryWarningDays: 'Gallery expires in {{days}} days.',
|
||||
};
|
||||
return (key: string, options?: Record<string, unknown>) => t(`limits.${key}`, defaults[key] ?? key, options);
|
||||
}
|
||||
|
||||
function AlertsAndHints({ event, stats, tasksEnabled }: { event: TenantEvent | null; stats: EventStats | null | undefined; tasksEnabled: boolean }) {
|
||||
const { t } = useTranslation('management');
|
||||
const { textStrong, warningBg, warningBorder, warningText, border, surface, surfaceMuted, shadow } = useAdminTheme();
|
||||
const { textStrong, warningBg, warningBorder, warningText, dangerBg, dangerText, border, surface, surfaceMuted, shadow } = useAdminTheme();
|
||||
const text = textStrong;
|
||||
if (!event) return null;
|
||||
|
||||
const alerts: string[] = [];
|
||||
if (stats?.pending_photos) {
|
||||
alerts.push(t('mobileDashboard.alertPending', '{{count}} new uploads awaiting moderation', { count: stats.pending_photos }));
|
||||
const alerts: Array<{ message: string; tone: 'warning' | 'danger' }> = [];
|
||||
const pendingCount = stats?.pending_photos ?? event.pending_photo_count ?? 0;
|
||||
if (pendingCount > 0) {
|
||||
alerts.push({
|
||||
message: t('mobileDashboard.alertPending', '{{count}} new uploads awaiting moderation', { count: pendingCount }),
|
||||
tone: 'warning',
|
||||
});
|
||||
}
|
||||
if (tasksEnabled && event.tasks_count) {
|
||||
alerts.push(t('mobileDashboard.alertTasks', '{{count}} tasks due or open', { count: event.tasks_count }));
|
||||
alerts.push({
|
||||
message: t('mobileDashboard.alertTasks', '{{count}} tasks due or open', { count: event.tasks_count }),
|
||||
tone: 'warning',
|
||||
});
|
||||
}
|
||||
const limitWarnings = buildLimitWarnings(event.limits ?? null, translateLimits(t as any));
|
||||
limitWarnings.forEach((warning) => {
|
||||
alerts.push({
|
||||
message: warning.message,
|
||||
tone: warning.tone === 'danger' ? 'danger' : 'warning',
|
||||
});
|
||||
});
|
||||
|
||||
if (alerts.length === 0) {
|
||||
return null;
|
||||
@@ -1813,15 +1916,197 @@ function AlertsAndHints({ event, stats, tasksEnabled }: { event: TenantEvent | n
|
||||
<YStack space="$2">
|
||||
{alerts.map((alert) => (
|
||||
<XStack
|
||||
key={alert}
|
||||
key={alert.message}
|
||||
padding="$2.5"
|
||||
borderRadius={16}
|
||||
borderWidth={1}
|
||||
borderColor={warningBorder}
|
||||
backgroundColor={warningBg}
|
||||
borderColor={alert.tone === 'danger' ? dangerText : warningBorder}
|
||||
backgroundColor={alert.tone === 'danger' ? dangerBg : warningBg}
|
||||
>
|
||||
<Text fontSize="$sm" color={warningText}>
|
||||
{alert}
|
||||
<Text fontSize="$sm" color={alert.tone === 'danger' ? dangerText : warningText}>
|
||||
{alert.message}
|
||||
</Text>
|
||||
</XStack>
|
||||
))}
|
||||
</YStack>
|
||||
</YStack>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function TodayStrip({
|
||||
event,
|
||||
stats,
|
||||
tasksEnabled,
|
||||
}: {
|
||||
event: TenantEvent | null;
|
||||
stats: EventStats | null | undefined;
|
||||
tasksEnabled: boolean;
|
||||
}) {
|
||||
const { t } = useTranslation('management');
|
||||
const { textStrong, muted, border, surface, surfaceMuted, shadow, accentSoft, primary } = useAdminTheme();
|
||||
if (!event) return null;
|
||||
|
||||
const items = [
|
||||
{
|
||||
key: 'uploads',
|
||||
label: t('mobileDashboard.todayUploads', 'Uploads (24h)'),
|
||||
value: stats?.uploads_24h ?? 0,
|
||||
icon: ImageIcon,
|
||||
},
|
||||
{
|
||||
key: 'pending',
|
||||
label: t('mobileDashboard.todayPending', 'Pending'),
|
||||
value: stats?.pending_photos ?? event.pending_photo_count ?? 0,
|
||||
icon: Bell,
|
||||
},
|
||||
{
|
||||
key: 'likes',
|
||||
label: t('mobileDashboard.todayLikes', 'Likes'),
|
||||
value: stats?.likes_total ?? event.like_count ?? 0,
|
||||
icon: Sparkles,
|
||||
},
|
||||
];
|
||||
|
||||
if (tasksEnabled) {
|
||||
items.unshift({
|
||||
key: 'tasks',
|
||||
label: t('mobileDashboard.todayTasks', 'Tasks'),
|
||||
value: event.tasks_count ?? 0,
|
||||
icon: ListTodo,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Card
|
||||
borderRadius={22}
|
||||
borderWidth={2}
|
||||
borderColor={border}
|
||||
backgroundColor={surface}
|
||||
padding="$3"
|
||||
shadowColor={shadow}
|
||||
shadowOpacity={0.12}
|
||||
shadowRadius={14}
|
||||
shadowOffset={{ width: 0, height: 8 }}
|
||||
>
|
||||
<YStack space="$2.5">
|
||||
<XStack alignItems="center" justifyContent="space-between" gap="$2">
|
||||
<XStack
|
||||
alignItems="center"
|
||||
paddingHorizontal="$3"
|
||||
paddingVertical="$1.5"
|
||||
borderRadius={999}
|
||||
borderWidth={1}
|
||||
borderColor={border}
|
||||
backgroundColor={surfaceMuted}
|
||||
>
|
||||
<Text fontSize="$xs" fontWeight="800" color={textStrong}>
|
||||
{t('mobileDashboard.todayTitle', 'Today at a glance')}
|
||||
</Text>
|
||||
</XStack>
|
||||
<Activity size={18} color={muted} />
|
||||
</XStack>
|
||||
<XStack space="$2" flexWrap="wrap">
|
||||
{items.slice(0, 3).map((item) => (
|
||||
<XStack
|
||||
key={item.key}
|
||||
alignItems="center"
|
||||
space="$2"
|
||||
padding="$2"
|
||||
borderRadius={14}
|
||||
borderWidth={1}
|
||||
borderColor={border}
|
||||
backgroundColor={surfaceMuted}
|
||||
flex={1}
|
||||
minWidth={120}
|
||||
>
|
||||
<XStack width={32} height={32} borderRadius={12} backgroundColor={accentSoft} alignItems="center" justifyContent="center">
|
||||
<item.icon size={16} color={primary} />
|
||||
</XStack>
|
||||
<YStack space="$0.5" flex={1}>
|
||||
<Text fontSize="$xs" color={muted}>
|
||||
{item.label}
|
||||
</Text>
|
||||
<Text fontSize="$md" fontWeight="800" color={textStrong}>
|
||||
{item.value}
|
||||
</Text>
|
||||
</YStack>
|
||||
</XStack>
|
||||
))}
|
||||
</XStack>
|
||||
</YStack>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function RecentActivity({ event, stats }: { event: TenantEvent | null; stats: EventStats | null | undefined }) {
|
||||
const { t } = useTranslation('management');
|
||||
const { textStrong, muted, border, surface, surfaceMuted, shadow } = useAdminTheme();
|
||||
if (!event) return null;
|
||||
|
||||
const activityItems = [
|
||||
{
|
||||
key: 'uploads',
|
||||
label: t('mobileDashboard.recentUploads', 'Uploads in the last 24h'),
|
||||
value: stats?.uploads_24h ?? 0,
|
||||
},
|
||||
{
|
||||
key: 'likes',
|
||||
label: t('mobileDashboard.recentLikes', 'Total likes'),
|
||||
value: stats?.likes_total ?? event.like_count ?? 0,
|
||||
},
|
||||
{
|
||||
key: 'pending',
|
||||
label: t('mobileDashboard.recentPending', 'Pending moderation'),
|
||||
value: stats?.pending_photos ?? event.pending_photo_count ?? 0,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Card
|
||||
borderRadius={22}
|
||||
borderWidth={2}
|
||||
borderColor={border}
|
||||
backgroundColor={surface}
|
||||
padding="$3"
|
||||
shadowColor={shadow}
|
||||
shadowOpacity={0.12}
|
||||
shadowRadius={14}
|
||||
shadowOffset={{ width: 0, height: 8 }}
|
||||
>
|
||||
<YStack space="$2.5">
|
||||
<XStack alignItems="center" justifyContent="space-between">
|
||||
<XStack
|
||||
alignItems="center"
|
||||
paddingHorizontal="$3"
|
||||
paddingVertical="$1.5"
|
||||
borderRadius={999}
|
||||
borderWidth={1}
|
||||
borderColor={border}
|
||||
backgroundColor={surfaceMuted}
|
||||
>
|
||||
<Text fontSize="$xs" fontWeight="800" color={textStrong}>
|
||||
{t('mobileDashboard.recentActivityTitle', 'Recent activity')}
|
||||
</Text>
|
||||
</XStack>
|
||||
</XStack>
|
||||
<YStack space="$2">
|
||||
{activityItems.map((item) => (
|
||||
<XStack
|
||||
key={item.key}
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
padding="$2.5"
|
||||
borderRadius={16}
|
||||
borderWidth={1}
|
||||
borderColor={border}
|
||||
backgroundColor={surfaceMuted}
|
||||
>
|
||||
<Text fontSize="$sm" color={textStrong}>
|
||||
{item.label}
|
||||
</Text>
|
||||
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
|
||||
{item.value}
|
||||
</Text>
|
||||
</XStack>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user