redesign take 1

This commit is contained in:
dimitar committed 2026-03-11 08:22:48 +01:00
1 parent 1143f8ca02
commit e3a3c3fccf
19 files changed
+2783 -1493

No files matched your search

+201 -258
View File
@@ -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,
},
});