110 lines
3.4 KiB
TypeScript
110 lines
3.4 KiB
TypeScript
import { useMemo, useState } from 'react';
|
|
import { Pressable, StyleSheet, Text, TextInput, View } from 'react-native';
|
|
import { router } from 'expo-router';
|
|
import type { MobileNote } from '../../api/notes';
|
|
import type { MobileWorkspace } from '../../api/workspaces';
|
|
import { useNotesStore, type NotesState } from '../../store/notes-store';
|
|
import { useWorkspaceStore, type WorkspaceState } from '../../store/workspace-store';
|
|
import { buttonA11y, textInputA11y } from '../../lib/accessibility';
|
|
import { colors } from '../../theme';
|
|
|
|
export default function SearchScreen() {
|
|
const [query, setQuery] = useState('');
|
|
const notes = useNotesStore((state: NotesState) => state.notes);
|
|
const isLoading = useNotesStore((state: NotesState) => state.isLoading);
|
|
const workspaces = useWorkspaceStore((state: WorkspaceState) => state.workspaces);
|
|
const activeWorkspaceId = useWorkspaceStore((state: WorkspaceState) => state.activeWorkspaceId);
|
|
const activeWorkspaceName =
|
|
workspaces.find((workspace: MobileWorkspace) => workspace.id === activeWorkspaceId)?.name ?? null;
|
|
|
|
const results = useMemo(() => {
|
|
const scopedNotes = notes.filter((note: MobileNote) =>
|
|
activeWorkspaceName ? note.workspaceName === activeWorkspaceName : true
|
|
);
|
|
|
|
if (!query.trim()) {
|
|
return scopedNotes;
|
|
}
|
|
|
|
const normalized = query.toLowerCase();
|
|
return scopedNotes.filter(
|
|
(note: MobileNote) =>
|
|
note.title.toLowerCase().includes(normalized) ||
|
|
note.body.toLowerCase().includes(normalized) ||
|
|
note.workspaceName.toLowerCase().includes(normalized)
|
|
);
|
|
}, [activeWorkspaceName, notes, query]);
|
|
|
|
return (
|
|
<View style={styles.container}>
|
|
<Text style={styles.title}>Search</Text>
|
|
<Text style={styles.scope}>Scope: {activeWorkspaceName ?? 'All workspaces'}</Text>
|
|
<TextInput
|
|
{...textInputA11y('Search notes')}
|
|
value={query}
|
|
onChangeText={setQuery}
|
|
placeholder="Search notes"
|
|
placeholderTextColor={colors.textTertiary}
|
|
style={styles.input}
|
|
/>
|
|
<View style={styles.list}>
|
|
{isLoading ? <Text style={styles.empty}>Loading searchable notes…</Text> : null}
|
|
{results.map((note: MobileNote) => (
|
|
<Pressable key={note.id} {...buttonA11y(`Open note ${note.title}`)} onPress={() => router.push(`/note/${note.id}`)} style={styles.row}>
|
|
<Text style={styles.rowText}>{note.title}</Text>
|
|
</Pressable>
|
|
))}
|
|
{!isLoading && results.length === 0 ? (
|
|
<Text style={styles.empty}>No notes match your search in this scope yet.</Text>
|
|
) : null}
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
flex: 1,
|
|
padding: 20,
|
|
backgroundColor: colors.bgCanvas,
|
|
gap: 14,
|
|
},
|
|
title: {
|
|
color: colors.textPrimary,
|
|
fontSize: 28,
|
|
fontWeight: '700',
|
|
},
|
|
input: {
|
|
borderWidth: 1,
|
|
borderColor: colors.borderDefault,
|
|
borderRadius: 12,
|
|
paddingHorizontal: 14,
|
|
paddingVertical: 12,
|
|
color: colors.textPrimary,
|
|
backgroundColor: colors.surfaceCard,
|
|
},
|
|
scope: {
|
|
color: colors.textSecondary,
|
|
fontSize: 14,
|
|
},
|
|
list: {
|
|
gap: 10,
|
|
},
|
|
row: {
|
|
backgroundColor: colors.surfaceCard,
|
|
borderRadius: 14,
|
|
padding: 14,
|
|
borderWidth: 1,
|
|
borderColor: colors.borderDefault,
|
|
},
|
|
rowText: {
|
|
color: colors.textPrimary,
|
|
fontSize: 16,
|
|
fontWeight: '600',
|
|
},
|
|
empty: {
|
|
color: colors.textSecondary,
|
|
fontSize: 15,
|
|
},
|
|
});
|