Compare commits
No commits in common. "ad3eba48b009fccc3bc67a019c5f8b10c8a3f4c0" and "42122ac34123899564200b07d1aed296e540045f" have entirely different histories.
ad3eba48b0
...
42122ac341
Binary file not shown.
@ -83,6 +83,12 @@ export default function TabLayout() {
|
|||||||
title: "Plans",
|
title: "Plans",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<Tabs.Screen
|
||||||
|
name="attendance"
|
||||||
|
options={{
|
||||||
|
title: "Attendance",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<Tabs.Screen
|
<Tabs.Screen
|
||||||
name="profile"
|
name="profile"
|
||||||
options={{
|
options={{
|
||||||
|
|||||||
348
apps/mobile/src/app/(tabs)/attendance.tsx
Normal file
348
apps/mobile/src/app/(tabs)/attendance.tsx
Normal file
@ -0,0 +1,348 @@
|
|||||||
|
import {
|
||||||
|
View,
|
||||||
|
Text,
|
||||||
|
StyleSheet,
|
||||||
|
TouchableOpacity,
|
||||||
|
ActivityIndicator,
|
||||||
|
ScrollView,
|
||||||
|
Alert,
|
||||||
|
} from "react-native";
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { useAuth } from "@clerk/clerk-expo";
|
||||||
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
|
import { useTheme } from "../../contexts/ThemeContext";
|
||||||
|
import { MinimalCard } from "../../components/MinimalCard";
|
||||||
|
import { SectionHeader } from "../../components/SectionHeader";
|
||||||
|
import { MinimalButton } from "../../components/MinimalButton";
|
||||||
|
import { Badge } from "../../components/Badge";
|
||||||
|
import { IconContainer } from "../../components/IconContainer";
|
||||||
|
import { attendanceApi, Attendance } from "../../api/attendance";
|
||||||
|
import { AttendanceCalendar } from "../../components/AttendanceCalendar";
|
||||||
|
import { useStatistics } from "../../contexts/StatisticsContext";
|
||||||
|
import { getErrorMessage } from "../../utils/error-helpers";
|
||||||
|
import log from "../../utils/logger";
|
||||||
|
|
||||||
|
export default function AttendanceScreen() {
|
||||||
|
const { getToken, userId } = useAuth();
|
||||||
|
const { colors, typography } = useTheme();
|
||||||
|
const { clearCache: clearStatisticsCache } = useStatistics();
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [activeCheckIn, setActiveCheckIn] = useState<Attendance | null>(null);
|
||||||
|
const [history, setHistory] = useState<Attendance[]>([]);
|
||||||
|
|
||||||
|
const fetchAttendance = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const token = await getToken();
|
||||||
|
if (!token) return;
|
||||||
|
|
||||||
|
log.debug("Fetching attendance history");
|
||||||
|
const data = await attendanceApi.getHistory(token);
|
||||||
|
setHistory(data);
|
||||||
|
|
||||||
|
// Check if there's an active check-in (latest one has no checkOutTime)
|
||||||
|
if (data.length > 0 && !data[0].checkOutTime) {
|
||||||
|
setActiveCheckIn(data[0]);
|
||||||
|
} else {
|
||||||
|
setActiveCheckIn(null);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
log.error("Failed to fetch attendance", error);
|
||||||
|
Alert.alert("Error", "Failed to load attendance data");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchAttendance();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleCheckIn = async () => {
|
||||||
|
try {
|
||||||
|
const token = await getToken();
|
||||||
|
if (!token) return;
|
||||||
|
|
||||||
|
await attendanceApi.checkIn("gym", token);
|
||||||
|
clearStatisticsCache();
|
||||||
|
fetchAttendance();
|
||||||
|
Alert.alert("Success", "Checked in successfully!");
|
||||||
|
} catch (error: unknown) {
|
||||||
|
log.error("Failed to check in", error);
|
||||||
|
Alert.alert("Error", getErrorMessage(error, "Failed to check in"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCheckOut = async () => {
|
||||||
|
try {
|
||||||
|
const token = await getToken();
|
||||||
|
if (!token) return;
|
||||||
|
|
||||||
|
await attendanceApi.checkOut(token);
|
||||||
|
clearStatisticsCache();
|
||||||
|
fetchAttendance();
|
||||||
|
Alert.alert("Success", "Checked out successfully!");
|
||||||
|
} catch (error: unknown) {
|
||||||
|
log.error("Failed to check out", error);
|
||||||
|
Alert.alert("Error", getErrorMessage(error, "Failed to check out"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading && !history.length) {
|
||||||
|
return (
|
||||||
|
<View style={[styles.centered, { backgroundColor: colors.background }]}>
|
||||||
|
<ActivityIndicator size="large" color={colors.primary} />
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ScrollView
|
||||||
|
style={[styles.container, { backgroundColor: colors.background }]}
|
||||||
|
contentContainerStyle={styles.content}
|
||||||
|
>
|
||||||
|
{/* Header */}
|
||||||
|
<View style={styles.header}>
|
||||||
|
<Text
|
||||||
|
style={[typography.h1, { color: colors.textPrimary, fontSize: 32 }]}
|
||||||
|
>
|
||||||
|
Attendance
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
typography.body,
|
||||||
|
{ color: colors.textSecondary, marginTop: 8 },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{activeCheckIn
|
||||||
|
? "You're crushing it today!"
|
||||||
|
: history.length === 0
|
||||||
|
? "Ready to start your fitness journey?"
|
||||||
|
: "Track your gym visits and build streaks!"}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Check In/Out Section */}
|
||||||
|
<View style={styles.section}>
|
||||||
|
{activeCheckIn ? (
|
||||||
|
<MinimalCard variant="bordered" style={styles.activeCard}>
|
||||||
|
<View style={styles.activeHeader}>
|
||||||
|
<View style={styles.activeHeaderLeft}>
|
||||||
|
<IconContainer
|
||||||
|
variant="colored"
|
||||||
|
backgroundColor={`${colors.success}20`}
|
||||||
|
size="lg"
|
||||||
|
>
|
||||||
|
<Ionicons
|
||||||
|
name="checkmark-circle"
|
||||||
|
size={28}
|
||||||
|
color={colors.success}
|
||||||
|
/>
|
||||||
|
</IconContainer>
|
||||||
|
<View style={{ marginLeft: 12 }}>
|
||||||
|
<Text style={[typography.h3, { color: colors.textPrimary }]}>
|
||||||
|
✅ Currently Checked In
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
typography.caption,
|
||||||
|
{ color: colors.textTertiary, marginTop: 2 },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
Since{" "}
|
||||||
|
{new Date(activeCheckIn.checkInTime).toLocaleTimeString(
|
||||||
|
[],
|
||||||
|
{
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<MinimalButton
|
||||||
|
title="Check Out"
|
||||||
|
onPress={handleCheckOut}
|
||||||
|
variant="danger"
|
||||||
|
size="lg"
|
||||||
|
style={{ marginTop: 16 }}
|
||||||
|
/>
|
||||||
|
</MinimalCard>
|
||||||
|
) : (
|
||||||
|
<MinimalButton
|
||||||
|
title="💪 Check In"
|
||||||
|
onPress={handleCheckIn}
|
||||||
|
variant="primary"
|
||||||
|
size="lg"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Attendance Calendar */}
|
||||||
|
<View style={styles.section}>
|
||||||
|
<SectionHeader title="📅 Calendar" />
|
||||||
|
<MinimalCard variant="default" style={{ borderRadius: 20 }}>
|
||||||
|
<AttendanceCalendar attendanceRecords={history} />
|
||||||
|
</MinimalCard>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Recent History */}
|
||||||
|
<View style={styles.section}>
|
||||||
|
<SectionHeader title="📊 Recent History" />
|
||||||
|
{history.length === 0 ? (
|
||||||
|
<MinimalCard variant="default" style={{ borderRadius: 20 }}>
|
||||||
|
<View style={styles.emptyState}>
|
||||||
|
<Text style={{ fontSize: 64 }}>📍</Text>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
typography.bodyEmphasis,
|
||||||
|
{ color: colors.textPrimary, marginTop: 16 },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
No attendance history yet
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
typography.body,
|
||||||
|
{
|
||||||
|
color: colors.textSecondary,
|
||||||
|
marginTop: 8,
|
||||||
|
textAlign: "center",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
Check in to start building your streak! 🔥
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</MinimalCard>
|
||||||
|
) : (
|
||||||
|
<View style={styles.historyList}>
|
||||||
|
{history.slice(0, 10).map((record, index) => {
|
||||||
|
const checkIn = new Date(record.checkInTime);
|
||||||
|
const checkOut = record.checkOutTime
|
||||||
|
? new Date(record.checkOutTime)
|
||||||
|
: null;
|
||||||
|
const duration = checkOut
|
||||||
|
? Math.round((checkOut.getTime() - checkIn.getTime()) / 60000)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MinimalCard
|
||||||
|
key={index}
|
||||||
|
variant="bordered"
|
||||||
|
style={{ borderRadius: 16 }}
|
||||||
|
>
|
||||||
|
<View style={styles.historyItem}>
|
||||||
|
<View style={styles.historyLeft}>
|
||||||
|
<IconContainer
|
||||||
|
variant="colored"
|
||||||
|
backgroundColor={
|
||||||
|
checkOut ? `${colors.success}20` : `${colors.info}20`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Ionicons
|
||||||
|
name={checkOut ? "checkmark-done" : "time"}
|
||||||
|
size={20}
|
||||||
|
color={checkOut ? colors.success : colors.info}
|
||||||
|
/>
|
||||||
|
</IconContainer>
|
||||||
|
<View style={{ marginLeft: 12, flex: 1 }}>
|
||||||
|
<Text
|
||||||
|
style={[typography.h3, { color: colors.textPrimary }]}
|
||||||
|
>
|
||||||
|
{checkIn.toLocaleDateString()}
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
typography.caption,
|
||||||
|
{ color: colors.textTertiary, marginTop: 2 },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{checkIn.toLocaleTimeString([], {
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
})}
|
||||||
|
{checkOut &&
|
||||||
|
` - ${checkOut.toLocaleTimeString([], {
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
})}`}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
{duration && (
|
||||||
|
<Badge
|
||||||
|
label={`${duration}m`}
|
||||||
|
variant="neutral"
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</MinimalCard>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Bottom Spacer */}
|
||||||
|
<View style={{ height: 100 }} />
|
||||||
|
</ScrollView>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
centered: {
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
paddingBottom: 20,
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
paddingHorizontal: 24,
|
||||||
|
paddingTop: 60,
|
||||||
|
paddingBottom: 24,
|
||||||
|
},
|
||||||
|
section: {
|
||||||
|
paddingHorizontal: 24,
|
||||||
|
marginBottom: 24,
|
||||||
|
},
|
||||||
|
activeCard: {
|
||||||
|
padding: 20,
|
||||||
|
borderRadius: 20,
|
||||||
|
},
|
||||||
|
activeHeader: {
|
||||||
|
flexDirection: "row",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
activeHeaderLeft: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
emptyState: {
|
||||||
|
alignItems: "center",
|
||||||
|
paddingVertical: 40,
|
||||||
|
paddingHorizontal: 20,
|
||||||
|
},
|
||||||
|
historyList: {
|
||||||
|
gap: 12,
|
||||||
|
},
|
||||||
|
historyItem: {
|
||||||
|
flexDirection: "row",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
historyLeft: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
303
apps/mobile/src/components/AttendanceCalendar.tsx
Normal file
303
apps/mobile/src/components/AttendanceCalendar.tsx
Normal file
@ -0,0 +1,303 @@
|
|||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import { View, Text, StyleSheet, TouchableOpacity } from "react-native";
|
||||||
|
import { LinearGradient } from "expo-linear-gradient";
|
||||||
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
|
import { theme } from "../styles/theme";
|
||||||
|
|
||||||
|
interface AttendanceRecord {
|
||||||
|
id: string;
|
||||||
|
checkInTime: string;
|
||||||
|
checkOutTime?: string | null;
|
||||||
|
duration?: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AttendanceCalendarProps {
|
||||||
|
attendanceRecords: AttendanceRecord[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AttendanceCalendar({
|
||||||
|
attendanceRecords,
|
||||||
|
}: AttendanceCalendarProps) {
|
||||||
|
const [currentMonth, setCurrentMonth] = useState(new Date());
|
||||||
|
const [calendarDays, setCalendarDays] = useState<
|
||||||
|
Array<{ date: Date | null; hasAttendance: boolean; isToday: boolean }>
|
||||||
|
>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
generateCalendar(currentMonth);
|
||||||
|
}, [currentMonth, attendanceRecords]);
|
||||||
|
|
||||||
|
const generateCalendar = (month: Date) => {
|
||||||
|
const year = month.getFullYear();
|
||||||
|
const monthIndex = month.getMonth();
|
||||||
|
|
||||||
|
// Get first day of month and number of days
|
||||||
|
const firstDay = new Date(year, monthIndex, 1);
|
||||||
|
const lastDay = new Date(year, monthIndex + 1, 0);
|
||||||
|
const daysInMonth = lastDay.getDate();
|
||||||
|
const startingDayOfWeek = firstDay.getDay();
|
||||||
|
|
||||||
|
// Create attendance lookup set
|
||||||
|
const attendanceDates = new Set(
|
||||||
|
attendanceRecords.map((record) =>
|
||||||
|
new Date(record.checkInTime).toDateString(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const today = new Date().toDateString();
|
||||||
|
|
||||||
|
// Build calendar array
|
||||||
|
const days: Array<{
|
||||||
|
date: Date | null;
|
||||||
|
hasAttendance: boolean;
|
||||||
|
isToday: boolean;
|
||||||
|
}> = [];
|
||||||
|
|
||||||
|
// Add empty cells for days before month starts
|
||||||
|
for (let i = 0; i < startingDayOfWeek; i++) {
|
||||||
|
days.push({ date: null, hasAttendance: false, isToday: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add days of the month
|
||||||
|
for (let day = 1; day <= daysInMonth; day++) {
|
||||||
|
const date = new Date(year, monthIndex, day);
|
||||||
|
const dateString = date.toDateString();
|
||||||
|
days.push({
|
||||||
|
date,
|
||||||
|
hasAttendance: attendanceDates.has(dateString),
|
||||||
|
isToday: dateString === today,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setCalendarDays(days);
|
||||||
|
};
|
||||||
|
|
||||||
|
const goToPreviousMonth = () => {
|
||||||
|
setCurrentMonth(
|
||||||
|
new Date(currentMonth.getFullYear(), currentMonth.getMonth() - 1),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const goToNextMonth = () => {
|
||||||
|
setCurrentMonth(
|
||||||
|
new Date(currentMonth.getFullYear(), currentMonth.getMonth() + 1),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const monthName = currentMonth.toLocaleDateString("en-US", {
|
||||||
|
month: "long",
|
||||||
|
year: "numeric",
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<LinearGradient
|
||||||
|
colors={[theme.colors.white, theme.colors.gray50]}
|
||||||
|
style={[styles.card, theme.shadows.medium]}
|
||||||
|
>
|
||||||
|
{/* Header */}
|
||||||
|
<View style={styles.header}>
|
||||||
|
<Text style={styles.title}>Attendance Calendar</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Month Navigation */}
|
||||||
|
<View style={styles.monthNav}>
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={goToPreviousMonth}
|
||||||
|
style={styles.navButton}
|
||||||
|
>
|
||||||
|
<Ionicons
|
||||||
|
name="chevron-back"
|
||||||
|
size={20}
|
||||||
|
color={theme.colors.primary}
|
||||||
|
/>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<Text style={styles.monthText}>{monthName}</Text>
|
||||||
|
<TouchableOpacity onPress={goToNextMonth} style={styles.navButton}>
|
||||||
|
<Ionicons
|
||||||
|
name="chevron-forward"
|
||||||
|
size={20}
|
||||||
|
color={theme.colors.primary}
|
||||||
|
/>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Day Headers */}
|
||||||
|
<View style={styles.dayHeaders}>
|
||||||
|
{["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"].map((day) => (
|
||||||
|
<Text key={day} style={styles.dayHeader}>
|
||||||
|
{day}
|
||||||
|
</Text>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Calendar Grid */}
|
||||||
|
<View style={styles.calendarGrid}>
|
||||||
|
{calendarDays.map((day, index) => (
|
||||||
|
<View key={index} style={styles.dayCell}>
|
||||||
|
{day.date ? (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.dayContent,
|
||||||
|
day.isToday && styles.todayContent,
|
||||||
|
day.hasAttendance && styles.attendanceContent,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.dayText,
|
||||||
|
day.isToday && styles.todayText,
|
||||||
|
day.hasAttendance && styles.attendanceText,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{day.date.getDate()}
|
||||||
|
</Text>
|
||||||
|
{day.hasAttendance && <View style={styles.attendanceDot} />}
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<View style={styles.emptyCell} />
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Legend */}
|
||||||
|
<View style={styles.legend}>
|
||||||
|
<View style={styles.legendItem}>
|
||||||
|
<View style={styles.legendDotAttendance} />
|
||||||
|
<Text style={styles.legendText}>Attended</Text>
|
||||||
|
</View>
|
||||||
|
<View style={styles.legendItem}>
|
||||||
|
<View style={styles.legendDotToday} />
|
||||||
|
<Text style={styles.legendText}>Today</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</LinearGradient>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
paddingHorizontal: 20,
|
||||||
|
marginBottom: 20,
|
||||||
|
},
|
||||||
|
card: {
|
||||||
|
borderRadius: theme.borderRadius["2xl"],
|
||||||
|
padding: 20,
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
marginBottom: 16,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
fontSize: theme.typography.fontSize.xl,
|
||||||
|
fontWeight: theme.typography.fontWeight.bold,
|
||||||
|
color: theme.colors.gray800,
|
||||||
|
},
|
||||||
|
monthNav: {
|
||||||
|
flexDirection: "row",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
marginBottom: 16,
|
||||||
|
paddingHorizontal: 8,
|
||||||
|
},
|
||||||
|
navButton: {
|
||||||
|
padding: 8,
|
||||||
|
},
|
||||||
|
monthText: {
|
||||||
|
fontSize: theme.typography.fontSize.lg,
|
||||||
|
fontWeight: theme.typography.fontWeight.semibold,
|
||||||
|
color: theme.colors.gray700,
|
||||||
|
},
|
||||||
|
dayHeaders: {
|
||||||
|
flexDirection: "row",
|
||||||
|
marginBottom: 8,
|
||||||
|
},
|
||||||
|
dayHeader: {
|
||||||
|
flex: 1,
|
||||||
|
textAlign: "center",
|
||||||
|
fontSize: theme.typography.fontSize.xs,
|
||||||
|
fontWeight: theme.typography.fontWeight.semibold,
|
||||||
|
color: theme.colors.gray500,
|
||||||
|
},
|
||||||
|
calendarGrid: {
|
||||||
|
flexDirection: "row",
|
||||||
|
flexWrap: "wrap",
|
||||||
|
},
|
||||||
|
dayCell: {
|
||||||
|
width: `${100 / 7}%`,
|
||||||
|
aspectRatio: 1,
|
||||||
|
padding: 2,
|
||||||
|
},
|
||||||
|
dayContent: {
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
borderRadius: theme.borderRadius.md,
|
||||||
|
position: "relative",
|
||||||
|
},
|
||||||
|
todayContent: {
|
||||||
|
backgroundColor: theme.colors.primaryLight,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: theme.colors.primary,
|
||||||
|
},
|
||||||
|
attendanceContent: {
|
||||||
|
backgroundColor: theme.colors.successLight,
|
||||||
|
},
|
||||||
|
emptyCell: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
dayText: {
|
||||||
|
fontSize: theme.typography.fontSize.sm,
|
||||||
|
color: theme.colors.gray700,
|
||||||
|
fontWeight: theme.typography.fontWeight.medium,
|
||||||
|
},
|
||||||
|
todayText: {
|
||||||
|
color: theme.colors.white,
|
||||||
|
fontWeight: theme.typography.fontWeight.bold,
|
||||||
|
},
|
||||||
|
attendanceText: {
|
||||||
|
color: theme.colors.white,
|
||||||
|
fontWeight: theme.typography.fontWeight.bold,
|
||||||
|
},
|
||||||
|
attendanceDot: {
|
||||||
|
position: "absolute",
|
||||||
|
bottom: 2,
|
||||||
|
width: 4,
|
||||||
|
height: 4,
|
||||||
|
borderRadius: 2,
|
||||||
|
backgroundColor: theme.colors.white,
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
flexDirection: "row",
|
||||||
|
justifyContent: "center",
|
||||||
|
gap: 20,
|
||||||
|
marginTop: 16,
|
||||||
|
paddingTop: 16,
|
||||||
|
borderTopWidth: 1,
|
||||||
|
borderTopColor: theme.colors.gray200,
|
||||||
|
},
|
||||||
|
legendItem: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 6,
|
||||||
|
},
|
||||||
|
legendDotAttendance: {
|
||||||
|
width: 12,
|
||||||
|
height: 12,
|
||||||
|
borderRadius: 6,
|
||||||
|
backgroundColor: theme.colors.successLight,
|
||||||
|
},
|
||||||
|
legendDotToday: {
|
||||||
|
width: 12,
|
||||||
|
height: 12,
|
||||||
|
borderRadius: 6,
|
||||||
|
backgroundColor: theme.colors.primaryLight,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: theme.colors.primary,
|
||||||
|
},
|
||||||
|
legendText: {
|
||||||
|
fontSize: theme.typography.fontSize.xs,
|
||||||
|
color: theme.colors.gray600,
|
||||||
|
},
|
||||||
|
});
|
||||||
@ -49,6 +49,8 @@ export function CustomTabBar({
|
|||||||
return focused ? "home" : "home-outline";
|
return focused ? "home" : "home-outline";
|
||||||
case "goals":
|
case "goals":
|
||||||
return focused ? "trophy" : "trophy-outline";
|
return focused ? "trophy" : "trophy-outline";
|
||||||
|
case "attendance":
|
||||||
|
return focused ? "calendar" : "calendar-outline";
|
||||||
case "recommendations":
|
case "recommendations":
|
||||||
return focused ? "sparkles" : "sparkles-outline";
|
return focused ? "sparkles" : "sparkles-outline";
|
||||||
case "profile":
|
case "profile":
|
||||||
@ -64,6 +66,8 @@ export function CustomTabBar({
|
|||||||
return "Home";
|
return "Home";
|
||||||
case "goals":
|
case "goals":
|
||||||
return "Goals";
|
return "Goals";
|
||||||
|
case "attendance":
|
||||||
|
return "Attendance";
|
||||||
case "recommendations":
|
case "recommendations":
|
||||||
return "Plans";
|
return "Plans";
|
||||||
case "profile":
|
case "profile":
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user