feat: add domain models and repositories for user, spool presets, and filaments
- Introduced SpoolPreset and User domain models with necessary DTOs and utility functions. - Created AuthRepository, FilamentRepository, and SpoolPresetRepository interfaces for authentication and data management. - Implemented UI components for filament display, including ColorSwatch, FilamentCard, and StockBar. - Developed layout components such as Header and Screen for consistent app structure. - Added reusable UI components like Badge, Button, Card, and Input for better user interaction. - Established global constants and theme settings for consistent styling across the application. - Implemented utility functions for filament calculations and formatting. - Created Zustand stores for managing authentication, filament, and preset states. - Configured TypeScript settings for improved development experience.
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
View, Text, FlatList, TouchableOpacity, StyleSheet, TextInput,
|
||||
} from 'react-native';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { useFilamentStore } from '@store/filamentStore';
|
||||
import { FilamentCard } from '@presentation/components/filament/FilamentCard';
|
||||
import type { Filament } from '@domain/Filament';
|
||||
import { colors, typography, spacing, radius } from '@shared/theme';
|
||||
import { MATERIALS } from '@shared/constants';
|
||||
|
||||
const TABS = ['Todos', ...MATERIALS.slice(0, 4)] as const;
|
||||
|
||||
/**
|
||||
* Tela de Inventário — CW-0
|
||||
* Busca, chips de material, lista de filamentos com peso e % de estoque.
|
||||
*/
|
||||
export default function InventoryScreen(): React.ReactElement {
|
||||
const router = useRouter();
|
||||
const { filaments } = useFilamentStore();
|
||||
const [search, setSearch] = useState('');
|
||||
const [activeTab, setActiveTab] = useState<string>('Todos');
|
||||
|
||||
const filtered = filaments.filter((f) => {
|
||||
const matchMaterial = activeTab === 'Todos' || f.material === activeTab;
|
||||
const matchSearch =
|
||||
search.length === 0 ||
|
||||
f.brand.toLowerCase().includes(search.toLowerCase()) ||
|
||||
(f.model ?? '').toLowerCase().includes(search.toLowerCase()) ||
|
||||
f.material.toLowerCase().includes(search.toLowerCase());
|
||||
return matchMaterial && matchSearch;
|
||||
});
|
||||
|
||||
function renderItem({ item }: { item: Filament }): React.ReactElement {
|
||||
return <FilamentCard filament={item} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.safe}>
|
||||
{/* Cabeçalho */}
|
||||
<View style={styles.header}>
|
||||
<View>
|
||||
<Text style={styles.headerSub}>Seu estoque</Text>
|
||||
<Text style={styles.headerTitle}>Inventário</Text>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
style={styles.filterBtn}
|
||||
onPress={() => router.push('/(app)/inventory/filters')}
|
||||
>
|
||||
<Ionicons name="options-outline" size={20} color={colors.textPrimary} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* Busca */}
|
||||
<View style={styles.searchRow}>
|
||||
<Ionicons name="search-outline" size={18} color={colors.textSecondary} style={styles.searchIcon} />
|
||||
<TextInput
|
||||
style={styles.searchInput}
|
||||
placeholder="Buscar por marca, material..."
|
||||
placeholderTextColor={colors.textSecondary}
|
||||
value={search}
|
||||
onChangeText={setSearch}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Chips de material */}
|
||||
<View style={styles.chipsRow}>
|
||||
{TABS.map((tab) => (
|
||||
<TouchableOpacity
|
||||
key={tab}
|
||||
onPress={() => setActiveTab(tab)}
|
||||
style={[styles.chip, activeTab === tab && styles.chipActive]}
|
||||
>
|
||||
<Text style={[styles.chipText, activeTab === tab && styles.chipTextActive]}>
|
||||
{tab}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{/* Contagem */}
|
||||
<View style={styles.countRow}>
|
||||
<Text style={styles.countText}>{filtered.length} FILAMENTOS</Text>
|
||||
<TouchableOpacity style={styles.sortBtn}>
|
||||
<Ionicons name="swap-vertical-outline" size={14} color={colors.textSecondary} />
|
||||
<Text style={styles.sortText}>Ordenar</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<FlatList
|
||||
data={filtered}
|
||||
keyExtractor={(item) => item.id}
|
||||
renderItem={renderItem}
|
||||
contentContainerStyle={styles.list}
|
||||
ItemSeparatorComponent={() => <View style={styles.separator} />}
|
||||
showsVerticalScrollIndicator={false}
|
||||
ListEmptyComponent={
|
||||
<View style={styles.empty}>
|
||||
<Ionicons name="layers-outline" size={40} color={colors.textSecondary} />
|
||||
<Text style={styles.emptyText}>Nenhum filamento encontrado.</Text>
|
||||
</View>
|
||||
}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
safe: { flex: 1, backgroundColor: colors.bgBase },
|
||||
header: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-start', paddingHorizontal: spacing[5], paddingTop: spacing[4] },
|
||||
headerSub: { fontFamily: typography.fontFamily.ui, fontSize: typography.fontSize.sm, color: colors.textSecondary },
|
||||
headerTitle: { fontFamily: typography.fontFamily.ui, fontSize: typography.fontSize['2xl'], fontWeight: typography.fontWeight.bold, color: colors.textPrimary },
|
||||
filterBtn: { width: 40, height: 40, borderRadius: radius.md, backgroundColor: colors.bgSurface, alignItems: 'center', justifyContent: 'center', borderWidth: 1, borderColor: colors.border },
|
||||
searchRow: { flexDirection: 'row', alignItems: 'center', backgroundColor: colors.bgSurface, borderRadius: radius.md, marginHorizontal: spacing[5], marginTop: spacing[4], paddingHorizontal: spacing[4], borderWidth: 1, borderColor: colors.border, height: 48 },
|
||||
searchIcon: { marginRight: spacing[2] },
|
||||
searchInput: { flex: 1, fontFamily: typography.fontFamily.ui, fontSize: typography.fontSize.base, color: colors.textPrimary },
|
||||
chipsRow: { flexDirection: 'row', gap: spacing[2], paddingHorizontal: spacing[5], marginTop: spacing[3], flexWrap: 'wrap' },
|
||||
chip: { paddingHorizontal: spacing[4], paddingVertical: spacing[2], borderRadius: radius.full, backgroundColor: colors.bgSurface, borderWidth: 1, borderColor: colors.border },
|
||||
chipActive: { backgroundColor: colors.accent, borderColor: colors.accent },
|
||||
chipText: { fontFamily: typography.fontFamily.ui, fontSize: typography.fontSize.sm, fontWeight: typography.fontWeight.medium, color: colors.textSecondary },
|
||||
chipTextActive: { color: colors.bgBase },
|
||||
countRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingHorizontal: spacing[5], marginTop: spacing[4], marginBottom: spacing[2] },
|
||||
countText: { fontFamily: typography.fontFamily.ui, fontSize: typography.fontSize.xs, fontWeight: typography.fontWeight.bold, color: colors.textSecondary, letterSpacing: 0.8 },
|
||||
sortBtn: { flexDirection: 'row', alignItems: 'center', gap: 4 },
|
||||
sortText: { fontFamily: typography.fontFamily.ui, fontSize: typography.fontSize.sm, color: colors.textSecondary },
|
||||
list: { paddingHorizontal: spacing[5], paddingBottom: spacing[10] },
|
||||
separator: { height: spacing[2] },
|
||||
empty: { alignItems: 'center', gap: spacing[3], paddingTop: spacing[12] },
|
||||
emptyText: { fontFamily: typography.fontFamily.ui, fontSize: typography.fontSize.base, color: colors.textSecondary },
|
||||
});
|
||||
Reference in New Issue
Block a user