Archived
- Add mobile/ directory with Expo React Native project - Create API client with JWT auth and token refresh using AsyncStorage - Implement AuthContext for login/register/logout flow - Add screens: Login, Register, Library, Search, Settings - Set up React Navigation with AuthStack and MainTabs - Create packages/shared/ with shared types and utilities - Add shared validation utilities (email, password strength) - Update root package.json workspaces to include mobile + shared - Add spec document docs/backend/009-expo-integration.md
130 lines
2.9 KiB
TypeScript
130 lines
2.9 KiB
TypeScript
import { useState, type ReactNode } from "react";
|
|
import {
|
|
View,
|
|
Text,
|
|
TextInput,
|
|
FlatList,
|
|
TouchableOpacity,
|
|
StyleSheet,
|
|
ActivityIndicator,
|
|
} from "react-native";
|
|
import { searchBooks } from "../api/books";
|
|
import type { Book, PaginatedResponse } from "@cloud-reader/shared";
|
|
|
|
export default function SearchScreen(): ReactNode {
|
|
const [query, setQuery] = useState("");
|
|
const [results, setResults] = useState<Book[]>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
const [searched, setSearched] = useState(false);
|
|
|
|
const handleSearch = async () => {
|
|
if (!query.trim()) return;
|
|
setLoading(true);
|
|
setSearched(true);
|
|
try {
|
|
const data: PaginatedResponse<Book> = await searchBooks(query.trim());
|
|
setResults(data.results);
|
|
} catch {
|
|
setResults([]);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const renderBook = ({ item }: { item: Book }) => (
|
|
<View style={styles.resultCard}>
|
|
<Text style={styles.resultTitle}>{item.title}</Text>
|
|
<Text style={styles.resultAuthor}>{item.author}</Text>
|
|
</View>
|
|
);
|
|
|
|
return (
|
|
<View style={styles.container}>
|
|
<View style={styles.searchBar}>
|
|
<TextInput
|
|
style={styles.input}
|
|
placeholder="Search books by title, author..."
|
|
placeholderTextColor="#666"
|
|
value={query}
|
|
onChangeText={setQuery}
|
|
onSubmitEditing={handleSearch}
|
|
returnKeyType="search"
|
|
autoCapitalize="none"
|
|
autoCorrect={false}
|
|
/>
|
|
</View>
|
|
|
|
{loading && (
|
|
<View style={styles.centered}>
|
|
<ActivityIndicator size="large" color="#4f8ef7" />
|
|
</View>
|
|
)}
|
|
|
|
{!loading && searched && results.length === 0 && (
|
|
<View style={styles.centered}>
|
|
<Text style={styles.noResults}>No books found for "{query}"</Text>
|
|
</View>
|
|
)}
|
|
|
|
<FlatList
|
|
data={results}
|
|
renderItem={renderBook}
|
|
keyExtractor={(item) => item.id}
|
|
contentContainerStyle={styles.list}
|
|
/>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
flex: 1,
|
|
backgroundColor: "#0f0f23",
|
|
},
|
|
searchBar: {
|
|
padding: 16,
|
|
borderBottomWidth: 1,
|
|
borderBottomColor: "#333",
|
|
},
|
|
input: {
|
|
backgroundColor: "#1a1a2e",
|
|
borderRadius: 8,
|
|
padding: 14,
|
|
fontSize: 16,
|
|
color: "#fff",
|
|
borderWidth: 1,
|
|
borderColor: "#333",
|
|
},
|
|
centered: {
|
|
flex: 1,
|
|
justifyContent: "center",
|
|
alignItems: "center",
|
|
padding: 24,
|
|
},
|
|
list: {
|
|
padding: 16,
|
|
},
|
|
resultCard: {
|
|
backgroundColor: "#1a1a2e",
|
|
borderRadius: 12,
|
|
padding: 16,
|
|
marginBottom: 8,
|
|
borderWidth: 1,
|
|
borderColor: "#333",
|
|
},
|
|
resultTitle: {
|
|
fontSize: 16,
|
|
fontWeight: "600",
|
|
color: "#fff",
|
|
marginBottom: 4,
|
|
},
|
|
resultAuthor: {
|
|
fontSize: 14,
|
|
color: "#888",
|
|
},
|
|
noResults: {
|
|
fontSize: 16,
|
|
color: "#888",
|
|
textAlign: "center",
|
|
},
|
|
}); |