bw-expo-app/app/points-details.tsx

288 lines
6.8 KiB
TypeScript

import Ionicons from '@expo/vector-icons/Ionicons';
import { useRouter } from 'expo-router';
import React, { useMemo, useState } from 'react';
import {
FlatList,
StatusBar,
StyleSheet,
Text,
TouchableOpacity,
View,
} from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
type LedgerKind = 'obtained' | 'consumed';
type LedgerEntry = {
id: string;
title: string;
happenedAt: string;
amount: number;
kind: LedgerKind;
};
type FilterKey = 'all' | LedgerKind;
const screenPalette = {
background: '#080808',
surface: '#111111',
surfaceActive: '#1C1C1C',
divider: '#1A1A1A',
primaryText: '#F5F5F5',
secondaryText: '#6C6C6C',
accent: '#FEB840',
accentHalo: 'rgba(254, 184, 64, 0.18)',
negative: '#B3B3B3',
};
const FILTERS: { key: FilterKey; label: string }[] = [
{ key: 'all', label: 'All records' },
{ key: 'consumed', label: 'Consumed' },
{ key: 'obtained', label: 'Obtained' },
];
const LEDGER: LedgerEntry[] = [
{
id: 'obtained-1',
title: 'Income Name',
happenedAt: '2025-10-22 11:11',
amount: 60,
kind: 'obtained',
},
{
id: 'consumed-1',
title: 'Expenditure Name',
happenedAt: '2025-10-22 11:11',
amount: -60,
kind: 'consumed',
},
{
id: 'consumed-2',
title: 'Expenditure Name',
happenedAt: '2025-10-22 11:11',
amount: -60,
kind: 'consumed',
},
{
id: 'obtained-2',
title: 'Income Name',
happenedAt: '2025-10-22 11:11',
amount: 60,
kind: 'obtained',
},
{
id: 'obtained-3',
title: 'Income Name',
happenedAt: '2025-10-22 11:11',
amount: 60,
kind: 'obtained',
},
{
id: 'obtained-4',
title: 'Income Name',
happenedAt: '2025-10-22 11:11',
amount: 60,
kind: 'obtained',
},
{
id: 'consumed-3',
title: 'Expenditure Name',
happenedAt: '2025-10-22 11:11',
amount: -60,
kind: 'consumed',
},
];
export default function PointsDetailsScreen() {
const insets = useSafeAreaInsets();
const router = useRouter();
const [activeFilter, setActiveFilter] = useState<FilterKey>('all');
const filteredEntries = useMemo(() => {
if (activeFilter === 'all') {
return LEDGER;
}
return LEDGER.filter(entry => entry.kind === activeFilter);
}, [activeFilter]);
return (
<View style={[styles.screen, { paddingTop: insets.top + 12 }]}>
<StatusBar barStyle="light-content" />
<View style={styles.headerRow}>
<TouchableOpacity
accessibilityRole="button"
onPress={() => router.back()}
style={styles.iconButton}
hitSlop={{ top: 12, bottom: 12, left: 12, right: 12 }}
>
<Ionicons name="chevron-back" size={24} color={screenPalette.primaryText} />
</TouchableOpacity>
<Text style={styles.headerTitle}>Points Details</Text>
<View style={styles.iconButton} />
</View>
<View style={styles.balanceCluster}>
<View style={styles.energyOrb}>
<Ionicons name="flash" size={20} color={screenPalette.accent} />
</View>
<Text style={styles.balanceValue}>60</Text>
</View>
<View style={styles.segmentRail}>
{FILTERS.map(filter => {
const isActive = filter.key === activeFilter;
return (
<TouchableOpacity
key={filter.key}
style={[styles.segmentChip, isActive && styles.segmentChipActive]}
onPress={() => setActiveFilter(filter.key)}
accessibilityRole="button"
>
<Text style={[styles.segmentLabel, isActive && styles.segmentLabelActive]}>
{filter.label}
</Text>
</TouchableOpacity>
);
})}
</View>
<FlatList
data={filteredEntries}
keyExtractor={item => item.id}
style={styles.list}
contentContainerStyle={{ paddingBottom: Math.max(insets.bottom + 24, 64) }}
showsVerticalScrollIndicator={false}
ItemSeparatorComponent={() => <View style={styles.listDivider} />}
renderItem={({ item }) => {
const amount = Math.abs(item.amount);
const isPositive = item.amount > 0;
return (
<View style={styles.recordRow}>
<View>
<Text style={styles.recordTitle}>{item.title}</Text>
<Text style={styles.recordTimestamp}>{item.happenedAt}</Text>
</View>
<Text
style={[
styles.recordValue,
isPositive ? styles.valuePositive : styles.valueNegative,
]}
>
{isPositive ? `+ ${amount}` : `- ${amount}`}
</Text>
</View>
);
}}
/>
</View>
);
}
const styles = StyleSheet.create({
screen: {
flex: 1,
backgroundColor: screenPalette.background,
paddingHorizontal: 20,
},
headerRow: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
},
iconButton: {
width: 44,
height: 44,
borderRadius: 22,
alignItems: 'center',
justifyContent: 'center',
},
headerTitle: {
fontSize: 18,
fontWeight: '600',
color: screenPalette.primaryText,
letterSpacing: 0.3,
},
balanceCluster: {
alignItems: 'center',
marginTop: 28,
marginBottom: 32,
gap: 12,
},
energyOrb: {
width: 56,
height: 56,
borderRadius: 28,
backgroundColor: screenPalette.accentHalo,
alignItems: 'center',
justifyContent: 'center',
},
balanceValue: {
fontSize: 44,
fontWeight: '700',
color: screenPalette.primaryText,
letterSpacing: 1,
},
segmentRail: {
flexDirection: 'row',
backgroundColor: screenPalette.surface,
padding: 6,
borderRadius: 28,
},
segmentChip: {
flex: 1,
borderRadius: 22,
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 10,
},
segmentChipActive: {
backgroundColor: screenPalette.surfaceActive,
},
segmentLabel: {
fontSize: 14,
color: screenPalette.secondaryText,
fontWeight: '500',
},
segmentLabelActive: {
color: screenPalette.primaryText,
},
list: {
flex: 1,
marginTop: 32,
},
listDivider: {
height: 1,
backgroundColor: screenPalette.divider,
marginVertical: 6,
},
recordRow: {
flexDirection: 'row',
alignItems: 'flex-start',
justifyContent: 'space-between',
paddingVertical: 10,
},
recordTitle: {
fontSize: 16,
fontWeight: '600',
color: screenPalette.primaryText,
},
recordTimestamp: {
marginTop: 4,
fontSize: 13,
color: screenPalette.secondaryText,
},
recordValue: {
fontSize: 16,
fontWeight: '600',
},
valuePositive: {
color: screenPalette.accent,
},
valueNegative: {
color: screenPalette.negative,
},
});