frontend first version
This commit is contained in:
516
src/screens/SentinelScreen.tsx
Normal file
516
src/screens/SentinelScreen.tsx
Normal file
@@ -0,0 +1,516 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
ScrollView,
|
||||
TouchableOpacity,
|
||||
SafeAreaView,
|
||||
Animated,
|
||||
} 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 { SystemStatus, KillSwitchLog } from '../types';
|
||||
|
||||
// Status configuration with nautical theme
|
||||
const statusConfig: Record<SystemStatus, {
|
||||
color: string;
|
||||
label: string;
|
||||
icon: string;
|
||||
description: string;
|
||||
gradientColors: [string, string];
|
||||
}> = {
|
||||
normal: {
|
||||
color: colors.sentinel.statusNormal,
|
||||
label: 'ALL CLEAR',
|
||||
icon: 'checkmark-circle',
|
||||
description: 'The lighthouse burns bright. All systems nominal.',
|
||||
gradientColors: ['#6BBF8A', '#4A9F6A'],
|
||||
},
|
||||
warning: {
|
||||
color: colors.sentinel.statusWarning,
|
||||
label: 'STORM WARNING',
|
||||
icon: 'warning',
|
||||
description: 'Anomaly detected. Captain\'s attention required.',
|
||||
gradientColors: ['#E5B873', '#C99953'],
|
||||
},
|
||||
releasing: {
|
||||
color: colors.sentinel.statusCritical,
|
||||
label: 'RELEASE ACTIVE',
|
||||
icon: 'alert-circle',
|
||||
description: 'Legacy release protocol initiated.',
|
||||
gradientColors: ['#E57373', '#C55353'],
|
||||
},
|
||||
};
|
||||
|
||||
// Mock data
|
||||
const initialLogs: KillSwitchLog[] = [
|
||||
{
|
||||
id: '1',
|
||||
action: 'HEARTBEAT_CONFIRMED',
|
||||
timestamp: new Date('2024-01-18T09:30:00'),
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
action: 'SUBSCRIPTION_VERIFIED',
|
||||
timestamp: new Date('2024-01-17T00:00:00'),
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
action: 'JOURNAL_ACTIVITY',
|
||||
timestamp: new Date('2024-01-16T15:42:00'),
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
action: 'HEARTBEAT_CONFIRMED',
|
||||
timestamp: new Date('2024-01-15T11:20:00'),
|
||||
},
|
||||
];
|
||||
|
||||
export default function SentinelScreen() {
|
||||
const [status, setStatus] = useState<SystemStatus>('normal');
|
||||
const [lastSubscriptionCheck] = useState(new Date('2024-01-18T00:00:00'));
|
||||
const [lastFlowActivity] = useState(new Date('2024-01-18T10:30:00'));
|
||||
const [logs, setLogs] = useState<KillSwitchLog[]>(initialLogs);
|
||||
const [pulseAnim] = useState(new Animated.Value(1));
|
||||
const [glowAnim] = useState(new Animated.Value(0.5));
|
||||
const [rotateAnim] = useState(new Animated.Value(0));
|
||||
|
||||
useEffect(() => {
|
||||
// Pulse animation
|
||||
Animated.loop(
|
||||
Animated.sequence([
|
||||
Animated.timing(pulseAnim, {
|
||||
toValue: 1.06,
|
||||
duration: 1200,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(pulseAnim, {
|
||||
toValue: 1,
|
||||
duration: 1200,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
])
|
||||
).start();
|
||||
|
||||
// Glow animation
|
||||
Animated.loop(
|
||||
Animated.sequence([
|
||||
Animated.timing(glowAnim, {
|
||||
toValue: 1,
|
||||
duration: 1500,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(glowAnim, {
|
||||
toValue: 0.5,
|
||||
duration: 1500,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
])
|
||||
).start();
|
||||
|
||||
// Slow rotate for ship wheel
|
||||
Animated.loop(
|
||||
Animated.timing(rotateAnim, {
|
||||
toValue: 1,
|
||||
duration: 30000,
|
||||
useNativeDriver: true,
|
||||
})
|
||||
).start();
|
||||
}, []);
|
||||
|
||||
const handleHeartbeat = () => {
|
||||
// Animate pulse
|
||||
Animated.sequence([
|
||||
Animated.timing(pulseAnim, {
|
||||
toValue: 1.15,
|
||||
duration: 150,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(pulseAnim, {
|
||||
toValue: 1,
|
||||
duration: 150,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
]).start();
|
||||
|
||||
// Add new log
|
||||
const newLog: KillSwitchLog = {
|
||||
id: Date.now().toString(),
|
||||
action: 'HEARTBEAT_CONFIRMED',
|
||||
timestamp: new Date(),
|
||||
};
|
||||
setLogs([newLog, ...logs]);
|
||||
|
||||
// Reset status if warning
|
||||
if (status === 'warning') {
|
||||
setStatus('normal');
|
||||
}
|
||||
};
|
||||
|
||||
const formatDateTime = (date: Date) => {
|
||||
return date.toLocaleString('en-US', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
};
|
||||
|
||||
const formatTimeAgo = (date: Date) => {
|
||||
const now = new Date();
|
||||
const diff = now.getTime() - date.getTime();
|
||||
const hours = Math.floor(diff / (1000 * 60 * 60));
|
||||
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
|
||||
|
||||
if (hours > 24) {
|
||||
const days = Math.floor(hours / 24);
|
||||
return `${days} days ago`;
|
||||
}
|
||||
if (hours > 0) {
|
||||
return `${hours}h ${minutes}m ago`;
|
||||
}
|
||||
return `${minutes}m ago`;
|
||||
};
|
||||
|
||||
const currentStatus = statusConfig[status];
|
||||
const spin = rotateAnim.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: ['0deg', '360deg'],
|
||||
});
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<LinearGradient
|
||||
colors={[colors.sentinel.backgroundGradientStart, colors.sentinel.backgroundGradientEnd]}
|
||||
style={styles.gradient}
|
||||
>
|
||||
<SafeAreaView style={styles.safeArea}>
|
||||
<ScrollView
|
||||
style={styles.scrollView}
|
||||
showsVerticalScrollIndicator={false}
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
>
|
||||
{/* Header */}
|
||||
<View style={styles.header}>
|
||||
<View style={styles.headerTitleRow}>
|
||||
<FontAwesome5 name="anchor" size={24} color={colors.sentinel.primary} />
|
||||
<Text style={styles.title}>LIGHTHOUSE</Text>
|
||||
</View>
|
||||
<Text style={styles.subtitle}>The Watchful Guardian</Text>
|
||||
</View>
|
||||
|
||||
{/* Status Display */}
|
||||
<View style={styles.statusContainer}>
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.statusCircleOuter,
|
||||
{
|
||||
transform: [{ scale: pulseAnim }],
|
||||
opacity: glowAnim,
|
||||
backgroundColor: `${currentStatus.color}20`,
|
||||
}
|
||||
]}
|
||||
/>
|
||||
<Animated.View style={{ transform: [{ scale: pulseAnim }] }}>
|
||||
<LinearGradient
|
||||
colors={currentStatus.gradientColors}
|
||||
style={styles.statusCircle}
|
||||
>
|
||||
<Ionicons name={currentStatus.icon as any} size={56} color="#fff" />
|
||||
</LinearGradient>
|
||||
</Animated.View>
|
||||
<Text style={[styles.statusLabel, { color: currentStatus.color }]}>
|
||||
{currentStatus.label}
|
||||
</Text>
|
||||
<Text style={styles.statusDescription}>
|
||||
{currentStatus.description}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Ship Wheel Watermark */}
|
||||
<View style={styles.wheelWatermark}>
|
||||
<Animated.View style={{ transform: [{ rotate: spin }] }}>
|
||||
<MaterialCommunityIcons
|
||||
name="ship-wheel"
|
||||
size={200}
|
||||
color={colors.sentinel.primary}
|
||||
style={{ opacity: 0.03 }}
|
||||
/>
|
||||
</Animated.View>
|
||||
</View>
|
||||
|
||||
{/* Metrics Grid */}
|
||||
<View style={styles.metricsGrid}>
|
||||
<View style={styles.metricCard}>
|
||||
<View style={styles.metricIconContainer}>
|
||||
<FontAwesome5 name="anchor" size={16} color={colors.sentinel.primary} />
|
||||
</View>
|
||||
<Text style={styles.metricLabel}>SUBSCRIPTION</Text>
|
||||
<Text style={styles.metricValue}>
|
||||
{formatTimeAgo(lastSubscriptionCheck)}
|
||||
</Text>
|
||||
<Text style={styles.metricTime}>
|
||||
{formatDateTime(lastSubscriptionCheck)}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.metricCard}>
|
||||
<View style={styles.metricIconContainer}>
|
||||
<Feather name="edit-3" size={16} color={colors.sentinel.primary} />
|
||||
</View>
|
||||
<Text style={styles.metricLabel}>LAST JOURNAL</Text>
|
||||
<Text style={styles.metricValue}>
|
||||
{formatTimeAgo(lastFlowActivity)}
|
||||
</Text>
|
||||
<Text style={styles.metricTime}>
|
||||
{formatDateTime(lastFlowActivity)}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Heartbeat Button */}
|
||||
<TouchableOpacity
|
||||
style={styles.heartbeatButton}
|
||||
onPress={handleHeartbeat}
|
||||
activeOpacity={0.9}
|
||||
>
|
||||
<LinearGradient
|
||||
colors={[colors.nautical.teal, colors.nautical.seafoam]}
|
||||
style={styles.heartbeatGradient}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 0 }}
|
||||
>
|
||||
<View style={styles.heartbeatContent}>
|
||||
<MaterialCommunityIcons name="lighthouse" size={32} color="#fff" />
|
||||
<View>
|
||||
<Text style={styles.heartbeatText}>SIGNAL THE WATCH</Text>
|
||||
<Text style={styles.heartbeatSubtext}>Confirm your presence, Captain</Text>
|
||||
</View>
|
||||
</View>
|
||||
</LinearGradient>
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* Watch Log */}
|
||||
<View style={styles.logsSection}>
|
||||
<View style={styles.logsSectionHeader}>
|
||||
<Feather name="activity" size={18} color={colors.sentinel.primary} />
|
||||
<Text style={styles.logsSectionTitle}>WATCH LOG</Text>
|
||||
</View>
|
||||
{logs.map((log) => (
|
||||
<View key={log.id} style={styles.logItem}>
|
||||
<View style={styles.logDot} />
|
||||
<View style={styles.logContent}>
|
||||
<Text style={styles.logAction}>{log.action}</Text>
|
||||
<Text style={styles.logTime}>
|
||||
{formatDateTime(log.timestamp)}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
</LinearGradient>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
gradient: {
|
||||
flex: 1,
|
||||
},
|
||||
safeArea: {
|
||||
flex: 1,
|
||||
},
|
||||
scrollView: {
|
||||
flex: 1,
|
||||
},
|
||||
scrollContent: {
|
||||
padding: spacing.lg,
|
||||
paddingBottom: 120,
|
||||
},
|
||||
header: {
|
||||
marginBottom: spacing.xl,
|
||||
},
|
||||
headerTitleRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: spacing.sm,
|
||||
marginBottom: spacing.xs,
|
||||
},
|
||||
title: {
|
||||
fontSize: typography.fontSize.xl,
|
||||
fontWeight: '700',
|
||||
color: colors.sentinel.text,
|
||||
letterSpacing: typography.letterSpacing.widest,
|
||||
fontFamily: typography.fontFamily.serif,
|
||||
},
|
||||
subtitle: {
|
||||
fontSize: typography.fontSize.sm,
|
||||
color: colors.sentinel.textSecondary,
|
||||
marginLeft: spacing.xl + spacing.sm,
|
||||
fontStyle: 'italic',
|
||||
},
|
||||
statusContainer: {
|
||||
alignItems: 'center',
|
||||
paddingVertical: spacing.xl,
|
||||
marginBottom: spacing.lg,
|
||||
position: 'relative',
|
||||
},
|
||||
statusCircleOuter: {
|
||||
position: 'absolute',
|
||||
width: 170,
|
||||
height: 170,
|
||||
borderRadius: 85,
|
||||
},
|
||||
statusCircle: {
|
||||
width: 140,
|
||||
height: 140,
|
||||
borderRadius: 70,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginBottom: spacing.md,
|
||||
...shadows.glow,
|
||||
},
|
||||
statusLabel: {
|
||||
fontSize: typography.fontSize.xl,
|
||||
fontWeight: '700',
|
||||
letterSpacing: typography.letterSpacing.widest,
|
||||
marginBottom: spacing.sm,
|
||||
},
|
||||
statusDescription: {
|
||||
fontSize: typography.fontSize.base,
|
||||
color: colors.sentinel.textSecondary,
|
||||
textAlign: 'center',
|
||||
maxWidth: 280,
|
||||
fontStyle: 'italic',
|
||||
},
|
||||
wheelWatermark: {
|
||||
position: 'absolute',
|
||||
top: 200,
|
||||
right: -60,
|
||||
opacity: 0.5,
|
||||
},
|
||||
metricsGrid: {
|
||||
flexDirection: 'row',
|
||||
gap: spacing.md,
|
||||
marginBottom: spacing.lg,
|
||||
},
|
||||
metricCard: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.sentinel.cardBackground,
|
||||
borderRadius: borderRadius.xl,
|
||||
padding: spacing.base,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.sentinel.cardBorder,
|
||||
},
|
||||
metricIconContainer: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 20,
|
||||
backgroundColor: `${colors.sentinel.primary}15`,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginBottom: spacing.sm,
|
||||
},
|
||||
metricLabel: {
|
||||
fontSize: typography.fontSize.xs,
|
||||
color: colors.sentinel.textSecondary,
|
||||
letterSpacing: typography.letterSpacing.wide,
|
||||
marginBottom: spacing.xs,
|
||||
fontWeight: '600',
|
||||
},
|
||||
metricValue: {
|
||||
fontSize: typography.fontSize.md,
|
||||
color: colors.sentinel.text,
|
||||
fontWeight: '700',
|
||||
marginBottom: spacing.xs,
|
||||
},
|
||||
metricTime: {
|
||||
fontSize: typography.fontSize.xs,
|
||||
color: colors.sentinel.textSecondary,
|
||||
fontFamily: typography.fontFamily.mono,
|
||||
},
|
||||
heartbeatButton: {
|
||||
borderRadius: borderRadius.xl,
|
||||
overflow: 'hidden',
|
||||
marginBottom: spacing.xl,
|
||||
...shadows.medium,
|
||||
},
|
||||
heartbeatGradient: {
|
||||
padding: spacing.lg,
|
||||
},
|
||||
heartbeatContent: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: spacing.md,
|
||||
},
|
||||
heartbeatText: {
|
||||
fontSize: typography.fontSize.lg,
|
||||
fontWeight: '700',
|
||||
color: '#fff',
|
||||
letterSpacing: typography.letterSpacing.wider,
|
||||
},
|
||||
heartbeatSubtext: {
|
||||
fontSize: typography.fontSize.sm,
|
||||
color: 'rgba(255, 255, 255, 0.8)',
|
||||
marginTop: 2,
|
||||
fontStyle: 'italic',
|
||||
},
|
||||
logsSection: {
|
||||
backgroundColor: colors.sentinel.cardBackground,
|
||||
borderRadius: borderRadius.xl,
|
||||
padding: spacing.base,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.sentinel.cardBorder,
|
||||
},
|
||||
logsSectionHeader: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: spacing.sm,
|
||||
marginBottom: spacing.md,
|
||||
paddingBottom: spacing.sm,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.sentinel.cardBorder,
|
||||
},
|
||||
logsSectionTitle: {
|
||||
fontSize: typography.fontSize.xs,
|
||||
color: colors.sentinel.textSecondary,
|
||||
letterSpacing: typography.letterSpacing.widest,
|
||||
fontWeight: '700',
|
||||
},
|
||||
logItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-start',
|
||||
paddingVertical: spacing.sm,
|
||||
},
|
||||
logDot: {
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
backgroundColor: colors.sentinel.primary,
|
||||
marginTop: 6,
|
||||
marginRight: spacing.md,
|
||||
},
|
||||
logContent: {
|
||||
flex: 1,
|
||||
},
|
||||
logAction: {
|
||||
fontSize: typography.fontSize.sm,
|
||||
color: colors.sentinel.text,
|
||||
fontFamily: typography.fontFamily.mono,
|
||||
fontWeight: '500',
|
||||
marginBottom: 2,
|
||||
},
|
||||
logTime: {
|
||||
fontSize: typography.fontSize.xs,
|
||||
color: colors.sentinel.textSecondary,
|
||||
fontFamily: typography.fontFamily.mono,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user