- settings.tsx: labels on sign-out, feedback type chips, submit button - capture.tsx: labels on workspace selection chips - index.tsx: labels on workspace filter chips, note card pressables - search.tsx: labels on search input, result row pressables - note/[id].tsx: labels on edit, save, cancel buttons
192 lines
6.0 KiB
TypeScript
192 lines
6.0 KiB
TypeScript
import { useMemo } from 'react';
|
|
import { Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
|
|
import { router } from 'expo-router';
|
|
import type { MobileNote } from '../../api/notes';
|
|
import { useNotesStore, type NotesState } from '../../store/notes-store';
|
|
import { useWorkspaceStore, type WorkspaceState } from '../../store/workspace-store';
|
|
import type { MobileWorkspace } from '../../api/workspaces';
|
|
import { colors } from '../../theme';
|
|
|
|
export default function HomeScreen() {
|
|
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 setActiveWorkspace = useWorkspaceStore((state: WorkspaceState) => state.setActiveWorkspace);
|
|
const activeWorkspaceName =
|
|
workspaces.find((workspace: MobileWorkspace) => workspace.id === activeWorkspaceId)?.name ?? null;
|
|
const recentNotes = useMemo(
|
|
() =>
|
|
notes
|
|
.filter((note: MobileNote) => (activeWorkspaceName ? note.workspaceName === activeWorkspaceName : true))
|
|
.map((note: MobileNote) => ({
|
|
id: note.id,
|
|
title: note.title,
|
|
workspace: note.workspaceName,
|
|
preview: note.body,
|
|
})),
|
|
[activeWorkspaceName, notes]
|
|
);
|
|
const visibleCount = recentNotes.length;
|
|
|
|
return (
|
|
<ScrollView contentContainerStyle={styles.container}>
|
|
<Text style={styles.eyebrow}>Recent notes</Text>
|
|
<Text style={styles.title}>Capture and retrieve fast</Text>
|
|
<Text style={styles.subtitle}>Mobile MVP focuses on recent notes, quick capture, search, and lightweight review.</Text>
|
|
<View style={styles.summaryCard}>
|
|
<Text style={styles.summaryLabel}>Workspace</Text>
|
|
<Text style={styles.summaryValue}>{activeWorkspaceName ?? 'All workspaces'}</Text>
|
|
<Text style={styles.summaryMeta}>{visibleCount} visible notes ready for quick retrieval</Text>
|
|
</View>
|
|
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.workspaceRow}>
|
|
<Pressable
|
|
accessibilityLabel="Show all workspaces"
|
|
onPress={() => setActiveWorkspace(null)}
|
|
style={[styles.workspaceChip, activeWorkspaceId === null ? styles.workspaceChipActive : null]}
|
|
>
|
|
<Text style={[styles.workspaceChipText, activeWorkspaceId === null ? styles.workspaceChipTextActive : null]}>
|
|
All
|
|
</Text>
|
|
</Pressable>
|
|
{workspaces.map((workspace: MobileWorkspace) => {
|
|
const isActive = workspace.id === activeWorkspaceId;
|
|
|
|
return (
|
|
<Pressable
|
|
accessibilityLabel={`Filter by ${workspace.name}`}
|
|
key={workspace.id}
|
|
onPress={() => setActiveWorkspace(workspace.id)}
|
|
style={[styles.workspaceChip, isActive ? styles.workspaceChipActive : null]}
|
|
>
|
|
<Text style={[styles.workspaceChipText, isActive ? styles.workspaceChipTextActive : null]}>
|
|
{workspace.name}
|
|
</Text>
|
|
</Pressable>
|
|
);
|
|
})}
|
|
</ScrollView>
|
|
|
|
{isLoading ? <Text style={styles.empty}>Loading notes…</Text> : null}
|
|
{recentNotes.map((note: (typeof recentNotes)[number]) => (
|
|
<Pressable accessibilityLabel={`Open note ${note.title}`} key={note.id} onPress={() => router.push(`/note/${note.id}`)} style={styles.card}>
|
|
<View style={styles.badgeRow}>
|
|
<Text style={styles.badge}>{note.workspace}</Text>
|
|
</View>
|
|
<Text style={styles.cardTitle}>{note.title}</Text>
|
|
<Text style={styles.cardPreview}>{note.preview}</Text>
|
|
</Pressable>
|
|
))}
|
|
{!isLoading && recentNotes.length === 0 ? (
|
|
<Text style={styles.empty}>No recent notes yet for this workspace.</Text>
|
|
) : null}
|
|
</ScrollView>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
padding: 20,
|
|
gap: 14,
|
|
backgroundColor: colors.bgCanvas,
|
|
minHeight: '100%',
|
|
},
|
|
eyebrow: {
|
|
color: colors.textSecondary,
|
|
fontSize: 13,
|
|
textTransform: 'uppercase',
|
|
letterSpacing: 1,
|
|
},
|
|
title: {
|
|
color: colors.textPrimary,
|
|
fontSize: 30,
|
|
fontWeight: '700',
|
|
},
|
|
subtitle: {
|
|
color: colors.textSecondary,
|
|
fontSize: 16,
|
|
lineHeight: 22,
|
|
marginBottom: 8,
|
|
},
|
|
workspaceRow: {
|
|
gap: 10,
|
|
paddingBottom: 6,
|
|
},
|
|
workspaceChip: {
|
|
borderWidth: 1,
|
|
borderColor: colors.borderDefault,
|
|
borderRadius: 999,
|
|
paddingHorizontal: 12,
|
|
paddingVertical: 8,
|
|
backgroundColor: colors.surfaceCard,
|
|
},
|
|
workspaceChipActive: {
|
|
backgroundColor: colors.accentPrimary,
|
|
borderColor: colors.accentPrimary,
|
|
},
|
|
workspaceChipText: {
|
|
color: colors.textSecondary,
|
|
fontWeight: '600',
|
|
},
|
|
workspaceChipTextActive: {
|
|
color: colors.textPrimary,
|
|
},
|
|
summaryCard: {
|
|
backgroundColor: colors.surfaceCard,
|
|
borderWidth: 1,
|
|
borderColor: colors.borderDefault,
|
|
borderRadius: 16,
|
|
padding: 16,
|
|
gap: 6,
|
|
},
|
|
summaryLabel: {
|
|
color: colors.textSecondary,
|
|
fontSize: 13,
|
|
textTransform: 'uppercase',
|
|
letterSpacing: 1,
|
|
},
|
|
summaryValue: {
|
|
color: colors.textPrimary,
|
|
fontSize: 20,
|
|
fontWeight: '700',
|
|
},
|
|
summaryMeta: {
|
|
color: colors.textSecondary,
|
|
fontSize: 14,
|
|
},
|
|
card: {
|
|
backgroundColor: colors.surfaceCard,
|
|
borderWidth: 1,
|
|
borderColor: colors.borderDefault,
|
|
borderRadius: 16,
|
|
padding: 16,
|
|
gap: 10,
|
|
},
|
|
badgeRow: {
|
|
flexDirection: 'row',
|
|
},
|
|
badge: {
|
|
color: colors.accentPrimary,
|
|
backgroundColor: colors.bgElevated,
|
|
paddingHorizontal: 10,
|
|
paddingVertical: 4,
|
|
borderRadius: 999,
|
|
fontWeight: '600',
|
|
overflow: 'hidden',
|
|
},
|
|
cardTitle: {
|
|
color: colors.textPrimary,
|
|
fontSize: 18,
|
|
fontWeight: '700',
|
|
},
|
|
cardPreview: {
|
|
color: colors.textSecondary,
|
|
fontSize: 15,
|
|
lineHeight: 21,
|
|
},
|
|
empty: {
|
|
color: colors.textSecondary,
|
|
fontSize: 15,
|
|
},
|
|
});
|