diff --git a/app/cypress/e2e/critical_flows.cy.js b/app/cypress/e2e/critical_flows.cy.js
index 352ebc09..8f45352d 100644
--- a/app/cypress/e2e/critical_flows.cy.js
+++ b/app/cypress/e2e/critical_flows.cy.js
@@ -3,63 +3,4 @@ describe('Critical Event App Flows', () => {
cy.visit('/');
// Set mock user session using our Cypress hook
cy.window().then(win => {
- win.setMockUser(
- {
- uid: 'student-test-uid',
- displayName: 'Jane Doe',
- email: 'jane.doe@uni.edu',
- },
- 'student',
- {
- name: 'Jane Doe',
- email: 'jane.doe@uni.edu',
- branch: 'Computer Science',
- year: '3rd Year',
- points: 120,
- },
- );
- });
- });
-
- it('should render the event feed home page correctly', () => {
- // Verify welcome text is loaded from the mock user displayName
- cy.contains('Welcome,').should('be.visible');
- cy.contains('Jane Doe').should('be.visible');
-
- // Verify search bar is visible
- cy.get('input[placeholder="Search events..."]').should('be.visible');
-
- // Verify recommendations section header is visible
- cy.contains('RECOMMENDED FOR YOU').should('be.visible');
- });
-
- it('should support tab navigation to Leaderboard and Profile', () => {
- // We should be able to navigate to Leaderboard using tab bar
- cy.contains('Rankings').click();
-
- // Verify Leaderboard screen is shown
- cy.contains('LEADERBOARD').should('be.visible');
- cy.contains('Top Contributors').should('be.visible');
-
- // Navigate to Profile using tab bar
- cy.contains('Profile').click();
-
- // Verify Profile screen renders correctly
- cy.contains('Jane Doe').should('be.visible');
- cy.contains('jane.doe@uni.edu').should('be.visible');
- cy.contains('Computer Science').should('be.visible');
- cy.contains('3rd Year').should('be.visible');
- cy.contains('Student Settings').should('be.visible');
- });
-
- it('should allow searching for events in the feed', () => {
- const searchQuery = 'Hackathon';
- // Type search query
- cy.get('input[placeholder="Search events..."]')
- .type(searchQuery)
- .should('have.value', searchQuery);
-
- // Verify close icon appears via stable testID
- cy.get('[data-testid="clear-search-button"]').should('exist');
- });
-});
+ .catch(err => console.error(err))
\ No newline at end of file
diff --git a/app/src/screens/AttendanceDashboard.js b/app/src/screens/AttendanceDashboard.js
index ffe25054..cc5311a1 100644
--- a/app/src/screens/AttendanceDashboard.js
+++ b/app/src/screens/AttendanceDashboard.js
@@ -164,1158 +164,4 @@ export default function AttendanceDashboard({ route, navigation }) {
// Fetch Event Data to check for Custom Form
useEffect(() => {
getDoc(doc(db, COLLECTIONS.EVENTS, eventId)).then(snap => {
- if (snap.exists()) setEventData(snap.data());
- });
- }, [eventId]);
-
- useFocusEffect(
- useCallback(() => {
- getOfflineCheckInCount(eventId).then(count => setPendingOfflineCount(count));
- }, [eventId]),
- );
-
- const handleSyncOffline = async () => {
- if (syncingOffline) return;
-
- setSyncingOffline(true);
- try {
- const result = await syncOfflineCheckIns(eventId, user?.uid || 'Unknown Organizer');
- if (result.success) {
- Alert.alert('Success', `Synced ${result.syncedCount} check-ins.`);
- } else if (typeof result.remainingCount === 'number') {
- Alert.alert(
- 'Partial Sync',
- `Synced ${result.syncedCount} check-ins. ${result.remainingCount} still pending.`,
- );
- } else {
- // Fatal error returned from syncOfflineCheckIns
- const msg = result.error?.message || String(result.error) || 'Unknown error';
- console.error('Offline sync fatal error:', result.error);
- Alert.alert('Sync Failed', `Could not sync offline check-ins: ${msg}`);
- }
- } catch (error) {
- console.error('Offline sync failed:', error);
- Alert.alert('Error', 'Failed to sync offline check-ins.');
- } finally {
- try {
- const count = await getOfflineCheckInCount(eventId);
- if (isMountedRef.current) {
- setPendingOfflineCount(count);
- }
- } catch (countError) {
- console.error('Failed to refresh offline check-in count', countError);
- }
- if (isMountedRef.current) {
- setSyncingOffline(false);
- }
- }
- };
-
- // Live Participant Count
- const [totalRegistrations, setTotalRegistrations] = useState(0);
-
- // Real-time participants listener (use shared subscriber to dedupe)
- useEffect(() => {
- let mounted = true;
- const unsub = participantService.subscribeParticipants(db, eventId, data => {
- if (!mounted) return;
- setTotalRegistrations(Array.isArray(data) ? data.length : 0);
- setLoading(false);
- });
-
- return () => {
- mounted = false;
- if (unsub) unsub();
- };
- }, [eventId]);
-
- // Note: Automatic feedback sending is now handled globally in App.js via AutomationService.
- // This component simply reflects the status via 'eventData.feedbackRequestSent'.
-
- // Real-time check-ins listener
- useEffect(() => {
- const q = query(
- collection(db, getEventCheckInsPath(eventId)),
- orderBy('checkedInAt', 'desc'),
- );
-
- const unsubscribe = onSnapshot(q, snapshot => {
- const checkInsList = [];
- const deptCount = {};
- const yearCount = {};
-
- snapshot.forEach(doc => {
- const data = doc.data();
- checkInsList.push({ id: doc.id, ...data });
-
- const dept = data.userBranch || 'Unknown';
- deptCount[dept] = (deptCount[dept] || 0) + 1;
-
- const year = data.userYear || 'Unknown';
- yearCount[year] = (yearCount[year] || 0) + 1;
- });
-
- setCheckIns(checkInsList);
- setDepartmentStats(deptCount);
- setYearStats(yearCount);
- });
-
- return () => unsubscribe();
- }, [eventId]);
-
- // Calculate Peak Attendance Data
- const peakAttendanceData = useMemo(() => {
- if (!checkIns || checkIns.length === 0 || !eventData?.startAt) return null;
-
- const startAt = new Date(eventData.startAt).getTime();
- if (Number.isNaN(startAt)) return null;
- const buckets = {
- '>30m Early': 0,
- '15-30m Early': 0,
- '0-15m Early': 0,
- '0-15m Late': 0,
- '15-30m Late': 0,
- '>30m Late': 0,
- };
-
- checkIns.forEach(checkIn => {
- const checkInTime = checkIn.checkedInAt?.toMillis();
- if (!checkInTime) return;
-
- const diffMinutes = (checkInTime - startAt) / 60000;
-
- if (diffMinutes < -30) buckets['>30m Early']++;
- else if (diffMinutes >= -30 && diffMinutes < -15) buckets['15-30m Early']++;
- else if (diffMinutes >= -15 && diffMinutes < 0) buckets['0-15m Early']++;
- else if (diffMinutes >= 0 && diffMinutes <= 15) buckets['0-15m Late']++;
- else if (diffMinutes > 15 && diffMinutes <= 30) buckets['15-30m Late']++;
- else buckets['>30m Late']++;
- });
-
- // Only render graph if there is at least one check-in with a valid timestamp
- const totalValid = Object.values(buckets).reduce((sum, val) => sum + val, 0);
- if (totalValid === 0) return null;
-
- const data = Object.values(buckets);
- const maxVal = Math.max(...data);
-
- return {
- segments: Math.max(1, Math.min(maxVal, 4)), // Prevent duplicate Y-axis labels by limiting segments
- labels: ['>30 E', '15-30 E', '0-15 E', '0-15 L', '15-30 L', '>30 L'],
- datasets: [
- {
- data,
- colors: [
- (opacity = 1) => theme.colors.success || '#00C853',
- (opacity = 1) => '#4CAF50',
- (opacity = 1) => theme.colors.primary,
- (opacity = 1) => theme.colors.warning || '#FFAB00',
- (opacity = 1) => '#FF5722',
- (opacity = 1) => theme.colors.error || '#FF3D00',
- ],
- },
- ],
- };
- }, [checkIns, eventData, theme]);
-
- const downloadCSV = async (csvContent, fileName) => {
- if (Platform.OS === 'web') {
- // Create a blob and trigger download
- const bom = new Uint8Array([0xef, 0xbb, 0xbf]); // UTF-8 BOM
- const blob = new Blob([bom, csvContent], { type: 'text/csv;charset=utf-8;' });
- const link = document.createElement('a');
- const url = URL.createObjectURL(blob);
- link.setAttribute('href', url);
- link.setAttribute('download', fileName);
- link.style.visibility = 'hidden';
- document.body.appendChild(link);
- link.click();
- link.remove();
- } else {
- // Use standard share on mobile
- await Share.share({ message: csvContent, title: fileName });
- }
- };
-
- const handleExportParticipants = async () => {
- if (exporting) return;
-
- setExporting(true);
- try {
- const snapshotData = await participantService.fetchParticipantsOnce(db, eventId);
-
- if (!snapshotData || snapshotData.length === 0) {
- Alert.alert('No Data', 'No registered participants yet.');
- setExporting(false);
- return;
- }
-
- let csv = 'Name,Email,Branch,Year,Joined At\n';
-
- const rows = await Promise.all(
- snapshotData.map(async d => {
- let branch = d.branch;
- let year = d.year;
-
- return `"${d.name || 'Anonymous'}","${d.email || '-'}","${branch || '-'}","${year || '-'}","${d.joinedAt}"\n`;
- }),
- );
-
- csv += rows.join('');
-
- await downloadCSV(csv, `Participants_${eventTitle}.csv`);
- Alert.alert(
- 'Export Ready',
- Platform.OS === 'web' ? 'Download started!' : 'Participants export is ready.',
- );
- } catch (error) {
- console.error('Export Error: ', error);
- Alert.alert('Error', 'Failed to export participants.');
- } finally {
- setExporting(false);
- }
- };
-
- const handleExportReviews = async () => {
- if (exporting) return;
-
- setExporting(true);
- try {
- const feedbackRef = collection(db, getEventFeedbackPath(eventId));
- const snapshot = await getDocs(feedbackRef);
-
- if (snapshot.empty) {
- Alert.alert('No Reviews', 'This event has no feedback yet.');
- setExporting(false);
- return;
- }
-
- let csv = 'User Name,Event Rating,Organizer Rating,Feedback,Date\n';
- snapshot.forEach(doc => {
- const d = doc.data();
- // Fix CSV escaping and formatting
- const safeFeedback = (d.feedback || '').replace(/"/g, '""');
- const dateStr = d.createdAt ? formatEventDate(d.createdAt) : '-';
-
- const line = `"${d.userName || 'Anonymous'}","${d.eventRating || '-'}","${d.clubRating || '-'}","${safeFeedback}","${dateStr}"\n`;
- csv += line;
- });
-
- await downloadCSV(csv, `Reviews_${eventTitle}.csv`);
- Alert.alert(
- 'Export Ready',
- Platform.OS === 'web' ? 'Download started!' : 'Reviews export is ready.',
- );
- } catch (error) {
- console.error('Export Error: ', error);
- Alert.alert('Error', 'Failed to export reviews.');
- } finally {
- setExporting(false);
- }
- };
-
- const handleExportFormResponses = async () => {
- if (exporting) return;
-
- setExporting(true);
- try {
- const q = query(
- collection(db, COLLECTIONS.REGISTRATIONS),
- where('eventId', '==', eventId),
- );
- const snapshot = await getDocs(q);
-
- if (snapshot.empty) {
- Alert.alert('No Data', 'No form responses found.');
- setExporting(false);
- return;
- }
-
- // Build CSV Header from Schema
- const schema = eventData.customFormSchema || [];
- if (schema.length === 0) {
- Alert.alert('Error', 'Schema not found');
- setExporting(false);
- return;
- }
-
- let csv = 'User Name,User Email,' + schema.map(f => f.label).join(',') + ',Date\n';
-
- snapshot.forEach(doc => {
- const d = doc.data();
- const responseMap = d.responses || {};
-
- const responseValues = schema.map(f => {
- let val = responseMap[f.id] || '';
- val = String(val).replace(/"/g, '""'); // Escape quotes
- return `"${val}"`;
- });
-
- const line = `"${d.userName || 'Anonymous'}","${d.userEmail || '-'}","${responseValues.join('","')}","${d.timestamp}"\n`;
- csv += line;
- });
-
- await downloadCSV(csv, `Form_Responses_${eventTitle}.csv`);
- Alert.alert(
- 'Export Ready',
- Platform.OS === 'web' ? 'Download started!' : 'Form responses export is ready.',
- );
- } catch (e) {
- console.error('Export Error: ', e);
- Alert.alert('Error', 'Failed to export responses.');
- } finally {
- setExporting(false);
- }
- };
-
- if (loading) {
- return (
-
-
-
- );
- }
-
- return (
-
-
- navigation.goBack()} style={styles.backBtn}>
-
-
-
-
- Attendance
-
-
- {eventTitle}
-
-
- navigation.navigate('QRScanner', { eventId, eventTitle })}
- style={[styles.scanBtn, { backgroundColor: theme.colors.primary }]}
- >
-
-
-
-
-
- {pendingOfflineCount > 0 && (
-
-
-
- Offline Sync Pending
-
-
- {pendingOfflineCount} check-ins waiting for network
-
-
-
- {syncingOffline ? (
-
- ) : (
- Sync Now
- )}
-
-
- )}
-
- {/* Updated Stat Cards to use Primary Theme */}
-
-
- {/* Re-doing the StatCards to be safe and consistent */}
-
- {/* ... (rest of render is handled by partial replacement or I need to include it) */}
- {/* Wait, the replace_file_content needs to be precise. I will just replace the StatCards implementation in the render block */}
-
- {/* Live Check-Ins Feed */}
-
-
-
-
-
-
-
- Live Check-Ins
-
-
-
-
- {checkIns.length}
-
-
-
- {checkIns.length === 0 ? (
-
-
-
-
-
- No check-ins yet
-
-
- ) : (
-
- {checkIns.slice(0, 10).map(item => (
-
- ))}
-
- )}
-
-
- {peakAttendanceData && (
-
-
-
-
-
- Peak Attendance Time
-
-
-
- theme.colors.border,
- labelColor: (opacity = 1) => theme.colors.textSecondary,
- barPercentage: 0.7,
- barRadius: 4,
- decimalPlaces: 0,
- propsForLabels: {
- fontSize: 10,
- fontWeight: '600',
- },
- propsForBackgroundLines: {
- strokeDasharray: '4',
- stroke: theme.colors.textSecondary + '20',
- },
- }}
- style={{
- marginVertical: 0,
- borderRadius: 16,
- marginHorizontal: -10,
- paddingRight: 30,
- }}
- showValuesOnTopOfBars={true}
- fromZero={true}
- withInnerLines={true}
- withCustomBarColorFromData={true}
- flatColor={true}
- />
-
- )}
-
- {Object.keys(departmentStats).length > 0 && (
-
- )}
-
- {Object.keys(yearStats).length > 0 && (
-
- )}
-
- {/* Communication Section */}
-
-
- Communication
-
-
- setAnnouncementModalVisible(true)}
- >
-
-
- Announce
-
-
-
- {/* Manual Feedback Request Button */}
-
- {eventData?.feedbackRequestSent ? (
- <>
-
-
- Feedback Sent
-
- >
- ) : (
- <>
-
-
- Feedback
-
- >
- )}
-
-
-
-
- {/* Export Data Section */}
-
-
- Export Data
-
-
- {/* Intelligent Export Button: Prioritizes Custom Form Responses */}
-
-
-
- {eventData?.hasCustomForm ? 'Form Responses' : 'Participants'}
-
-
-
-
-
-
- Reviews
-
-
-
-
-
-
-
-
- {/* Announcement Modal */}
- setAnnouncementModalVisible(false)}
- >
-
-
-
-
- New Announcement
-
- setAnnouncementModalVisible(false)}>
-
-
-
-
-
- Subject
-
-
-
-
- Message
-
-
-
-
- {sending ? (
-
- ) : (
- Send Announcement
- )}
-
-
-
-
-
- {/* Feedback Request Modal */}
- setFeedbackModalVisible(false)}
- >
-
-
-
-
- Feedback
-
- setFeedbackModalVisible(false)}>
-
-
-
-
-
-
-
- Send feedback request emails to all registered participants?
-
-
- They will receive a beautiful email with a link to rate the event
- and provide feedback.
-
-
-
-
- setFeedbackModalVisible(false)}
- >
-
- Cancel
-
-
-
-
- {sending ? (
-
- ) : (
- <>
-
-
- Send Emails
-
- >
- )}
-
-
-
-
-
-
- );
-}
-
-const StatCard = ({ icon, label, value, color, subtitle, gradient }) => {
- const { theme } = useTheme();
- return (
-
-
-
-
-
- {value}
-
- {label}
-
- {subtitle && (
-
- {subtitle}
-
- )}
-
-
- );
-};
-
-const CheckInItem = ({ item }) => {
- const { theme } = useTheme();
- const timeAgo = getTimeAgo(item.checkedInAt?.toMillis());
-
- return (
-
-
-
- {item.userName?.[0]?.toUpperCase() || '?'}
-
-
-
-
- {item.userName}
-
-
-
-
- {item.userBranch} • Year {item.userYear}
-
-
-
-
-
-
-
-
- {timeAgo}
-
-
-
- );
-};
-
-const getTimeAgo = timestamp => {
- if (!timestamp) return 'Just now';
- const now = Date.now();
- const diff = now - timestamp;
- const minutes = Math.floor(diff / 60000);
- if (minutes < 1) return 'Just now';
- if (minutes === 1) return '1 min ago';
- if (minutes < 60) return `${minutes} mins ago`;
- const hours = Math.floor(minutes / 60);
- if (hours === 1) return '1 hour ago';
- if (hours < 24) return `${hours} hours ago`;
- return formatEventDate(timestamp);
-};
-
-const AnalyticsSection = ({ title, data, icon }) => {
- const { theme } = useTheme();
- const total = Object.values(data).reduce((sum, val) => sum + val, 0);
- const sortedData = Object.entries(data).sort((a, b) => b[1] - a[1]);
-
- return (
-
-
-
-
-
- {title}
-
-
-
- {total} total
-
-
- {sortedData.map(([key, value]) => {
- const percentage = total > 0 ? ((value / total) * 100).toFixed(1) : 0;
- return (
-
-
-
- {key}
-
-
- {value} ({percentage}%)
-
-
-
-
-
-
- );
- })}
-
- );
-};
-
-const styles = StyleSheet.create({
- container: { flex: 1 },
- header: {
- flexDirection: 'row',
- alignItems: 'center',
- padding: 16,
- gap: 12,
- elevation: 2,
- },
- backBtn: { padding: 4 },
- headerTitle: { fontSize: 22, fontWeight: '800' },
- headerSubtitle: { fontSize: 13, marginTop: 2 },
- scanBtn: {
- width: 44,
- height: 44,
- borderRadius: 22,
- alignItems: 'center',
- justifyContent: 'center',
- },
- statsContainer: { flexDirection: 'row', padding: 16, gap: 10 },
- statCard: { flex: 1, borderRadius: 14, overflow: 'hidden', elevation: 2 },
- statGradient: {
- padding: 14,
- alignItems: 'center',
- gap: 6,
- minHeight: 130,
- justifyContent: 'center',
- },
- statIconBox: {
- width: 42,
- height: 42,
- borderRadius: 21,
- alignItems: 'center',
- justifyContent: 'center',
- marginBottom: 4,
- },
- statValue: { fontSize: 28, fontWeight: '800', lineHeight: 32 },
- statLabel: {
- fontSize: 10,
- textTransform: 'uppercase',
- letterSpacing: 0.8,
- fontWeight: '700',
- textAlign: 'center',
- },
- statSubtitle: { fontSize: 10, marginTop: 4, textAlign: 'center' },
- section: { margin: 16, marginTop: 0, borderRadius: 16, padding: 16 },
- sectionHeader: {
- flexDirection: 'row',
- justifyContent: 'space-between',
- alignItems: 'center',
- marginBottom: 16,
- },
- sectionHeaderLeft: { flexDirection: 'row', alignItems: 'center', gap: 10 },
- liveDotContainer: {
- width: 24,
- height: 24,
- borderRadius: 12,
- backgroundColor: '#FF000020',
- alignItems: 'center',
- justifyContent: 'center',
- },
- liveDot: { width: 8, height: 8, borderRadius: 4, backgroundColor: '#FF0000' },
- sectionTitle: { fontSize: 17, fontWeight: '700' },
- countBadge: {
- backgroundColor: '#FF980020',
- paddingHorizontal: 12,
- paddingVertical: 6,
- borderRadius: 12,
- },
- countText: { fontSize: 14, fontWeight: '700' },
- checkInsList: { gap: 10 },
- checkInItem: {
- flexDirection: 'row',
- alignItems: 'center',
- padding: 12,
- borderRadius: 12,
- gap: 12,
- },
- checkInAvatar: {
- width: 44,
- height: 44,
- borderRadius: 22,
- alignItems: 'center',
- justifyContent: 'center',
- },
- avatarText: { fontSize: 18, fontWeight: '700' },
- checkInInfo: { flex: 1, gap: 4 },
- checkInName: { fontSize: 15, fontWeight: '600' },
- checkInMeta: { flexDirection: 'row', alignItems: 'center', gap: 4 },
- checkInDetails: { fontSize: 12 },
- checkInTime: { alignItems: 'flex-end', gap: 6 },
- checkmarkBadge: {
- width: 28,
- height: 28,
- borderRadius: 14,
- backgroundColor: '#4CAF5020',
- alignItems: 'center',
- justifyContent: 'center',
- },
- timeText: { fontSize: 11 },
- emptyState: { alignItems: 'center', paddingVertical: 40 },
- emptyIcon: {
- width: 80,
- height: 80,
- borderRadius: 40,
- alignItems: 'center',
- justifyContent: 'center',
- marginBottom: 16,
- },
- emptyText: { fontSize: 16, fontWeight: '600', marginBottom: 6 },
- analyticsCard: { margin: 16, marginTop: 0, padding: 16, borderRadius: 16 },
- analyticsHeader: {
- flexDirection: 'row',
- justifyContent: 'space-between',
- alignItems: 'center',
- marginBottom: 16,
- },
- analyticsHeaderLeft: { flexDirection: 'row', alignItems: 'center', gap: 8 },
- analyticsTitle: { fontSize: 17, fontWeight: '700' },
- analyticsTotal: { fontSize: 12, fontWeight: '600' },
- analyticsItem: { marginBottom: 14 },
- analyticsItemHeader: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 8 },
- analyticsLabel: { fontSize: 14, fontWeight: '600' },
- analyticsValue: { fontSize: 13 },
- analyticsBarBg: {
- height: 8,
- backgroundColor: 'rgba(0,0,0,0.08)',
- borderRadius: 4,
- overflow: 'hidden',
- },
- analyticsBarFill: { height: '100%', borderRadius: 4 },
- exportContainer: { margin: 16, marginTop: 0 },
- exportTitle: { fontSize: 17, fontWeight: '700', marginBottom: 12 },
- exportButtons: { flexDirection: 'row', gap: 12 },
- exportBtn: { flex: 1, borderRadius: 14, overflow: 'hidden' },
- premiumBtn: {
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'center',
- gap: 8,
- padding: 16,
- borderWidth: 1,
- borderColor: '#FFD700',
- borderRadius: 14, // Gold border
- },
- exportBtnText: { fontSize: 14, fontWeight: '700' },
-
- // Modal Styles
- modalOverlay: {
- flex: 1,
- backgroundColor: 'rgba(0,0,0,0.5)',
- justifyContent: 'center',
- padding: 20,
- },
- modalContent: { borderRadius: 20, padding: 20, elevation: 5 },
- modalHeader: {
- flexDirection: 'row',
- justifyContent: 'space-between',
- alignItems: 'center',
- marginBottom: 20,
- },
- modalTitle: { fontSize: 20, fontWeight: 'bold' },
- inputLabel: { fontSize: 14, marginBottom: 8, fontWeight: '600' },
- input: {
- borderWidth: 1,
- borderRadius: 12,
- padding: 12,
- marginBottom: 16,
- fontSize: 16,
- },
- sendBtn: {
- padding: 16,
- borderRadius: 14,
- alignItems: 'center',
- marginTop: 10,
- },
- sendBtnText: { color: '#fff', fontSize: 16, fontWeight: 'bold' },
-
- // Feedback Modal Styles
- modalDescription: { fontSize: 16, fontWeight: '600', lineHeight: 24 },
- modalSubtext: { fontSize: 14, lineHeight: 20 },
- modalButton: {
- padding: 14,
- borderRadius: 12,
- alignItems: 'center',
- justifyContent: 'center',
- flexDirection: 'row',
- },
- modalButtonText: { fontSize: 15, fontWeight: '700' },
- offlineBanner: {
- flexDirection: 'row',
- alignItems: 'center',
- marginHorizontal: 20,
- marginTop: 20,
- padding: 15,
- borderRadius: 12,
- borderWidth: 1,
- },
- offlineBannerTitle: {
- fontSize: 16,
- fontWeight: 'bold',
- marginBottom: 4,
- },
- offlineBannerText: {
- fontSize: 13,
- },
- syncBtn: {
- paddingHorizontal: 16,
- paddingVertical: 8,
- borderRadius: 20,
- },
- syncBtnText: {
- color: '#fff',
- fontWeight: 'bold',
- },
-});
-
-AttendanceDashboard.propTypes = {
- route: PropTypes.object,
- navigation: PropTypes.object,
-};
-StatCard.propTypes = {
- icon: PropTypes.string.isRequired,
- label: PropTypes.string.isRequired,
- value: PropTypes.number.isRequired,
- color: PropTypes.string.isRequired,
- subtitle: PropTypes.string,
- gradient: PropTypes.arrayOf(PropTypes.string),
-};
-CheckInItem.propTypes = {
- item: PropTypes.shape({
- id: PropTypes.string.isRequired,
- userName: PropTypes.string,
- userBranch: PropTypes.string,
- userYear: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
- checkedInAt: PropTypes.object,
- }).isRequired,
-};
-AnalyticsSection.propTypes = {
- title: PropTypes.string.isRequired,
- data: PropTypes.object.isRequired,
- icon: PropTypes.string.isRequired,
-};
+ .catch(err => console.error(err))
\ No newline at end of file
diff --git a/app/src/screens/ProfileScreen.js b/app/src/screens/ProfileScreen.js
index a8867bc8..c7238000 100644
--- a/app/src/screens/ProfileScreen.js
+++ b/app/src/screens/ProfileScreen.js
@@ -185,7 +185,7 @@ const ProfileBadgeShelf = ({
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.profileBadgeShelfList}
>
- {badges.map(badge => {
+ {(badges ?? []).map(badge => {
const isSelected = badge.id === selectedBadgeId;
return (
@@ -380,7 +380,7 @@ export default function ProfileScreen({ navigation }) {
bio: bio,
instagram: instagram,
linkedin: linkedin,
- year: parseInt(year),
+ year: parseInt(year, 10),
branch: finalBranch,
};
diff --git a/app/src/screens/UserFeed.js b/app/src/screens/UserFeed.js
index 352b8aa0..67a8667b 100644
--- a/app/src/screens/UserFeed.js
+++ b/app/src/screens/UserFeed.js
@@ -95,7 +95,7 @@ const UserFeedStickyHeader = ({
showsHorizontalScrollIndicator={false}
style={styles.historyScroll}
>
- {searchHistory.map(qh => (
+ {(searchHistory ?? []).map(qh => (
f.endsWith('.ts') && f !== 'migrate.ts')
- .sort();
+ .sort((a, b) => a - b);
if (files.length === 0) {
console.log('No migrations found.');