166 lines
6.0 KiB
TypeScript
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',
|
|
},
|
|
});
|