kintex/mobile/components/tickets/TicketCard.tsx
zio becbc55152 feat(mobile): SCR-M15 ticket wallet + ticket-type select (sample, QR placeholder)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 05:26:30 +09:00

166 lines
6.0 KiB
TypeScript

/*
* 티켓 카드 — SCR-M15 리스트 항목.
* 행사명·권종·매수·상태 배지 + 미니 QR(placeholder) + "입장 QR 보기".
* 사용됨/취소 티켓은 흐리게(dim). 좌측 상태 액센트 바.
*/
import { Ionicons } from '@expo/vector-icons';
import React from 'react';
import { Pressable, StyleSheet, Text, View } from 'react-native';
import { colors, radius, spacing, touch, type } from '../../theme';
import { QrPlaceholder } from './QrPlaceholder';
import { STATUS_META, type SampleTicket } from './sampleTickets';
const badgeStyle: Record<'success' | 'muted' | 'error', { bg: string; fg: string }> = {
success: { bg: '#E7F6EF', fg: colors.success },
muted: { bg: colors.neutral050, fg: colors.neutral500 },
error: { bg: '#FEF3F2', fg: colors.error },
};
export function TicketCard({
ticket,
onOpenQr,
onDetail,
}: {
ticket: SampleTicket;
onOpenQr: (t: SampleTicket) => void;
onDetail: (t: SampleTicket) => void;
}) {
const dim = ticket.status !== 'usable';
const sm = STATUS_META[ticket.status];
const bs = badgeStyle[sm.tone];
return (
<View style={[styles.card, dim && styles.cardDim]}>
<View style={[styles.accent, { backgroundColor: dim ? colors.neutral200 : colors.primary600 }]} />
<View style={styles.body}>
<View style={styles.topRow}>
<Text style={styles.eventName} numberOfLines={2}>
{ticket.eventName}
</Text>
<View style={[styles.badge, { backgroundColor: bs.bg }]}>
<Text style={[styles.badgeText, { color: bs.fg }]}>{sm.label}</Text>
</View>
</View>
<View style={styles.metaRow}>
<Ionicons name="calendar-outline" size={15} color={colors.neutral500} />
<Text style={styles.metaText}>{ticket.period}</Text>
</View>
<View style={styles.metaRow}>
<Ionicons name="location-outline" size={15} color={colors.neutral500} />
<Text style={styles.metaText}>{ticket.hallLabel}</Text>
</View>
<View style={styles.divider} />
<View style={styles.infoRow}>
<View style={styles.miniQrWrap}>
<QrPlaceholder seed={ticket.qrSeed} size={64} quiet={false} />
</View>
<View style={styles.infoCols}>
<View style={styles.infoCol}>
<Text style={styles.infoLabel}></Text>
<Text style={styles.infoValue}>{ticket.ticketType}</Text>
</View>
<View style={styles.infoCol}>
<Text style={styles.infoLabel}></Text>
<Text style={styles.infoValue}>{ticket.quantity}</Text>
</View>
<View style={styles.infoCol}>
<Text style={styles.infoLabel}></Text>
<Text style={styles.infoMono}>{ticket.bookingNoMasked}</Text>
</View>
</View>
</View>
{ticket.status === 'usable' ? (
<Pressable
accessibilityRole="button"
style={({ pressed }: { pressed: boolean }) => [styles.qrBtn, pressed && { opacity: 0.85 }]}
onPress={() => onOpenQr(ticket)}
>
<Ionicons name="qr-code-outline" size={20} color={colors.white} />
<Text style={styles.qrBtnText}> QR </Text>
</Pressable>
) : (
<View style={[styles.qrBtn, styles.qrBtnDisabled]}>
<Text style={styles.qrBtnDisabledText}>
{ticket.status === 'used' ? '입장 완료된 티켓입니다' : '취소된 티켓입니다'}
</Text>
</View>
)}
<Pressable
accessibilityRole="link"
style={styles.detailBtn}
onPress={() => onDetail(ticket)}
>
<Text style={styles.detailText}> ·</Text>
</Pressable>
</View>
</View>
);
}
const styles = StyleSheet.create({
card: {
flexDirection: 'row',
backgroundColor: colors.white,
borderWidth: 1,
borderColor: colors.neutral200,
borderRadius: radius.md,
overflow: 'hidden',
},
cardDim: { opacity: 0.6 },
accent: { width: 4 },
body: { flex: 1, padding: spacing.md, gap: 8 },
topRow: { flexDirection: 'row', justifyContent: 'space-between', gap: 8 },
eventName: { flex: 1, fontSize: type.h3.fontSize, fontWeight: '700', color: colors.neutral900 },
badge: {
alignSelf: 'flex-start',
borderRadius: radius.sm,
paddingHorizontal: 8,
paddingVertical: 3,
},
badgeText: { fontSize: 11, fontWeight: '700' },
metaRow: { flexDirection: 'row', alignItems: 'center', gap: 6 },
metaText: { fontSize: type.caption.fontSize, color: colors.neutral700 },
divider: {
borderTopWidth: 1,
borderTopColor: colors.neutral200,
borderStyle: 'dashed',
marginVertical: 4,
},
infoRow: { flexDirection: 'row', gap: 12, alignItems: 'center' },
miniQrWrap: {
borderWidth: 1,
borderColor: colors.neutral200,
borderRadius: radius.sm,
padding: 3,
},
infoCols: { flex: 1, flexDirection: 'row', flexWrap: 'wrap', gap: 12 },
infoCol: { minWidth: 60 },
infoLabel: { fontSize: 11, color: colors.neutral500, marginBottom: 2 },
infoValue: { fontSize: type.body.fontSize, fontWeight: '700', color: colors.neutral900 },
infoMono: { fontSize: type.caption.fontSize, color: colors.neutral700, fontVariant: ['tabular-nums'] },
qrBtn: {
minHeight: touch.min,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
gap: 8,
backgroundColor: colors.primary600,
borderRadius: radius.sm,
marginTop: 4,
},
qrBtnText: { color: colors.white, fontSize: type.h3.fontSize, fontWeight: '700' },
qrBtnDisabled: { backgroundColor: colors.neutral050, borderWidth: 1, borderColor: colors.neutral200 },
qrBtnDisabledText: { color: colors.neutral500, fontSize: type.body.fontSize, fontWeight: '600' },
detailBtn: { minHeight: 40, alignItems: 'center', justifyContent: 'center' },
detailText: {
fontSize: type.caption.fontSize,
color: colors.neutral500,
textDecorationLine: 'underline',
},
});