QR Print Page: add expiry notice
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -2553,6 +2553,8 @@
|
|||||||
"title": "QR-Code & Druck-Layouts",
|
"title": "QR-Code & Druck-Layouts",
|
||||||
"heroTitle": "Einlass-QR-Code",
|
"heroTitle": "Einlass-QR-Code",
|
||||||
"description": "Scannen, um zur Gäste-App zu gelangen.",
|
"description": "Scannen, um zur Gäste-App zu gelangen.",
|
||||||
|
"expiresAt": "Gültig bis {{date}}",
|
||||||
|
"noExpiry": "Gültig ohne Ablaufdatum",
|
||||||
"qrAlt": "QR-Code",
|
"qrAlt": "QR-Code",
|
||||||
"previewAlt": "QR-Layout Vorschau",
|
"previewAlt": "QR-Layout Vorschau",
|
||||||
"bottomNote": "Unterer Hinweistext",
|
"bottomNote": "Unterer Hinweistext",
|
||||||
|
|||||||
@@ -2557,6 +2557,8 @@
|
|||||||
"title": "QR Code & Print Layouts",
|
"title": "QR Code & Print Layouts",
|
||||||
"heroTitle": "Entrance QR Code",
|
"heroTitle": "Entrance QR Code",
|
||||||
"description": "Scan to access the event guest app.",
|
"description": "Scan to access the event guest app.",
|
||||||
|
"expiresAt": "Valid until {{date}}",
|
||||||
|
"noExpiry": "No expiry configured",
|
||||||
"qrAlt": "QR code",
|
"qrAlt": "QR code",
|
||||||
"previewAlt": "QR layout preview",
|
"previewAlt": "QR layout preview",
|
||||||
"bottomNote": "Bottom note text",
|
"bottomNote": "Bottom note text",
|
||||||
|
|||||||
@@ -132,6 +132,13 @@ export default function MobileQrPrintPage() {
|
|||||||
{qrUrl}
|
{qrUrl}
|
||||||
</Text>
|
</Text>
|
||||||
) : null}
|
) : null}
|
||||||
|
<Text fontSize="$xs" color={muted} marginTop="$1">
|
||||||
|
{selectedInvite?.expires_at
|
||||||
|
? t('events.qr.expiresAt', 'Valid until {{date}}', {
|
||||||
|
date: formatExpiry(selectedInvite.expires_at),
|
||||||
|
})
|
||||||
|
: t('events.qr.noExpiry', 'No expiry configured')}
|
||||||
|
</Text>
|
||||||
<Text fontSize="$xs" color={muted}>
|
<Text fontSize="$xs" color={muted}>
|
||||||
{t('events.qr.description', 'Scan to access the event guest app.')}
|
{t('events.qr.description', 'Scan to access the event guest app.')}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -238,6 +245,12 @@ export default function MobileQrPrintPage() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatExpiry(value: string): string {
|
||||||
|
const date = new Date(value);
|
||||||
|
if (Number.isNaN(date.getTime())) return value;
|
||||||
|
return date.toLocaleString(undefined, { day: '2-digit', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit' });
|
||||||
|
}
|
||||||
|
|
||||||
function FormatSelection({
|
function FormatSelection({
|
||||||
layouts,
|
layouts,
|
||||||
selectedFormat,
|
selectedFormat,
|
||||||
|
|||||||
124
resources/js/admin/mobile/__tests__/QrPrintPage.test.tsx
Normal file
124
resources/js/admin/mobile/__tests__/QrPrintPage.test.tsx
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { describe, expect, it, vi } from 'vitest';
|
||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
|
||||||
|
const backMock = vi.fn();
|
||||||
|
const navigateMock = vi.fn();
|
||||||
|
|
||||||
|
vi.mock('react-router-dom', () => ({
|
||||||
|
useNavigate: () => navigateMock,
|
||||||
|
useParams: () => ({ slug: 'demo-event' }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('react-i18next', () => ({
|
||||||
|
useTranslation: () => ({
|
||||||
|
t: (key: string) => key,
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../hooks/useBackNavigation', () => ({
|
||||||
|
useBackNavigation: () => backMock,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../../api', () => ({
|
||||||
|
getEvent: vi.fn().mockResolvedValue({ slug: 'demo-event', name: 'Demo' }),
|
||||||
|
getEventQrInvites: vi.fn().mockResolvedValue([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
token: 'demo-token',
|
||||||
|
url: 'https://example.test/g/demo-token',
|
||||||
|
label: null,
|
||||||
|
qr_code_data_url: 'data:image/png;base64,abc',
|
||||||
|
usage_limit: null,
|
||||||
|
usage_count: 0,
|
||||||
|
expires_at: '2026-01-12T12:00:00Z',
|
||||||
|
revoked_at: null,
|
||||||
|
is_active: true,
|
||||||
|
created_at: null,
|
||||||
|
metadata: {},
|
||||||
|
layouts_url: null,
|
||||||
|
layouts: [
|
||||||
|
{
|
||||||
|
id: 'layout-1',
|
||||||
|
name: 'Poster',
|
||||||
|
description: '',
|
||||||
|
subtitle: '',
|
||||||
|
formats: [],
|
||||||
|
preview: {
|
||||||
|
background: null,
|
||||||
|
background_gradient: null,
|
||||||
|
accent: null,
|
||||||
|
text: null,
|
||||||
|
},
|
||||||
|
paper: 'a4',
|
||||||
|
orientation: 'portrait',
|
||||||
|
panel_mode: 'single',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
createQrInvite: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('react-hot-toast', () => ({
|
||||||
|
default: {
|
||||||
|
error: vi.fn(),
|
||||||
|
success: vi.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@tamagui/react-native-web-lite', () => ({
|
||||||
|
Pressable: ({ children, onPress }: { children: React.ReactNode; onPress?: () => void }) => (
|
||||||
|
<button type="button" onClick={onPress}>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../components/MobileShell', () => ({
|
||||||
|
MobileShell: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||||
|
HeaderActionButton: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../components/Primitives', () => ({
|
||||||
|
MobileCard: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||||
|
CTAButton: ({ label, onPress }: { label: string; onPress?: () => void }) => (
|
||||||
|
<button type="button" onClick={onPress}>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
),
|
||||||
|
PillBadge: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@tamagui/stacks', () => ({
|
||||||
|
YStack: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||||
|
XStack: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@tamagui/text', () => ({
|
||||||
|
SizableText: ({ children }: { children: React.ReactNode }) => <span>{children}</span>,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../theme', () => ({
|
||||||
|
useAdminTheme: () => ({
|
||||||
|
textStrong: '#111827',
|
||||||
|
text: '#111827',
|
||||||
|
muted: '#6b7280',
|
||||||
|
subtle: '#94a3b8',
|
||||||
|
border: '#e5e7eb',
|
||||||
|
surfaceMuted: '#fffdfb',
|
||||||
|
primary: '#ff5a5f',
|
||||||
|
danger: '#b91c1c',
|
||||||
|
accentSoft: '#ffe5ec',
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
import MobileQrPrintPage from '../QrPrintPage';
|
||||||
|
|
||||||
|
describe('MobileQrPrintPage', () => {
|
||||||
|
it('shows token expiry info when the invite has expires_at', async () => {
|
||||||
|
render(<MobileQrPrintPage />);
|
||||||
|
|
||||||
|
expect(await screen.findByText('events.qr.expiresAt')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user