Files
DailyMeals/meal-plan-frontend-mobile/app/(main)/(tabs)/shopping.tsx
Piotr Kus f15bb7a916 * Extended functionalities
* Added different UIs
2026-06-24 21:43:40 +02:00

127 lines
5.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useMemo, useState } from 'react';
import { Alert, Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
import { useQuery } from '@tanstack/react-query';
import * as Print from 'expo-print';
import * as Sharing from 'expo-sharing';
import { useTranslation } from 'react-i18next';
import { fetchShoppingList } from '@/src/api/meal-plan.api';
import { Button } from '@/src/components/Button';
import { ErrorView } from '@/src/components/ErrorView';
import { Screen } from '@/src/components/Screen';
import { useAppearance } from '@/src/context/AppearanceContext';
import { addDays, formatDisplayDate, weekRange } from '@/src/utils/date';
import { buildShoppingListHtml, formatShoppingQuantity } from '@/src/utils/pdfExport';
export default function ShoppingScreen() {
const { t, i18n } = useTranslation();
const { colors } = useAppearance();
const [weekAnchor, setWeekAnchor] = useState(() => weekRange().from);
const { from, to } = useMemo(() => weekRange(weekAnchor), [weekAnchor]);
const [checked, setChecked] = useState<Record<string, boolean>>({});
const [exporting, setExporting] = useState(false);
const listQuery = useQuery({
queryKey: ['shopping-list', from, to],
queryFn: () => fetchShoppingList(from, to),
});
async function handleExportPdf() {
const items = listQuery.data ?? [];
if (items.length === 0) {
Alert.alert(t('errors.pdfNothingToExport'));
return;
}
setExporting(true);
try {
const html = buildShoppingListHtml(
items,
t('pdf.shoppingList'),
t('pdf.generatedOn', { date: new Date().toLocaleDateString(), count: items.length }),
t('pdf.toTaste'),
);
const { uri } = await Print.printToFileAsync({ html });
if (await Sharing.isAvailableAsync()) {
await Sharing.shareAsync(uri, { UTI: '.pdf', mimeType: 'application/pdf' });
}
} catch {
Alert.alert(t('errors.pdfGenerateFailed'));
} finally {
setExporting(false);
}
}
return (
<Screen title={t('shopping.title')} subtitle={t('shopping.subtitle')}>
<View style={styles.header}>
<Pressable
onPress={() => setWeekAnchor(addDays(from, -7))}
style={[styles.navBtn, { borderColor: colors.border, backgroundColor: colors.surface }]}
>
<Text style={{ color: colors.text }}></Text>
</Pressable>
<Text style={{ color: colors.text, fontWeight: '700', flex: 1, textAlign: 'center' }}>
{formatDisplayDate(from, i18n.language)} {formatDisplayDate(to, i18n.language)}
</Text>
<Pressable
onPress={() => setWeekAnchor(addDays(from, 7))}
style={[styles.navBtn, { borderColor: colors.border, backgroundColor: colors.surface }]}
>
<Text style={{ color: colors.text }}></Text>
</Pressable>
</View>
<Button
title={exporting ? t('pdf.generating') : t('pdf.generate')}
variant="secondary"
loading={exporting}
onPress={() => void handleExportPdf()}
/>
{listQuery.isLoading ? (
<Text style={{ color: colors.textMuted }}>{t('common.loading')}</Text>
) : listQuery.isError ? (
<ErrorView message={t('errors.loadShoppingFailed')} onRetry={() => listQuery.refetch()} />
) : (listQuery.data?.length ?? 0) === 0 ? (
<Text style={{ color: colors.textMuted }}>{t('shopping.empty')}</Text>
) : (
<ScrollView contentContainerStyle={styles.list}>
{listQuery.data!.map((item) => {
const key = item.name;
const isChecked = checked[key] ?? false;
return (
<Pressable
key={key}
onPress={() => setChecked((prev) => ({ ...prev, [key]: !isChecked }))}
style={[styles.row, { borderColor: colors.border, backgroundColor: colors.surface }]}
>
<View style={[styles.checkbox, { borderColor: colors.brand, backgroundColor: isChecked ? colors.brand : 'transparent' }]}>
{isChecked ? <Text style={{ color: '#fff', fontSize: 12 }}></Text> : null}
</View>
<View style={{ flex: 1 }}>
<Text style={{ color: isChecked ? colors.textMuted : colors.text, textDecorationLine: isChecked ? 'line-through' : 'none', fontWeight: '600' }}>
{item.name}
</Text>
{item.breakdown.length > 0 ? (
<Text style={{ color: colors.textMuted, fontSize: 11 }}>{item.breakdown.join(' · ')}</Text>
) : null}
</View>
<Text style={{ color: colors.textMuted, fontSize: 13 }}>
{formatShoppingQuantity(item, t('pdf.toTaste'))}
</Text>
</Pressable>
);
})}
</ScrollView>
)}
</Screen>
);
}
const styles = StyleSheet.create({
header: { flexDirection: 'row', alignItems: 'center', gap: 8, marginBottom: 8 },
navBtn: { borderWidth: 1, borderRadius: 8, width: 36, height: 36, alignItems: 'center', justifyContent: 'center' },
list: { gap: 8, paddingBottom: 24 },
row: { flexDirection: 'row', alignItems: 'center', gap: 10, borderWidth: 1, borderRadius: 12, padding: 12 },
checkbox: { width: 22, height: 22, borderRadius: 6, borderWidth: 2, alignItems: 'center', justifyContent: 'center' },
});