frontend first version

This commit is contained in:
Ada
2026-01-20 21:11:04 -08:00
commit 7944b9f7ed
29 changed files with 16468 additions and 0 deletions

625
src/screens/FlowScreen.tsx Normal file
View File

@@ -0,0 +1,625 @@
import React, { useState } from 'react';
import {
View,
Text,
StyleSheet,
ScrollView,
TouchableOpacity,
Modal,
TextInput,
SafeAreaView,
} from 'react-native';
import { LinearGradient } from 'expo-linear-gradient';
import { Ionicons, Feather, MaterialCommunityIcons, FontAwesome5 } from '@expo/vector-icons';
import { colors, typography, spacing, borderRadius, shadows } from '../theme/colors';
import { FlowRecord } from '../types';
import BiometricModal from '../components/common/BiometricModal';
import VaultDoorAnimation from '../components/common/VaultDoorAnimation';
// Mock data
const initialRecords: FlowRecord[] = [
{
id: '1',
type: 'text',
content: 'Beautiful sunny day today. Went to the park with family. Felt the simple joy of life.',
createdAt: new Date('2024-01-18T10:30:00'),
emotion: 'Calm',
isArchived: false,
},
{
id: '2',
type: 'text',
content: 'Completed an important project. The process was challenging, but the result is satisfying.',
createdAt: new Date('2024-01-17T16:45:00'),
emotion: 'Content',
isArchived: false,
},
{
id: '3',
type: 'text',
content: 'Archived a reflection',
createdAt: new Date('2024-01-15T09:20:00'),
isArchived: true,
archivedAt: new Date('2024-01-16T14:00:00'),
},
{
id: '4',
type: 'voice',
content: '[Voice recording 2:30]',
createdAt: new Date('2024-01-14T20:15:00'),
emotion: 'Grateful',
isArchived: false,
},
];
// Emotion config
const emotionConfig: Record<string, { color: string; icon: string }> = {
'Calm': { color: colors.nautical.seafoam, icon: 'water' },
'Content': { color: colors.nautical.teal, icon: 'checkmark-circle' },
'Grateful': { color: colors.nautical.gold, icon: 'star' },
'Nostalgic': { color: colors.nautical.sage, icon: 'time' },
'Hopeful': { color: colors.nautical.mint, icon: 'sunny' },
};
export default function FlowScreen() {
const [records, setRecords] = useState<FlowRecord[]>(initialRecords);
const [showAddModal, setShowAddModal] = useState(false);
const [newContent, setNewContent] = useState('');
const [selectedRecord, setSelectedRecord] = useState<FlowRecord | null>(null);
const [showArchiveWarning, setShowArchiveWarning] = useState(false);
const [showBiometric, setShowBiometric] = useState(false);
const [showVaultAnimation, setShowVaultAnimation] = useState(false);
const [selectedInputType, setSelectedInputType] = useState<'text' | 'voice' | 'image'>('text');
const today = new Date();
const dateStr = today.toLocaleDateString('en-US', {
weekday: 'long',
month: 'long',
day: 'numeric'
});
const handleAddRecord = () => {
if (!newContent.trim()) return;
const newRecord: FlowRecord = {
id: Date.now().toString(),
type: selectedInputType,
content: newContent,
createdAt: new Date(),
emotion: 'Calm',
isArchived: false,
};
setRecords([newRecord, ...records]);
setNewContent('');
setShowAddModal(false);
};
const handleSendToVault = (record: FlowRecord) => {
setSelectedRecord(record);
setShowArchiveWarning(true);
};
const handleConfirmArchive = () => {
setShowArchiveWarning(false);
setShowBiometric(true);
};
const handleBiometricSuccess = () => {
setShowBiometric(false);
setShowVaultAnimation(true);
};
const handleVaultAnimationComplete = () => {
setShowVaultAnimation(false);
if (selectedRecord) {
setRecords(
records.map((r) =>
r.id === selectedRecord.id
? { ...r, content: 'Archived a reflection', isArchived: true, archivedAt: new Date() }
: r
)
);
setSelectedRecord(null);
}
};
const formatTime = (date: Date) => {
return date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' });
};
const formatDate = (date: Date) => {
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
};
return (
<View style={styles.container}>
<LinearGradient
colors={[colors.flow.backgroundGradientStart, colors.flow.backgroundGradientEnd]}
style={styles.gradient}
>
<SafeAreaView style={styles.safeArea}>
{/* Header */}
<View style={styles.header}>
<View style={styles.headerLeft}>
<View style={styles.iconCircle}>
<FontAwesome5 name="scroll" size={18} color={colors.flow.primary} />
</View>
<View>
<Text style={styles.headerTitle}>Journal</Text>
<Text style={styles.headerDate}>{dateStr}</Text>
</View>
</View>
<View style={styles.moodBadge}>
<MaterialCommunityIcons name="weather-sunny" size={14} color={colors.nautical.gold} />
<Text style={styles.moodText}>Serene</Text>
</View>
</View>
{/* Timeline */}
<ScrollView
style={styles.timeline}
showsVerticalScrollIndicator={false}
contentContainerStyle={styles.timelineContent}
>
{records.map((record, index) => {
const emotionInfo = record.emotion ? emotionConfig[record.emotion] : null;
const showDateHeader = index === 0 ||
formatDate(record.createdAt) !== formatDate(records[index - 1].createdAt);
return (
<View key={record.id}>
{showDateHeader && (
<View style={styles.dateHeader}>
<View style={styles.dateLine} />
<Text style={styles.dateHeaderText}>{formatDate(record.createdAt)}</Text>
<View style={styles.dateLine} />
</View>
)}
<View style={[styles.card, record.isArchived && styles.archivedCard]}>
<View style={styles.cardHeader}>
<Text style={[styles.cardTime, record.isArchived && styles.archivedText]}>
{formatTime(record.createdAt)}
</Text>
<View style={styles.cardBadges}>
{emotionInfo && !record.isArchived && (
<View style={[styles.emotionBadge, { backgroundColor: `${emotionInfo.color}15` }]}>
<Ionicons name={emotionInfo.icon as any} size={11} color={emotionInfo.color} />
<Text style={[styles.emotionText, { color: emotionInfo.color }]}>{record.emotion}</Text>
</View>
)}
{record.type === 'voice' && !record.isArchived && (
<Ionicons name="mic" size={14} color={colors.flow.secondary} />
)}
</View>
</View>
<Text style={[styles.cardContent, record.isArchived && styles.archivedText]}>
{record.isArchived
? `📦 ${record.archivedAt?.toLocaleDateString('en-US')}${record.content}`
: record.content
}
</Text>
{!record.isArchived && (
<TouchableOpacity
style={styles.vaultButton}
onPress={() => handleSendToVault(record)}
>
<MaterialCommunityIcons name="treasure-chest" size={14} color={colors.flow.primary} />
<Text style={styles.vaultButtonText}>Seal in Vault</Text>
</TouchableOpacity>
)}
</View>
</View>
);
})}
<View style={{ height: 100 }} />
</ScrollView>
{/* FAB */}
<TouchableOpacity
style={styles.fab}
onPress={() => setShowAddModal(true)}
activeOpacity={0.9}
>
<LinearGradient
colors={[colors.nautical.teal, colors.nautical.seafoam]}
style={styles.fabGradient}
>
<Feather name="feather" size={24} color="#fff" />
</LinearGradient>
</TouchableOpacity>
</SafeAreaView>
</LinearGradient>
{/* Add Modal */}
<Modal visible={showAddModal} animationType="slide" transparent onRequestClose={() => setShowAddModal(false)}>
<View style={styles.modalOverlay}>
<View style={styles.modalContent}>
<View style={styles.modalHandle} />
<Text style={styles.modalTitle}>New Entry</Text>
<TextInput
style={styles.input}
placeholder="What's on your mind, Captain?"
placeholderTextColor={colors.flow.textSecondary}
value={newContent}
onChangeText={setNewContent}
multiline
textAlignVertical="top"
/>
<View style={styles.inputTypeRow}>
{[
{ type: 'text', icon: 'type', label: 'Text' },
{ type: 'voice', icon: 'mic', label: 'Voice' },
{ type: 'image', icon: 'image', label: 'Photo' },
].map((item) => (
<TouchableOpacity
key={item.type}
style={[styles.inputTypeButton, selectedInputType === item.type && styles.inputTypeActive]}
onPress={() => setSelectedInputType(item.type as any)}
>
<Feather name={item.icon as any} size={20} color={selectedInputType === item.type ? colors.flow.primary : colors.flow.textSecondary} />
<Text style={[styles.inputTypeLabel, selectedInputType === item.type && styles.inputTypeLabelActive]}>{item.label}</Text>
</TouchableOpacity>
))}
</View>
<View style={styles.modalButtons}>
<TouchableOpacity style={styles.cancelButton} onPress={() => { setShowAddModal(false); setNewContent(''); }}>
<Text style={styles.cancelButtonText}>Cancel</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.confirmButton} onPress={handleAddRecord}>
<LinearGradient colors={[colors.nautical.teal, colors.nautical.seafoam]} style={styles.confirmButtonGradient}>
<Feather name="check" size={18} color="#fff" />
<Text style={styles.confirmButtonText}>Save</Text>
</LinearGradient>
</TouchableOpacity>
</View>
</View>
</View>
</Modal>
{/* Archive Warning */}
<Modal visible={showArchiveWarning} animationType="fade" transparent onRequestClose={() => setShowArchiveWarning(false)}>
<View style={styles.warningOverlay}>
<View style={styles.warningModal}>
<View style={styles.warningIcon}>
<MaterialCommunityIcons name="treasure-chest" size={40} color={colors.nautical.teal} />
</View>
<Text style={styles.warningTitle}>Seal Entry?</Text>
<Text style={styles.warningText}>
This entry will be encrypted and stored in the vault. The original will be replaced with a sealed record.
</Text>
<View style={styles.warningButtons}>
<TouchableOpacity style={styles.warningCancelButton} onPress={() => setShowArchiveWarning(false)}>
<Text style={styles.warningCancelText}>Cancel</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.warningConfirmButton} onPress={handleConfirmArchive}>
<MaterialCommunityIcons name="lock" size={16} color="#fff" />
<Text style={styles.warningConfirmText}>Seal</Text>
</TouchableOpacity>
</View>
</View>
</View>
</Modal>
<BiometricModal
visible={showBiometric}
onSuccess={handleBiometricSuccess}
onCancel={() => { setShowBiometric(false); setSelectedRecord(null); }}
title="Verify Identity"
message="Authenticate to seal this entry"
/>
<VaultDoorAnimation visible={showVaultAnimation} onComplete={handleVaultAnimationComplete} />
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1 },
gradient: { flex: 1 },
safeArea: { flex: 1 },
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: spacing.base,
paddingTop: spacing.sm,
paddingBottom: spacing.md,
},
headerLeft: {
flexDirection: 'row',
alignItems: 'center',
gap: spacing.sm,
},
iconCircle: {
width: 44,
height: 44,
borderRadius: 14,
backgroundColor: colors.flow.cardBackground,
justifyContent: 'center',
alignItems: 'center',
...shadows.soft,
},
headerTitle: {
fontSize: typography.fontSize.xl,
fontWeight: '700',
color: colors.flow.text,
},
headerDate: {
fontSize: typography.fontSize.sm,
color: colors.flow.textSecondary,
},
moodBadge: {
flexDirection: 'row',
alignItems: 'center',
gap: 4,
backgroundColor: `${colors.nautical.gold}15`,
paddingHorizontal: spacing.sm,
paddingVertical: 6,
borderRadius: borderRadius.full,
},
moodText: {
fontSize: typography.fontSize.sm,
fontWeight: '600',
color: colors.nautical.gold,
},
timeline: { flex: 1 },
timelineContent: { padding: spacing.base, paddingTop: 0 },
dateHeader: {
flexDirection: 'row',
alignItems: 'center',
marginVertical: spacing.md,
},
dateLine: {
flex: 1,
height: 1,
backgroundColor: colors.flow.cardBorder,
},
dateHeaderText: {
fontSize: typography.fontSize.xs,
fontWeight: '600',
color: colors.flow.textSecondary,
paddingHorizontal: spacing.md,
},
card: {
backgroundColor: colors.flow.cardBackground,
borderRadius: borderRadius.xl,
padding: spacing.base,
marginBottom: spacing.sm,
...shadows.soft,
},
archivedCard: {
backgroundColor: colors.flow.archived,
shadowOpacity: 0,
},
cardHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: spacing.sm,
},
cardTime: {
fontSize: typography.fontSize.xs,
color: colors.flow.textSecondary,
fontWeight: '500',
},
archivedText: { color: colors.flow.archivedText },
cardBadges: {
flexDirection: 'row',
alignItems: 'center',
gap: spacing.sm,
},
emotionBadge: {
flexDirection: 'row',
alignItems: 'center',
gap: 3,
paddingHorizontal: 8,
paddingVertical: 3,
borderRadius: borderRadius.full,
},
emotionText: {
fontSize: 10,
fontWeight: '600',
},
cardContent: {
fontSize: typography.fontSize.base,
color: colors.flow.text,
lineHeight: typography.fontSize.base * 1.6,
},
vaultButton: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'flex-end',
gap: 4,
marginTop: spacing.md,
paddingTop: spacing.sm,
borderTopWidth: 1,
borderTopColor: colors.flow.cardBorder,
},
vaultButtonText: {
fontSize: typography.fontSize.sm,
color: colors.flow.primary,
fontWeight: '600',
},
fab: {
position: 'absolute',
bottom: 100,
right: spacing.base,
...shadows.medium,
},
fabGradient: {
width: 56,
height: 56,
borderRadius: 18,
justifyContent: 'center',
alignItems: 'center',
},
modalOverlay: {
flex: 1,
backgroundColor: 'rgba(26, 58, 74, 0.4)',
justifyContent: 'flex-end',
},
modalContent: {
backgroundColor: colors.flow.cardBackground,
borderTopLeftRadius: borderRadius.xxl,
borderTopRightRadius: borderRadius.xxl,
padding: spacing.lg,
paddingBottom: spacing.xxl,
},
modalHandle: {
width: 36,
height: 4,
backgroundColor: colors.flow.cardBorder,
borderRadius: 2,
alignSelf: 'center',
marginBottom: spacing.md,
},
modalTitle: {
fontSize: typography.fontSize.lg,
fontWeight: '700',
color: colors.flow.text,
marginBottom: spacing.md,
},
input: {
backgroundColor: colors.nautical.paleAqua,
borderRadius: borderRadius.lg,
padding: spacing.base,
fontSize: typography.fontSize.base,
color: colors.flow.text,
minHeight: 100,
marginBottom: spacing.md,
},
inputTypeRow: {
flexDirection: 'row',
gap: spacing.sm,
marginBottom: spacing.lg,
},
inputTypeButton: {
flex: 1,
alignItems: 'center',
paddingVertical: spacing.md,
borderRadius: borderRadius.lg,
backgroundColor: colors.nautical.paleAqua,
borderWidth: 2,
borderColor: 'transparent',
},
inputTypeActive: {
borderColor: colors.flow.primary,
backgroundColor: colors.nautical.lightMint,
},
inputTypeLabel: {
fontSize: typography.fontSize.xs,
color: colors.flow.textSecondary,
marginTop: 4,
fontWeight: '500',
},
inputTypeLabelActive: {
color: colors.flow.primary,
fontWeight: '600',
},
modalButtons: {
flexDirection: 'row',
gap: spacing.md,
},
cancelButton: {
flex: 1,
paddingVertical: spacing.md,
borderRadius: borderRadius.lg,
backgroundColor: colors.nautical.paleAqua,
alignItems: 'center',
},
cancelButtonText: {
fontSize: typography.fontSize.base,
color: colors.flow.textSecondary,
fontWeight: '600',
},
confirmButton: {
flex: 1,
borderRadius: borderRadius.lg,
overflow: 'hidden',
},
confirmButtonGradient: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingVertical: spacing.md,
gap: spacing.sm,
},
confirmButtonText: {
fontSize: typography.fontSize.base,
color: '#fff',
fontWeight: '600',
},
warningOverlay: {
flex: 1,
backgroundColor: 'rgba(26, 58, 74, 0.6)',
justifyContent: 'center',
alignItems: 'center',
padding: spacing.lg,
},
warningModal: {
backgroundColor: colors.flow.cardBackground,
borderRadius: borderRadius.xxl,
padding: spacing.xl,
alignItems: 'center',
width: '100%',
...shadows.medium,
},
warningIcon: {
width: 72,
height: 72,
borderRadius: 24,
backgroundColor: colors.nautical.lightMint,
justifyContent: 'center',
alignItems: 'center',
marginBottom: spacing.md,
},
warningTitle: {
fontSize: typography.fontSize.lg,
fontWeight: '700',
color: colors.flow.text,
marginBottom: spacing.sm,
},
warningText: {
fontSize: typography.fontSize.base,
color: colors.flow.textSecondary,
textAlign: 'center',
lineHeight: typography.fontSize.base * 1.5,
marginBottom: spacing.lg,
},
warningButtons: {
flexDirection: 'row',
gap: spacing.md,
width: '100%',
},
warningCancelButton: {
flex: 1,
paddingVertical: spacing.md,
borderRadius: borderRadius.lg,
backgroundColor: colors.nautical.paleAqua,
alignItems: 'center',
},
warningCancelText: {
fontSize: typography.fontSize.base,
color: colors.flow.textSecondary,
fontWeight: '600',
},
warningConfirmButton: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingVertical: spacing.md,
borderRadius: borderRadius.lg,
backgroundColor: colors.nautical.teal,
gap: spacing.xs,
},
warningConfirmText: {
fontSize: typography.fontSize.base,
color: '#fff',
fontWeight: '600',
},
});