feat: refine today schedule and navigation interactions

This commit is contained in:
2026-09-05 10:44:12 +05:30
parent d58bde6c15
commit 0ec5724541
2 changed files with 44 additions and 19 deletions

View File

@@ -78,8 +78,15 @@ export default function TodayScreen() {
collegeApi.profile().then((result) => {
if (!active) return;
setProfile(result);
setWeekendSchedule(Boolean(result.weekendSchedule));
if (!result.weekendSchedule) setActiveDate((current) => current.getDay() === 0 || current.getDay() === 6 ? mondayOfWeek(current) : current);
const weekendsEnabled = Boolean(result.weekendSchedule);
setWeekendSchedule(weekendsEnabled);
if (!weekendsEnabled) {
setActiveDate((current) => {
if (current.getDay() === 6) return addDays(current, 2);
if (current.getDay() === 0) return addDays(current, 1);
return current;
});
}
}).catch(() => undefined);
return () => { active = false; };
}, []));
@@ -139,13 +146,26 @@ export default function TodayScreen() {
const updateStatus = (session: Session, status: AttendanceStatus) => {
const previous = classes;
setClasses((items) => items.map((item) => item.id === session.id ? { ...item, status } : item));
setSummary((current) => {
const wasHeld = session.status === 'attended' || session.status === 'absent';
const isHeld = status === 'attended' || status === 'absent';
const total = current.total + (isHeld ? 1 : 0) - (wasHeld ? 1 : 0);
const attended = current.attended + (status === 'attended' ? 1 : 0) - (session.status === 'attended' ? 1 : 0);
return { total, attended, percentage: total ? Math.round((attended / total) * 100) : 0 };
});
collegeApi.markAttendance(session.id, status)
.then(() => {
if (Platform.OS !== 'web') void Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
setRefresh((v) => v + 1);
})
.catch((error: Error) => {
setClasses(previous);
setSummary((current) => {
const wasHeld = session.status === 'attended' || session.status === 'absent';
const isHeld = status === 'attended' || status === 'absent';
const total = current.total + (wasHeld ? 1 : 0) - (isHeld ? 1 : 0);
const attended = current.attended + (session.status === 'attended' ? 1 : 0) - (status === 'attended' ? 1 : 0);
return { total, attended, percentage: total ? Math.round((attended / total) * 100) : 0 };
});
Alert.alert('Couldnt save attendance', error.message);
});
};
@@ -177,10 +197,13 @@ export default function TodayScreen() {
const currentTimeLabel = formatCurrentTime();
const pendingCount = classes.filter((item) => item.status === 'pending').length;
const metricTone = summary.total ? (insightTone === 'neutral' ? 'brand' : insightTone) : 'brand';
const collapseCalendar = () => {
if (calendarExpanded) setCalendarExpanded(false);
};
return <Screen scroll={false} contentContainerStyle={styles.content}>
<View style={styles.stickyHeader}>
<View style={styles.headerRow}>
<View style={styles.headerRow} onTouchStart={collapseCalendar}>
<AppHeader
title={formatHeaderDate(activeDate)}
style={styles.headerTitle}
@@ -196,7 +219,7 @@ export default function TodayScreen() {
) : null}
</View>
<View style={styles.attendanceOverview}>
<View style={styles.attendanceOverview} onTouchStart={collapseCalendar}>
<View style={styles.overviewPrimary}>
<AttendanceRing
percentage={summary.percentage}
@@ -219,7 +242,7 @@ export default function TodayScreen() {
</View>
</View>
<View style={styles.pickerContainer}>
<View style={styles.pickerContainer} onTouchStart={(event) => event.stopPropagation()}>
{calendarExpanded ? (
<InlineCalendarWidget
month={calendarMonth}
@@ -250,19 +273,18 @@ export default function TodayScreen() {
</Pressable>
</View>
<View style={styles.sectionHeader}>
<View style={styles.sectionHeader} onTouchStart={collapseCalendar}>
<View style={styles.sectionCopy}>
<AppText variant="heading2">{isToday ? 'Todays classes' : `${formatDayHeading(activeDate).split(',')[0]}s classes`}</AppText>
<AppText variant="heading2">Schedule</AppText>
<AppText variant="bodySmall" color={colors.neutral.textMuted} style={styles.sectionSubtitle}>
{classes.length ? `${classes.length} ${classes.length === 1 ? 'class' : 'classes'}${pendingCount ? ` · ${pendingCount} to mark` : ''}` : 'Your agenda for this date'}
</AppText>
</View>
<IconButton icon="add" label="Add a class" onPress={() => setAddClassOpen(true)} />
</View>
</View>
<ScrollView style={styles.classList} contentContainerStyle={styles.classListContent} showsVerticalScrollIndicator={false} keyboardShouldPersistTaps="handled">
<ScrollView style={styles.classList} contentContainerStyle={styles.classListContent} showsVerticalScrollIndicator={false} keyboardShouldPersistTaps="handled" onTouchStart={collapseCalendar}>
{loadError ? <InlineBanner title="Couldnt load this day" message={loadError} tone="danger" action={<Button label="Retry" size="compact" variant="ghost" fullWidth={false} onPress={loadSchedule} />} /> : null}
{loading ? <Card tone="skySoft" style={styles.loading}><ActivityIndicator color={colors.brand.cobalt} /><AppText variant="bodySmall" color={colors.neutral.textSecondary}>Loading your classes</AppText></Card> : null}
{!loading && !loadError && classes.length === 0 && !recessEnabled ? <EmptyState icon="calendar-clear-outline" title="No classes scheduled" message="Enjoy the break, or add a one-off class for this date." action={<Button label="Add a class" variant="secondary" onPress={() => setAddClassOpen(true)} leading={<Ionicons name="add" size={18} color={colors.brand.cobalt} />} />} /> : null}
@@ -453,7 +475,7 @@ const styles = StyleSheet.create({
// Bottom padding belongs to the scrollable class content, not this fixed shell.
content: { paddingTop: spacing[1], paddingBottom: 0 },
stickyHeader: { flexShrink: 0 },
stickyHeader: { flexShrink: 0, position: 'relative', zIndex: 20, elevation: 20 },
headerRow: { flexDirection: 'row', alignItems: 'flex-start', justifyContent: 'space-between', gap: spacing[2], marginBottom: spacing[3] },
headerTitle: { flex: 1, minHeight: 0 },
headerActions: { flexDirection: 'row', alignItems: 'center', gap: spacing[2], paddingTop: spacing[1] },
@@ -482,6 +504,9 @@ const styles = StyleSheet.create({
overviewTargetValue: { fontVariant: ['tabular-nums'] },
pickerContainer: {
marginTop: spacing[1],
position: 'relative',
zIndex: 30,
elevation: 30,
borderRadius: radius.feature,
borderCurve: 'continuous',
backgroundColor: colors.brand.skySoft,

View File

@@ -57,7 +57,7 @@ export function BottomTabBar({ state, navigation, insets }: BottomTabBarProps) {
<Ionicons
name={focused ? config.activeIcon : config.icon}
size={21}
color={focused ? colors.brand.ink : colors.neutral.textSecondary}
color={focused ? colors.neutral.surface : colors.neutral.textSecondary}
/>
</Pressable>
);
@@ -69,7 +69,7 @@ export function BottomTabBar({ state, navigation, insets }: BottomTabBarProps) {
accessibilityLabel="Add a class"
onPress={addClass}
style={({ pressed }) => [styles.addButton, pressed && styles.addPressed]}>
<Ionicons name="add" size={26} color={colors.brand.ink} />
<Ionicons name="add" size={28} color={colors.neutral.surface} />
</Pressable>
</View>
);
@@ -90,7 +90,7 @@ const styles = StyleSheet.create({
},
navigationGroup: {
flex: 1,
minHeight: 58,
minHeight: 64,
flexDirection: 'row',
alignItems: 'center',
gap: spacing[1],
@@ -112,16 +112,16 @@ const styles = StyleSheet.create({
borderCurve: 'continuous',
},
tabActive: {
backgroundColor: colors.brand.coral,
backgroundColor: colors.brand.cobalt,
},
addButton: {
width: 58,
height: 58,
width: 64,
height: 64,
alignItems: 'center',
justifyContent: 'center',
borderRadius: radius.feature,
borderCurve: 'continuous',
backgroundColor: colors.brand.coral,
backgroundColor: colors.brand.cobalt,
...shadow.floating,
},
pressed: {
@@ -129,7 +129,7 @@ const styles = StyleSheet.create({
transform: [{ scale: 0.97 }],
},
addPressed: {
backgroundColor: colors.brand.coralSoft,
backgroundColor: colors.brand.cobaltPressed,
transform: [{ scale: 0.96 }],
},
});