bw-expo-app/app/points.tsx

312 lines
7.1 KiB
TypeScript

import Ionicons from '@expo/vector-icons/Ionicons';
import { useRouter } from 'expo-router';
import React, { useCallback, useMemo, useState } from 'react';
import type { ListRenderItem } from 'react-native';
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: '#101010',
surfaceActive: '#1C1C1C',
surfaceOutline: '#1D1D1D',
divider: '#161616',
primaryText: '#F5F5F5',
secondaryText: '#6C6C6C',
accent: '#FEB840',
accentHalo: 'rgba(254, 184, 64, 0.22)',
negative: '#8F8F8F',
};
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]);
const listContentStyle = useMemo(
() => ({
paddingBottom: Math.max(insets.bottom + 36, 72),
paddingTop: 4,
}),
[insets.bottom],
);
const keyExtractor = useCallback((entry: LedgerEntry) => entry.id, []);
const renderSeparator = useCallback(() => <View style={styles.listDivider} />, []);
const renderLedgerEntry: ListRenderItem<LedgerEntry> = useCallback(({ 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>
);
}, []);
return (
<View style={[styles.screen, { paddingTop: insets.top + 12 }]}>
<StatusBar barStyle="light-content" />
<View style={styles.balanceCluster}>
<View style={styles.energyOrb}>
<Ionicons name="flash" size={22} 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={keyExtractor}
style={styles.list}
contentContainerStyle={listContentStyle}
showsVerticalScrollIndicator={false}
ItemSeparatorComponent={renderSeparator}
renderItem={renderLedgerEntry}
/>
</View>
);
}
const styles = StyleSheet.create({
screen: {
flex: 1,
backgroundColor: screenPalette.background,
paddingHorizontal: 24,
},
headerRow: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
minHeight: 48,
},
iconButton: {
width: 40,
height: 40,
borderRadius: 20,
alignItems: 'center',
justifyContent: 'center',
},
headerTitle: {
fontSize: 17,
lineHeight: 24,
fontWeight: '600',
color: screenPalette.primaryText,
letterSpacing: 0.2,
},
balanceCluster: {
alignItems: 'center',
marginTop: 32,
marginBottom: 28,
gap: 10,
},
energyOrb: {
width: 60,
height: 60,
borderRadius: 30,
backgroundColor: screenPalette.accentHalo,
alignItems: 'center',
justifyContent: 'center',
shadowColor: screenPalette.accent,
shadowOpacity: 0.3,
shadowRadius: 16,
shadowOffset: { width: 0, height: 8 },
elevation: 8,
},
balanceValue: {
fontSize: 46,
lineHeight: 52,
fontWeight: '700',
color: screenPalette.primaryText,
letterSpacing: 0.5,
fontVariant: ['tabular-nums'],
},
segmentRail: {
flexDirection: 'row',
backgroundColor: screenPalette.surface,
padding: 4,
borderRadius: 28,
borderWidth: StyleSheet.hairlineWidth,
borderColor: screenPalette.surfaceOutline,
gap: 4,
},
segmentChip: {
flex: 1,
borderRadius: 24,
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 11,
},
segmentChipActive: {
backgroundColor: screenPalette.surfaceActive,
},
segmentLabel: {
fontSize: 14,
lineHeight: 20,
color: screenPalette.secondaryText,
fontWeight: '500',
letterSpacing: 0.2,
},
segmentLabelActive: {
color: screenPalette.primaryText,
},
list: {
flex: 1,
marginTop: 28,
},
listDivider: {
height: StyleSheet.hairlineWidth,
backgroundColor: screenPalette.divider,
marginVertical: 0,
},
recordRow: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingVertical: 14,
},
recordTitle: {
fontSize: 15,
lineHeight: 22,
fontWeight: '600',
color: screenPalette.primaryText,
},
recordTimestamp: {
marginTop: 6,
fontSize: 12,
lineHeight: 18,
color: screenPalette.secondaryText,
letterSpacing: 0.2,
},
recordValue: {
fontSize: 16,
lineHeight: 22,
fontWeight: '600',
fontVariant: ['tabular-nums'],
},
valuePositive: {
color: screenPalette.accent,
},
valueNegative: {
color: screenPalette.negative,
},
});