redesign take 1
This commit is contained in:
1 parent
1143f8ca02
commit
e3a3c3fccf
19 files changed
+2783
-1493
No files matched your search
@@ -7,46 +7,28 @@ import {
|
||||
ScrollView,
|
||||
Alert,
|
||||
} from "react-native";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useAuth } from "@clerk/clerk-expo";
|
||||
import { LinearGradient } from "expo-linear-gradient";
|
||||
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 { theme } from "../../styles/theme";
|
||||
import { Animated } from "react-native";
|
||||
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 pulseAnim = useRef(new Animated.Value(1)).current;
|
||||
|
||||
useEffect(() => {
|
||||
if (activeCheckIn) {
|
||||
const pulse = Animated.loop(
|
||||
Animated.sequence([
|
||||
Animated.timing(pulseAnim, {
|
||||
toValue: 1.05,
|
||||
duration: 1000,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(pulseAnim, {
|
||||
toValue: 1,
|
||||
duration: 1000,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
]),
|
||||
);
|
||||
pulse.start();
|
||||
return () => pulse.stop();
|
||||
}
|
||||
}, [activeCheckIn]);
|
||||
|
||||
const fetchAttendance = async () => {
|
||||
try {
|
||||
@@ -82,10 +64,7 @@ export default function AttendanceScreen() {
|
||||
if (!token) return;
|
||||
|
||||
await attendanceApi.checkIn("gym", token);
|
||||
|
||||
// Clear statistics cache to force refresh on home screen
|
||||
clearStatisticsCache();
|
||||
|
||||
fetchAttendance();
|
||||
Alert.alert("Success", "Checked in successfully!");
|
||||
} catch (error: unknown) {
|
||||
@@ -100,10 +79,7 @@ export default function AttendanceScreen() {
|
||||
if (!token) return;
|
||||
|
||||
await attendanceApi.checkOut(token);
|
||||
|
||||
// Clear statistics cache to force refresh on home screen
|
||||
clearStatisticsCache();
|
||||
|
||||
fetchAttendance();
|
||||
Alert.alert("Success", "Checked out successfully!");
|
||||
} catch (error: unknown) {
|
||||
@@ -114,143 +90,204 @@ export default function AttendanceScreen() {
|
||||
|
||||
if (loading && !history.length) {
|
||||
return (
|
||||
<View style={styles.centered}>
|
||||
<ActivityIndicator size="large" color={theme.colors.primary} />
|
||||
<View style={[styles.centered, { backgroundColor: colors.background }]}>
|
||||
<ActivityIndicator size="large" color={colors.primary} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
|
||||
<LinearGradient
|
||||
colors={theme.gradients.primary}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 1 }}
|
||||
style={styles.header}
|
||||
>
|
||||
<Text style={styles.title}>Attendance</Text>
|
||||
<Text style={styles.subtitle}>Track your gym visits</Text>
|
||||
</LinearGradient>
|
||||
<ScrollView
|
||||
style={[styles.container, { backgroundColor: colors.background }]}
|
||||
contentContainerStyle={styles.content}
|
||||
>
|
||||
{/* Header */}
|
||||
<View style={styles.header}>
|
||||
<Text style={[typography.h1, { color: colors.textPrimary }]}>
|
||||
Attendance
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
typography.body,
|
||||
{ color: colors.textSecondary, marginTop: 4 },
|
||||
]}
|
||||
>
|
||||
Track your gym visits
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.actionContainer}>
|
||||
{/* Check In/Out Section */}
|
||||
<View style={styles.section}>
|
||||
{activeCheckIn ? (
|
||||
<LinearGradient
|
||||
colors={["rgba(16, 185, 129, 0.15)", "rgba(5, 150, 105, 0.1)"]}
|
||||
style={[styles.activeCard, theme.shadows.medium]}
|
||||
>
|
||||
<View style={styles.activeCardContent}>
|
||||
<View style={styles.activeIconContainer}>
|
||||
<LinearGradient
|
||||
colors={theme.gradients.success}
|
||||
style={styles.activeIcon}
|
||||
<MinimalCard variant="elevated" 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={32} color="#fff" />
|
||||
</LinearGradient>
|
||||
</View>
|
||||
<View style={styles.activeTextContainer}>
|
||||
<Text style={styles.activeText}>Currently Checked In</Text>
|
||||
<Text style={styles.timeText}>
|
||||
Since{" "}
|
||||
{new Date(activeCheckIn.checkInTime).toLocaleTimeString([], {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})}
|
||||
</Text>
|
||||
<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>
|
||||
<TouchableOpacity onPress={handleCheckOut} activeOpacity={0.8}>
|
||||
<LinearGradient
|
||||
colors={theme.gradients.danger}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 0 }}
|
||||
style={[styles.checkOutButton, theme.shadows.medium]}
|
||||
>
|
||||
<Ionicons
|
||||
name="log-out-outline"
|
||||
size={20}
|
||||
color="#fff"
|
||||
style={{ marginRight: 8 }}
|
||||
/>
|
||||
<Text style={styles.buttonText}>Check Out</Text>
|
||||
</LinearGradient>
|
||||
</TouchableOpacity>
|
||||
</LinearGradient>
|
||||
<MinimalButton
|
||||
title="Check Out"
|
||||
onPress={handleCheckOut}
|
||||
variant="danger"
|
||||
size="lg"
|
||||
style={{ marginTop: 16 }}
|
||||
/>
|
||||
</MinimalCard>
|
||||
) : (
|
||||
<Animated.View style={{ transform: [{ scale: pulseAnim }] }}>
|
||||
<TouchableOpacity onPress={handleCheckIn} activeOpacity={0.8}>
|
||||
<LinearGradient
|
||||
colors={theme.gradients.primary}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 0 }}
|
||||
style={[styles.checkInButton, theme.shadows.glow]}
|
||||
>
|
||||
<Ionicons
|
||||
name="log-in-outline"
|
||||
size={24}
|
||||
color="#fff"
|
||||
style={{ marginRight: 8 }}
|
||||
/>
|
||||
<Text style={styles.checkInButtonText}>Check In</Text>
|
||||
</LinearGradient>
|
||||
</TouchableOpacity>
|
||||
</Animated.View>
|
||||
<MinimalButton
|
||||
title="Check In"
|
||||
onPress={handleCheckIn}
|
||||
variant="primary"
|
||||
size="lg"
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Attendance Calendar */}
|
||||
{history.length > 0 && <AttendanceCalendar attendanceRecords={history} />}
|
||||
<View style={styles.section}>
|
||||
<SectionHeader title="Calendar" />
|
||||
<MinimalCard variant="default">
|
||||
<AttendanceCalendar attendanceRecords={history} />
|
||||
</MinimalCard>
|
||||
</View>
|
||||
|
||||
<Text style={styles.sectionTitle}>Recent History</Text>
|
||||
{history.map((item) => (
|
||||
<LinearGradient
|
||||
key={item.id}
|
||||
colors={["rgba(255, 255, 255, 1)", "rgba(249, 250, 251, 1)"] as const}
|
||||
style={[styles.historyItem, theme.shadows.medium]}
|
||||
>
|
||||
<View style={styles.historyLeft}>
|
||||
<View style={styles.historyIconContainer}>
|
||||
<LinearGradient
|
||||
colors={
|
||||
item.checkOutTime
|
||||
? theme.gradients.success
|
||||
: theme.gradients.primary
|
||||
}
|
||||
style={styles.historyIcon}
|
||||
{/* Recent History */}
|
||||
<View style={styles.section}>
|
||||
<SectionHeader title="Recent History" />
|
||||
{history.length === 0 ? (
|
||||
<MinimalCard variant="default">
|
||||
<View style={styles.emptyState}>
|
||||
<IconContainer
|
||||
variant="colored"
|
||||
backgroundColor={`${colors.primary}15`}
|
||||
size="lg"
|
||||
>
|
||||
<Ionicons
|
||||
name={item.checkOutTime ? "checkmark" : "time-outline"}
|
||||
size={16}
|
||||
color="#fff"
|
||||
name="calendar-outline"
|
||||
size={32}
|
||||
color={colors.primary}
|
||||
/>
|
||||
</LinearGradient>
|
||||
</View>
|
||||
<View>
|
||||
<Text style={styles.dateText}>
|
||||
{new Date(item.checkInTime).toLocaleDateString()}
|
||||
</IconContainer>
|
||||
<Text
|
||||
style={[
|
||||
typography.bodyEmphasis,
|
||||
{ color: colors.textPrimary, marginTop: 16 },
|
||||
]}
|
||||
>
|
||||
No attendance history yet
|
||||
</Text>
|
||||
<Text style={styles.typeText}>{item.type.toUpperCase()}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.timeContainer}>
|
||||
<Text style={styles.historyTime}>
|
||||
In:{" "}
|
||||
{new Date(item.checkInTime).toLocaleTimeString([], {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})}
|
||||
</Text>
|
||||
{item.checkOutTime && (
|
||||
<Text style={styles.historyTime}>
|
||||
Out:{" "}
|
||||
{new Date(item.checkOutTime).toLocaleTimeString([], {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})}
|
||||
<Text
|
||||
style={[
|
||||
typography.body,
|
||||
{
|
||||
color: colors.textSecondary,
|
||||
marginTop: 8,
|
||||
textAlign: "center",
|
||||
},
|
||||
]}
|
||||
>
|
||||
Your check-in history will appear here
|
||||
</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="default">
|
||||
<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>
|
||||
</LinearGradient>
|
||||
))}
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Bottom Spacer */}
|
||||
<View style={{ height: 100 }} />
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
@@ -258,147 +295,53 @@ export default function AttendanceScreen() {
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: theme.colors.background,
|
||||
},
|
||||
content: {
|
||||
paddingBottom: 20,
|
||||
},
|
||||
centered: {
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
},
|
||||
content: {
|
||||
paddingBottom: 20,
|
||||
},
|
||||
header: {
|
||||
paddingHorizontal: 24,
|
||||
paddingTop: 60,
|
||||
paddingBottom: 24,
|
||||
},
|
||||
section: {
|
||||
paddingHorizontal: 24,
|
||||
marginBottom: 24,
|
||||
borderBottomLeftRadius: theme.borderRadius.xl,
|
||||
borderBottomRightRadius: theme.borderRadius.xl,
|
||||
},
|
||||
title: {
|
||||
fontSize: theme.typography.fontSize["3xl"],
|
||||
fontWeight: theme.typography.fontWeight.bold,
|
||||
color: theme.colors.white,
|
||||
marginBottom: 4,
|
||||
},
|
||||
subtitle: {
|
||||
fontSize: theme.typography.fontSize.base,
|
||||
color: "rgba(255, 255, 255, 0.9)",
|
||||
},
|
||||
actionContainer: {
|
||||
marginBottom: 32,
|
||||
paddingHorizontal: 20,
|
||||
},
|
||||
checkInButton: {
|
||||
paddingVertical: 20,
|
||||
paddingHorizontal: 24,
|
||||
borderRadius: theme.borderRadius.xl,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
checkInButtonText: {
|
||||
color: theme.colors.white,
|
||||
fontSize: theme.typography.fontSize.xl,
|
||||
fontWeight: theme.typography.fontWeight.semibold,
|
||||
},
|
||||
checkOutButton: {
|
||||
paddingVertical: 14,
|
||||
paddingHorizontal: 20,
|
||||
borderRadius: theme.borderRadius.lg,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
marginTop: 16,
|
||||
},
|
||||
buttonText: {
|
||||
color: theme.colors.white,
|
||||
fontSize: theme.typography.fontSize.base,
|
||||
fontWeight: theme.typography.fontWeight.semibold,
|
||||
},
|
||||
activeCard: {
|
||||
padding: 20,
|
||||
borderRadius: theme.borderRadius.xl,
|
||||
borderWidth: 1,
|
||||
borderColor: "rgba(16, 185, 129, 0.2)",
|
||||
},
|
||||
activeCardContent: {
|
||||
activeHeader: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
marginBottom: 16,
|
||||
},
|
||||
activeIconContainer: {
|
||||
marginRight: 16,
|
||||
},
|
||||
activeIcon: {
|
||||
width: 56,
|
||||
height: 56,
|
||||
borderRadius: 28,
|
||||
justifyContent: "center",
|
||||
activeHeaderLeft: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
},
|
||||
activeTextContainer: {
|
||||
flex: 1,
|
||||
},
|
||||
activeText: {
|
||||
fontSize: theme.typography.fontSize.lg,
|
||||
fontWeight: theme.typography.fontWeight.semibold,
|
||||
color: theme.colors.gray900,
|
||||
marginBottom: 4,
|
||||
},
|
||||
timeText: {
|
||||
fontSize: theme.typography.fontSize.sm,
|
||||
color: theme.colors.gray600,
|
||||
emptyState: {
|
||||
alignItems: "center",
|
||||
paddingVertical: 40,
|
||||
paddingHorizontal: 20,
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: theme.typography.fontSize.xl,
|
||||
fontWeight: theme.typography.fontWeight.semibold,
|
||||
marginBottom: 16,
|
||||
paddingHorizontal: 20,
|
||||
color: theme.colors.gray900,
|
||||
historyList: {
|
||||
gap: 12,
|
||||
},
|
||||
historyItem: {
|
||||
padding: 16,
|
||||
borderRadius: theme.borderRadius.xl,
|
||||
marginBottom: 12,
|
||||
marginHorizontal: 20,
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
borderWidth: 1,
|
||||
borderColor: "rgba(59, 130, 246, 0.1)",
|
||||
},
|
||||
historyLeft: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 12,
|
||||
},
|
||||
historyIconContainer: {
|
||||
marginRight: 4,
|
||||
},
|
||||
historyIcon: {
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 16,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
},
|
||||
dateText: {
|
||||
fontSize: theme.typography.fontSize.base,
|
||||
fontWeight: theme.typography.fontWeight.semibold,
|
||||
color: theme.colors.gray900,
|
||||
},
|
||||
typeText: {
|
||||
fontSize: theme.typography.fontSize.xs,
|
||||
color: theme.colors.gray600,
|
||||
marginTop: 2,
|
||||
},
|
||||
timeContainer: {
|
||||
alignItems: "flex-end",
|
||||
},
|
||||
historyTime: {
|
||||
fontSize: theme.typography.fontSize.sm,
|
||||
color: theme.colors.gray700,
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
Reference in new issue
Block a user