Files
clg-kit/app/account.tsx
Aditya Gupta 2aac9c3fb4 feat: redesign day picker with expandable calendar, status markers & Playwright E2E testing
- Replace modal calendar with inline expandable calendar widget integrated into the day picker.
- Add status indicator dots (attended/absent/pending) to month calendar grid matching day picker.
- Disable and gray out weekend days in calendar when weekend schedule is turned off.
- Redesign extend bar with integrated handle that aligns with the palette.
- Remove start-to-end timing on Today screen's class time rail, showing only start time.
- Implement Playwright E2E and visual testing suite with multi-screen capture scripts.
- Add cascade DELETE endpoints for subjects and versioned recurring timetable entries.
2026-09-05 09:00:17 +05:30

143 lines
6.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Ionicons } from '@expo/vector-icons';
import { router } from 'expo-router';
import { useCallback, useEffect, useState } from 'react';
import { ActivityIndicator, StyleSheet, View } from 'react-native';
import {
AppHeader,
AppText,
Button,
Card,
FormField,
IconButton,
InlineBanner,
Screen,
colors,
radius,
spacing,
} from '@/components/ui';
import { collegeApi, type Profile } from '@/lib/api';
export default function AccountScreen() {
const [profile, setProfile] = useState<Profile | null>(null);
const [loading, setLoading] = useState(true);
const [loadError, setLoadError] = useState('');
const [editing, setEditing] = useState(false);
const [name, setName] = useState('');
const [college, setCollege] = useState('');
const [programme, setProgramme] = useState('');
const [semester, setSemester] = useState('');
const [saving, setSaving] = useState(false);
const [formError, setFormError] = useState('');
const load = useCallback(() => {
setLoading(true);
setLoadError('');
collegeApi.profile()
.then((result) => {
setProfile(result);
setName(result.name);
setCollege(result.college);
setProgramme(result.programme);
setSemester(result.semester);
})
.catch((error: Error) => setLoadError(error.message))
.finally(() => setLoading(false));
}, []);
useEffect(() => { load(); }, [load]);
const save = async () => {
if (!name.trim() || !college.trim()) {
setFormError('Your name and college are required.');
return;
}
setSaving(true);
setFormError('');
try {
const updated = await collegeApi.updateProfile({ name, college, programme, semester });
setProfile(updated);
setEditing(false);
} catch (error) {
setFormError(error instanceof Error ? error.message : 'Please try again.');
} finally {
setSaving(false);
}
};
return <Screen contentContainerStyle={styles.content}>
<AppHeader
compact
title="Account"
leading={<IconButton icon="chevron-back" label="Go back" onPress={() => router.back()} />}
trailing={profile ? <IconButton icon={editing ? 'close' : 'create-outline'} label={editing ? 'Cancel editing' : 'Edit profile'} onPress={() => { setEditing((value) => !value); setFormError(''); }} /> : undefined}
/>
{loading ? <View style={styles.loading}><ActivityIndicator color={colors.brand.cobalt} /><AppText variant="bodySmall" color={colors.neutral.textSecondary}>Loading profile</AppText></View> : null}
{loadError ? <InlineBanner title="Couldnt load your account" message={loadError} tone="danger" action={<Button label="Retry" variant="ghost" size="compact" fullWidth={false} onPress={load} />} style={styles.error} /> : null}
{profile ? <>
<View style={styles.profileHero}>
<View style={styles.avatar}>
<AppText variant="heading2">{profile.initials || '?'}</AppText>
<View style={styles.avatarDot} />
</View>
<AppText variant="heading2" style={styles.name}>{profile.name}</AppText>
<AppText variant="bodySmall" color={colors.neutral.textSecondary} style={styles.college}>{profile.college}</AppText>
</View>
{editing ? <Card style={styles.editCard}>
<AppText variant="heading3">Profile details</AppText>
{formError ? <InlineBanner title="Check your details" message={formError} tone="danger" style={styles.formError} /> : null}
<View style={styles.fields}>
<FormField label="Your name" value={name} onChangeText={setName} placeholder="Your name" autoCapitalize="words" autoComplete="name" />
<FormField label="College or university" value={college} onChangeText={setCollege} placeholder="College or university" autoCapitalize="words" />
<FormField label="Programme (optional)" value={programme} onChangeText={setProgramme} placeholder="e.g. B.Tech Computer Science" autoCapitalize="words" />
<FormField label="Semester (optional)" value={semester} onChangeText={setSemester} placeholder="e.g. Semester 3" autoCapitalize="words" />
</View>
<Button label="Save profile" loading={saving} haptic="success" onPress={save} />
</Card> : <>
<AppText variant="heading3" style={styles.sectionTitle}>College details</AppText>
<Card padding={0} style={styles.details}>
<Info icon="school-outline" label="College" value={profile.college || 'Not added'} />
<View style={styles.divider} />
<Info icon="book-outline" label="Programme" value={profile.programme || 'Not added'} />
<View style={styles.divider} />
<Info icon="calendar-outline" label="Current semester" value={profile.semester || 'Not added'} />
</Card>
</>}
</> : null}
</Screen>;
}
function Info({ icon, label, value }: { icon: keyof typeof Ionicons.glyphMap; label: string; value: string }) {
return <View style={styles.info}>
<View style={styles.infoIcon}><Ionicons name={icon} size={20} color={colors.brand.cobalt} /></View>
<View style={styles.infoCopy}>
<AppText variant="caption" color={colors.neutral.textMuted}>{label}</AppText>
<AppText variant="label" style={styles.infoValue}>{value}</AppText>
</View>
</View>;
}
const styles = StyleSheet.create({
content: { paddingTop: spacing[1], paddingBottom: spacing[9] },
loading: { minHeight: 220, alignItems: 'center', justifyContent: 'center', gap: spacing[3] },
error: { marginTop: spacing[4] },
profileHero: { marginTop: spacing[3], alignItems: 'center', borderRadius: radius.feature, borderCurve: 'continuous', backgroundColor: colors.brand.sky, padding: spacing[7] },
avatar: { width: 82, height: 82, borderRadius: 27, borderCurve: 'continuous', alignItems: 'center', justifyContent: 'center', backgroundColor: colors.neutral.surface },
avatarDot: { position: 'absolute', right: 2, top: 2, width: 14, height: 14, borderRadius: 7, backgroundColor: colors.brand.coral, borderWidth: 3, borderColor: colors.neutral.surface },
name: { marginTop: spacing[4], textAlign: 'center' },
college: { marginTop: spacing[1], textAlign: 'center' },
editCard: { marginTop: spacing[4] },
formError: { marginTop: spacing[4] },
fields: { gap: spacing[4], marginVertical: spacing[5] },
sectionTitle: { marginTop: spacing[8], marginBottom: spacing[3] },
details: { overflow: 'hidden' },
info: { minHeight: 78, flexDirection: 'row', alignItems: 'center', gap: spacing[3], paddingHorizontal: spacing[4], paddingVertical: spacing[3] },
infoIcon: { width: 40, height: 40, borderRadius: radius.control, borderCurve: 'continuous', alignItems: 'center', justifyContent: 'center', backgroundColor: colors.brand.cobaltSoft },
infoCopy: { flex: 1 },
infoValue: { marginTop: spacing[1] },
divider: { height: 1, marginLeft: 68, marginRight: spacing[4], backgroundColor: colors.neutral.divider },
});