Update app and ignore local artifacts
This commit is contained in:
81
components/ui/schedule-event-card.tsx
Normal file
81
components/ui/schedule-event-card.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import type { ReactNode } from 'react';
|
||||
import { Pressable, StyleSheet, View, type GestureResponderEvent, type StyleProp, type ViewStyle } from 'react-native';
|
||||
import { AppText } from './app-text';
|
||||
import { colors, radius, spacing, type SubjectTone } from './tokens';
|
||||
|
||||
export type ClassAttendanceState = 'pending' | 'attended' | 'absent' | 'cancelled';
|
||||
type Props = {
|
||||
title: string;
|
||||
timeRange: string;
|
||||
subjectTone: SubjectTone;
|
||||
room?: string;
|
||||
kind?: string;
|
||||
classType?: string;
|
||||
state?: ClassAttendanceState;
|
||||
isNow?: boolean;
|
||||
topAction?: ReactNode;
|
||||
footer?: ReactNode;
|
||||
onPress?: (event: GestureResponderEvent) => void;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
testID?: string;
|
||||
};
|
||||
|
||||
/** Editorial calendar block: subject color leads; attendance changes only the card state. */
|
||||
export function ScheduleEventCard({ title, timeRange, subjectTone, room, kind, classType = 'Lecture', state = 'pending', isNow = false, topAction, footer, onPress, style, testID }: Props) {
|
||||
const subject = colors.subject[subjectTone];
|
||||
const cancelled = state === 'cancelled';
|
||||
const absent = state === 'absent';
|
||||
const backgroundColor = cancelled ? colors.neutral.surfaceSubtle : absent ? colors.semantic.danger.soft : subject.surface;
|
||||
const foreground = cancelled ? colors.neutral.textMuted : absent ? colors.semantic.danger.text : subject.accent;
|
||||
const content = <>
|
||||
<View style={styles.titleRow}>
|
||||
<AppText variant="title" color={foreground} numberOfLines={2} style={styles.title}>{title}</AppText>
|
||||
{topAction ? <View style={styles.topAction}>{topAction}</View> : null}
|
||||
</View>
|
||||
<View style={styles.meta}>
|
||||
<AppText variant="bodySmall" color={foreground} numberOfLines={1} style={styles.code}>{kind || 'Class'}</AppText>
|
||||
{room ? <View style={styles.room}><AppText variant="bodySmall" color={cancelled ? colors.neutral.textMuted : colors.neutral.textSecondary} numberOfLines={1}>{room}</AppText></View> : null}
|
||||
</View>
|
||||
<View style={styles.bottomRow}>
|
||||
<View style={styles.typeRow}>
|
||||
<Ionicons name="school-outline" size={14} color={foreground} />
|
||||
<AppText variant="label" color={foreground}>{classType}</AppText>
|
||||
</View>
|
||||
{isNow ? <View style={styles.now}><AppText variant="caption" color={colors.brand.ink}>Now</AppText></View> : cancelled ? <AppText variant="label" color={colors.neutral.textMuted}>Cancelled</AppText> : <ParticipantStack accent={foreground} />}
|
||||
</View>
|
||||
{footer ? <View style={styles.footer}>{footer}</View> : null}
|
||||
</>;
|
||||
|
||||
const cardStyle = [styles.card, { backgroundColor }, cancelled && styles.cancelled, style];
|
||||
if (!onPress) return <View testID={testID} accessibilityLabel={`${title}, ${timeRange}${room ? `, ${room}` : ''}`} style={cardStyle}>{content}</View>;
|
||||
return <Pressable testID={testID} accessibilityRole="button" accessibilityLabel={`${title}, ${timeRange}${room ? `, ${room}` : ''}`} onPress={onPress} style={({ pressed }) => [cardStyle, pressed && styles.pressed]}>{content}</Pressable>;
|
||||
}
|
||||
|
||||
function ParticipantStack({ accent }: { accent: string }) {
|
||||
return <View accessibilityLabel="Three classmates and eight more" style={styles.participants}>
|
||||
{['A', 'K', 'S'].map((initial, index) => <View key={initial} style={[styles.avatar, { backgroundColor: index === 0 ? colors.brand.sky : index === 1 ? colors.subject.lilac.surface : colors.subject.peach.surface, marginLeft: index ? -spacing[2] : 0 }]}>
|
||||
<AppText variant="caption" color={accent}>{initial}</AppText>
|
||||
</View>)}
|
||||
<AppText variant="label" color={accent} style={styles.more}>+8</AppText>
|
||||
</View>;
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
card: { minHeight: 116, overflow: 'hidden', borderRadius: radius.feature, padding: spacing[5] },
|
||||
titleRow: { minHeight: 40, flexDirection: 'row', alignItems: 'flex-start', gap: spacing[2] },
|
||||
title: { flex: 1, paddingTop: spacing[1] },
|
||||
topAction: { marginRight: -spacing[3], marginTop: -spacing[3] },
|
||||
meta: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', gap: spacing[3], marginTop: spacing[1] },
|
||||
code: { flexShrink: 1 },
|
||||
room: { maxWidth: '52%', alignItems: 'flex-end' },
|
||||
bottomRow: { minHeight: 30, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', gap: spacing[3], marginTop: spacing[3] },
|
||||
typeRow: { flexDirection: 'row', alignItems: 'center', gap: spacing[2] },
|
||||
now: { borderRadius: radius.pill, backgroundColor: colors.brand.coral, paddingHorizontal: spacing[3], paddingVertical: 3 },
|
||||
participants: { flexDirection: 'row', alignItems: 'center' },
|
||||
avatar: { width: 24, height: 24, alignItems: 'center', justifyContent: 'center', borderRadius: radius.pill, borderWidth: 2, borderColor: colors.neutral.surface },
|
||||
more: { marginLeft: spacing[2] },
|
||||
footer: { marginTop: spacing[3] },
|
||||
cancelled: { opacity: 0.72 },
|
||||
pressed: { opacity: 0.78 },
|
||||
});
|
||||
Reference in New Issue
Block a user