mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-04-06 00:32:12 +02:00
Refactor Settings, Login, and Server URL screens to use Tailwind CSS for styling; remove unused styles and theme properties; integrate NativeWind for utility-first styling approach; update theme context to support dark mode; enhance accessibility and maintainability.
This commit is contained in:
@@ -6,12 +6,12 @@
|
||||
"orientation": "portrait",
|
||||
"icon": "./assets/icon.png",
|
||||
"scheme": "oneuptime",
|
||||
"userInterfaceStyle": "dark",
|
||||
"userInterfaceStyle": "automatic",
|
||||
"newArchEnabled": true,
|
||||
"splash": {
|
||||
"image": "./assets/splash-icon.png",
|
||||
"resizeMode": "contain",
|
||||
"backgroundColor": "#0D1117"
|
||||
"backgroundColor": "#FFFFFF"
|
||||
},
|
||||
"ios": {
|
||||
"supportsTablet": true,
|
||||
|
||||
7
MobileApp/babel.config.js
Normal file
7
MobileApp/babel.config.js
Normal file
@@ -0,0 +1,7 @@
|
||||
module.exports = function (api) {
|
||||
api.cache(true);
|
||||
return {
|
||||
presets: ["babel-preset-expo"],
|
||||
plugins: ["nativewind/babel"],
|
||||
};
|
||||
};
|
||||
83
MobileApp/global.css
Normal file
83
MobileApp/global.css
Normal file
@@ -0,0 +1,83 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
--color-bg-primary: #FFFFFF;
|
||||
--color-bg-secondary: #F9FAFB;
|
||||
--color-bg-tertiary: #F3F4F6;
|
||||
--color-bg-elevated: #FFFFFF;
|
||||
--color-border-default: #E5E7EB;
|
||||
--color-border-subtle: #F3F4F6;
|
||||
--color-text-primary: #111827;
|
||||
--color-text-secondary: #6B7280;
|
||||
--color-text-tertiary: #9CA3AF;
|
||||
--color-text-inverse: #FFFFFF;
|
||||
--color-severity-critical: #CF222E;
|
||||
--color-severity-critical-bg: #CF222E1A;
|
||||
--color-severity-major: #BC4C00;
|
||||
--color-severity-major-bg: #BC4C001A;
|
||||
--color-severity-minor: #9A6700;
|
||||
--color-severity-minor-bg: #9A67001A;
|
||||
--color-severity-warning: #BF8700;
|
||||
--color-severity-warning-bg: #BF87001A;
|
||||
--color-severity-info: #0969DA;
|
||||
--color-severity-info-bg: #0969DA1A;
|
||||
--color-state-created: #CF222E;
|
||||
--color-state-acknowledged: #9A6700;
|
||||
--color-state-resolved: #1A7F37;
|
||||
--color-state-investigating: #BC4C00;
|
||||
--color-state-muted: #8C959F;
|
||||
--color-oncall-active: #1A7F37;
|
||||
--color-oncall-active-bg: #1A7F371A;
|
||||
--color-oncall-inactive: #8C959F;
|
||||
--color-oncall-inactive-bg: #8C959F1A;
|
||||
--color-action-primary: #6366F1;
|
||||
--color-action-primary-pressed: #4F46E5;
|
||||
--color-action-destructive: #CF222E;
|
||||
--color-action-destructive-pressed: #A40E26;
|
||||
--color-status-success: #1A7F37;
|
||||
--color-status-success-bg: #1A7F371A;
|
||||
--color-status-error: #CF222E;
|
||||
--color-status-error-bg: #CF222E1A;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--color-bg-primary: #0D1117;
|
||||
--color-bg-secondary: #161B22;
|
||||
--color-bg-tertiary: #21262D;
|
||||
--color-bg-elevated: #1C2128;
|
||||
--color-border-default: #30363D;
|
||||
--color-border-subtle: #21262D;
|
||||
--color-text-primary: #E6EDF3;
|
||||
--color-text-secondary: #8B949E;
|
||||
--color-text-tertiary: #6E7681;
|
||||
--color-text-inverse: #0D1117;
|
||||
--color-severity-critical: #F85149;
|
||||
--color-severity-critical-bg: #F8514926;
|
||||
--color-severity-major: #F0883E;
|
||||
--color-severity-major-bg: #F0883E26;
|
||||
--color-severity-minor: #D29922;
|
||||
--color-severity-minor-bg: #D2992226;
|
||||
--color-severity-warning: #E3B341;
|
||||
--color-severity-warning-bg: #E3B34126;
|
||||
--color-severity-info: #58A6FF;
|
||||
--color-severity-info-bg: #58A6FF26;
|
||||
--color-state-created: #F85149;
|
||||
--color-state-acknowledged: #D29922;
|
||||
--color-state-resolved: #3FB950;
|
||||
--color-state-investigating: #F0883E;
|
||||
--color-state-muted: #6E7681;
|
||||
--color-oncall-active: #3FB950;
|
||||
--color-oncall-active-bg: #3FB95026;
|
||||
--color-oncall-inactive: #6E7681;
|
||||
--color-oncall-inactive-bg: #6E768126;
|
||||
--color-action-primary: #6366F1;
|
||||
--color-action-primary-pressed: #4F46E5;
|
||||
--color-action-destructive: #F85149;
|
||||
--color-action-destructive-pressed: #DA3633;
|
||||
--color-status-success: #3FB950;
|
||||
--color-status-success-bg: #3FB95026;
|
||||
--color-status-error: #F85149;
|
||||
--color-status-error-bg: #F8514926;
|
||||
}
|
||||
6
MobileApp/metro.config.js
Normal file
6
MobileApp/metro.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
const { getDefaultConfig } = require("expo/metro-config");
|
||||
const { withNativeWind } = require("nativewind/metro");
|
||||
|
||||
const config = getDefaultConfig(__dirname);
|
||||
|
||||
module.exports = withNativeWind(config, { input: "./global.css" });
|
||||
3
MobileApp/nativewind-env.d.ts
vendored
Normal file
3
MobileApp/nativewind-env.d.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
/// <reference types="nativewind/types" />
|
||||
|
||||
// NOTE: This file should not be edited and should be committed with your source code. It is generated by NativeWind.
|
||||
1019
MobileApp/package-lock.json
generated
1019
MobileApp/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -29,13 +29,16 @@
|
||||
"expo-splash-screen": "^31.0.13",
|
||||
"expo-status-bar": "~3.0.9",
|
||||
"expo-system-ui": "~6.0.9",
|
||||
"nativewind": "^4.2.1",
|
||||
"postcss": "^8.5.6",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"react-native": "0.81.5",
|
||||
"react-native-keychain": "^10.0.0",
|
||||
"react-native-safe-area-context": "^5.6.2",
|
||||
"react-native-screens": "~4.16.0",
|
||||
"react-native-web": "^0.21.0"
|
||||
"react-native-web": "^0.21.0",
|
||||
"tailwindcss": "^3.4.19"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "~19.1.0",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import "../global.css";
|
||||
import React from "react";
|
||||
import { View, StyleSheet, ViewStyle } from "react-native";
|
||||
import { View } from "react-native";
|
||||
import { StatusBar } from "expo-status-bar";
|
||||
import { QueryClient } from "@tanstack/react-query";
|
||||
import { PersistQueryClientProvider } from "@tanstack/react-query-persist-client";
|
||||
@@ -29,12 +30,7 @@ function AppContent(): React.JSX.Element {
|
||||
const { theme } = useTheme();
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.container,
|
||||
{ backgroundColor: theme.colors.backgroundPrimary },
|
||||
]}
|
||||
>
|
||||
<View className="flex-1 bg-bg-primary">
|
||||
<StatusBar style={theme.isDark ? "light" : "dark"} />
|
||||
<RootNavigator />
|
||||
<OfflineBanner />
|
||||
@@ -58,9 +54,3 @@ export default function App(): React.JSX.Element {
|
||||
</PersistQueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
const styles: { container: ViewStyle } = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
TouchableOpacity,
|
||||
Modal,
|
||||
ActivityIndicator,
|
||||
StyleSheet,
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
} from "react-native";
|
||||
@@ -49,35 +48,17 @@ export default function AddNoteModal({
|
||||
onRequestClose={handleClose}
|
||||
>
|
||||
<KeyboardAvoidingView
|
||||
style={styles.overlay}
|
||||
className="flex-1 justify-end"
|
||||
style={{ backgroundColor: "rgba(0,0,0,0.4)" }}
|
||||
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
||||
>
|
||||
<View
|
||||
style={[
|
||||
styles.container,
|
||||
{
|
||||
backgroundColor: theme.colors.backgroundPrimary,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
theme.typography.titleMedium,
|
||||
{ color: theme.colors.textPrimary, marginBottom: 16 },
|
||||
]}
|
||||
>
|
||||
<View className="rounded-t-3xl p-5 pb-9 bg-bg-primary">
|
||||
<Text className="text-title-md text-text-primary mb-4">
|
||||
Add Note
|
||||
</Text>
|
||||
|
||||
<TextInput
|
||||
style={[
|
||||
styles.input,
|
||||
{
|
||||
backgroundColor: theme.colors.backgroundPrimary,
|
||||
color: theme.colors.textPrimary,
|
||||
borderColor: theme.colors.borderDefault,
|
||||
},
|
||||
]}
|
||||
className="min-h-[120px] rounded-[14px] border p-3 text-[15px] bg-bg-primary text-text-primary border-border-default"
|
||||
placeholder="Add a note..."
|
||||
placeholderTextColor={theme.colors.textTertiary}
|
||||
value={noteText}
|
||||
@@ -87,39 +68,25 @@ export default function AddNoteModal({
|
||||
editable={!isSubmitting}
|
||||
/>
|
||||
|
||||
<View style={styles.buttonRow}>
|
||||
<View className="flex-row gap-3 mt-4">
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.button,
|
||||
{
|
||||
backgroundColor: theme.colors.backgroundTertiary,
|
||||
borderColor: theme.colors.borderSubtle,
|
||||
borderWidth: 1,
|
||||
},
|
||||
]}
|
||||
className="flex-1 py-3.5 rounded-[14px] items-center justify-center min-h-[48px] bg-bg-tertiary border border-border-subtle"
|
||||
onPress={handleClose}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.buttonText,
|
||||
{ color: theme.colors.textSecondary },
|
||||
]}
|
||||
>
|
||||
<Text className="text-[15px] font-bold text-text-secondary">
|
||||
Cancel
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.button,
|
||||
{
|
||||
backgroundColor:
|
||||
noteText.trim() && !isSubmitting
|
||||
? theme.colors.actionPrimary
|
||||
: theme.colors.backgroundTertiary,
|
||||
},
|
||||
]}
|
||||
className="flex-1 py-3.5 rounded-[14px] items-center justify-center min-h-[48px]"
|
||||
style={{
|
||||
backgroundColor:
|
||||
noteText.trim() && !isSubmitting
|
||||
? theme.colors.actionPrimary
|
||||
: theme.colors.backgroundTertiary,
|
||||
}}
|
||||
onPress={handleSubmit}
|
||||
disabled={!noteText.trim() || isSubmitting}
|
||||
>
|
||||
@@ -130,14 +97,12 @@ export default function AddNoteModal({
|
||||
/>
|
||||
) : (
|
||||
<Text
|
||||
style={[
|
||||
styles.buttonText,
|
||||
{
|
||||
color: noteText.trim()
|
||||
? theme.colors.textInverse
|
||||
: theme.colors.textTertiary,
|
||||
},
|
||||
]}
|
||||
className="text-[15px] font-bold"
|
||||
style={{
|
||||
color: noteText.trim()
|
||||
? theme.colors.textInverse
|
||||
: theme.colors.textTertiary,
|
||||
}}
|
||||
>
|
||||
Submit
|
||||
</Text>
|
||||
@@ -149,41 +114,3 @@ export default function AddNoteModal({
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
const styles: ReturnType<typeof StyleSheet.create> = StyleSheet.create({
|
||||
overlay: {
|
||||
flex: 1,
|
||||
backgroundColor: "rgba(0,0,0,0.4)",
|
||||
justifyContent: "flex-end",
|
||||
},
|
||||
container: {
|
||||
borderTopLeftRadius: 24,
|
||||
borderTopRightRadius: 24,
|
||||
padding: 20,
|
||||
paddingBottom: 36,
|
||||
},
|
||||
input: {
|
||||
minHeight: 120,
|
||||
borderRadius: 14,
|
||||
borderWidth: 1,
|
||||
padding: 12,
|
||||
fontSize: 15,
|
||||
},
|
||||
buttonRow: {
|
||||
flexDirection: "row",
|
||||
gap: 12,
|
||||
marginTop: 16,
|
||||
},
|
||||
button: {
|
||||
flex: 1,
|
||||
paddingVertical: 14,
|
||||
borderRadius: 14,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
minHeight: 48,
|
||||
},
|
||||
buttonText: {
|
||||
fontSize: 15,
|
||||
fontWeight: "700",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import { View, Text, StyleSheet, TouchableOpacity } from "react-native";
|
||||
import { View, Text, TouchableOpacity } from "react-native";
|
||||
import { useTheme } from "../theme";
|
||||
import { rgbToHex } from "../utils/color";
|
||||
import { formatRelativeTime } from "../utils/date";
|
||||
@@ -28,49 +28,34 @@ export default function AlertCard({
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.card,
|
||||
theme.shadows.sm,
|
||||
{
|
||||
backgroundColor: theme.colors.backgroundElevated,
|
||||
},
|
||||
]}
|
||||
className="p-[18px] rounded-2xl mb-3 bg-bg-elevated shadow-sm"
|
||||
onPress={onPress}
|
||||
activeOpacity={0.7}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={`Alert ${alert.alertNumberWithPrefix || alert.alertNumber}, ${alert.title}. State: ${alert.currentAlertState?.name ?? "unknown"}. Severity: ${alert.alertSeverity?.name ?? "unknown"}.`}
|
||||
>
|
||||
<View style={styles.topRow}>
|
||||
<Text style={[styles.number, { color: theme.colors.textTertiary }]}>
|
||||
<View className="flex-row justify-between items-center mb-1.5">
|
||||
<Text className="text-[13px] font-semibold text-text-tertiary">
|
||||
{alert.alertNumberWithPrefix || `#${alert.alertNumber}`}
|
||||
</Text>
|
||||
<Text style={[styles.time, { color: theme.colors.textTertiary }]}>
|
||||
{timeString}
|
||||
</Text>
|
||||
<Text className="text-xs text-text-tertiary">{timeString}</Text>
|
||||
</View>
|
||||
|
||||
<Text
|
||||
style={[
|
||||
theme.typography.bodyLarge,
|
||||
{ color: theme.colors.textPrimary, fontWeight: "600" },
|
||||
]}
|
||||
className="text-body-lg text-text-primary font-semibold"
|
||||
numberOfLines={2}
|
||||
>
|
||||
{alert.title}
|
||||
</Text>
|
||||
|
||||
<View style={styles.badgeRow}>
|
||||
<View className="flex-row flex-wrap gap-2 mt-2.5">
|
||||
{alert.currentAlertState ? (
|
||||
<View
|
||||
style={[
|
||||
styles.badge,
|
||||
{ backgroundColor: theme.colors.backgroundTertiary },
|
||||
]}
|
||||
>
|
||||
<View style={[styles.dot, { backgroundColor: stateColor }]} />
|
||||
<Text
|
||||
style={[styles.badgeText, { color: theme.colors.textPrimary }]}
|
||||
>
|
||||
<View className="flex-row items-center px-2 py-1 rounded-md bg-bg-tertiary">
|
||||
<View
|
||||
className="w-2 h-2 rounded-full mr-1.5"
|
||||
style={{ backgroundColor: stateColor }}
|
||||
/>
|
||||
<Text className="text-xs font-semibold text-text-primary">
|
||||
{alert.currentAlertState.name}
|
||||
</Text>
|
||||
</View>
|
||||
@@ -78,9 +63,13 @@ export default function AlertCard({
|
||||
|
||||
{alert.alertSeverity ? (
|
||||
<View
|
||||
style={[styles.badge, { backgroundColor: severityColor + "26" }]}
|
||||
className="flex-row items-center px-2 py-1 rounded-md"
|
||||
style={{ backgroundColor: severityColor + "26" }}
|
||||
>
|
||||
<Text style={[styles.badgeText, { color: severityColor }]}>
|
||||
<Text
|
||||
className="text-xs font-semibold"
|
||||
style={{ color: severityColor }}
|
||||
>
|
||||
{alert.alertSeverity.name}
|
||||
</Text>
|
||||
</View>
|
||||
@@ -88,61 +77,10 @@ export default function AlertCard({
|
||||
</View>
|
||||
|
||||
{alert.monitor ? (
|
||||
<Text
|
||||
style={[styles.monitor, { color: theme.colors.textSecondary }]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
<Text className="text-xs text-text-secondary mt-2" numberOfLines={1}>
|
||||
{alert.monitor.name}
|
||||
</Text>
|
||||
) : null}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
const styles: ReturnType<typeof StyleSheet.create> = StyleSheet.create({
|
||||
card: {
|
||||
padding: 18,
|
||||
borderRadius: 16,
|
||||
marginBottom: 12,
|
||||
},
|
||||
topRow: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
marginBottom: 6,
|
||||
},
|
||||
number: {
|
||||
fontSize: 13,
|
||||
fontWeight: "600",
|
||||
},
|
||||
time: {
|
||||
fontSize: 12,
|
||||
},
|
||||
badgeRow: {
|
||||
flexDirection: "row",
|
||||
flexWrap: "wrap",
|
||||
gap: 8,
|
||||
marginTop: 10,
|
||||
},
|
||||
badge: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 4,
|
||||
borderRadius: 6,
|
||||
},
|
||||
dot: {
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
marginRight: 6,
|
||||
},
|
||||
badgeText: {
|
||||
fontSize: 12,
|
||||
fontWeight: "600",
|
||||
},
|
||||
monitor: {
|
||||
fontSize: 12,
|
||||
marginTop: 8,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import { View, Text, StyleSheet } from "react-native";
|
||||
import { View, Text } from "react-native";
|
||||
import { useTheme } from "../theme";
|
||||
|
||||
type EmptyIcon = "incidents" | "alerts" | "episodes" | "notes" | "default";
|
||||
@@ -10,22 +10,32 @@ interface EmptyStateProps {
|
||||
icon?: EmptyIcon;
|
||||
}
|
||||
|
||||
function EmptyIcon({
|
||||
function EmptyIconView({
|
||||
icon,
|
||||
color,
|
||||
}: {
|
||||
icon: EmptyIcon;
|
||||
color: string;
|
||||
}): React.JSX.Element {
|
||||
/*
|
||||
* Simple geometric SVG-style icons using View primitives
|
||||
* Monochrome, clean, professional — not cartoon/playful
|
||||
*/
|
||||
if (icon === "incidents") {
|
||||
return (
|
||||
<View style={styles.iconContainer}>
|
||||
<View style={[styles.iconShield, { borderColor: color }]}>
|
||||
<View style={[styles.iconCheckmark, { backgroundColor: color }]} />
|
||||
<View className="w-16 h-16 items-center justify-center">
|
||||
<View
|
||||
className="w-[44px] h-[52px] rounded-md items-center justify-center"
|
||||
style={{
|
||||
borderWidth: 1.5,
|
||||
borderColor: color,
|
||||
borderBottomLeftRadius: 22,
|
||||
borderBottomRightRadius: 22,
|
||||
}}
|
||||
>
|
||||
<View
|
||||
className="w-4 h-[3px] rounded-sm"
|
||||
style={{
|
||||
backgroundColor: color,
|
||||
transform: [{ rotate: "-45deg" }],
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
@@ -33,9 +43,21 @@ function EmptyIcon({
|
||||
|
||||
if (icon === "alerts") {
|
||||
return (
|
||||
<View style={styles.iconContainer}>
|
||||
<View style={[styles.iconBell, { borderColor: color }]}>
|
||||
<View style={[styles.iconBellClapper, { backgroundColor: color }]} />
|
||||
<View className="w-16 h-16 items-center justify-center">
|
||||
<View
|
||||
className="w-9 h-9 items-center justify-end pb-1"
|
||||
style={{
|
||||
borderWidth: 1.5,
|
||||
borderColor: color,
|
||||
borderRadius: 18,
|
||||
borderBottomLeftRadius: 4,
|
||||
borderBottomRightRadius: 4,
|
||||
}}
|
||||
>
|
||||
<View
|
||||
className="w-2 h-2 rounded-full"
|
||||
style={{ backgroundColor: color }}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
@@ -43,20 +65,29 @@ function EmptyIcon({
|
||||
|
||||
if (icon === "episodes") {
|
||||
return (
|
||||
<View style={styles.iconContainer}>
|
||||
<View style={[styles.iconStack, { borderColor: color }]} />
|
||||
<View className="w-16 h-16 items-center justify-center">
|
||||
<View
|
||||
style={[styles.iconStackBack, { borderColor: color, opacity: 0.4 }]}
|
||||
className="w-10 h-8 rounded-lg absolute top-3"
|
||||
style={{ borderWidth: 1.5, borderColor: color }}
|
||||
/>
|
||||
<View
|
||||
className="w-8 h-7 rounded-md absolute top-1.5"
|
||||
style={{ borderWidth: 1.5, borderColor: color, opacity: 0.4 }}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
// Default: simple circle with line through it
|
||||
return (
|
||||
<View style={styles.iconContainer}>
|
||||
<View style={[styles.iconCircle, { borderColor: color }]}>
|
||||
<View style={[styles.iconLine, { backgroundColor: color }]} />
|
||||
<View className="w-16 h-16 items-center justify-center">
|
||||
<View
|
||||
className="w-12 h-12 rounded-full items-center justify-center"
|
||||
style={{ borderWidth: 1.5, borderColor: color }}
|
||||
>
|
||||
<View
|
||||
className="w-5 h-0.5 rounded-sm"
|
||||
style={{ backgroundColor: color }}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
@@ -70,116 +101,16 @@ export default function EmptyState({
|
||||
const { theme } = useTheme();
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<EmptyIcon icon={icon} color={theme.colors.textTertiary} />
|
||||
<Text
|
||||
style={[
|
||||
theme.typography.titleSmall,
|
||||
{
|
||||
color: theme.colors.textPrimary,
|
||||
textAlign: "center",
|
||||
marginTop: 20,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<View className="flex-1 items-center justify-center px-10 py-20">
|
||||
<EmptyIconView icon={icon} color={theme.colors.textTertiary} />
|
||||
<Text className="text-title-sm text-text-primary text-center mt-5">
|
||||
{title}
|
||||
</Text>
|
||||
{subtitle ? (
|
||||
<Text
|
||||
style={[
|
||||
theme.typography.bodyMedium,
|
||||
{
|
||||
color: theme.colors.textSecondary,
|
||||
textAlign: "center",
|
||||
marginTop: theme.spacing.sm,
|
||||
lineHeight: 20,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Text className="text-body-md text-text-secondary text-center mt-2 leading-5">
|
||||
{subtitle}
|
||||
</Text>
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles: ReturnType<typeof StyleSheet.create> = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
paddingHorizontal: 40,
|
||||
paddingVertical: 80,
|
||||
},
|
||||
iconContainer: {
|
||||
width: 64,
|
||||
height: 64,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
// Shield icon (incidents)
|
||||
iconShield: {
|
||||
width: 44,
|
||||
height: 52,
|
||||
borderWidth: 1.5,
|
||||
borderRadius: 6,
|
||||
borderBottomLeftRadius: 22,
|
||||
borderBottomRightRadius: 22,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
iconCheckmark: {
|
||||
width: 16,
|
||||
height: 3,
|
||||
borderRadius: 1.5,
|
||||
transform: [{ rotate: "-45deg" }],
|
||||
},
|
||||
// Bell icon (alerts)
|
||||
iconBell: {
|
||||
width: 36,
|
||||
height: 36,
|
||||
borderWidth: 1.5,
|
||||
borderRadius: 18,
|
||||
borderBottomLeftRadius: 4,
|
||||
borderBottomRightRadius: 4,
|
||||
alignItems: "center",
|
||||
justifyContent: "flex-end",
|
||||
paddingBottom: 4,
|
||||
},
|
||||
iconBellClapper: {
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
},
|
||||
// Stack icon (episodes)
|
||||
iconStack: {
|
||||
width: 40,
|
||||
height: 32,
|
||||
borderWidth: 1.5,
|
||||
borderRadius: 8,
|
||||
position: "absolute",
|
||||
top: 12,
|
||||
},
|
||||
iconStackBack: {
|
||||
width: 32,
|
||||
height: 28,
|
||||
borderWidth: 1.5,
|
||||
borderRadius: 6,
|
||||
position: "absolute",
|
||||
top: 6,
|
||||
},
|
||||
// Default circle icon
|
||||
iconCircle: {
|
||||
width: 48,
|
||||
height: 48,
|
||||
borderWidth: 1.5,
|
||||
borderRadius: 24,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
iconLine: {
|
||||
width: 20,
|
||||
height: 2,
|
||||
borderRadius: 1,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import { View, Text, StyleSheet, TouchableOpacity } from "react-native";
|
||||
import { View, Text, TouchableOpacity } from "react-native";
|
||||
import { useTheme } from "../theme";
|
||||
import { rgbToHex } from "../utils/color";
|
||||
import { formatRelativeTime } from "../utils/date";
|
||||
@@ -56,47 +56,32 @@ export default function EpisodeCard(
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.card,
|
||||
theme.shadows.sm,
|
||||
{
|
||||
backgroundColor: theme.colors.backgroundElevated,
|
||||
},
|
||||
]}
|
||||
className="p-[18px] rounded-2xl mb-3 bg-bg-elevated shadow-sm"
|
||||
onPress={onPress}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<View style={styles.topRow}>
|
||||
<Text style={[styles.number, { color: theme.colors.textTertiary }]}>
|
||||
<View className="flex-row justify-between items-center mb-1.5">
|
||||
<Text className="text-[13px] font-semibold text-text-tertiary">
|
||||
{episode.episodeNumberWithPrefix || `#${episode.episodeNumber}`}
|
||||
</Text>
|
||||
<Text style={[styles.time, { color: theme.colors.textTertiary }]}>
|
||||
{timeString}
|
||||
</Text>
|
||||
<Text className="text-xs text-text-tertiary">{timeString}</Text>
|
||||
</View>
|
||||
|
||||
<Text
|
||||
style={[
|
||||
theme.typography.bodyLarge,
|
||||
{ color: theme.colors.textPrimary, fontWeight: "600" },
|
||||
]}
|
||||
className="text-body-lg text-text-primary font-semibold"
|
||||
numberOfLines={2}
|
||||
>
|
||||
{episode.title}
|
||||
</Text>
|
||||
|
||||
<View style={styles.badgeRow}>
|
||||
<View className="flex-row flex-wrap gap-2 mt-2.5">
|
||||
{state ? (
|
||||
<View
|
||||
style={[
|
||||
styles.badge,
|
||||
{ backgroundColor: theme.colors.backgroundTertiary },
|
||||
]}
|
||||
>
|
||||
<View style={[styles.dot, { backgroundColor: stateColor }]} />
|
||||
<Text
|
||||
style={[styles.badgeText, { color: theme.colors.textPrimary }]}
|
||||
>
|
||||
<View className="flex-row items-center px-2 py-1 rounded-md bg-bg-tertiary">
|
||||
<View
|
||||
className="w-2 h-2 rounded-full mr-1.5"
|
||||
style={{ backgroundColor: stateColor }}
|
||||
/>
|
||||
<Text className="text-xs font-semibold text-text-primary">
|
||||
{state.name}
|
||||
</Text>
|
||||
</View>
|
||||
@@ -104,9 +89,13 @@ export default function EpisodeCard(
|
||||
|
||||
{severity ? (
|
||||
<View
|
||||
style={[styles.badge, { backgroundColor: severityColor + "26" }]}
|
||||
className="flex-row items-center px-2 py-1 rounded-md"
|
||||
style={{ backgroundColor: severityColor + "26" }}
|
||||
>
|
||||
<Text style={[styles.badgeText, { color: severityColor }]}>
|
||||
<Text
|
||||
className="text-xs font-semibold"
|
||||
style={{ color: severityColor }}
|
||||
>
|
||||
{severity.name}
|
||||
</Text>
|
||||
</View>
|
||||
@@ -114,9 +103,7 @@ export default function EpisodeCard(
|
||||
</View>
|
||||
|
||||
{childCount > 0 ? (
|
||||
<Text
|
||||
style={[styles.childCount, { color: theme.colors.textSecondary }]}
|
||||
>
|
||||
<Text className="text-xs text-text-secondary mt-2">
|
||||
{childCount} {type === "incident" ? "incident" : "alert"}
|
||||
{childCount !== 1 ? "s" : ""}
|
||||
</Text>
|
||||
@@ -124,51 +111,3 @@ export default function EpisodeCard(
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
const styles: ReturnType<typeof StyleSheet.create> = StyleSheet.create({
|
||||
card: {
|
||||
padding: 18,
|
||||
borderRadius: 16,
|
||||
marginBottom: 12,
|
||||
},
|
||||
topRow: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
marginBottom: 6,
|
||||
},
|
||||
number: {
|
||||
fontSize: 13,
|
||||
fontWeight: "600",
|
||||
},
|
||||
time: {
|
||||
fontSize: 12,
|
||||
},
|
||||
badgeRow: {
|
||||
flexDirection: "row",
|
||||
flexWrap: "wrap",
|
||||
gap: 8,
|
||||
marginTop: 10,
|
||||
},
|
||||
badge: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 4,
|
||||
borderRadius: 6,
|
||||
},
|
||||
dot: {
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
marginRight: 6,
|
||||
},
|
||||
badgeText: {
|
||||
fontSize: 12,
|
||||
fontWeight: "600",
|
||||
},
|
||||
childCount: {
|
||||
fontSize: 12,
|
||||
marginTop: 8,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import { View, Text, StyleSheet, TouchableOpacity } from "react-native";
|
||||
import { View, Text, TouchableOpacity } from "react-native";
|
||||
import { useTheme } from "../theme";
|
||||
import { rgbToHex } from "../utils/color";
|
||||
import { formatRelativeTime } from "../utils/date";
|
||||
@@ -31,49 +31,34 @@ export default function IncidentCard({
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.card,
|
||||
theme.shadows.sm,
|
||||
{
|
||||
backgroundColor: theme.colors.backgroundElevated,
|
||||
},
|
||||
]}
|
||||
className="p-[18px] rounded-2xl mb-3 bg-bg-elevated shadow-sm"
|
||||
onPress={onPress}
|
||||
activeOpacity={0.7}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={`Incident ${incident.incidentNumberWithPrefix || incident.incidentNumber}, ${incident.title}. State: ${incident.currentIncidentState?.name ?? "unknown"}. Severity: ${incident.incidentSeverity?.name ?? "unknown"}.`}
|
||||
>
|
||||
<View style={styles.topRow}>
|
||||
<Text style={[styles.number, { color: theme.colors.textTertiary }]}>
|
||||
<View className="flex-row justify-between items-center mb-1.5">
|
||||
<Text className="text-[13px] font-semibold text-text-tertiary">
|
||||
{incident.incidentNumberWithPrefix || `#${incident.incidentNumber}`}
|
||||
</Text>
|
||||
<Text style={[styles.time, { color: theme.colors.textTertiary }]}>
|
||||
{timeString}
|
||||
</Text>
|
||||
<Text className="text-xs text-text-tertiary">{timeString}</Text>
|
||||
</View>
|
||||
|
||||
<Text
|
||||
style={[
|
||||
theme.typography.bodyLarge,
|
||||
{ color: theme.colors.textPrimary, fontWeight: "600" },
|
||||
]}
|
||||
className="text-body-lg text-text-primary font-semibold"
|
||||
numberOfLines={2}
|
||||
>
|
||||
{incident.title}
|
||||
</Text>
|
||||
|
||||
<View style={styles.badgeRow}>
|
||||
<View className="flex-row flex-wrap gap-2 mt-2.5">
|
||||
{incident.currentIncidentState ? (
|
||||
<View
|
||||
style={[
|
||||
styles.badge,
|
||||
{ backgroundColor: theme.colors.backgroundTertiary },
|
||||
]}
|
||||
>
|
||||
<View style={[styles.dot, { backgroundColor: stateColor }]} />
|
||||
<Text
|
||||
style={[styles.badgeText, { color: theme.colors.textPrimary }]}
|
||||
>
|
||||
<View className="flex-row items-center px-2 py-1 rounded-md bg-bg-tertiary">
|
||||
<View
|
||||
className="w-2 h-2 rounded-full mr-1.5"
|
||||
style={{ backgroundColor: stateColor }}
|
||||
/>
|
||||
<Text className="text-xs font-semibold text-text-primary">
|
||||
{incident.currentIncidentState.name}
|
||||
</Text>
|
||||
</View>
|
||||
@@ -81,9 +66,13 @@ export default function IncidentCard({
|
||||
|
||||
{incident.incidentSeverity ? (
|
||||
<View
|
||||
style={[styles.badge, { backgroundColor: severityColor + "26" }]}
|
||||
className="flex-row items-center px-2 py-1 rounded-md"
|
||||
style={{ backgroundColor: severityColor + "26" }}
|
||||
>
|
||||
<Text style={[styles.badgeText, { color: severityColor }]}>
|
||||
<Text
|
||||
className="text-xs font-semibold"
|
||||
style={{ color: severityColor }}
|
||||
>
|
||||
{incident.incidentSeverity.name}
|
||||
</Text>
|
||||
</View>
|
||||
@@ -91,10 +80,7 @@ export default function IncidentCard({
|
||||
</View>
|
||||
|
||||
{monitorCount > 0 ? (
|
||||
<Text
|
||||
style={[styles.monitors, { color: theme.colors.textSecondary }]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
<Text className="text-xs text-text-secondary mt-2" numberOfLines={1}>
|
||||
{incident.monitors
|
||||
.map((m: NamedEntity) => {
|
||||
return m.name;
|
||||
@@ -105,51 +91,3 @@ export default function IncidentCard({
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
const styles: ReturnType<typeof StyleSheet.create> = StyleSheet.create({
|
||||
card: {
|
||||
padding: 18,
|
||||
borderRadius: 16,
|
||||
marginBottom: 12,
|
||||
},
|
||||
topRow: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
marginBottom: 6,
|
||||
},
|
||||
number: {
|
||||
fontSize: 13,
|
||||
fontWeight: "600",
|
||||
},
|
||||
time: {
|
||||
fontSize: 12,
|
||||
},
|
||||
badgeRow: {
|
||||
flexDirection: "row",
|
||||
flexWrap: "wrap",
|
||||
gap: 8,
|
||||
marginTop: 10,
|
||||
},
|
||||
badge: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 4,
|
||||
borderRadius: 6,
|
||||
},
|
||||
dot: {
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
marginRight: 6,
|
||||
},
|
||||
badgeText: {
|
||||
fontSize: 12,
|
||||
fontWeight: "600",
|
||||
},
|
||||
monitors: {
|
||||
fontSize: 12,
|
||||
marginTop: 8,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import { View, Text, StyleSheet, Animated } from "react-native";
|
||||
import { View, Text, Animated } from "react-native";
|
||||
import { useTheme } from "../theme";
|
||||
import { useNetworkStatus } from "../hooks/useNetworkStatus";
|
||||
|
||||
@@ -25,51 +25,18 @@ export default function OfflineBanner(): React.JSX.Element | null {
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.container,
|
||||
{
|
||||
backgroundColor: theme.colors.statusError,
|
||||
transform: [{ translateY: slideAnim }],
|
||||
},
|
||||
]}
|
||||
className="absolute top-0 left-0 right-0 z-[100] pt-[50px] pb-2 px-4"
|
||||
style={{
|
||||
backgroundColor: theme.colors.statusError,
|
||||
transform: [{ translateY: slideAnim }],
|
||||
}}
|
||||
>
|
||||
<View style={styles.content}>
|
||||
<View style={styles.dot} />
|
||||
<Text style={[styles.text, { color: "#FFFFFF" }]}>
|
||||
<View className="flex-row items-center justify-center">
|
||||
<View className="w-1.5 h-1.5 rounded-full bg-white mr-2 opacity-80" />
|
||||
<Text className="text-[13px] font-semibold tracking-tight text-white">
|
||||
No internet connection
|
||||
</Text>
|
||||
</View>
|
||||
</Animated.View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles: ReturnType<typeof StyleSheet.create> = StyleSheet.create({
|
||||
container: {
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
zIndex: 100,
|
||||
paddingTop: 50,
|
||||
paddingBottom: 8,
|
||||
paddingHorizontal: 16,
|
||||
},
|
||||
content: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
dot: {
|
||||
width: 6,
|
||||
height: 6,
|
||||
borderRadius: 3,
|
||||
backgroundColor: "#FFFFFF",
|
||||
marginRight: 8,
|
||||
opacity: 0.8,
|
||||
},
|
||||
text: {
|
||||
fontSize: 13,
|
||||
fontWeight: "600",
|
||||
letterSpacing: 0.2,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React from "react";
|
||||
import { View, Text, StyleSheet } from "react-native";
|
||||
import { useTheme } from "../theme";
|
||||
import { View, Text } from "react-native";
|
||||
|
||||
interface ProjectBadgeProps {
|
||||
name: string;
|
||||
@@ -11,35 +10,15 @@ export default function ProjectBadge({
|
||||
name,
|
||||
color,
|
||||
}: ProjectBadgeProps): React.JSX.Element {
|
||||
const { theme } = useTheme();
|
||||
|
||||
const dotColor: string = color || theme.colors.actionPrimary;
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={[styles.dot, { backgroundColor: dotColor }]} />
|
||||
<Text
|
||||
style={[
|
||||
theme.typography.bodySmall,
|
||||
{ color: theme.colors.textSecondary },
|
||||
]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
<View className="flex-row items-center">
|
||||
<View
|
||||
className="w-2 h-2 rounded-full mr-1.5"
|
||||
style={color ? { backgroundColor: color } : undefined}
|
||||
/>
|
||||
<Text className="text-body-sm text-text-secondary" numberOfLines={1}>
|
||||
{name}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles: ReturnType<typeof StyleSheet.create> = StyleSheet.create({
|
||||
container: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
},
|
||||
dot: {
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
marginRight: 6,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import { View, Text, StyleSheet } from "react-native";
|
||||
import { View, Text } from "react-native";
|
||||
import { useTheme } from "../theme";
|
||||
|
||||
export type SeverityLevel = "critical" | "major" | "minor" | "warning" | "info";
|
||||
@@ -42,24 +42,16 @@ export default function SeverityBadge({
|
||||
const displayLabel: string = label || severity;
|
||||
|
||||
return (
|
||||
<View style={[styles.badge, { backgroundColor: colors.bg }]}>
|
||||
<Text style={[styles.text, { color: colors.text }]}>
|
||||
<View
|
||||
className="px-2 py-1 rounded-md self-start"
|
||||
style={{ backgroundColor: colors.bg }}
|
||||
>
|
||||
<Text
|
||||
className="text-xs font-semibold tracking-wide"
|
||||
style={{ color: colors.text }}
|
||||
>
|
||||
{displayLabel.toUpperCase()}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles: ReturnType<typeof StyleSheet.create> = StyleSheet.create({
|
||||
badge: {
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 4,
|
||||
borderRadius: 6,
|
||||
alignSelf: "flex-start",
|
||||
},
|
||||
text: {
|
||||
fontSize: 12,
|
||||
fontWeight: "600",
|
||||
letterSpacing: 0.5,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import {
|
||||
View,
|
||||
StyleSheet,
|
||||
Animated,
|
||||
DimensionValue,
|
||||
AccessibilityInfo,
|
||||
} from "react-native";
|
||||
import { View, Animated, DimensionValue, AccessibilityInfo } from "react-native";
|
||||
import { useTheme } from "../theme";
|
||||
|
||||
interface SkeletonCardProps {
|
||||
@@ -60,36 +54,19 @@ export default function SkeletonCard({
|
||||
if (variant === "compact") {
|
||||
return (
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.card,
|
||||
{
|
||||
backgroundColor: theme.colors.backgroundElevated,
|
||||
opacity,
|
||||
},
|
||||
]}
|
||||
className="p-[18px] rounded-2xl mb-3"
|
||||
style={{
|
||||
backgroundColor: theme.colors.backgroundElevated,
|
||||
opacity,
|
||||
}}
|
||||
accessibilityLabel="Loading content"
|
||||
accessibilityRole="progressbar"
|
||||
>
|
||||
<View style={styles.compactRow}>
|
||||
<View
|
||||
style={[
|
||||
styles.compactBadge,
|
||||
{ backgroundColor: theme.colors.backgroundTertiary },
|
||||
]}
|
||||
/>
|
||||
<View
|
||||
style={[
|
||||
styles.compactTime,
|
||||
{ backgroundColor: theme.colors.backgroundTertiary },
|
||||
]}
|
||||
/>
|
||||
<View className="flex-row justify-between items-center mb-2.5">
|
||||
<View className="h-3.5 w-12 rounded bg-bg-tertiary" />
|
||||
<View className="h-3 w-8 rounded bg-bg-tertiary" />
|
||||
</View>
|
||||
<View
|
||||
style={[
|
||||
styles.titleLine,
|
||||
{ backgroundColor: theme.colors.backgroundTertiary, width: "75%" },
|
||||
]}
|
||||
/>
|
||||
<View className="h-[18px] rounded w-3/4 mb-3 bg-bg-tertiary" />
|
||||
</Animated.View>
|
||||
);
|
||||
}
|
||||
@@ -97,57 +74,28 @@ export default function SkeletonCard({
|
||||
if (variant === "detail") {
|
||||
return (
|
||||
<Animated.View
|
||||
style={[styles.detailContainer, { opacity }]}
|
||||
className="p-5"
|
||||
style={{ opacity }}
|
||||
accessibilityLabel="Loading content"
|
||||
accessibilityRole="progressbar"
|
||||
>
|
||||
{/* Badge row */}
|
||||
<View style={styles.badgeRow}>
|
||||
<View
|
||||
style={[
|
||||
styles.badgeSkeleton,
|
||||
{ backgroundColor: theme.colors.backgroundTertiary },
|
||||
]}
|
||||
/>
|
||||
<View
|
||||
style={[
|
||||
styles.badgeSkeleton,
|
||||
{ backgroundColor: theme.colors.backgroundTertiary, width: 64 },
|
||||
]}
|
||||
/>
|
||||
<View className="flex-row gap-2 mb-3">
|
||||
<View className="h-6 w-20 rounded-md bg-bg-tertiary" />
|
||||
<View className="h-6 w-16 rounded-md bg-bg-tertiary" />
|
||||
</View>
|
||||
{/* Title */}
|
||||
<View className="h-6 w-4/5 rounded mb-5 bg-bg-tertiary" />
|
||||
<View
|
||||
style={[
|
||||
styles.detailTitle,
|
||||
{ backgroundColor: theme.colors.backgroundTertiary },
|
||||
]}
|
||||
/>
|
||||
{/* Detail card */}
|
||||
<View
|
||||
style={[
|
||||
styles.detailCard,
|
||||
{
|
||||
backgroundColor: theme.colors.backgroundSecondary,
|
||||
borderColor: theme.colors.borderSubtle,
|
||||
},
|
||||
]}
|
||||
className="rounded-2xl p-4"
|
||||
style={{
|
||||
backgroundColor: theme.colors.backgroundSecondary,
|
||||
borderColor: theme.colors.borderSubtle,
|
||||
}}
|
||||
>
|
||||
{Array.from({ length: 3 }).map((_: unknown, index: number) => {
|
||||
return (
|
||||
<View key={index} style={styles.detailRow}>
|
||||
<View
|
||||
style={[
|
||||
styles.detailLabel,
|
||||
{ backgroundColor: theme.colors.backgroundTertiary },
|
||||
]}
|
||||
/>
|
||||
<View
|
||||
style={[
|
||||
styles.detailValue,
|
||||
{ backgroundColor: theme.colors.backgroundTertiary },
|
||||
]}
|
||||
/>
|
||||
<View key={index} className="flex-row mb-3">
|
||||
<View className="h-3.5 w-20 rounded mr-4 bg-bg-tertiary" />
|
||||
<View className="h-3.5 w-[120px] rounded bg-bg-tertiary" />
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
@@ -158,67 +106,31 @@ export default function SkeletonCard({
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.card,
|
||||
{
|
||||
backgroundColor: theme.colors.backgroundSecondary,
|
||||
borderColor: theme.colors.borderSubtle,
|
||||
opacity,
|
||||
},
|
||||
]}
|
||||
className="p-[18px] rounded-2xl mb-3"
|
||||
style={{
|
||||
backgroundColor: theme.colors.backgroundSecondary,
|
||||
borderColor: theme.colors.borderSubtle,
|
||||
opacity,
|
||||
}}
|
||||
accessibilityLabel="Loading content"
|
||||
accessibilityRole="progressbar"
|
||||
>
|
||||
{/* Top row: badge + time */}
|
||||
<View style={styles.topRow}>
|
||||
<View
|
||||
style={[
|
||||
styles.numberSkeleton,
|
||||
{ backgroundColor: theme.colors.backgroundTertiary },
|
||||
]}
|
||||
/>
|
||||
<View
|
||||
style={[
|
||||
styles.timeSkeleton,
|
||||
{ backgroundColor: theme.colors.backgroundTertiary },
|
||||
]}
|
||||
/>
|
||||
<View className="flex-row justify-between items-center mb-3">
|
||||
<View className="h-3.5 w-12 rounded bg-bg-tertiary" />
|
||||
<View className="h-3 w-9 rounded bg-bg-tertiary" />
|
||||
</View>
|
||||
{/* Title */}
|
||||
<View
|
||||
style={[
|
||||
styles.titleLine,
|
||||
{ backgroundColor: theme.colors.backgroundTertiary },
|
||||
]}
|
||||
/>
|
||||
{/* Badge row */}
|
||||
<View style={styles.badgeRow}>
|
||||
<View
|
||||
style={[
|
||||
styles.badgeSkeleton,
|
||||
{ backgroundColor: theme.colors.backgroundTertiary },
|
||||
]}
|
||||
/>
|
||||
<View
|
||||
style={[
|
||||
styles.badgeSkeleton,
|
||||
{ backgroundColor: theme.colors.backgroundTertiary, width: 56 },
|
||||
]}
|
||||
/>
|
||||
<View className="h-[18px] rounded w-[70%] mb-3 bg-bg-tertiary" />
|
||||
<View className="flex-row gap-2 mb-3">
|
||||
<View className="h-6 w-20 rounded-md bg-bg-tertiary" />
|
||||
<View className="h-6 w-14 rounded-md bg-bg-tertiary" />
|
||||
</View>
|
||||
{/* Body lines */}
|
||||
{Array.from({ length: Math.max(lines - 1, 1) }).map(
|
||||
(_: unknown, index: number) => {
|
||||
return (
|
||||
<View
|
||||
key={index}
|
||||
style={[
|
||||
styles.line,
|
||||
{
|
||||
backgroundColor: theme.colors.backgroundTertiary,
|
||||
width: lineWidths[index % lineWidths.length],
|
||||
},
|
||||
]}
|
||||
className="h-3 rounded mb-2 bg-bg-tertiary"
|
||||
style={{ width: lineWidths[index % lineWidths.length] }}
|
||||
/>
|
||||
);
|
||||
},
|
||||
@@ -226,94 +138,3 @@ export default function SkeletonCard({
|
||||
</Animated.View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles: ReturnType<typeof StyleSheet.create> = StyleSheet.create({
|
||||
card: {
|
||||
padding: 18,
|
||||
borderRadius: 16,
|
||||
marginBottom: 12,
|
||||
},
|
||||
topRow: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
marginBottom: 12,
|
||||
},
|
||||
numberSkeleton: {
|
||||
height: 14,
|
||||
width: 48,
|
||||
borderRadius: 4,
|
||||
},
|
||||
timeSkeleton: {
|
||||
height: 12,
|
||||
width: 36,
|
||||
borderRadius: 4,
|
||||
},
|
||||
titleLine: {
|
||||
height: 18,
|
||||
borderRadius: 4,
|
||||
width: "70%",
|
||||
marginBottom: 12,
|
||||
},
|
||||
badgeRow: {
|
||||
flexDirection: "row",
|
||||
gap: 8,
|
||||
marginBottom: 12,
|
||||
},
|
||||
badgeSkeleton: {
|
||||
height: 24,
|
||||
width: 80,
|
||||
borderRadius: 6,
|
||||
},
|
||||
line: {
|
||||
height: 12,
|
||||
borderRadius: 4,
|
||||
marginBottom: 8,
|
||||
},
|
||||
// Compact variant
|
||||
compactRow: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
marginBottom: 10,
|
||||
},
|
||||
compactBadge: {
|
||||
height: 14,
|
||||
width: 48,
|
||||
borderRadius: 4,
|
||||
},
|
||||
compactTime: {
|
||||
height: 12,
|
||||
width: 32,
|
||||
borderRadius: 4,
|
||||
},
|
||||
// Detail variant
|
||||
detailContainer: {
|
||||
padding: 20,
|
||||
},
|
||||
detailTitle: {
|
||||
height: 24,
|
||||
width: "80%",
|
||||
borderRadius: 4,
|
||||
marginBottom: 20,
|
||||
},
|
||||
detailCard: {
|
||||
borderRadius: 16,
|
||||
padding: 16,
|
||||
},
|
||||
detailRow: {
|
||||
flexDirection: "row",
|
||||
marginBottom: 12,
|
||||
},
|
||||
detailLabel: {
|
||||
height: 14,
|
||||
width: 80,
|
||||
borderRadius: 4,
|
||||
marginRight: 16,
|
||||
},
|
||||
detailValue: {
|
||||
height: 14,
|
||||
width: 120,
|
||||
borderRadius: 4,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import { View, Text, StyleSheet } from "react-native";
|
||||
import { View, Text } from "react-native";
|
||||
import { useTheme } from "../theme";
|
||||
|
||||
export type StateType =
|
||||
@@ -32,39 +32,14 @@ export default function StateBadge({
|
||||
const displayLabel: string = label || state;
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.badge,
|
||||
{
|
||||
backgroundColor: theme.colors.backgroundTertiary,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<View style={[styles.dot, { backgroundColor: color }]} />
|
||||
<Text style={[styles.text, { color: theme.colors.textPrimary }]}>
|
||||
<View className="flex-row items-center px-2 py-1 rounded-md self-start bg-bg-tertiary">
|
||||
<View
|
||||
className="w-2 h-2 rounded-full mr-1.5"
|
||||
style={{ backgroundColor: color }}
|
||||
/>
|
||||
<Text className="text-xs font-semibold text-text-primary">
|
||||
{displayLabel.charAt(0).toUpperCase() + displayLabel.slice(1)}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles: ReturnType<typeof StyleSheet.create> = StyleSheet.create({
|
||||
badge: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 4,
|
||||
borderRadius: 6,
|
||||
alignSelf: "flex-start",
|
||||
},
|
||||
dot: {
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
marginRight: 6,
|
||||
},
|
||||
text: {
|
||||
fontSize: 12,
|
||||
fontWeight: "600",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -2,7 +2,6 @@ import React, { useRef } from "react";
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
Animated,
|
||||
PanResponder,
|
||||
type GestureResponderEvent,
|
||||
@@ -48,7 +47,6 @@ export default function SwipeableCard({
|
||||
_: GestureResponderEvent,
|
||||
gestureState: PanResponderGestureState,
|
||||
) => {
|
||||
// Limit swipe range
|
||||
const maxSwipe: number = 120;
|
||||
let dx: number = gestureState.dx;
|
||||
if (!rightAction && dx < 0) {
|
||||
@@ -60,7 +58,6 @@ export default function SwipeableCard({
|
||||
dx = Math.max(-maxSwipe, Math.min(maxSwipe, dx));
|
||||
translateX.setValue(dx);
|
||||
|
||||
// Haptic feedback at threshold
|
||||
if (Math.abs(dx) >= SWIPE_THRESHOLD && !hasTriggeredHaptic.current) {
|
||||
hasTriggeredHaptic.current = true;
|
||||
mediumImpact();
|
||||
@@ -97,34 +94,35 @@ export default function SwipeableCard({
|
||||
).current;
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View className="overflow-hidden rounded-xl">
|
||||
{/* Background actions */}
|
||||
<View style={styles.actionsContainer}>
|
||||
<View className="absolute inset-0 flex-row justify-between items-center">
|
||||
{leftAction ? (
|
||||
<View
|
||||
style={[styles.actionLeft, { backgroundColor: leftAction.color }]}
|
||||
className="flex-1 h-full justify-center pl-5 rounded-xl"
|
||||
style={{ backgroundColor: leftAction.color }}
|
||||
>
|
||||
<Text style={styles.actionText}>{leftAction.label}</Text>
|
||||
<Text className="text-white text-sm font-bold tracking-tight">
|
||||
{leftAction.label}
|
||||
</Text>
|
||||
</View>
|
||||
) : null}
|
||||
{rightAction ? (
|
||||
<View
|
||||
style={[styles.actionRight, { backgroundColor: rightAction.color }]}
|
||||
className="flex-1 h-full justify-center items-end pr-5 rounded-xl"
|
||||
style={{ backgroundColor: rightAction.color }}
|
||||
>
|
||||
<Text style={styles.actionText}>{rightAction.label}</Text>
|
||||
<Text className="text-white text-sm font-bold tracking-tight">
|
||||
{rightAction.label}
|
||||
</Text>
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
{/* Foreground content */}
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.foreground,
|
||||
{
|
||||
backgroundColor: theme.colors.backgroundPrimary,
|
||||
transform: [{ translateX }],
|
||||
},
|
||||
]}
|
||||
className="z-[1] bg-bg-primary"
|
||||
style={{ transform: [{ translateX }] }}
|
||||
{...panResponder.panHandlers}
|
||||
>
|
||||
{children}
|
||||
@@ -132,40 +130,3 @@ export default function SwipeableCard({
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles: ReturnType<typeof StyleSheet.create> = StyleSheet.create({
|
||||
container: {
|
||||
overflow: "hidden",
|
||||
borderRadius: 12,
|
||||
},
|
||||
actionsContainer: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
},
|
||||
actionLeft: {
|
||||
flex: 1,
|
||||
height: "100%",
|
||||
justifyContent: "center",
|
||||
paddingLeft: 20,
|
||||
borderRadius: 12,
|
||||
},
|
||||
actionRight: {
|
||||
flex: 1,
|
||||
height: "100%",
|
||||
justifyContent: "center",
|
||||
alignItems: "flex-end",
|
||||
paddingRight: 20,
|
||||
borderRadius: 12,
|
||||
},
|
||||
actionText: {
|
||||
color: "#FFFFFF",
|
||||
fontSize: 14,
|
||||
fontWeight: "700",
|
||||
letterSpacing: 0.3,
|
||||
},
|
||||
foreground: {
|
||||
zIndex: 1,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -15,7 +15,7 @@ import AuthStackNavigator from "./AuthStackNavigator";
|
||||
import MainTabNavigator from "./MainTabNavigator";
|
||||
import ProjectSelectionScreen from "../screens/ProjectSelectionScreen";
|
||||
import BiometricLockScreen from "../screens/BiometricLockScreen";
|
||||
import { View, ActivityIndicator, StyleSheet } from "react-native";
|
||||
import { View, ActivityIndicator } from "react-native";
|
||||
|
||||
const prefix: string = Linking.createURL("/");
|
||||
|
||||
@@ -96,12 +96,7 @@ export default function RootNavigator(): React.JSX.Element {
|
||||
|
||||
if (isLoading || !biometricChecked) {
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.loading,
|
||||
{ backgroundColor: theme.colors.backgroundPrimary },
|
||||
]}
|
||||
>
|
||||
<View className="flex-1 items-center justify-center bg-bg-primary">
|
||||
<ActivityIndicator size="large" color={theme.colors.actionPrimary} />
|
||||
</View>
|
||||
);
|
||||
@@ -130,12 +125,7 @@ export default function RootNavigator(): React.JSX.Element {
|
||||
|
||||
if (isLoadingProjects) {
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.loading,
|
||||
{ backgroundColor: theme.colors.backgroundPrimary },
|
||||
]}
|
||||
>
|
||||
<View className="flex-1 items-center justify-center bg-bg-primary">
|
||||
<ActivityIndicator size="large" color={theme.colors.actionPrimary} />
|
||||
</View>
|
||||
);
|
||||
@@ -158,11 +148,3 @@ export default function RootNavigator(): React.JSX.Element {
|
||||
</NavigationContainer>
|
||||
);
|
||||
}
|
||||
|
||||
const styles: ReturnType<typeof StyleSheet.create> = StyleSheet.create({
|
||||
loading: {
|
||||
flex: 1,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
ActivityIndicator,
|
||||
RefreshControl,
|
||||
Alert,
|
||||
StyleSheet,
|
||||
} from "react-native";
|
||||
import type { NativeStackScreenProps } from "@react-navigation/native-stack";
|
||||
import { useTheme } from "../theme";
|
||||
@@ -128,9 +127,7 @@ export default function AlertDetailScreen({ route }: Props): React.JSX.Element {
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<View
|
||||
style={[{ flex: 1, backgroundColor: theme.colors.backgroundPrimary }]}
|
||||
>
|
||||
<View className="flex-1 bg-bg-primary">
|
||||
<SkeletonCard variant="detail" />
|
||||
</View>
|
||||
);
|
||||
@@ -138,18 +135,8 @@ export default function AlertDetailScreen({ route }: Props): React.JSX.Element {
|
||||
|
||||
if (!alert) {
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.centered,
|
||||
{ backgroundColor: theme.colors.backgroundPrimary },
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
theme.typography.bodyMedium,
|
||||
{ color: theme.colors.textSecondary },
|
||||
]}
|
||||
>
|
||||
<View className="flex-1 items-center justify-center bg-bg-primary">
|
||||
<Text className="text-body-md text-text-secondary">
|
||||
Alert not found.
|
||||
</Text>
|
||||
</View>
|
||||
@@ -180,46 +167,32 @@ export default function AlertDetailScreen({ route }: Props): React.JSX.Element {
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
style={[{ backgroundColor: theme.colors.backgroundPrimary }]}
|
||||
contentContainerStyle={styles.content}
|
||||
className="bg-bg-primary"
|
||||
contentContainerStyle={{ padding: 20, paddingBottom: 40 }}
|
||||
refreshControl={
|
||||
<RefreshControl refreshing={false} onRefresh={onRefresh} />
|
||||
}
|
||||
>
|
||||
{/* Header */}
|
||||
<View
|
||||
style={[
|
||||
styles.numberBadge,
|
||||
{ backgroundColor: theme.colors.backgroundTertiary },
|
||||
]}
|
||||
>
|
||||
<Text style={[styles.number, { color: theme.colors.textSecondary }]}>
|
||||
<View className="self-start px-2.5 py-1 rounded-lg bg-bg-tertiary">
|
||||
<Text className="text-sm font-semibold text-text-secondary">
|
||||
{alert.alertNumberWithPrefix || `#${alert.alertNumber}`}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<Text
|
||||
style={[
|
||||
theme.typography.titleLarge,
|
||||
{ color: theme.colors.textPrimary, marginTop: 4 },
|
||||
]}
|
||||
>
|
||||
<Text className="text-title-lg text-text-primary mt-1">
|
||||
{alert.title}
|
||||
</Text>
|
||||
|
||||
{/* Badges */}
|
||||
<View style={styles.badgeRow}>
|
||||
<View className="flex-row flex-wrap gap-2 mt-3">
|
||||
{alert.currentAlertState ? (
|
||||
<View
|
||||
style={[
|
||||
styles.badge,
|
||||
{ backgroundColor: theme.colors.backgroundTertiary },
|
||||
]}
|
||||
>
|
||||
<View style={[styles.dot, { backgroundColor: stateColor }]} />
|
||||
<Text
|
||||
style={[styles.badgeText, { color: theme.colors.textPrimary }]}
|
||||
>
|
||||
<View className="flex-row items-center px-2.5 py-[5px] rounded-md bg-bg-tertiary">
|
||||
<View
|
||||
className="w-2 h-2 rounded-full mr-1.5"
|
||||
style={{ backgroundColor: stateColor }}
|
||||
/>
|
||||
<Text className="text-[13px] font-semibold text-text-primary">
|
||||
{alert.currentAlertState.name}
|
||||
</Text>
|
||||
</View>
|
||||
@@ -227,9 +200,13 @@ export default function AlertDetailScreen({ route }: Props): React.JSX.Element {
|
||||
|
||||
{alert.alertSeverity ? (
|
||||
<View
|
||||
style={[styles.badge, { backgroundColor: severityColor + "26" }]}
|
||||
className="flex-row items-center px-2.5 py-[5px] rounded-md"
|
||||
style={{ backgroundColor: severityColor + "26" }}
|
||||
>
|
||||
<Text style={[styles.badgeText, { color: severityColor }]}>
|
||||
<Text
|
||||
className="text-[13px] font-semibold"
|
||||
style={{ color: severityColor }}
|
||||
>
|
||||
{alert.alertSeverity.name}
|
||||
</Text>
|
||||
</View>
|
||||
@@ -238,69 +215,38 @@ export default function AlertDetailScreen({ route }: Props): React.JSX.Element {
|
||||
|
||||
{/* Description */}
|
||||
{alert.description ? (
|
||||
<View style={styles.section}>
|
||||
<Text
|
||||
style={[styles.sectionTitle, { color: theme.colors.textSecondary }]}
|
||||
>
|
||||
<View className="mt-6">
|
||||
<Text className="text-[13px] font-semibold uppercase tracking-wide mb-2.5 text-text-secondary">
|
||||
Description
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
theme.typography.bodyMedium,
|
||||
{ color: theme.colors.textPrimary },
|
||||
]}
|
||||
>
|
||||
<Text className="text-body-md text-text-primary">
|
||||
{alert.description}
|
||||
</Text>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{/* Details */}
|
||||
<View style={styles.section}>
|
||||
<Text
|
||||
style={[styles.sectionTitle, { color: theme.colors.textSecondary }]}
|
||||
>
|
||||
<View className="mt-6">
|
||||
<Text className="text-[13px] font-semibold uppercase tracking-wide mb-2.5 text-text-secondary">
|
||||
Details
|
||||
</Text>
|
||||
|
||||
<View
|
||||
style={[
|
||||
styles.detailCard,
|
||||
theme.shadows.sm,
|
||||
{
|
||||
backgroundColor: theme.colors.backgroundElevated,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<View style={styles.detailRow}>
|
||||
<Text
|
||||
style={[styles.detailLabel, { color: theme.colors.textTertiary }]}
|
||||
>
|
||||
<View className="rounded-2xl p-4 bg-bg-elevated shadow-sm">
|
||||
<View className="flex-row mb-2.5">
|
||||
<Text className="text-sm w-[90px] text-text-tertiary">
|
||||
Created
|
||||
</Text>
|
||||
<Text
|
||||
style={[styles.detailValue, { color: theme.colors.textPrimary }]}
|
||||
>
|
||||
<Text className="text-sm text-text-primary">
|
||||
{formatDateTime(alert.createdAt)}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{alert.monitor ? (
|
||||
<View style={styles.detailRow}>
|
||||
<Text
|
||||
style={[
|
||||
styles.detailLabel,
|
||||
{ color: theme.colors.textTertiary },
|
||||
]}
|
||||
>
|
||||
<View className="flex-row mb-2.5">
|
||||
<Text className="text-sm w-[90px] text-text-tertiary">
|
||||
Monitor
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
styles.detailValue,
|
||||
{ color: theme.colors.textPrimary },
|
||||
]}
|
||||
>
|
||||
<Text className="text-sm text-text-primary">
|
||||
{alert.monitor.name}
|
||||
</Text>
|
||||
</View>
|
||||
@@ -310,20 +256,15 @@ export default function AlertDetailScreen({ route }: Props): React.JSX.Element {
|
||||
|
||||
{/* State Change Actions */}
|
||||
{!isResolved ? (
|
||||
<View style={styles.section}>
|
||||
<Text
|
||||
style={[styles.sectionTitle, { color: theme.colors.textSecondary }]}
|
||||
>
|
||||
<View className="mt-6">
|
||||
<Text className="text-[13px] font-semibold uppercase tracking-wide mb-2.5 text-text-secondary">
|
||||
Actions
|
||||
</Text>
|
||||
<View style={styles.actionRow}>
|
||||
<View className="flex-row gap-3">
|
||||
{!isAcknowledged && !isResolved && acknowledgeState ? (
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.actionButton,
|
||||
theme.shadows.md,
|
||||
{ backgroundColor: theme.colors.stateAcknowledged },
|
||||
]}
|
||||
className="flex-1 py-3.5 rounded-[14px] items-center justify-center min-h-[50px] shadow-md"
|
||||
style={{ backgroundColor: theme.colors.stateAcknowledged }}
|
||||
onPress={() => {
|
||||
return handleStateChange(
|
||||
acknowledgeState._id,
|
||||
@@ -340,12 +281,7 @@ export default function AlertDetailScreen({ route }: Props): React.JSX.Element {
|
||||
color={theme.colors.textInverse}
|
||||
/>
|
||||
) : (
|
||||
<Text
|
||||
style={[
|
||||
styles.actionButtonText,
|
||||
{ color: theme.colors.textInverse },
|
||||
]}
|
||||
>
|
||||
<Text className="text-[15px] font-bold text-text-inverse">
|
||||
Acknowledge
|
||||
</Text>
|
||||
)}
|
||||
@@ -354,11 +290,8 @@ export default function AlertDetailScreen({ route }: Props): React.JSX.Element {
|
||||
|
||||
{resolveState ? (
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.actionButton,
|
||||
theme.shadows.md,
|
||||
{ backgroundColor: theme.colors.stateResolved },
|
||||
]}
|
||||
className="flex-1 py-3.5 rounded-[14px] items-center justify-center min-h-[50px] shadow-md"
|
||||
style={{ backgroundColor: theme.colors.stateResolved }}
|
||||
onPress={() => {
|
||||
return handleStateChange(resolveState._id, resolveState.name);
|
||||
}}
|
||||
@@ -372,12 +305,7 @@ export default function AlertDetailScreen({ route }: Props): React.JSX.Element {
|
||||
color={theme.colors.textInverse}
|
||||
/>
|
||||
) : (
|
||||
<Text
|
||||
style={[
|
||||
styles.actionButtonText,
|
||||
{ color: theme.colors.textInverse },
|
||||
]}
|
||||
>
|
||||
<Text className="text-[15px] font-bold text-text-inverse">
|
||||
Resolve
|
||||
</Text>
|
||||
)}
|
||||
@@ -389,10 +317,8 @@ export default function AlertDetailScreen({ route }: Props): React.JSX.Element {
|
||||
|
||||
{/* State Timeline */}
|
||||
{timeline && timeline.length > 0 ? (
|
||||
<View style={styles.section}>
|
||||
<Text
|
||||
style={[styles.sectionTitle, { color: theme.colors.textSecondary }]}
|
||||
>
|
||||
<View className="mt-6">
|
||||
<Text className="text-[13px] font-semibold uppercase tracking-wide mb-2.5 text-text-secondary">
|
||||
State Timeline
|
||||
</Text>
|
||||
{timeline.map((entry: StateTimelineItem) => {
|
||||
@@ -402,32 +328,17 @@ export default function AlertDetailScreen({ route }: Props): React.JSX.Element {
|
||||
return (
|
||||
<View
|
||||
key={entry._id}
|
||||
style={[
|
||||
styles.timelineEntry,
|
||||
theme.shadows.sm,
|
||||
{
|
||||
backgroundColor: theme.colors.backgroundElevated,
|
||||
},
|
||||
]}
|
||||
className="flex-row items-center p-3.5 rounded-xl mb-2 bg-bg-elevated shadow-sm"
|
||||
>
|
||||
<View
|
||||
style={[styles.timelineDot, { backgroundColor: entryColor }]}
|
||||
className="w-2.5 h-2.5 rounded-full mr-3"
|
||||
style={{ backgroundColor: entryColor }}
|
||||
/>
|
||||
<View style={styles.timelineInfo}>
|
||||
<Text
|
||||
style={[
|
||||
theme.typography.bodyMedium,
|
||||
{ color: theme.colors.textPrimary, fontWeight: "600" },
|
||||
]}
|
||||
>
|
||||
<View className="flex-1">
|
||||
<Text className="text-body-md text-text-primary font-semibold">
|
||||
{entry.alertState?.name ?? "Unknown"}
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
theme.typography.bodySmall,
|
||||
{ color: theme.colors.textTertiary },
|
||||
]}
|
||||
>
|
||||
<Text className="text-body-sm text-text-tertiary">
|
||||
{formatDateTime(entry.createdAt)}
|
||||
</Text>
|
||||
</View>
|
||||
@@ -438,31 +349,19 @@ export default function AlertDetailScreen({ route }: Props): React.JSX.Element {
|
||||
) : null}
|
||||
|
||||
{/* Internal Notes */}
|
||||
<View style={styles.section}>
|
||||
<View style={styles.sectionHeader}>
|
||||
<Text
|
||||
style={[
|
||||
styles.sectionTitle,
|
||||
{ color: theme.colors.textSecondary, marginBottom: 0 },
|
||||
]}
|
||||
>
|
||||
<View className="mt-6">
|
||||
<View className="flex-row justify-between items-center mb-2.5">
|
||||
<Text className="text-[13px] font-semibold uppercase tracking-wide text-text-secondary">
|
||||
Internal Notes
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.addNoteButton,
|
||||
{ backgroundColor: theme.colors.actionPrimary },
|
||||
]}
|
||||
className="px-3 py-1.5 rounded-lg"
|
||||
style={{ backgroundColor: theme.colors.actionPrimary }}
|
||||
onPress={() => {
|
||||
return setNoteModalVisible(true);
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.addNoteButtonText,
|
||||
{ color: theme.colors.textInverse },
|
||||
]}
|
||||
>
|
||||
<Text className="text-[13px] font-semibold text-text-inverse">
|
||||
Add Note
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
@@ -473,39 +372,18 @@ export default function AlertDetailScreen({ route }: Props): React.JSX.Element {
|
||||
return (
|
||||
<View
|
||||
key={note._id}
|
||||
style={[
|
||||
styles.noteCard,
|
||||
theme.shadows.sm,
|
||||
{
|
||||
backgroundColor: theme.colors.backgroundElevated,
|
||||
},
|
||||
]}
|
||||
className="rounded-xl p-3.5 mb-2 bg-bg-elevated shadow-sm"
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
theme.typography.bodyMedium,
|
||||
{ color: theme.colors.textPrimary },
|
||||
]}
|
||||
>
|
||||
<Text className="text-body-md text-text-primary">
|
||||
{note.note}
|
||||
</Text>
|
||||
<View style={styles.noteMeta}>
|
||||
<View className="flex-row justify-between mt-2">
|
||||
{note.createdByUser ? (
|
||||
<Text
|
||||
style={[
|
||||
theme.typography.bodySmall,
|
||||
{ color: theme.colors.textTertiary },
|
||||
]}
|
||||
>
|
||||
<Text className="text-body-sm text-text-tertiary">
|
||||
{note.createdByUser.name}
|
||||
</Text>
|
||||
) : null}
|
||||
<Text
|
||||
style={[
|
||||
theme.typography.bodySmall,
|
||||
{ color: theme.colors.textTertiary },
|
||||
]}
|
||||
>
|
||||
<Text className="text-body-sm text-text-tertiary">
|
||||
{formatDateTime(note.createdAt)}
|
||||
</Text>
|
||||
</View>
|
||||
@@ -515,12 +393,7 @@ export default function AlertDetailScreen({ route }: Props): React.JSX.Element {
|
||||
: null}
|
||||
|
||||
{notes && notes.length === 0 ? (
|
||||
<Text
|
||||
style={[
|
||||
theme.typography.bodySmall,
|
||||
{ color: theme.colors.textTertiary },
|
||||
]}
|
||||
>
|
||||
<Text className="text-body-sm text-text-tertiary">
|
||||
No notes yet.
|
||||
</Text>
|
||||
) : null}
|
||||
@@ -537,130 +410,3 @@ export default function AlertDetailScreen({ route }: Props): React.JSX.Element {
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles: ReturnType<typeof StyleSheet.create> = StyleSheet.create({
|
||||
centered: {
|
||||
flex: 1,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
content: {
|
||||
padding: 20,
|
||||
paddingBottom: 40,
|
||||
},
|
||||
numberBadge: {
|
||||
alignSelf: "flex-start",
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 4,
|
||||
borderRadius: 8,
|
||||
},
|
||||
number: {
|
||||
fontSize: 14,
|
||||
fontWeight: "600",
|
||||
},
|
||||
badgeRow: {
|
||||
flexDirection: "row",
|
||||
flexWrap: "wrap",
|
||||
gap: 8,
|
||||
marginTop: 12,
|
||||
},
|
||||
badge: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 5,
|
||||
borderRadius: 6,
|
||||
},
|
||||
dot: {
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
marginRight: 6,
|
||||
},
|
||||
badgeText: {
|
||||
fontSize: 13,
|
||||
fontWeight: "600",
|
||||
},
|
||||
section: {
|
||||
marginTop: 24,
|
||||
},
|
||||
sectionHeader: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
marginBottom: 10,
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 13,
|
||||
fontWeight: "600",
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: 0.5,
|
||||
marginBottom: 10,
|
||||
},
|
||||
detailCard: {
|
||||
borderRadius: 16,
|
||||
padding: 16,
|
||||
},
|
||||
detailRow: {
|
||||
flexDirection: "row",
|
||||
marginBottom: 10,
|
||||
},
|
||||
detailLabel: {
|
||||
fontSize: 14,
|
||||
width: 90,
|
||||
},
|
||||
detailValue: {
|
||||
fontSize: 14,
|
||||
},
|
||||
actionRow: {
|
||||
flexDirection: "row",
|
||||
gap: 12,
|
||||
},
|
||||
actionButton: {
|
||||
flex: 1,
|
||||
paddingVertical: 14,
|
||||
borderRadius: 14,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
minHeight: 50,
|
||||
},
|
||||
actionButtonText: {
|
||||
fontSize: 15,
|
||||
fontWeight: "700",
|
||||
},
|
||||
timelineEntry: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
padding: 14,
|
||||
borderRadius: 12,
|
||||
marginBottom: 8,
|
||||
},
|
||||
timelineDot: {
|
||||
width: 10,
|
||||
height: 10,
|
||||
borderRadius: 5,
|
||||
marginRight: 12,
|
||||
},
|
||||
timelineInfo: {
|
||||
flex: 1,
|
||||
},
|
||||
addNoteButton: {
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 6,
|
||||
borderRadius: 8,
|
||||
},
|
||||
addNoteButtonText: {
|
||||
fontSize: 13,
|
||||
fontWeight: "600",
|
||||
},
|
||||
noteCard: {
|
||||
borderRadius: 12,
|
||||
padding: 14,
|
||||
marginBottom: 8,
|
||||
},
|
||||
noteMeta: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
marginTop: 8,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
ActivityIndicator,
|
||||
RefreshControl,
|
||||
Alert,
|
||||
StyleSheet,
|
||||
} from "react-native";
|
||||
import type { NativeStackScreenProps } from "@react-navigation/native-stack";
|
||||
import { useTheme } from "../theme";
|
||||
@@ -135,9 +134,7 @@ export default function AlertEpisodeDetailScreen({
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<View
|
||||
style={[{ flex: 1, backgroundColor: theme.colors.backgroundPrimary }]}
|
||||
>
|
||||
<View className="flex-1 bg-bg-primary">
|
||||
<SkeletonCard variant="detail" />
|
||||
</View>
|
||||
);
|
||||
@@ -145,18 +142,8 @@ export default function AlertEpisodeDetailScreen({
|
||||
|
||||
if (!episode) {
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.centered,
|
||||
{ backgroundColor: theme.colors.backgroundPrimary },
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
theme.typography.bodyMedium,
|
||||
{ color: theme.colors.textSecondary },
|
||||
]}
|
||||
>
|
||||
<View className="flex-1 items-center justify-center bg-bg-primary">
|
||||
<Text className="text-body-md text-text-secondary">
|
||||
Episode not found.
|
||||
</Text>
|
||||
</View>
|
||||
@@ -186,46 +173,32 @@ export default function AlertEpisodeDetailScreen({
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
style={[{ backgroundColor: theme.colors.backgroundPrimary }]}
|
||||
contentContainerStyle={styles.content}
|
||||
className="bg-bg-primary"
|
||||
contentContainerStyle={{ padding: 20, paddingBottom: 40 }}
|
||||
refreshControl={
|
||||
<RefreshControl refreshing={false} onRefresh={onRefresh} />
|
||||
}
|
||||
>
|
||||
{/* Header */}
|
||||
<View
|
||||
style={[
|
||||
styles.numberBadge,
|
||||
{ backgroundColor: theme.colors.backgroundTertiary },
|
||||
]}
|
||||
>
|
||||
<Text style={[styles.number, { color: theme.colors.textSecondary }]}>
|
||||
<View className="self-start px-2.5 py-1 rounded-lg bg-bg-tertiary">
|
||||
<Text className="text-sm font-semibold text-text-secondary">
|
||||
{episode.episodeNumberWithPrefix || `#${episode.episodeNumber}`}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<Text
|
||||
style={[
|
||||
theme.typography.titleLarge,
|
||||
{ color: theme.colors.textPrimary, marginTop: 4 },
|
||||
]}
|
||||
>
|
||||
<Text className="text-title-lg text-text-primary mt-1">
|
||||
{episode.title}
|
||||
</Text>
|
||||
|
||||
{/* Badges */}
|
||||
<View style={styles.badgeRow}>
|
||||
<View className="flex-row flex-wrap gap-2 mt-3">
|
||||
{episode.currentAlertState ? (
|
||||
<View
|
||||
style={[
|
||||
styles.badge,
|
||||
{ backgroundColor: theme.colors.backgroundTertiary },
|
||||
]}
|
||||
>
|
||||
<View style={[styles.dot, { backgroundColor: stateColor }]} />
|
||||
<Text
|
||||
style={[styles.badgeText, { color: theme.colors.textPrimary }]}
|
||||
>
|
||||
<View className="flex-row items-center px-2.5 py-[5px] rounded-md bg-bg-tertiary">
|
||||
<View
|
||||
className="w-2 h-2 rounded-full mr-1.5"
|
||||
style={{ backgroundColor: stateColor }}
|
||||
/>
|
||||
<Text className="text-[13px] font-semibold text-text-primary">
|
||||
{episode.currentAlertState.name}
|
||||
</Text>
|
||||
</View>
|
||||
@@ -233,9 +206,13 @@ export default function AlertEpisodeDetailScreen({
|
||||
|
||||
{episode.alertSeverity ? (
|
||||
<View
|
||||
style={[styles.badge, { backgroundColor: severityColor + "26" }]}
|
||||
className="flex-row items-center px-2.5 py-[5px] rounded-md"
|
||||
style={{ backgroundColor: severityColor + "26" }}
|
||||
>
|
||||
<Text style={[styles.badgeText, { color: severityColor }]}>
|
||||
<Text
|
||||
className="text-[13px] font-semibold"
|
||||
style={{ color: severityColor }}
|
||||
>
|
||||
{episode.alertSeverity.name}
|
||||
</Text>
|
||||
</View>
|
||||
@@ -244,62 +221,37 @@ export default function AlertEpisodeDetailScreen({
|
||||
|
||||
{/* Description */}
|
||||
{episode.description ? (
|
||||
<View style={styles.section}>
|
||||
<Text
|
||||
style={[styles.sectionTitle, { color: theme.colors.textSecondary }]}
|
||||
>
|
||||
<View className="mt-6">
|
||||
<Text className="text-[13px] font-semibold uppercase tracking-wide mb-2.5 text-text-secondary">
|
||||
Description
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
theme.typography.bodyMedium,
|
||||
{ color: theme.colors.textPrimary },
|
||||
]}
|
||||
>
|
||||
<Text className="text-body-md text-text-primary">
|
||||
{episode.description}
|
||||
</Text>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{/* Details */}
|
||||
<View style={styles.section}>
|
||||
<Text
|
||||
style={[styles.sectionTitle, { color: theme.colors.textSecondary }]}
|
||||
>
|
||||
<View className="mt-6">
|
||||
<Text className="text-[13px] font-semibold uppercase tracking-wide mb-2.5 text-text-secondary">
|
||||
Details
|
||||
</Text>
|
||||
|
||||
<View
|
||||
style={[
|
||||
styles.detailCard,
|
||||
theme.shadows.sm,
|
||||
{
|
||||
backgroundColor: theme.colors.backgroundElevated,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<View style={styles.detailRow}>
|
||||
<Text
|
||||
style={[styles.detailLabel, { color: theme.colors.textTertiary }]}
|
||||
>
|
||||
<View className="rounded-2xl p-4 bg-bg-elevated shadow-sm">
|
||||
<View className="flex-row mb-2.5">
|
||||
<Text className="text-sm w-[90px] text-text-tertiary">
|
||||
Created
|
||||
</Text>
|
||||
<Text
|
||||
style={[styles.detailValue, { color: theme.colors.textPrimary }]}
|
||||
>
|
||||
<Text className="text-sm text-text-primary">
|
||||
{formatDateTime(episode.createdAt)}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.detailRow}>
|
||||
<Text
|
||||
style={[styles.detailLabel, { color: theme.colors.textTertiary }]}
|
||||
>
|
||||
<View className="flex-row mb-2.5">
|
||||
<Text className="text-sm w-[90px] text-text-tertiary">
|
||||
Alerts
|
||||
</Text>
|
||||
<Text
|
||||
style={[styles.detailValue, { color: theme.colors.textPrimary }]}
|
||||
>
|
||||
<Text className="text-sm text-text-primary">
|
||||
{episode.alertCount ?? 0}
|
||||
</Text>
|
||||
</View>
|
||||
@@ -308,20 +260,15 @@ export default function AlertEpisodeDetailScreen({
|
||||
|
||||
{/* State Change Actions */}
|
||||
{!isResolved ? (
|
||||
<View style={styles.section}>
|
||||
<Text
|
||||
style={[styles.sectionTitle, { color: theme.colors.textSecondary }]}
|
||||
>
|
||||
<View className="mt-6">
|
||||
<Text className="text-[13px] font-semibold uppercase tracking-wide mb-2.5 text-text-secondary">
|
||||
Actions
|
||||
</Text>
|
||||
<View style={styles.actionRow}>
|
||||
<View className="flex-row gap-3">
|
||||
{!isAcknowledged && !isResolved && acknowledgeState ? (
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.actionButton,
|
||||
theme.shadows.md,
|
||||
{ backgroundColor: theme.colors.stateAcknowledged },
|
||||
]}
|
||||
className="flex-1 py-3.5 rounded-[14px] items-center justify-center min-h-[50px] shadow-md"
|
||||
style={{ backgroundColor: theme.colors.stateAcknowledged }}
|
||||
onPress={() => {
|
||||
return handleStateChange(
|
||||
acknowledgeState._id,
|
||||
@@ -336,12 +283,7 @@ export default function AlertEpisodeDetailScreen({
|
||||
color={theme.colors.textInverse}
|
||||
/>
|
||||
) : (
|
||||
<Text
|
||||
style={[
|
||||
styles.actionButtonText,
|
||||
{ color: theme.colors.textInverse },
|
||||
]}
|
||||
>
|
||||
<Text className="text-[15px] font-bold text-text-inverse">
|
||||
Acknowledge
|
||||
</Text>
|
||||
)}
|
||||
@@ -350,11 +292,8 @@ export default function AlertEpisodeDetailScreen({
|
||||
|
||||
{resolveState ? (
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.actionButton,
|
||||
theme.shadows.md,
|
||||
{ backgroundColor: theme.colors.stateResolved },
|
||||
]}
|
||||
className="flex-1 py-3.5 rounded-[14px] items-center justify-center min-h-[50px] shadow-md"
|
||||
style={{ backgroundColor: theme.colors.stateResolved }}
|
||||
onPress={() => {
|
||||
return handleStateChange(resolveState._id, resolveState.name);
|
||||
}}
|
||||
@@ -366,12 +305,7 @@ export default function AlertEpisodeDetailScreen({
|
||||
color={theme.colors.textInverse}
|
||||
/>
|
||||
) : (
|
||||
<Text
|
||||
style={[
|
||||
styles.actionButtonText,
|
||||
{ color: theme.colors.textInverse },
|
||||
]}
|
||||
>
|
||||
<Text className="text-[15px] font-bold text-text-inverse">
|
||||
Resolve
|
||||
</Text>
|
||||
)}
|
||||
@@ -383,10 +317,8 @@ export default function AlertEpisodeDetailScreen({
|
||||
|
||||
{/* State Timeline */}
|
||||
{timeline && timeline.length > 0 ? (
|
||||
<View style={styles.section}>
|
||||
<Text
|
||||
style={[styles.sectionTitle, { color: theme.colors.textSecondary }]}
|
||||
>
|
||||
<View className="mt-6">
|
||||
<Text className="text-[13px] font-semibold uppercase tracking-wide mb-2.5 text-text-secondary">
|
||||
State Timeline
|
||||
</Text>
|
||||
{timeline.map((entry: StateTimelineItem) => {
|
||||
@@ -396,35 +328,17 @@ export default function AlertEpisodeDetailScreen({
|
||||
return (
|
||||
<View
|
||||
key={entry._id}
|
||||
style={[
|
||||
styles.timelineEntry,
|
||||
theme.shadows.sm,
|
||||
{
|
||||
backgroundColor: theme.colors.backgroundElevated,
|
||||
},
|
||||
]}
|
||||
className="flex-row items-center p-3.5 rounded-xl mb-2 bg-bg-elevated shadow-sm"
|
||||
>
|
||||
<View
|
||||
style={[styles.timelineDot, { backgroundColor: entryColor }]}
|
||||
className="w-2.5 h-2.5 rounded-full mr-3"
|
||||
style={{ backgroundColor: entryColor }}
|
||||
/>
|
||||
<View style={styles.timelineInfo}>
|
||||
<Text
|
||||
style={[
|
||||
theme.typography.bodyMedium,
|
||||
{
|
||||
color: theme.colors.textPrimary,
|
||||
fontWeight: "600",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<View className="flex-1">
|
||||
<Text className="text-body-md text-text-primary font-semibold">
|
||||
{entry.alertState?.name ?? "Unknown"}
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
theme.typography.bodySmall,
|
||||
{ color: theme.colors.textTertiary },
|
||||
]}
|
||||
>
|
||||
<Text className="text-body-sm text-text-tertiary">
|
||||
{formatDateTime(entry.createdAt)}
|
||||
</Text>
|
||||
</View>
|
||||
@@ -435,31 +349,19 @@ export default function AlertEpisodeDetailScreen({
|
||||
) : null}
|
||||
|
||||
{/* Internal Notes */}
|
||||
<View style={styles.section}>
|
||||
<View style={styles.sectionHeader}>
|
||||
<Text
|
||||
style={[
|
||||
styles.sectionTitle,
|
||||
{ color: theme.colors.textSecondary, marginBottom: 0 },
|
||||
]}
|
||||
>
|
||||
<View className="mt-6">
|
||||
<View className="flex-row justify-between items-center mb-2.5">
|
||||
<Text className="text-[13px] font-semibold uppercase tracking-wide text-text-secondary">
|
||||
Internal Notes
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.addNoteButton,
|
||||
{ backgroundColor: theme.colors.actionPrimary },
|
||||
]}
|
||||
className="px-3 py-1.5 rounded-lg"
|
||||
style={{ backgroundColor: theme.colors.actionPrimary }}
|
||||
onPress={() => {
|
||||
return setNoteModalVisible(true);
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.addNoteButtonText,
|
||||
{ color: theme.colors.textInverse },
|
||||
]}
|
||||
>
|
||||
<Text className="text-[13px] font-semibold text-text-inverse">
|
||||
Add Note
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
@@ -470,39 +372,18 @@ export default function AlertEpisodeDetailScreen({
|
||||
return (
|
||||
<View
|
||||
key={note._id}
|
||||
style={[
|
||||
styles.noteCard,
|
||||
theme.shadows.sm,
|
||||
{
|
||||
backgroundColor: theme.colors.backgroundElevated,
|
||||
},
|
||||
]}
|
||||
className="rounded-xl p-3.5 mb-2 bg-bg-elevated shadow-sm"
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
theme.typography.bodyMedium,
|
||||
{ color: theme.colors.textPrimary },
|
||||
]}
|
||||
>
|
||||
<Text className="text-body-md text-text-primary">
|
||||
{note.note}
|
||||
</Text>
|
||||
<View style={styles.noteMeta}>
|
||||
<View className="flex-row justify-between mt-2">
|
||||
{note.createdByUser ? (
|
||||
<Text
|
||||
style={[
|
||||
theme.typography.bodySmall,
|
||||
{ color: theme.colors.textTertiary },
|
||||
]}
|
||||
>
|
||||
<Text className="text-body-sm text-text-tertiary">
|
||||
{note.createdByUser.name}
|
||||
</Text>
|
||||
) : null}
|
||||
<Text
|
||||
style={[
|
||||
theme.typography.bodySmall,
|
||||
{ color: theme.colors.textTertiary },
|
||||
]}
|
||||
>
|
||||
<Text className="text-body-sm text-text-tertiary">
|
||||
{formatDateTime(note.createdAt)}
|
||||
</Text>
|
||||
</View>
|
||||
@@ -512,12 +393,7 @@ export default function AlertEpisodeDetailScreen({
|
||||
: null}
|
||||
|
||||
{notes && notes.length === 0 ? (
|
||||
<Text
|
||||
style={[
|
||||
theme.typography.bodySmall,
|
||||
{ color: theme.colors.textTertiary },
|
||||
]}
|
||||
>
|
||||
<Text className="text-body-sm text-text-tertiary">
|
||||
No notes yet.
|
||||
</Text>
|
||||
) : null}
|
||||
@@ -534,130 +410,3 @@ export default function AlertEpisodeDetailScreen({
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles: ReturnType<typeof StyleSheet.create> = StyleSheet.create({
|
||||
centered: {
|
||||
flex: 1,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
content: {
|
||||
padding: 20,
|
||||
paddingBottom: 40,
|
||||
},
|
||||
numberBadge: {
|
||||
alignSelf: "flex-start",
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 4,
|
||||
borderRadius: 8,
|
||||
},
|
||||
number: {
|
||||
fontSize: 14,
|
||||
fontWeight: "600",
|
||||
},
|
||||
badgeRow: {
|
||||
flexDirection: "row",
|
||||
flexWrap: "wrap",
|
||||
gap: 8,
|
||||
marginTop: 12,
|
||||
},
|
||||
badge: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 5,
|
||||
borderRadius: 6,
|
||||
},
|
||||
dot: {
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
marginRight: 6,
|
||||
},
|
||||
badgeText: {
|
||||
fontSize: 13,
|
||||
fontWeight: "600",
|
||||
},
|
||||
section: {
|
||||
marginTop: 24,
|
||||
},
|
||||
sectionHeader: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
marginBottom: 10,
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 13,
|
||||
fontWeight: "600",
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: 0.5,
|
||||
marginBottom: 10,
|
||||
},
|
||||
detailCard: {
|
||||
borderRadius: 16,
|
||||
padding: 16,
|
||||
},
|
||||
detailRow: {
|
||||
flexDirection: "row",
|
||||
marginBottom: 10,
|
||||
},
|
||||
detailLabel: {
|
||||
fontSize: 14,
|
||||
width: 90,
|
||||
},
|
||||
detailValue: {
|
||||
fontSize: 14,
|
||||
},
|
||||
actionRow: {
|
||||
flexDirection: "row",
|
||||
gap: 12,
|
||||
},
|
||||
actionButton: {
|
||||
flex: 1,
|
||||
paddingVertical: 14,
|
||||
borderRadius: 14,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
minHeight: 50,
|
||||
},
|
||||
actionButtonText: {
|
||||
fontSize: 15,
|
||||
fontWeight: "700",
|
||||
},
|
||||
timelineEntry: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
padding: 14,
|
||||
borderRadius: 12,
|
||||
marginBottom: 8,
|
||||
},
|
||||
timelineDot: {
|
||||
width: 10,
|
||||
height: 10,
|
||||
borderRadius: 5,
|
||||
marginRight: 12,
|
||||
},
|
||||
timelineInfo: {
|
||||
flex: 1,
|
||||
},
|
||||
addNoteButton: {
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 6,
|
||||
borderRadius: 8,
|
||||
},
|
||||
addNoteButtonText: {
|
||||
fontSize: 13,
|
||||
fontWeight: "600",
|
||||
},
|
||||
noteCard: {
|
||||
borderRadius: 12,
|
||||
padding: 14,
|
||||
marginBottom: 8,
|
||||
},
|
||||
noteMeta: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
marginTop: 8,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
RefreshControl,
|
||||
TouchableOpacity,
|
||||
Text,
|
||||
StyleSheet,
|
||||
ListRenderItemInfo,
|
||||
} from "react-native";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
@@ -72,13 +71,8 @@ export default function AlertEpisodesScreen(): React.JSX.Element {
|
||||
|
||||
if (isLoading && episodes.length === 0) {
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.container,
|
||||
{ backgroundColor: theme.colors.backgroundPrimary },
|
||||
]}
|
||||
>
|
||||
<View style={styles.skeletonList}>
|
||||
<View className="flex-1 bg-bg-primary">
|
||||
<View className="p-4">
|
||||
<SkeletonCard />
|
||||
<SkeletonCard />
|
||||
<SkeletonCard />
|
||||
@@ -89,36 +83,18 @@ export default function AlertEpisodesScreen(): React.JSX.Element {
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.centered,
|
||||
{ backgroundColor: theme.colors.backgroundPrimary },
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
theme.typography.bodyMedium,
|
||||
{ color: theme.colors.textSecondary, textAlign: "center" },
|
||||
]}
|
||||
>
|
||||
<View className="flex-1 items-center justify-center px-8 bg-bg-primary">
|
||||
<Text className="text-body-md text-text-secondary text-center">
|
||||
Failed to load alert episodes.
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.retryButton,
|
||||
theme.shadows.md,
|
||||
{ backgroundColor: theme.colors.actionPrimary },
|
||||
]}
|
||||
className="mt-4 px-6 py-3 rounded-[10px] shadow-md"
|
||||
style={{ backgroundColor: theme.colors.actionPrimary }}
|
||||
onPress={() => {
|
||||
return refetch();
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
theme.typography.bodyMedium,
|
||||
{ color: theme.colors.textInverse, fontWeight: "600" },
|
||||
]}
|
||||
>
|
||||
<Text className="text-body-md text-text-inverse font-semibold">
|
||||
Retry
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
@@ -127,19 +103,14 @@ export default function AlertEpisodesScreen(): React.JSX.Element {
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.container,
|
||||
{ backgroundColor: theme.colors.backgroundPrimary },
|
||||
]}
|
||||
>
|
||||
<View className="flex-1 bg-bg-primary">
|
||||
<FlatList
|
||||
data={episodes}
|
||||
keyExtractor={(item: AlertEpisodeItem) => {
|
||||
return item._id;
|
||||
}}
|
||||
contentContainerStyle={
|
||||
episodes.length === 0 ? styles.emptyContainer : styles.list
|
||||
episodes.length === 0 ? { flex: 1 } : { padding: 16 }
|
||||
}
|
||||
renderItem={({ item }: ListRenderItemInfo<AlertEpisodeItem>) => {
|
||||
return (
|
||||
@@ -168,30 +139,3 @@ export default function AlertEpisodesScreen(): React.JSX.Element {
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles: ReturnType<typeof StyleSheet.create> = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
centered: {
|
||||
flex: 1,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
paddingHorizontal: 32,
|
||||
},
|
||||
list: {
|
||||
padding: 16,
|
||||
},
|
||||
emptyContainer: {
|
||||
flex: 1,
|
||||
},
|
||||
skeletonList: {
|
||||
padding: 16,
|
||||
},
|
||||
retryButton: {
|
||||
marginTop: 16,
|
||||
paddingHorizontal: 24,
|
||||
paddingVertical: 12,
|
||||
borderRadius: 10,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
RefreshControl,
|
||||
TouchableOpacity,
|
||||
Text,
|
||||
StyleSheet,
|
||||
ListRenderItemInfo,
|
||||
} from "react-native";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
@@ -103,13 +102,8 @@ export default function AlertsScreen(): React.JSX.Element {
|
||||
|
||||
if (isLoading && alerts.length === 0) {
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.container,
|
||||
{ backgroundColor: theme.colors.backgroundPrimary },
|
||||
]}
|
||||
>
|
||||
<View style={styles.skeletonList}>
|
||||
<View className="flex-1 bg-bg-primary">
|
||||
<View className="p-4">
|
||||
<SkeletonCard />
|
||||
<SkeletonCard />
|
||||
<SkeletonCard />
|
||||
@@ -120,36 +114,18 @@ export default function AlertsScreen(): React.JSX.Element {
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.centered,
|
||||
{ backgroundColor: theme.colors.backgroundPrimary },
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
theme.typography.bodyMedium,
|
||||
{ color: theme.colors.textSecondary, textAlign: "center" },
|
||||
]}
|
||||
>
|
||||
<View className="flex-1 items-center justify-center px-8 bg-bg-primary">
|
||||
<Text className="text-body-md text-text-secondary text-center">
|
||||
Failed to load alerts.
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.retryButton,
|
||||
theme.shadows.md,
|
||||
{ backgroundColor: theme.colors.actionPrimary },
|
||||
]}
|
||||
className="mt-4 px-6 py-3 rounded-[10px] shadow-md"
|
||||
style={{ backgroundColor: theme.colors.actionPrimary }}
|
||||
onPress={() => {
|
||||
return refetch();
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
theme.typography.bodyMedium,
|
||||
{ color: theme.colors.textInverse, fontWeight: "600" },
|
||||
]}
|
||||
>
|
||||
<Text className="text-body-md text-text-inverse font-semibold">
|
||||
Retry
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
@@ -158,19 +134,14 @@ export default function AlertsScreen(): React.JSX.Element {
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.container,
|
||||
{ backgroundColor: theme.colors.backgroundPrimary },
|
||||
]}
|
||||
>
|
||||
<View className="flex-1 bg-bg-primary">
|
||||
<FlatList
|
||||
data={alerts}
|
||||
keyExtractor={(item: AlertItem) => {
|
||||
return item._id;
|
||||
}}
|
||||
contentContainerStyle={
|
||||
alerts.length === 0 ? styles.emptyContainer : styles.list
|
||||
alerts.length === 0 ? { flex: 1 } : { padding: 16 }
|
||||
}
|
||||
renderItem={({ item }: ListRenderItemInfo<AlertItem>) => {
|
||||
return (
|
||||
@@ -213,30 +184,3 @@ export default function AlertsScreen(): React.JSX.Element {
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles: ReturnType<typeof StyleSheet.create> = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
centered: {
|
||||
flex: 1,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
paddingHorizontal: 32,
|
||||
},
|
||||
list: {
|
||||
padding: 16,
|
||||
},
|
||||
emptyContainer: {
|
||||
flex: 1,
|
||||
},
|
||||
skeletonList: {
|
||||
padding: 16,
|
||||
},
|
||||
retryButton: {
|
||||
marginTop: 16,
|
||||
paddingHorizontal: 24,
|
||||
paddingVertical: 12,
|
||||
borderRadius: 10,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { View, Text, TouchableOpacity, StyleSheet } from "react-native";
|
||||
import { View, Text, TouchableOpacity } from "react-native";
|
||||
import { useTheme } from "../theme";
|
||||
import * as LocalAuthentication from "expo-local-authentication";
|
||||
|
||||
@@ -32,117 +32,44 @@ export default function BiometricLockScreen({
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.container,
|
||||
{ backgroundColor: theme.colors.backgroundPrimary },
|
||||
]}
|
||||
>
|
||||
<View className="flex-1 items-center justify-center px-10 bg-bg-primary">
|
||||
{/* Lock icon */}
|
||||
<View
|
||||
style={[
|
||||
styles.iconContainer,
|
||||
{ borderColor: theme.colors.borderDefault },
|
||||
]}
|
||||
className="w-20 h-20 rounded-full items-center justify-center"
|
||||
style={{ borderWidth: 1.5, borderColor: theme.colors.borderDefault }}
|
||||
>
|
||||
<View
|
||||
style={[
|
||||
styles.lockBody,
|
||||
{ backgroundColor: theme.colors.textTertiary },
|
||||
]}
|
||||
className="w-7 h-[22px] rounded mt-2"
|
||||
style={{ backgroundColor: theme.colors.textTertiary }}
|
||||
/>
|
||||
<View
|
||||
style={[
|
||||
styles.lockShackle,
|
||||
{ borderColor: theme.colors.textTertiary },
|
||||
]}
|
||||
className="w-5 h-4 absolute top-4"
|
||||
style={{
|
||||
borderWidth: 3,
|
||||
borderBottomWidth: 0,
|
||||
borderTopLeftRadius: 10,
|
||||
borderTopRightRadius: 10,
|
||||
borderColor: theme.colors.textTertiary,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<Text
|
||||
style={[
|
||||
theme.typography.titleMedium,
|
||||
{
|
||||
color: theme.colors.textPrimary,
|
||||
marginTop: 24,
|
||||
textAlign: "center",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Text className="text-title-md text-text-primary mt-6 text-center">
|
||||
OneUptime is Locked
|
||||
</Text>
|
||||
|
||||
<Text
|
||||
style={[
|
||||
theme.typography.bodyMedium,
|
||||
{
|
||||
color: theme.colors.textSecondary,
|
||||
marginTop: 8,
|
||||
textAlign: "center",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Text className="text-body-md text-text-secondary mt-2 text-center">
|
||||
Use {biometricType.toLowerCase()} to unlock
|
||||
</Text>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.unlockButton,
|
||||
theme.shadows.md,
|
||||
{ backgroundColor: theme.colors.actionPrimary },
|
||||
]}
|
||||
className="mt-8 py-4 px-12 rounded-[14px] min-w-[200px] items-center shadow-md"
|
||||
style={{ backgroundColor: theme.colors.actionPrimary }}
|
||||
onPress={authenticate}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<Text style={[styles.unlockButtonText, { color: "#FFFFFF" }]}>
|
||||
Unlock
|
||||
</Text>
|
||||
<Text className="text-[17px] font-semibold text-white">Unlock</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles: ReturnType<typeof StyleSheet.create> = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
paddingHorizontal: 40,
|
||||
},
|
||||
iconContainer: {
|
||||
width: 80,
|
||||
height: 80,
|
||||
borderRadius: 40,
|
||||
borderWidth: 1.5,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
lockBody: {
|
||||
width: 28,
|
||||
height: 22,
|
||||
borderRadius: 4,
|
||||
marginTop: 8,
|
||||
},
|
||||
lockShackle: {
|
||||
width: 20,
|
||||
height: 16,
|
||||
borderWidth: 3,
|
||||
borderBottomWidth: 0,
|
||||
borderTopLeftRadius: 10,
|
||||
borderTopRightRadius: 10,
|
||||
position: "absolute",
|
||||
top: 16,
|
||||
},
|
||||
unlockButton: {
|
||||
marginTop: 32,
|
||||
paddingVertical: 16,
|
||||
paddingHorizontal: 48,
|
||||
borderRadius: 14,
|
||||
minWidth: 200,
|
||||
alignItems: "center",
|
||||
},
|
||||
unlockButtonText: {
|
||||
fontSize: 17,
|
||||
fontWeight: "600",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
TouchableOpacity,
|
||||
ScrollView,
|
||||
RefreshControl,
|
||||
StyleSheet,
|
||||
} from "react-native";
|
||||
import { useTheme } from "../theme";
|
||||
import { useProject } from "../hooks/useProject";
|
||||
@@ -35,7 +34,6 @@ function StatCard({
|
||||
isLoading,
|
||||
onPress,
|
||||
}: StatCardProps): React.JSX.Element {
|
||||
const { theme } = useTheme();
|
||||
const { lightImpact } = useHaptics();
|
||||
|
||||
const handlePress: () => void = (): void => {
|
||||
@@ -45,22 +43,19 @@ function StatCard({
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.summaryCard,
|
||||
theme.shadows.md,
|
||||
{
|
||||
backgroundColor: theme.colors.backgroundElevated,
|
||||
},
|
||||
]}
|
||||
className="flex-1 p-5 rounded-2xl items-center bg-bg-elevated shadow-md"
|
||||
onPress={handlePress}
|
||||
activeOpacity={0.7}
|
||||
accessibilityLabel={`${count ?? 0} ${label}. Tap to view.`}
|
||||
accessibilityRole="button"
|
||||
>
|
||||
<Text style={[styles.cardCount, { color }]}>
|
||||
<Text
|
||||
className="text-[40px] font-bold"
|
||||
style={{ color, fontVariant: ["tabular-nums"] }}
|
||||
>
|
||||
{isLoading ? "--" : count ?? 0}
|
||||
</Text>
|
||||
<Text style={[styles.cardLabel, { color: theme.colors.textSecondary }]}>
|
||||
<Text className="text-sm font-medium mt-1 text-text-secondary">
|
||||
{label}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
@@ -73,27 +68,15 @@ interface QuickLinkProps {
|
||||
}
|
||||
|
||||
function QuickLink({ label, onPress }: QuickLinkProps): React.JSX.Element {
|
||||
const { theme } = useTheme();
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.linkCard,
|
||||
theme.shadows.sm,
|
||||
{
|
||||
backgroundColor: theme.colors.backgroundElevated,
|
||||
},
|
||||
]}
|
||||
className="flex-row justify-between items-center p-[18px] rounded-2xl mb-2.5 bg-bg-elevated shadow-sm"
|
||||
onPress={onPress}
|
||||
activeOpacity={0.7}
|
||||
accessibilityRole="button"
|
||||
>
|
||||
<Text style={[styles.linkLabel, { color: theme.colors.textPrimary }]}>
|
||||
{label}
|
||||
</Text>
|
||||
<Text style={[styles.chevron, { color: theme.colors.textTertiary }]}>
|
||||
›
|
||||
</Text>
|
||||
<Text className="text-base font-medium text-text-primary">{label}</Text>
|
||||
<Text className="text-2xl font-light text-text-tertiary">{">"}</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
@@ -142,8 +125,8 @@ export default function HomeScreen(): React.JSX.Element {
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
style={[{ backgroundColor: theme.colors.backgroundPrimary }]}
|
||||
contentContainerStyle={styles.content}
|
||||
className="bg-bg-primary"
|
||||
contentContainerStyle={{ padding: 24, paddingBottom: 40 }}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={false}
|
||||
@@ -152,27 +135,14 @@ export default function HomeScreen(): React.JSX.Element {
|
||||
/>
|
||||
}
|
||||
>
|
||||
{/* Header */}
|
||||
<Text
|
||||
style={[
|
||||
theme.typography.titleLarge,
|
||||
{ color: theme.colors.textPrimary },
|
||||
]}
|
||||
accessibilityRole="header"
|
||||
>
|
||||
<Text className="text-title-lg text-text-primary" accessibilityRole="header">
|
||||
{selectedProject?.name ?? "OneUptime"}
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
theme.typography.bodyMedium,
|
||||
{ color: theme.colors.textSecondary, marginTop: theme.spacing.xs },
|
||||
]}
|
||||
>
|
||||
<Text className="text-body-md text-text-secondary mt-1">
|
||||
Project overview
|
||||
</Text>
|
||||
|
||||
{/* Stats Grid */}
|
||||
<View style={styles.cardRow}>
|
||||
<View className="flex-row gap-3 mt-4">
|
||||
<StatCard
|
||||
count={incidentCount}
|
||||
label="Active Incidents"
|
||||
@@ -193,7 +163,7 @@ export default function HomeScreen(): React.JSX.Element {
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.cardRow}>
|
||||
<View className="flex-row gap-3 mt-4">
|
||||
<StatCard
|
||||
count={incidentEpisodeCount}
|
||||
label="Inc Episodes"
|
||||
@@ -214,11 +184,8 @@ export default function HomeScreen(): React.JSX.Element {
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Quick Links */}
|
||||
<View style={styles.quickLinksSection}>
|
||||
<Text
|
||||
style={[styles.sectionTitle, { color: theme.colors.textSecondary }]}
|
||||
>
|
||||
<View className="mt-8">
|
||||
<Text className="text-[13px] font-semibold uppercase tracking-widest mb-3 ml-1 text-text-secondary">
|
||||
Quick Links
|
||||
</Text>
|
||||
<QuickLink
|
||||
@@ -249,58 +216,3 @@ export default function HomeScreen(): React.JSX.Element {
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles: ReturnType<typeof StyleSheet.create> = StyleSheet.create({
|
||||
content: {
|
||||
padding: 24,
|
||||
paddingBottom: 40,
|
||||
},
|
||||
cardRow: {
|
||||
flexDirection: "row",
|
||||
gap: 12,
|
||||
marginTop: 16,
|
||||
},
|
||||
summaryCard: {
|
||||
flex: 1,
|
||||
padding: 20,
|
||||
borderRadius: 16,
|
||||
alignItems: "center",
|
||||
},
|
||||
cardCount: {
|
||||
fontSize: 40,
|
||||
fontWeight: "700",
|
||||
fontVariant: ["tabular-nums"],
|
||||
},
|
||||
cardLabel: {
|
||||
fontSize: 14,
|
||||
fontWeight: "500",
|
||||
marginTop: 4,
|
||||
},
|
||||
quickLinksSection: {
|
||||
marginTop: 32,
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 13,
|
||||
fontWeight: "600",
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: 0.8,
|
||||
marginBottom: 12,
|
||||
marginLeft: 4,
|
||||
},
|
||||
linkCard: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
padding: 18,
|
||||
borderRadius: 16,
|
||||
marginBottom: 10,
|
||||
},
|
||||
linkLabel: {
|
||||
fontSize: 16,
|
||||
fontWeight: "500",
|
||||
},
|
||||
chevron: {
|
||||
fontSize: 24,
|
||||
fontWeight: "300",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -7,10 +7,9 @@ import {
|
||||
ActivityIndicator,
|
||||
RefreshControl,
|
||||
Alert,
|
||||
StyleSheet,
|
||||
} from "react-native";
|
||||
import type { NativeStackScreenProps } from "@react-navigation/native-stack";
|
||||
import { useTheme, type Theme } from "../theme";
|
||||
import { useTheme } from "../theme";
|
||||
import { useProject } from "../hooks/useProject";
|
||||
import {
|
||||
useIncidentDetail,
|
||||
@@ -41,7 +40,7 @@ export default function IncidentDetailScreen({
|
||||
route,
|
||||
}: Props): React.JSX.Element {
|
||||
const { incidentId } = route.params;
|
||||
const { theme }: { theme: Theme } = useTheme();
|
||||
const { theme } = useTheme();
|
||||
const { selectedProject } = useProject();
|
||||
const projectId: string = selectedProject?._id ?? "";
|
||||
const queryClient: QueryClient = useQueryClient();
|
||||
@@ -139,9 +138,7 @@ export default function IncidentDetailScreen({
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<View
|
||||
style={[{ flex: 1, backgroundColor: theme.colors.backgroundPrimary }]}
|
||||
>
|
||||
<View className="flex-1 bg-bg-primary">
|
||||
<SkeletonCard variant="detail" />
|
||||
</View>
|
||||
);
|
||||
@@ -149,18 +146,8 @@ export default function IncidentDetailScreen({
|
||||
|
||||
if (!incident) {
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.centered,
|
||||
{ backgroundColor: theme.colors.backgroundPrimary },
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
theme.typography.bodyMedium,
|
||||
{ color: theme.colors.textSecondary },
|
||||
]}
|
||||
>
|
||||
<View className="flex-1 items-center justify-center bg-bg-primary">
|
||||
<Text className="text-body-md text-text-secondary">
|
||||
Incident not found.
|
||||
</Text>
|
||||
</View>
|
||||
@@ -193,46 +180,32 @@ export default function IncidentDetailScreen({
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
style={[{ backgroundColor: theme.colors.backgroundPrimary }]}
|
||||
contentContainerStyle={styles.content}
|
||||
className="bg-bg-primary"
|
||||
contentContainerStyle={{ padding: 20, paddingBottom: 40 }}
|
||||
refreshControl={
|
||||
<RefreshControl refreshing={false} onRefresh={onRefresh} />
|
||||
}
|
||||
>
|
||||
{/* Header */}
|
||||
<View
|
||||
style={[
|
||||
styles.numberBadge,
|
||||
{ backgroundColor: theme.colors.backgroundTertiary },
|
||||
]}
|
||||
>
|
||||
<Text style={[styles.number, { color: theme.colors.textSecondary }]}>
|
||||
<View className="self-start px-2.5 py-1 rounded-lg bg-bg-tertiary">
|
||||
<Text className="text-sm font-semibold text-text-secondary">
|
||||
{incident.incidentNumberWithPrefix || `#${incident.incidentNumber}`}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<Text
|
||||
style={[
|
||||
theme.typography.titleLarge,
|
||||
{ color: theme.colors.textPrimary, marginTop: 4 },
|
||||
]}
|
||||
>
|
||||
<Text className="text-title-lg text-text-primary mt-1">
|
||||
{incident.title}
|
||||
</Text>
|
||||
|
||||
{/* Badges */}
|
||||
<View style={styles.badgeRow}>
|
||||
<View className="flex-row flex-wrap gap-2 mt-3">
|
||||
{incident.currentIncidentState ? (
|
||||
<View
|
||||
style={[
|
||||
styles.badge,
|
||||
{ backgroundColor: theme.colors.backgroundTertiary },
|
||||
]}
|
||||
>
|
||||
<View style={[styles.dot, { backgroundColor: stateColor }]} />
|
||||
<Text
|
||||
style={[styles.badgeText, { color: theme.colors.textPrimary }]}
|
||||
>
|
||||
<View className="flex-row items-center px-2.5 py-[5px] rounded-md bg-bg-tertiary">
|
||||
<View
|
||||
className="w-2 h-2 rounded-full mr-1.5"
|
||||
style={{ backgroundColor: stateColor }}
|
||||
/>
|
||||
<Text className="text-[13px] font-semibold text-text-primary">
|
||||
{incident.currentIncidentState.name}
|
||||
</Text>
|
||||
</View>
|
||||
@@ -240,9 +213,13 @@ export default function IncidentDetailScreen({
|
||||
|
||||
{incident.incidentSeverity ? (
|
||||
<View
|
||||
style={[styles.badge, { backgroundColor: severityColor + "26" }]}
|
||||
className="flex-row items-center px-2.5 py-[5px] rounded-md"
|
||||
style={{ backgroundColor: severityColor + "26" }}
|
||||
>
|
||||
<Text style={[styles.badgeText, { color: severityColor }]}>
|
||||
<Text
|
||||
className="text-[13px] font-semibold"
|
||||
style={{ color: severityColor }}
|
||||
>
|
||||
{incident.incidentSeverity.name}
|
||||
</Text>
|
||||
</View>
|
||||
@@ -251,90 +228,49 @@ export default function IncidentDetailScreen({
|
||||
|
||||
{/* Description */}
|
||||
{incident.description ? (
|
||||
<View style={styles.section}>
|
||||
<Text
|
||||
style={[styles.sectionTitle, { color: theme.colors.textSecondary }]}
|
||||
>
|
||||
<View className="mt-6">
|
||||
<Text className="text-[13px] font-semibold uppercase tracking-wide mb-2.5 text-text-secondary">
|
||||
Description
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
theme.typography.bodyMedium,
|
||||
{ color: theme.colors.textPrimary },
|
||||
]}
|
||||
>
|
||||
<Text className="text-body-md text-text-primary">
|
||||
{incident.description}
|
||||
</Text>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{/* Details */}
|
||||
<View style={styles.section}>
|
||||
<Text
|
||||
style={[styles.sectionTitle, { color: theme.colors.textSecondary }]}
|
||||
>
|
||||
<View className="mt-6">
|
||||
<Text className="text-[13px] font-semibold uppercase tracking-wide mb-2.5 text-text-secondary">
|
||||
Details
|
||||
</Text>
|
||||
|
||||
<View
|
||||
style={[
|
||||
styles.detailCard,
|
||||
theme.shadows.sm,
|
||||
{
|
||||
backgroundColor: theme.colors.backgroundElevated,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<View className="rounded-2xl p-4 bg-bg-elevated shadow-sm">
|
||||
{incident.declaredAt ? (
|
||||
<View style={styles.detailRow}>
|
||||
<Text
|
||||
style={[
|
||||
styles.detailLabel,
|
||||
{ color: theme.colors.textTertiary },
|
||||
]}
|
||||
>
|
||||
<View className="flex-row mb-2.5">
|
||||
<Text className="text-sm w-[90px] text-text-tertiary">
|
||||
Declared
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
styles.detailValue,
|
||||
{ color: theme.colors.textPrimary },
|
||||
]}
|
||||
>
|
||||
<Text className="text-sm text-text-primary">
|
||||
{formatDateTime(incident.declaredAt)}
|
||||
</Text>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
<View style={styles.detailRow}>
|
||||
<Text
|
||||
style={[styles.detailLabel, { color: theme.colors.textTertiary }]}
|
||||
>
|
||||
<View className="flex-row mb-2.5">
|
||||
<Text className="text-sm w-[90px] text-text-tertiary">
|
||||
Created
|
||||
</Text>
|
||||
<Text
|
||||
style={[styles.detailValue, { color: theme.colors.textPrimary }]}
|
||||
>
|
||||
<Text className="text-sm text-text-primary">
|
||||
{formatDateTime(incident.createdAt)}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{incident.monitors?.length > 0 ? (
|
||||
<View style={styles.detailRow}>
|
||||
<Text
|
||||
style={[
|
||||
styles.detailLabel,
|
||||
{ color: theme.colors.textTertiary },
|
||||
]}
|
||||
>
|
||||
<View className="flex-row mb-2.5">
|
||||
<Text className="text-sm w-[90px] text-text-tertiary">
|
||||
Monitors
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
styles.detailValue,
|
||||
{ color: theme.colors.textPrimary, flex: 1 },
|
||||
]}
|
||||
>
|
||||
<Text className="text-sm text-text-primary flex-1">
|
||||
{incident.monitors
|
||||
.map((m: NamedEntity) => {
|
||||
return m.name;
|
||||
@@ -348,20 +284,15 @@ export default function IncidentDetailScreen({
|
||||
|
||||
{/* State Change Actions */}
|
||||
{!isResolved ? (
|
||||
<View style={styles.section}>
|
||||
<Text
|
||||
style={[styles.sectionTitle, { color: theme.colors.textSecondary }]}
|
||||
>
|
||||
<View className="mt-6">
|
||||
<Text className="text-[13px] font-semibold uppercase tracking-wide mb-2.5 text-text-secondary">
|
||||
Actions
|
||||
</Text>
|
||||
<View style={styles.actionRow}>
|
||||
<View className="flex-row gap-3">
|
||||
{!isAcknowledged && !isResolved && acknowledgeState ? (
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.actionButton,
|
||||
theme.shadows.md,
|
||||
{ backgroundColor: theme.colors.stateAcknowledged },
|
||||
]}
|
||||
className="flex-1 py-3.5 rounded-[14px] items-center justify-center min-h-[50px] shadow-md"
|
||||
style={{ backgroundColor: theme.colors.stateAcknowledged }}
|
||||
onPress={() => {
|
||||
return handleStateChange(
|
||||
acknowledgeState._id,
|
||||
@@ -378,12 +309,7 @@ export default function IncidentDetailScreen({
|
||||
color={theme.colors.textInverse}
|
||||
/>
|
||||
) : (
|
||||
<Text
|
||||
style={[
|
||||
styles.actionButtonText,
|
||||
{ color: theme.colors.textInverse },
|
||||
]}
|
||||
>
|
||||
<Text className="text-[15px] font-bold text-text-inverse">
|
||||
Acknowledge
|
||||
</Text>
|
||||
)}
|
||||
@@ -392,11 +318,8 @@ export default function IncidentDetailScreen({
|
||||
|
||||
{resolveState ? (
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.actionButton,
|
||||
theme.shadows.md,
|
||||
{ backgroundColor: theme.colors.stateResolved },
|
||||
]}
|
||||
className="flex-1 py-3.5 rounded-[14px] items-center justify-center min-h-[50px] shadow-md"
|
||||
style={{ backgroundColor: theme.colors.stateResolved }}
|
||||
onPress={() => {
|
||||
return handleStateChange(resolveState._id, resolveState.name);
|
||||
}}
|
||||
@@ -410,12 +333,7 @@ export default function IncidentDetailScreen({
|
||||
color={theme.colors.textInverse}
|
||||
/>
|
||||
) : (
|
||||
<Text
|
||||
style={[
|
||||
styles.actionButtonText,
|
||||
{ color: theme.colors.textInverse },
|
||||
]}
|
||||
>
|
||||
<Text className="text-[15px] font-bold text-text-inverse">
|
||||
Resolve
|
||||
</Text>
|
||||
)}
|
||||
@@ -427,10 +345,8 @@ export default function IncidentDetailScreen({
|
||||
|
||||
{/* State Timeline */}
|
||||
{timeline && timeline.length > 0 ? (
|
||||
<View style={styles.section}>
|
||||
<Text
|
||||
style={[styles.sectionTitle, { color: theme.colors.textSecondary }]}
|
||||
>
|
||||
<View className="mt-6">
|
||||
<Text className="text-[13px] font-semibold uppercase tracking-wide mb-2.5 text-text-secondary">
|
||||
State Timeline
|
||||
</Text>
|
||||
{timeline.map((entry: StateTimelineItem) => {
|
||||
@@ -440,32 +356,17 @@ export default function IncidentDetailScreen({
|
||||
return (
|
||||
<View
|
||||
key={entry._id}
|
||||
style={[
|
||||
styles.timelineEntry,
|
||||
theme.shadows.sm,
|
||||
{
|
||||
backgroundColor: theme.colors.backgroundElevated,
|
||||
},
|
||||
]}
|
||||
className="flex-row items-center p-3.5 rounded-xl mb-2 bg-bg-elevated shadow-sm"
|
||||
>
|
||||
<View
|
||||
style={[styles.timelineDot, { backgroundColor: entryColor }]}
|
||||
className="w-2.5 h-2.5 rounded-full mr-3"
|
||||
style={{ backgroundColor: entryColor }}
|
||||
/>
|
||||
<View style={styles.timelineInfo}>
|
||||
<Text
|
||||
style={[
|
||||
theme.typography.bodyMedium,
|
||||
{ color: theme.colors.textPrimary, fontWeight: "600" },
|
||||
]}
|
||||
>
|
||||
<View className="flex-1">
|
||||
<Text className="text-body-md text-text-primary font-semibold">
|
||||
{entry.incidentState?.name ?? "Unknown"}
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
theme.typography.bodySmall,
|
||||
{ color: theme.colors.textTertiary },
|
||||
]}
|
||||
>
|
||||
<Text className="text-body-sm text-text-tertiary">
|
||||
{formatDateTime(entry.createdAt)}
|
||||
</Text>
|
||||
</View>
|
||||
@@ -476,31 +377,19 @@ export default function IncidentDetailScreen({
|
||||
) : null}
|
||||
|
||||
{/* Internal Notes */}
|
||||
<View style={styles.section}>
|
||||
<View style={styles.sectionHeader}>
|
||||
<Text
|
||||
style={[
|
||||
styles.sectionTitle,
|
||||
{ color: theme.colors.textSecondary, marginBottom: 0 },
|
||||
]}
|
||||
>
|
||||
<View className="mt-6">
|
||||
<View className="flex-row justify-between items-center mb-2.5">
|
||||
<Text className="text-[13px] font-semibold uppercase tracking-wide text-text-secondary">
|
||||
Internal Notes
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.addNoteButton,
|
||||
{ backgroundColor: theme.colors.actionPrimary },
|
||||
]}
|
||||
className="px-3 py-1.5 rounded-lg"
|
||||
style={{ backgroundColor: theme.colors.actionPrimary }}
|
||||
onPress={() => {
|
||||
return setNoteModalVisible(true);
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.addNoteButtonText,
|
||||
{ color: theme.colors.textInverse },
|
||||
]}
|
||||
>
|
||||
<Text className="text-[13px] font-semibold text-text-inverse">
|
||||
Add Note
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
@@ -511,39 +400,18 @@ export default function IncidentDetailScreen({
|
||||
return (
|
||||
<View
|
||||
key={note._id}
|
||||
style={[
|
||||
styles.noteCard,
|
||||
theme.shadows.sm,
|
||||
{
|
||||
backgroundColor: theme.colors.backgroundElevated,
|
||||
},
|
||||
]}
|
||||
className="rounded-xl p-3.5 mb-2 bg-bg-elevated shadow-sm"
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
theme.typography.bodyMedium,
|
||||
{ color: theme.colors.textPrimary },
|
||||
]}
|
||||
>
|
||||
<Text className="text-body-md text-text-primary">
|
||||
{note.note}
|
||||
</Text>
|
||||
<View style={styles.noteMeta}>
|
||||
<View className="flex-row justify-between mt-2">
|
||||
{note.createdByUser ? (
|
||||
<Text
|
||||
style={[
|
||||
theme.typography.bodySmall,
|
||||
{ color: theme.colors.textTertiary },
|
||||
]}
|
||||
>
|
||||
<Text className="text-body-sm text-text-tertiary">
|
||||
{note.createdByUser.name}
|
||||
</Text>
|
||||
) : null}
|
||||
<Text
|
||||
style={[
|
||||
theme.typography.bodySmall,
|
||||
{ color: theme.colors.textTertiary },
|
||||
]}
|
||||
>
|
||||
<Text className="text-body-sm text-text-tertiary">
|
||||
{formatDateTime(note.createdAt)}
|
||||
</Text>
|
||||
</View>
|
||||
@@ -553,12 +421,7 @@ export default function IncidentDetailScreen({
|
||||
: null}
|
||||
|
||||
{notes && notes.length === 0 ? (
|
||||
<Text
|
||||
style={[
|
||||
theme.typography.bodySmall,
|
||||
{ color: theme.colors.textTertiary },
|
||||
]}
|
||||
>
|
||||
<Text className="text-body-sm text-text-tertiary">
|
||||
No notes yet.
|
||||
</Text>
|
||||
) : null}
|
||||
@@ -575,130 +438,3 @@ export default function IncidentDetailScreen({
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles: ReturnType<typeof StyleSheet.create> = StyleSheet.create({
|
||||
centered: {
|
||||
flex: 1,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
content: {
|
||||
padding: 20,
|
||||
paddingBottom: 40,
|
||||
},
|
||||
numberBadge: {
|
||||
alignSelf: "flex-start",
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 4,
|
||||
borderRadius: 8,
|
||||
},
|
||||
number: {
|
||||
fontSize: 14,
|
||||
fontWeight: "600",
|
||||
},
|
||||
badgeRow: {
|
||||
flexDirection: "row",
|
||||
flexWrap: "wrap",
|
||||
gap: 8,
|
||||
marginTop: 12,
|
||||
},
|
||||
badge: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 5,
|
||||
borderRadius: 6,
|
||||
},
|
||||
dot: {
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
marginRight: 6,
|
||||
},
|
||||
badgeText: {
|
||||
fontSize: 13,
|
||||
fontWeight: "600",
|
||||
},
|
||||
section: {
|
||||
marginTop: 24,
|
||||
},
|
||||
sectionHeader: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
marginBottom: 10,
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 13,
|
||||
fontWeight: "600",
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: 0.5,
|
||||
marginBottom: 10,
|
||||
},
|
||||
detailCard: {
|
||||
borderRadius: 16,
|
||||
padding: 16,
|
||||
},
|
||||
detailRow: {
|
||||
flexDirection: "row",
|
||||
marginBottom: 10,
|
||||
},
|
||||
detailLabel: {
|
||||
fontSize: 14,
|
||||
width: 90,
|
||||
},
|
||||
detailValue: {
|
||||
fontSize: 14,
|
||||
},
|
||||
actionRow: {
|
||||
flexDirection: "row",
|
||||
gap: 12,
|
||||
},
|
||||
actionButton: {
|
||||
flex: 1,
|
||||
paddingVertical: 14,
|
||||
borderRadius: 14,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
minHeight: 50,
|
||||
},
|
||||
actionButtonText: {
|
||||
fontSize: 15,
|
||||
fontWeight: "700",
|
||||
},
|
||||
timelineEntry: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
padding: 14,
|
||||
borderRadius: 12,
|
||||
marginBottom: 8,
|
||||
},
|
||||
timelineDot: {
|
||||
width: 10,
|
||||
height: 10,
|
||||
borderRadius: 5,
|
||||
marginRight: 12,
|
||||
},
|
||||
timelineInfo: {
|
||||
flex: 1,
|
||||
},
|
||||
addNoteButton: {
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 6,
|
||||
borderRadius: 8,
|
||||
},
|
||||
addNoteButtonText: {
|
||||
fontSize: 13,
|
||||
fontWeight: "600",
|
||||
},
|
||||
noteCard: {
|
||||
borderRadius: 12,
|
||||
padding: 14,
|
||||
marginBottom: 8,
|
||||
},
|
||||
noteMeta: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
marginTop: 8,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
ActivityIndicator,
|
||||
RefreshControl,
|
||||
Alert,
|
||||
StyleSheet,
|
||||
} from "react-native";
|
||||
import type { NativeStackScreenProps } from "@react-navigation/native-stack";
|
||||
import { useTheme } from "../theme";
|
||||
@@ -138,9 +137,7 @@ export default function IncidentEpisodeDetailScreen({
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<View
|
||||
style={[{ flex: 1, backgroundColor: theme.colors.backgroundPrimary }]}
|
||||
>
|
||||
<View className="flex-1 bg-bg-primary">
|
||||
<SkeletonCard variant="detail" />
|
||||
</View>
|
||||
);
|
||||
@@ -148,18 +145,8 @@ export default function IncidentEpisodeDetailScreen({
|
||||
|
||||
if (!episode) {
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.centered,
|
||||
{ backgroundColor: theme.colors.backgroundPrimary },
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
theme.typography.bodyMedium,
|
||||
{ color: theme.colors.textSecondary },
|
||||
]}
|
||||
>
|
||||
<View className="flex-1 items-center justify-center bg-bg-primary">
|
||||
<Text className="text-body-md text-text-secondary">
|
||||
Episode not found.
|
||||
</Text>
|
||||
</View>
|
||||
@@ -191,46 +178,32 @@ export default function IncidentEpisodeDetailScreen({
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
style={[{ backgroundColor: theme.colors.backgroundPrimary }]}
|
||||
contentContainerStyle={styles.content}
|
||||
className="bg-bg-primary"
|
||||
contentContainerStyle={{ padding: 20, paddingBottom: 40 }}
|
||||
refreshControl={
|
||||
<RefreshControl refreshing={false} onRefresh={onRefresh} />
|
||||
}
|
||||
>
|
||||
{/* Header */}
|
||||
<View
|
||||
style={[
|
||||
styles.numberBadge,
|
||||
{ backgroundColor: theme.colors.backgroundTertiary },
|
||||
]}
|
||||
>
|
||||
<Text style={[styles.number, { color: theme.colors.textSecondary }]}>
|
||||
<View className="self-start px-2.5 py-1 rounded-lg bg-bg-tertiary">
|
||||
<Text className="text-sm font-semibold text-text-secondary">
|
||||
{episode.episodeNumberWithPrefix || `#${episode.episodeNumber}`}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<Text
|
||||
style={[
|
||||
theme.typography.titleLarge,
|
||||
{ color: theme.colors.textPrimary, marginTop: 4 },
|
||||
]}
|
||||
>
|
||||
<Text className="text-title-lg text-text-primary mt-1">
|
||||
{episode.title}
|
||||
</Text>
|
||||
|
||||
{/* Badges */}
|
||||
<View style={styles.badgeRow}>
|
||||
<View className="flex-row flex-wrap gap-2 mt-3">
|
||||
{episode.currentIncidentState ? (
|
||||
<View
|
||||
style={[
|
||||
styles.badge,
|
||||
{ backgroundColor: theme.colors.backgroundTertiary },
|
||||
]}
|
||||
>
|
||||
<View style={[styles.dot, { backgroundColor: stateColor }]} />
|
||||
<Text
|
||||
style={[styles.badgeText, { color: theme.colors.textPrimary }]}
|
||||
>
|
||||
<View className="flex-row items-center px-2.5 py-[5px] rounded-md bg-bg-tertiary">
|
||||
<View
|
||||
className="w-2 h-2 rounded-full mr-1.5"
|
||||
style={{ backgroundColor: stateColor }}
|
||||
/>
|
||||
<Text className="text-[13px] font-semibold text-text-primary">
|
||||
{episode.currentIncidentState.name}
|
||||
</Text>
|
||||
</View>
|
||||
@@ -238,9 +211,13 @@ export default function IncidentEpisodeDetailScreen({
|
||||
|
||||
{episode.incidentSeverity ? (
|
||||
<View
|
||||
style={[styles.badge, { backgroundColor: severityColor + "26" }]}
|
||||
className="flex-row items-center px-2.5 py-[5px] rounded-md"
|
||||
style={{ backgroundColor: severityColor + "26" }}
|
||||
>
|
||||
<Text style={[styles.badgeText, { color: severityColor }]}>
|
||||
<Text
|
||||
className="text-[13px] font-semibold"
|
||||
style={{ color: severityColor }}
|
||||
>
|
||||
{episode.incidentSeverity.name}
|
||||
</Text>
|
||||
</View>
|
||||
@@ -249,83 +226,48 @@ export default function IncidentEpisodeDetailScreen({
|
||||
|
||||
{/* Description */}
|
||||
{episode.description ? (
|
||||
<View style={styles.section}>
|
||||
<Text
|
||||
style={[styles.sectionTitle, { color: theme.colors.textSecondary }]}
|
||||
>
|
||||
<View className="mt-6">
|
||||
<Text className="text-[13px] font-semibold uppercase tracking-wide mb-2.5 text-text-secondary">
|
||||
Description
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
theme.typography.bodyMedium,
|
||||
{ color: theme.colors.textPrimary },
|
||||
]}
|
||||
>
|
||||
<Text className="text-body-md text-text-primary">
|
||||
{episode.description}
|
||||
</Text>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{/* Details */}
|
||||
<View style={styles.section}>
|
||||
<Text
|
||||
style={[styles.sectionTitle, { color: theme.colors.textSecondary }]}
|
||||
>
|
||||
<View className="mt-6">
|
||||
<Text className="text-[13px] font-semibold uppercase tracking-wide mb-2.5 text-text-secondary">
|
||||
Details
|
||||
</Text>
|
||||
|
||||
<View
|
||||
style={[
|
||||
styles.detailCard,
|
||||
theme.shadows.sm,
|
||||
{
|
||||
backgroundColor: theme.colors.backgroundElevated,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<View className="rounded-2xl p-4 bg-bg-elevated shadow-sm">
|
||||
{episode.declaredAt ? (
|
||||
<View style={styles.detailRow}>
|
||||
<Text
|
||||
style={[
|
||||
styles.detailLabel,
|
||||
{ color: theme.colors.textTertiary },
|
||||
]}
|
||||
>
|
||||
<View className="flex-row mb-2.5">
|
||||
<Text className="text-sm w-[90px] text-text-tertiary">
|
||||
Declared
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
styles.detailValue,
|
||||
{ color: theme.colors.textPrimary },
|
||||
]}
|
||||
>
|
||||
<Text className="text-sm text-text-primary">
|
||||
{formatDateTime(episode.declaredAt)}
|
||||
</Text>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
<View style={styles.detailRow}>
|
||||
<Text
|
||||
style={[styles.detailLabel, { color: theme.colors.textTertiary }]}
|
||||
>
|
||||
<View className="flex-row mb-2.5">
|
||||
<Text className="text-sm w-[90px] text-text-tertiary">
|
||||
Created
|
||||
</Text>
|
||||
<Text
|
||||
style={[styles.detailValue, { color: theme.colors.textPrimary }]}
|
||||
>
|
||||
<Text className="text-sm text-text-primary">
|
||||
{formatDateTime(episode.createdAt)}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.detailRow}>
|
||||
<Text
|
||||
style={[styles.detailLabel, { color: theme.colors.textTertiary }]}
|
||||
>
|
||||
<View className="flex-row mb-2.5">
|
||||
<Text className="text-sm w-[90px] text-text-tertiary">
|
||||
Incidents
|
||||
</Text>
|
||||
<Text
|
||||
style={[styles.detailValue, { color: theme.colors.textPrimary }]}
|
||||
>
|
||||
<Text className="text-sm text-text-primary">
|
||||
{episode.incidentCount ?? 0}
|
||||
</Text>
|
||||
</View>
|
||||
@@ -334,20 +276,15 @@ export default function IncidentEpisodeDetailScreen({
|
||||
|
||||
{/* State Change Actions */}
|
||||
{!isResolved ? (
|
||||
<View style={styles.section}>
|
||||
<Text
|
||||
style={[styles.sectionTitle, { color: theme.colors.textSecondary }]}
|
||||
>
|
||||
<View className="mt-6">
|
||||
<Text className="text-[13px] font-semibold uppercase tracking-wide mb-2.5 text-text-secondary">
|
||||
Actions
|
||||
</Text>
|
||||
<View style={styles.actionRow}>
|
||||
<View className="flex-row gap-3">
|
||||
{!isAcknowledged && !isResolved && acknowledgeState ? (
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.actionButton,
|
||||
theme.shadows.md,
|
||||
{ backgroundColor: theme.colors.stateAcknowledged },
|
||||
]}
|
||||
className="flex-1 py-3.5 rounded-[14px] items-center justify-center min-h-[50px] shadow-md"
|
||||
style={{ backgroundColor: theme.colors.stateAcknowledged }}
|
||||
onPress={() => {
|
||||
return handleStateChange(
|
||||
acknowledgeState._id,
|
||||
@@ -362,12 +299,7 @@ export default function IncidentEpisodeDetailScreen({
|
||||
color={theme.colors.textInverse}
|
||||
/>
|
||||
) : (
|
||||
<Text
|
||||
style={[
|
||||
styles.actionButtonText,
|
||||
{ color: theme.colors.textInverse },
|
||||
]}
|
||||
>
|
||||
<Text className="text-[15px] font-bold text-text-inverse">
|
||||
Acknowledge
|
||||
</Text>
|
||||
)}
|
||||
@@ -376,11 +308,8 @@ export default function IncidentEpisodeDetailScreen({
|
||||
|
||||
{resolveState ? (
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.actionButton,
|
||||
theme.shadows.md,
|
||||
{ backgroundColor: theme.colors.stateResolved },
|
||||
]}
|
||||
className="flex-1 py-3.5 rounded-[14px] items-center justify-center min-h-[50px] shadow-md"
|
||||
style={{ backgroundColor: theme.colors.stateResolved }}
|
||||
onPress={() => {
|
||||
return handleStateChange(resolveState._id, resolveState.name);
|
||||
}}
|
||||
@@ -392,12 +321,7 @@ export default function IncidentEpisodeDetailScreen({
|
||||
color={theme.colors.textInverse}
|
||||
/>
|
||||
) : (
|
||||
<Text
|
||||
style={[
|
||||
styles.actionButtonText,
|
||||
{ color: theme.colors.textInverse },
|
||||
]}
|
||||
>
|
||||
<Text className="text-[15px] font-bold text-text-inverse">
|
||||
Resolve
|
||||
</Text>
|
||||
)}
|
||||
@@ -409,10 +333,8 @@ export default function IncidentEpisodeDetailScreen({
|
||||
|
||||
{/* State Timeline */}
|
||||
{timeline && timeline.length > 0 ? (
|
||||
<View style={styles.section}>
|
||||
<Text
|
||||
style={[styles.sectionTitle, { color: theme.colors.textSecondary }]}
|
||||
>
|
||||
<View className="mt-6">
|
||||
<Text className="text-[13px] font-semibold uppercase tracking-wide mb-2.5 text-text-secondary">
|
||||
State Timeline
|
||||
</Text>
|
||||
{timeline.map((entry: StateTimelineItem) => {
|
||||
@@ -422,35 +344,17 @@ export default function IncidentEpisodeDetailScreen({
|
||||
return (
|
||||
<View
|
||||
key={entry._id}
|
||||
style={[
|
||||
styles.timelineEntry,
|
||||
theme.shadows.sm,
|
||||
{
|
||||
backgroundColor: theme.colors.backgroundElevated,
|
||||
},
|
||||
]}
|
||||
className="flex-row items-center p-3.5 rounded-xl mb-2 bg-bg-elevated shadow-sm"
|
||||
>
|
||||
<View
|
||||
style={[styles.timelineDot, { backgroundColor: entryColor }]}
|
||||
className="w-2.5 h-2.5 rounded-full mr-3"
|
||||
style={{ backgroundColor: entryColor }}
|
||||
/>
|
||||
<View style={styles.timelineInfo}>
|
||||
<Text
|
||||
style={[
|
||||
theme.typography.bodyMedium,
|
||||
{
|
||||
color: theme.colors.textPrimary,
|
||||
fontWeight: "600",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<View className="flex-1">
|
||||
<Text className="text-body-md text-text-primary font-semibold">
|
||||
{entry.incidentState?.name ?? "Unknown"}
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
theme.typography.bodySmall,
|
||||
{ color: theme.colors.textTertiary },
|
||||
]}
|
||||
>
|
||||
<Text className="text-body-sm text-text-tertiary">
|
||||
{formatDateTime(entry.createdAt)}
|
||||
</Text>
|
||||
</View>
|
||||
@@ -461,31 +365,19 @@ export default function IncidentEpisodeDetailScreen({
|
||||
) : null}
|
||||
|
||||
{/* Internal Notes */}
|
||||
<View style={styles.section}>
|
||||
<View style={styles.sectionHeader}>
|
||||
<Text
|
||||
style={[
|
||||
styles.sectionTitle,
|
||||
{ color: theme.colors.textSecondary, marginBottom: 0 },
|
||||
]}
|
||||
>
|
||||
<View className="mt-6">
|
||||
<View className="flex-row justify-between items-center mb-2.5">
|
||||
<Text className="text-[13px] font-semibold uppercase tracking-wide text-text-secondary">
|
||||
Internal Notes
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.addNoteButton,
|
||||
{ backgroundColor: theme.colors.actionPrimary },
|
||||
]}
|
||||
className="px-3 py-1.5 rounded-lg"
|
||||
style={{ backgroundColor: theme.colors.actionPrimary }}
|
||||
onPress={() => {
|
||||
return setNoteModalVisible(true);
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.addNoteButtonText,
|
||||
{ color: theme.colors.textInverse },
|
||||
]}
|
||||
>
|
||||
<Text className="text-[13px] font-semibold text-text-inverse">
|
||||
Add Note
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
@@ -496,39 +388,18 @@ export default function IncidentEpisodeDetailScreen({
|
||||
return (
|
||||
<View
|
||||
key={note._id}
|
||||
style={[
|
||||
styles.noteCard,
|
||||
theme.shadows.sm,
|
||||
{
|
||||
backgroundColor: theme.colors.backgroundElevated,
|
||||
},
|
||||
]}
|
||||
className="rounded-xl p-3.5 mb-2 bg-bg-elevated shadow-sm"
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
theme.typography.bodyMedium,
|
||||
{ color: theme.colors.textPrimary },
|
||||
]}
|
||||
>
|
||||
<Text className="text-body-md text-text-primary">
|
||||
{note.note}
|
||||
</Text>
|
||||
<View style={styles.noteMeta}>
|
||||
<View className="flex-row justify-between mt-2">
|
||||
{note.createdByUser ? (
|
||||
<Text
|
||||
style={[
|
||||
theme.typography.bodySmall,
|
||||
{ color: theme.colors.textTertiary },
|
||||
]}
|
||||
>
|
||||
<Text className="text-body-sm text-text-tertiary">
|
||||
{note.createdByUser.name}
|
||||
</Text>
|
||||
) : null}
|
||||
<Text
|
||||
style={[
|
||||
theme.typography.bodySmall,
|
||||
{ color: theme.colors.textTertiary },
|
||||
]}
|
||||
>
|
||||
<Text className="text-body-sm text-text-tertiary">
|
||||
{formatDateTime(note.createdAt)}
|
||||
</Text>
|
||||
</View>
|
||||
@@ -538,12 +409,7 @@ export default function IncidentEpisodeDetailScreen({
|
||||
: null}
|
||||
|
||||
{notes && notes.length === 0 ? (
|
||||
<Text
|
||||
style={[
|
||||
theme.typography.bodySmall,
|
||||
{ color: theme.colors.textTertiary },
|
||||
]}
|
||||
>
|
||||
<Text className="text-body-sm text-text-tertiary">
|
||||
No notes yet.
|
||||
</Text>
|
||||
) : null}
|
||||
@@ -560,130 +426,3 @@ export default function IncidentEpisodeDetailScreen({
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles: ReturnType<typeof StyleSheet.create> = StyleSheet.create({
|
||||
centered: {
|
||||
flex: 1,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
content: {
|
||||
padding: 20,
|
||||
paddingBottom: 40,
|
||||
},
|
||||
numberBadge: {
|
||||
alignSelf: "flex-start",
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 4,
|
||||
borderRadius: 8,
|
||||
},
|
||||
number: {
|
||||
fontSize: 14,
|
||||
fontWeight: "600",
|
||||
},
|
||||
badgeRow: {
|
||||
flexDirection: "row",
|
||||
flexWrap: "wrap",
|
||||
gap: 8,
|
||||
marginTop: 12,
|
||||
},
|
||||
badge: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 5,
|
||||
borderRadius: 6,
|
||||
},
|
||||
dot: {
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
marginRight: 6,
|
||||
},
|
||||
badgeText: {
|
||||
fontSize: 13,
|
||||
fontWeight: "600",
|
||||
},
|
||||
section: {
|
||||
marginTop: 24,
|
||||
},
|
||||
sectionHeader: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
marginBottom: 10,
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 13,
|
||||
fontWeight: "600",
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: 0.5,
|
||||
marginBottom: 10,
|
||||
},
|
||||
detailCard: {
|
||||
borderRadius: 16,
|
||||
padding: 16,
|
||||
},
|
||||
detailRow: {
|
||||
flexDirection: "row",
|
||||
marginBottom: 10,
|
||||
},
|
||||
detailLabel: {
|
||||
fontSize: 14,
|
||||
width: 90,
|
||||
},
|
||||
detailValue: {
|
||||
fontSize: 14,
|
||||
},
|
||||
actionRow: {
|
||||
flexDirection: "row",
|
||||
gap: 12,
|
||||
},
|
||||
actionButton: {
|
||||
flex: 1,
|
||||
paddingVertical: 14,
|
||||
borderRadius: 14,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
minHeight: 50,
|
||||
},
|
||||
actionButtonText: {
|
||||
fontSize: 15,
|
||||
fontWeight: "700",
|
||||
},
|
||||
timelineEntry: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
padding: 14,
|
||||
borderRadius: 12,
|
||||
marginBottom: 8,
|
||||
},
|
||||
timelineDot: {
|
||||
width: 10,
|
||||
height: 10,
|
||||
borderRadius: 5,
|
||||
marginRight: 12,
|
||||
},
|
||||
timelineInfo: {
|
||||
flex: 1,
|
||||
},
|
||||
addNoteButton: {
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 6,
|
||||
borderRadius: 8,
|
||||
},
|
||||
addNoteButtonText: {
|
||||
fontSize: 13,
|
||||
fontWeight: "600",
|
||||
},
|
||||
noteCard: {
|
||||
borderRadius: 12,
|
||||
padding: 14,
|
||||
marginBottom: 8,
|
||||
},
|
||||
noteMeta: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
marginTop: 8,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
RefreshControl,
|
||||
TouchableOpacity,
|
||||
Text,
|
||||
StyleSheet,
|
||||
ListRenderItemInfo,
|
||||
} from "react-native";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
@@ -73,13 +72,8 @@ export default function IncidentEpisodesScreen(): React.JSX.Element {
|
||||
|
||||
if (isLoading && episodes.length === 0) {
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.container,
|
||||
{ backgroundColor: theme.colors.backgroundPrimary },
|
||||
]}
|
||||
>
|
||||
<View style={styles.skeletonList}>
|
||||
<View className="flex-1 bg-bg-primary">
|
||||
<View className="p-4">
|
||||
<SkeletonCard />
|
||||
<SkeletonCard />
|
||||
<SkeletonCard />
|
||||
@@ -90,36 +84,18 @@ export default function IncidentEpisodesScreen(): React.JSX.Element {
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.centered,
|
||||
{ backgroundColor: theme.colors.backgroundPrimary },
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
theme.typography.bodyMedium,
|
||||
{ color: theme.colors.textSecondary, textAlign: "center" },
|
||||
]}
|
||||
>
|
||||
<View className="flex-1 items-center justify-center px-8 bg-bg-primary">
|
||||
<Text className="text-body-md text-text-secondary text-center">
|
||||
Failed to load incident episodes.
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.retryButton,
|
||||
theme.shadows.md,
|
||||
{ backgroundColor: theme.colors.actionPrimary },
|
||||
]}
|
||||
className="mt-4 px-6 py-3 rounded-[10px] shadow-md"
|
||||
style={{ backgroundColor: theme.colors.actionPrimary }}
|
||||
onPress={() => {
|
||||
return refetch();
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
theme.typography.bodyMedium,
|
||||
{ color: theme.colors.textInverse, fontWeight: "600" },
|
||||
]}
|
||||
>
|
||||
<Text className="text-body-md text-text-inverse font-semibold">
|
||||
Retry
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
@@ -128,19 +104,14 @@ export default function IncidentEpisodesScreen(): React.JSX.Element {
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.container,
|
||||
{ backgroundColor: theme.colors.backgroundPrimary },
|
||||
]}
|
||||
>
|
||||
<View className="flex-1 bg-bg-primary">
|
||||
<FlatList
|
||||
data={episodes}
|
||||
keyExtractor={(item: IncidentEpisodeItem) => {
|
||||
return item._id;
|
||||
}}
|
||||
contentContainerStyle={
|
||||
episodes.length === 0 ? styles.emptyContainer : styles.list
|
||||
episodes.length === 0 ? { flex: 1 } : { padding: 16 }
|
||||
}
|
||||
renderItem={({ item }: ListRenderItemInfo<IncidentEpisodeItem>) => {
|
||||
return (
|
||||
@@ -169,30 +140,3 @@ export default function IncidentEpisodesScreen(): React.JSX.Element {
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles: ReturnType<typeof StyleSheet.create> = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
centered: {
|
||||
flex: 1,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
paddingHorizontal: 32,
|
||||
},
|
||||
list: {
|
||||
padding: 16,
|
||||
},
|
||||
emptyContainer: {
|
||||
flex: 1,
|
||||
},
|
||||
skeletonList: {
|
||||
padding: 16,
|
||||
},
|
||||
retryButton: {
|
||||
marginTop: 16,
|
||||
paddingHorizontal: 24,
|
||||
paddingVertical: 12,
|
||||
borderRadius: 10,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -5,12 +5,11 @@ import {
|
||||
RefreshControl,
|
||||
TouchableOpacity,
|
||||
Text,
|
||||
StyleSheet,
|
||||
ListRenderItemInfo,
|
||||
} from "react-native";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import type { NativeStackNavigationProp } from "@react-navigation/native-stack";
|
||||
import { useTheme, type Theme } from "../theme";
|
||||
import { useTheme } from "../theme";
|
||||
import { useProject } from "../hooks/useProject";
|
||||
import { useIncidents } from "../hooks/useIncidents";
|
||||
import { useIncidentStates } from "../hooks/useIncidentDetail";
|
||||
@@ -32,7 +31,7 @@ type NavProp = NativeStackNavigationProp<
|
||||
>;
|
||||
|
||||
export default function IncidentsScreen(): React.JSX.Element {
|
||||
const { theme }: { theme: Theme } = useTheme();
|
||||
const { theme } = useTheme();
|
||||
const { selectedProject } = useProject();
|
||||
const projectId: string = selectedProject?._id ?? "";
|
||||
const navigation: NativeStackNavigationProp<
|
||||
@@ -114,13 +113,8 @@ export default function IncidentsScreen(): React.JSX.Element {
|
||||
|
||||
if (isLoading && incidents.length === 0) {
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.container,
|
||||
{ backgroundColor: theme.colors.backgroundPrimary },
|
||||
]}
|
||||
>
|
||||
<View style={styles.skeletonList}>
|
||||
<View className="flex-1 bg-bg-primary">
|
||||
<View className="p-4">
|
||||
<SkeletonCard />
|
||||
<SkeletonCard />
|
||||
<SkeletonCard />
|
||||
@@ -131,36 +125,18 @@ export default function IncidentsScreen(): React.JSX.Element {
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.centered,
|
||||
{ backgroundColor: theme.colors.backgroundPrimary },
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
theme.typography.bodyMedium,
|
||||
{ color: theme.colors.textSecondary, textAlign: "center" },
|
||||
]}
|
||||
>
|
||||
<View className="flex-1 items-center justify-center px-8 bg-bg-primary">
|
||||
<Text className="text-body-md text-text-secondary text-center">
|
||||
Failed to load incidents.
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.retryButton,
|
||||
theme.shadows.md,
|
||||
{ backgroundColor: theme.colors.actionPrimary },
|
||||
]}
|
||||
className="mt-4 px-6 py-3 rounded-[10px] shadow-md"
|
||||
style={{ backgroundColor: theme.colors.actionPrimary }}
|
||||
onPress={() => {
|
||||
return refetch();
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
theme.typography.bodyMedium,
|
||||
{ color: theme.colors.textInverse, fontWeight: "600" },
|
||||
]}
|
||||
>
|
||||
<Text className="text-body-md text-text-inverse font-semibold">
|
||||
Retry
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
@@ -169,19 +145,14 @@ export default function IncidentsScreen(): React.JSX.Element {
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.container,
|
||||
{ backgroundColor: theme.colors.backgroundPrimary },
|
||||
]}
|
||||
>
|
||||
<View className="flex-1 bg-bg-primary">
|
||||
<FlatList
|
||||
data={incidents}
|
||||
keyExtractor={(item: IncidentItem) => {
|
||||
return item._id;
|
||||
}}
|
||||
contentContainerStyle={
|
||||
incidents.length === 0 ? styles.emptyContainer : styles.list
|
||||
incidents.length === 0 ? { flex: 1 } : { padding: 16 }
|
||||
}
|
||||
renderItem={({ item }: ListRenderItemInfo<IncidentItem>) => {
|
||||
return (
|
||||
@@ -224,30 +195,3 @@ export default function IncidentsScreen(): React.JSX.Element {
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles: ReturnType<typeof StyleSheet.create> = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
centered: {
|
||||
flex: 1,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
paddingHorizontal: 32,
|
||||
},
|
||||
list: {
|
||||
padding: 16,
|
||||
},
|
||||
emptyContainer: {
|
||||
flex: 1,
|
||||
},
|
||||
skeletonList: {
|
||||
padding: 16,
|
||||
},
|
||||
retryButton: {
|
||||
marginTop: 16,
|
||||
paddingHorizontal: 24,
|
||||
paddingVertical: 12,
|
||||
borderRadius: 10,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,13 +1,5 @@
|
||||
import React, { useState, useEffect, useCallback } from "react";
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
ScrollView,
|
||||
Switch,
|
||||
StyleSheet,
|
||||
ViewStyle,
|
||||
TextStyle,
|
||||
} from "react-native";
|
||||
import { View, Text, ScrollView, Switch } from "react-native";
|
||||
import { useTheme } from "../theme";
|
||||
import { useHaptics } from "../hooks/useHaptics";
|
||||
import {
|
||||
@@ -33,24 +25,16 @@ function PrefRow({
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.row,
|
||||
theme.shadows.sm,
|
||||
{
|
||||
backgroundColor: theme.colors.backgroundElevated,
|
||||
},
|
||||
]}
|
||||
className="flex-row justify-between items-center p-4 rounded-2xl mb-2.5 bg-bg-elevated shadow-sm"
|
||||
accessibilityRole="switch"
|
||||
accessibilityState={{ checked: value }}
|
||||
accessibilityLabel={`${label}. ${description}`}
|
||||
>
|
||||
<View style={styles.rowText}>
|
||||
<Text style={[styles.rowLabel, { color: theme.colors.textPrimary }]}>
|
||||
<View className="flex-1 mr-3">
|
||||
<Text className="text-base font-medium text-text-primary">
|
||||
{label}
|
||||
</Text>
|
||||
<Text
|
||||
style={[styles.rowDescription, { color: theme.colors.textTertiary }]}
|
||||
>
|
||||
<Text className="text-[13px] mt-0.5 leading-[18px] text-text-tertiary">
|
||||
{description}
|
||||
</Text>
|
||||
</View>
|
||||
@@ -102,33 +86,27 @@ export default function NotificationPreferencesScreen(): React.JSX.Element {
|
||||
if (!loaded) {
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.container,
|
||||
{ backgroundColor: theme.colors.backgroundPrimary },
|
||||
]}
|
||||
className="flex-1"
|
||||
style={{ backgroundColor: theme.colors.backgroundPrimary }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
style={[{ backgroundColor: theme.colors.backgroundPrimary }]}
|
||||
contentContainerStyle={styles.content}
|
||||
className="bg-bg-primary"
|
||||
contentContainerStyle={{ padding: 20, paddingBottom: 60 }}
|
||||
>
|
||||
{/* Event Types */}
|
||||
<View style={styles.section}>
|
||||
<Text
|
||||
style={[styles.sectionTitle, { color: theme.colors.textSecondary }]}
|
||||
>
|
||||
<View className="mb-7">
|
||||
<Text className="text-[13px] font-semibold uppercase tracking-widest mb-1 ml-1 text-text-secondary">
|
||||
Event Types
|
||||
</Text>
|
||||
<Text
|
||||
style={[styles.sectionHint, { color: theme.colors.textTertiary }]}
|
||||
>
|
||||
<Text className="text-xs mb-3 ml-1 leading-4 text-text-tertiary">
|
||||
Choose which event types send push notifications
|
||||
</Text>
|
||||
|
||||
<View style={styles.rowGroup}>
|
||||
<View className="gap-px">
|
||||
<PrefRow
|
||||
label="Incidents"
|
||||
description="New incidents and state changes"
|
||||
@@ -165,14 +143,12 @@ export default function NotificationPreferencesScreen(): React.JSX.Element {
|
||||
</View>
|
||||
|
||||
{/* Priority Filter */}
|
||||
<View style={styles.section}>
|
||||
<Text
|
||||
style={[styles.sectionTitle, { color: theme.colors.textSecondary }]}
|
||||
>
|
||||
<View className="mb-7">
|
||||
<Text className="text-[13px] font-semibold uppercase tracking-widest mb-2.5 ml-1 text-text-secondary">
|
||||
Priority
|
||||
</Text>
|
||||
|
||||
<View style={styles.rowGroup}>
|
||||
<View className="gap-px">
|
||||
<PrefRow
|
||||
label="Critical Only"
|
||||
description="Only receive notifications for critical and high severity events"
|
||||
@@ -185,8 +161,8 @@ export default function NotificationPreferencesScreen(): React.JSX.Element {
|
||||
</View>
|
||||
|
||||
{/* Info */}
|
||||
<View style={styles.infoSection}>
|
||||
<Text style={[styles.infoText, { color: theme.colors.textTertiary }]}>
|
||||
<View className="mt-1 px-1">
|
||||
<Text className="text-xs leading-[18px] text-text-tertiary">
|
||||
Notification preferences are stored locally on this device.
|
||||
Server-side notification rules configured in your project settings
|
||||
take precedence.
|
||||
@@ -195,75 +171,3 @@ export default function NotificationPreferencesScreen(): React.JSX.Element {
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles: {
|
||||
container: ViewStyle;
|
||||
content: ViewStyle;
|
||||
section: ViewStyle;
|
||||
sectionTitle: TextStyle;
|
||||
sectionHint: TextStyle;
|
||||
rowGroup: ViewStyle;
|
||||
row: ViewStyle;
|
||||
rowText: ViewStyle;
|
||||
rowLabel: TextStyle;
|
||||
rowDescription: TextStyle;
|
||||
infoSection: ViewStyle;
|
||||
infoText: TextStyle;
|
||||
} = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
content: {
|
||||
padding: 20,
|
||||
paddingBottom: 60,
|
||||
},
|
||||
section: {
|
||||
marginBottom: 28,
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 13,
|
||||
fontWeight: "600",
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: 0.8,
|
||||
marginBottom: 4,
|
||||
marginLeft: 4,
|
||||
},
|
||||
sectionHint: {
|
||||
fontSize: 12,
|
||||
marginBottom: 12,
|
||||
marginLeft: 4,
|
||||
lineHeight: 16,
|
||||
},
|
||||
rowGroup: {
|
||||
gap: 1,
|
||||
},
|
||||
row: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
padding: 16,
|
||||
borderRadius: 16,
|
||||
marginBottom: 10,
|
||||
},
|
||||
rowText: {
|
||||
flex: 1,
|
||||
marginRight: 12,
|
||||
},
|
||||
rowLabel: {
|
||||
fontSize: 16,
|
||||
fontWeight: "500",
|
||||
},
|
||||
rowDescription: {
|
||||
fontSize: 13,
|
||||
marginTop: 2,
|
||||
lineHeight: 18,
|
||||
},
|
||||
infoSection: {
|
||||
marginTop: 4,
|
||||
paddingHorizontal: 4,
|
||||
},
|
||||
infoText: {
|
||||
fontSize: 12,
|
||||
lineHeight: 18,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
FlatList,
|
||||
TouchableOpacity,
|
||||
ActivityIndicator,
|
||||
StyleSheet,
|
||||
ListRenderItemInfo,
|
||||
} from "react-native";
|
||||
import { useTheme } from "../theme";
|
||||
@@ -25,19 +24,9 @@ export default function ProjectSelectionScreen(): React.JSX.Element {
|
||||
|
||||
if (isLoadingProjects) {
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.centered,
|
||||
{ backgroundColor: theme.colors.backgroundPrimary },
|
||||
]}
|
||||
>
|
||||
<View className="flex-1 items-center justify-center px-8 bg-bg-primary">
|
||||
<ActivityIndicator size="large" color={theme.colors.actionPrimary} />
|
||||
<Text
|
||||
style={[
|
||||
theme.typography.bodyMedium,
|
||||
{ color: theme.colors.textSecondary, marginTop: theme.spacing.md },
|
||||
]}
|
||||
>
|
||||
<Text className="text-body-md text-text-secondary mt-4">
|
||||
Loading projects...
|
||||
</Text>
|
||||
</View>
|
||||
@@ -46,45 +35,19 @@ export default function ProjectSelectionScreen(): React.JSX.Element {
|
||||
|
||||
if (projectList.length === 0) {
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.centered,
|
||||
{ backgroundColor: theme.colors.backgroundPrimary },
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
theme.typography.titleSmall,
|
||||
{ color: theme.colors.textPrimary, textAlign: "center" },
|
||||
]}
|
||||
>
|
||||
<View className="flex-1 items-center justify-center px-8 bg-bg-primary">
|
||||
<Text className="text-title-sm text-text-primary text-center">
|
||||
No Projects Found
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
theme.typography.bodyMedium,
|
||||
{
|
||||
color: theme.colors.textSecondary,
|
||||
textAlign: "center",
|
||||
marginTop: theme.spacing.sm,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Text className="text-body-md text-text-secondary text-center mt-2">
|
||||
{"You don't have access to any projects."}
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.retryButton,
|
||||
{ backgroundColor: theme.colors.actionPrimary },
|
||||
]}
|
||||
className="mt-6 px-8 py-3.5 rounded-xl"
|
||||
style={{ backgroundColor: theme.colors.actionPrimary }}
|
||||
onPress={refreshProjects}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
theme.typography.bodyMedium,
|
||||
{ color: theme.colors.textInverse, fontWeight: "600" },
|
||||
]}
|
||||
>
|
||||
<Text className="text-body-md text-text-inverse font-semibold">
|
||||
Retry
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
@@ -93,27 +56,12 @@ export default function ProjectSelectionScreen(): React.JSX.Element {
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.container,
|
||||
{ backgroundColor: theme.colors.backgroundPrimary },
|
||||
]}
|
||||
>
|
||||
<View style={styles.header}>
|
||||
<Text
|
||||
style={[
|
||||
theme.typography.titleLarge,
|
||||
{ color: theme.colors.textPrimary },
|
||||
]}
|
||||
>
|
||||
<View className="flex-1 bg-bg-primary">
|
||||
<View className="px-5 pt-4 pb-2">
|
||||
<Text className="text-title-lg text-text-primary">
|
||||
Select Project
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
theme.typography.bodyMedium,
|
||||
{ color: theme.colors.textSecondary, marginTop: theme.spacing.xs },
|
||||
]}
|
||||
>
|
||||
<Text className="text-body-md text-text-secondary mt-1">
|
||||
Choose a project to view incidents and alerts.
|
||||
</Text>
|
||||
</View>
|
||||
@@ -123,44 +71,26 @@ export default function ProjectSelectionScreen(): React.JSX.Element {
|
||||
keyExtractor={(item: ProjectItem) => {
|
||||
return item._id;
|
||||
}}
|
||||
contentContainerStyle={styles.list}
|
||||
contentContainerStyle={{ padding: 20, paddingTop: 12 }}
|
||||
renderItem={({ item }: ListRenderItemInfo<ProjectItem>) => {
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.projectCard,
|
||||
theme.shadows.sm,
|
||||
{
|
||||
backgroundColor: theme.colors.backgroundElevated,
|
||||
},
|
||||
]}
|
||||
className="flex-row items-center p-[18px] rounded-2xl mb-3 bg-bg-elevated shadow-sm"
|
||||
onPress={() => {
|
||||
return handleSelect(item);
|
||||
}}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<View
|
||||
style={[
|
||||
styles.projectDot,
|
||||
{ backgroundColor: theme.colors.actionPrimary },
|
||||
]}
|
||||
className="w-3.5 h-3.5 rounded-full mr-3"
|
||||
style={{ backgroundColor: theme.colors.actionPrimary }}
|
||||
/>
|
||||
<View style={styles.projectInfo}>
|
||||
<Text
|
||||
style={[
|
||||
theme.typography.bodyLarge,
|
||||
{ color: theme.colors.textPrimary, fontWeight: "600" },
|
||||
]}
|
||||
>
|
||||
<View className="flex-1">
|
||||
<Text className="text-body-lg text-text-primary font-semibold">
|
||||
{item.name}
|
||||
</Text>
|
||||
{item.slug ? (
|
||||
<Text
|
||||
style={[
|
||||
theme.typography.bodySmall,
|
||||
{ color: theme.colors.textTertiary },
|
||||
]}
|
||||
>
|
||||
<Text className="text-body-sm text-text-tertiary">
|
||||
{item.slug}
|
||||
</Text>
|
||||
) : null}
|
||||
@@ -172,46 +102,3 @@ export default function ProjectSelectionScreen(): React.JSX.Element {
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles: ReturnType<typeof StyleSheet.create> = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
centered: {
|
||||
flex: 1,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
paddingHorizontal: 32,
|
||||
},
|
||||
header: {
|
||||
paddingHorizontal: 20,
|
||||
paddingTop: 16,
|
||||
paddingBottom: 8,
|
||||
},
|
||||
list: {
|
||||
padding: 20,
|
||||
paddingTop: 12,
|
||||
},
|
||||
projectCard: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
padding: 18,
|
||||
borderRadius: 16,
|
||||
marginBottom: 12,
|
||||
},
|
||||
projectDot: {
|
||||
width: 14,
|
||||
height: 14,
|
||||
borderRadius: 7,
|
||||
marginRight: 12,
|
||||
},
|
||||
projectInfo: {
|
||||
flex: 1,
|
||||
},
|
||||
retryButton: {
|
||||
marginTop: 24,
|
||||
paddingHorizontal: 32,
|
||||
paddingVertical: 14,
|
||||
borderRadius: 12,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
TouchableOpacity,
|
||||
ScrollView,
|
||||
Switch,
|
||||
StyleSheet,
|
||||
} from "react-native";
|
||||
import { useTheme, ThemeMode } from "../theme";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
@@ -44,38 +43,22 @@ function SettingsRow({
|
||||
const { theme } = useTheme();
|
||||
|
||||
const content: React.JSX.Element = (
|
||||
<View
|
||||
style={[
|
||||
styles.row,
|
||||
theme.shadows.sm,
|
||||
{
|
||||
backgroundColor: theme.colors.backgroundElevated,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<View className="flex-row justify-between items-center p-4 rounded-2xl min-h-[52px] bg-bg-elevated shadow-sm">
|
||||
<Text
|
||||
style={[
|
||||
styles.rowLabel,
|
||||
{
|
||||
color: destructive
|
||||
? theme.colors.actionDestructive
|
||||
: textColor || theme.colors.textPrimary,
|
||||
},
|
||||
]}
|
||||
className="text-base font-medium"
|
||||
style={{
|
||||
color: destructive
|
||||
? theme.colors.actionDestructive
|
||||
: textColor || theme.colors.textPrimary,
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
{rightElement ??
|
||||
(value ? (
|
||||
<Text
|
||||
style={[styles.rowValue, { color: theme.colors.textSecondary }]}
|
||||
>
|
||||
{value}
|
||||
</Text>
|
||||
<Text className="text-[15px] text-text-secondary">{value}</Text>
|
||||
) : onPress ? (
|
||||
<Text style={[styles.chevron, { color: theme.colors.textTertiary }]}>
|
||||
›
|
||||
</Text>
|
||||
<Text className="text-2xl font-light text-text-tertiary">{">"}</Text>
|
||||
) : null)}
|
||||
</View>
|
||||
);
|
||||
@@ -126,61 +109,47 @@ export default function SettingsScreen(): React.JSX.Element {
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
style={[{ backgroundColor: theme.colors.backgroundPrimary }]}
|
||||
contentContainerStyle={styles.content}
|
||||
className="bg-bg-primary"
|
||||
contentContainerStyle={{ padding: 20, paddingBottom: 60 }}
|
||||
>
|
||||
{/* Appearance */}
|
||||
<View style={styles.section}>
|
||||
<Text
|
||||
style={[styles.sectionTitle, { color: theme.colors.textSecondary }]}
|
||||
>
|
||||
<View className="mb-7">
|
||||
<Text className="text-[13px] font-semibold uppercase tracking-widest mb-2.5 ml-1 text-text-secondary">
|
||||
Appearance
|
||||
</Text>
|
||||
<View
|
||||
style={[
|
||||
styles.themeSelector,
|
||||
theme.shadows.sm,
|
||||
{
|
||||
backgroundColor: theme.colors.backgroundElevated,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<View className="flex-row rounded-2xl p-1 gap-1 bg-bg-elevated shadow-sm">
|
||||
{(["dark", "light", "system"] as ThemeMode[]).map(
|
||||
(mode: ThemeMode) => {
|
||||
const isActive: boolean = themeMode === mode;
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={mode}
|
||||
style={[
|
||||
styles.themeOption,
|
||||
isActive && {
|
||||
backgroundColor: theme.colors.actionPrimary,
|
||||
},
|
||||
]}
|
||||
className="flex-1 flex-row items-center justify-center py-2.5 rounded-lg gap-1.5"
|
||||
style={
|
||||
isActive
|
||||
? { backgroundColor: theme.colors.actionPrimary }
|
||||
: undefined
|
||||
}
|
||||
onPress={() => {
|
||||
return handleThemeChange(mode);
|
||||
}}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.themeOptionIcon,
|
||||
{
|
||||
color: isActive
|
||||
? "#FFFFFF"
|
||||
: theme.colors.textSecondary,
|
||||
},
|
||||
]}
|
||||
className="text-base"
|
||||
style={{
|
||||
color: isActive
|
||||
? "#FFFFFF"
|
||||
: theme.colors.textSecondary,
|
||||
}}
|
||||
>
|
||||
{mode === "dark" ? "◗" : mode === "light" ? "○" : "◑"}
|
||||
{mode === "dark" ? "\u25D7" : mode === "light" ? "\u25CB" : "\u25D1"}
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
styles.themeOptionLabel,
|
||||
{
|
||||
color: isActive ? "#FFFFFF" : theme.colors.textPrimary,
|
||||
},
|
||||
]}
|
||||
className="text-sm font-semibold"
|
||||
style={{
|
||||
color: isActive ? "#FFFFFF" : theme.colors.textPrimary,
|
||||
}}
|
||||
>
|
||||
{mode.charAt(0).toUpperCase() + mode.slice(1)}
|
||||
</Text>
|
||||
@@ -192,10 +161,8 @@ export default function SettingsScreen(): React.JSX.Element {
|
||||
</View>
|
||||
|
||||
{/* Notifications */}
|
||||
<View style={styles.section}>
|
||||
<Text
|
||||
style={[styles.sectionTitle, { color: theme.colors.textSecondary }]}
|
||||
>
|
||||
<View className="mb-7">
|
||||
<Text className="text-[13px] font-semibold uppercase tracking-widest mb-2.5 ml-1 text-text-secondary">
|
||||
Notifications
|
||||
</Text>
|
||||
<SettingsRow
|
||||
@@ -208,10 +175,8 @@ export default function SettingsScreen(): React.JSX.Element {
|
||||
|
||||
{/* Security */}
|
||||
{biometric.isAvailable ? (
|
||||
<View style={styles.section}>
|
||||
<Text
|
||||
style={[styles.sectionTitle, { color: theme.colors.textSecondary }]}
|
||||
>
|
||||
<View className="mb-7">
|
||||
<Text className="text-[13px] font-semibold uppercase tracking-widest mb-2.5 ml-1 text-text-secondary">
|
||||
Security
|
||||
</Text>
|
||||
<SettingsRow
|
||||
@@ -228,9 +193,7 @@ export default function SettingsScreen(): React.JSX.Element {
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Text
|
||||
style={[styles.sectionHint, { color: theme.colors.textTertiary }]}
|
||||
>
|
||||
<Text className="text-xs mt-2 ml-1 leading-4 text-text-tertiary">
|
||||
Require {biometric.biometricType.toLowerCase()} to unlock the app
|
||||
</Text>
|
||||
</View>
|
||||
@@ -238,10 +201,8 @@ export default function SettingsScreen(): React.JSX.Element {
|
||||
|
||||
{/* Project */}
|
||||
{selectedProject ? (
|
||||
<View style={styles.section}>
|
||||
<Text
|
||||
style={[styles.sectionTitle, { color: theme.colors.textSecondary }]}
|
||||
>
|
||||
<View className="mb-7">
|
||||
<Text className="text-[13px] font-semibold uppercase tracking-widest mb-2.5 ml-1 text-text-secondary">
|
||||
Project
|
||||
</Text>
|
||||
<SettingsRow
|
||||
@@ -252,118 +213,37 @@ export default function SettingsScreen(): React.JSX.Element {
|
||||
) : null}
|
||||
|
||||
{/* Server */}
|
||||
<View style={styles.section}>
|
||||
<Text
|
||||
style={[styles.sectionTitle, { color: theme.colors.textSecondary }]}
|
||||
>
|
||||
<View className="mb-7">
|
||||
<Text className="text-[13px] font-semibold uppercase tracking-widest mb-2.5 ml-1 text-text-secondary">
|
||||
Server
|
||||
</Text>
|
||||
<SettingsRow label="Server URL" value={serverUrl || "oneuptime.com"} />
|
||||
</View>
|
||||
|
||||
{/* Account */}
|
||||
<View style={styles.section}>
|
||||
<Text
|
||||
style={[styles.sectionTitle, { color: theme.colors.textSecondary }]}
|
||||
>
|
||||
<View className="mb-7">
|
||||
<Text className="text-[13px] font-semibold uppercase tracking-widest mb-2.5 ml-1 text-text-secondary">
|
||||
Account
|
||||
</Text>
|
||||
<SettingsRow label="Log Out" onPress={logout} destructive />
|
||||
</View>
|
||||
|
||||
{/* About */}
|
||||
<View style={styles.section}>
|
||||
<Text
|
||||
style={[styles.sectionTitle, { color: theme.colors.textSecondary }]}
|
||||
>
|
||||
<View className="mb-7">
|
||||
<Text className="text-[13px] font-semibold uppercase tracking-widest mb-2.5 ml-1 text-text-secondary">
|
||||
About
|
||||
</Text>
|
||||
<SettingsRow label="Version" value={APP_VERSION} />
|
||||
<View style={{ height: 1 }} />
|
||||
<View className="h-px" />
|
||||
<SettingsRow label="Build" value="1" />
|
||||
</View>
|
||||
|
||||
{/* Footer branding */}
|
||||
<View style={styles.footer}>
|
||||
<Text style={[styles.footerText, { color: theme.colors.textTertiary }]}>
|
||||
<View className="items-center pt-3">
|
||||
<Text className="text-xs font-medium text-text-tertiary">
|
||||
OneUptime On-Call
|
||||
</Text>
|
||||
</View>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles: ReturnType<typeof StyleSheet.create> = StyleSheet.create({
|
||||
content: {
|
||||
padding: 20,
|
||||
paddingBottom: 60,
|
||||
},
|
||||
section: {
|
||||
marginBottom: 28,
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 13,
|
||||
fontWeight: "600",
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: 0.8,
|
||||
marginBottom: 10,
|
||||
marginLeft: 4,
|
||||
},
|
||||
sectionHint: {
|
||||
fontSize: 12,
|
||||
marginTop: 8,
|
||||
marginLeft: 4,
|
||||
lineHeight: 16,
|
||||
},
|
||||
row: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
padding: 16,
|
||||
borderRadius: 16,
|
||||
minHeight: 52,
|
||||
},
|
||||
rowLabel: {
|
||||
fontSize: 16,
|
||||
fontWeight: "500",
|
||||
},
|
||||
rowValue: {
|
||||
fontSize: 15,
|
||||
},
|
||||
chevron: {
|
||||
fontSize: 24,
|
||||
fontWeight: "300",
|
||||
},
|
||||
// Theme selector
|
||||
themeSelector: {
|
||||
flexDirection: "row",
|
||||
borderRadius: 16,
|
||||
padding: 4,
|
||||
gap: 4,
|
||||
},
|
||||
themeOption: {
|
||||
flex: 1,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
paddingVertical: 10,
|
||||
borderRadius: 8,
|
||||
gap: 6,
|
||||
},
|
||||
themeOptionIcon: {
|
||||
fontSize: 16,
|
||||
},
|
||||
themeOptionLabel: {
|
||||
fontSize: 14,
|
||||
fontWeight: "600",
|
||||
},
|
||||
// Footer
|
||||
footer: {
|
||||
alignItems: "center",
|
||||
paddingTop: 12,
|
||||
},
|
||||
footerText: {
|
||||
fontSize: 12,
|
||||
fontWeight: "500",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -4,13 +4,10 @@ import {
|
||||
Text,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
StyleSheet,
|
||||
ActivityIndicator,
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
ScrollView,
|
||||
ViewStyle,
|
||||
TextStyle,
|
||||
} from "react-native";
|
||||
import { useTheme } from "../../theme";
|
||||
import { useAuth } from "../../hooks/useAuth";
|
||||
@@ -74,62 +71,29 @@ export default function LoginScreen(): React.JSX.Element {
|
||||
|
||||
return (
|
||||
<KeyboardAvoidingView
|
||||
style={[styles.flex, { backgroundColor: theme.colors.backgroundPrimary }]}
|
||||
className="flex-1 bg-bg-primary"
|
||||
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
||||
>
|
||||
<ScrollView
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
contentContainerStyle={{ flexGrow: 1 }}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
>
|
||||
<View style={styles.container}>
|
||||
<View style={styles.header}>
|
||||
<Text
|
||||
style={[
|
||||
theme.typography.titleLarge,
|
||||
{
|
||||
color: theme.colors.textPrimary,
|
||||
fontWeight: "800",
|
||||
fontSize: 28,
|
||||
letterSpacing: -0.5,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<View className="flex-1 justify-center px-6">
|
||||
<View className="items-center mb-12">
|
||||
<Text className="text-text-primary font-extrabold text-[28px] tracking-tight">
|
||||
OneUptime
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
theme.typography.bodySmall,
|
||||
{
|
||||
color: theme.colors.textTertiary,
|
||||
marginTop: theme.spacing.xs,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Text className="text-body-sm text-text-tertiary mt-1">
|
||||
{serverUrl}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.form}>
|
||||
<Text
|
||||
style={[
|
||||
theme.typography.bodySmall,
|
||||
{
|
||||
color: theme.colors.textSecondary,
|
||||
marginBottom: theme.spacing.xs,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<View className="w-full">
|
||||
<Text className="text-body-sm text-text-secondary mb-1">
|
||||
Email
|
||||
</Text>
|
||||
<TextInput
|
||||
style={[
|
||||
styles.input,
|
||||
{
|
||||
backgroundColor: theme.colors.backgroundPrimary,
|
||||
borderColor: theme.colors.borderDefault,
|
||||
color: theme.colors.textPrimary,
|
||||
},
|
||||
]}
|
||||
className="h-14 border rounded-[14px] px-4 text-base bg-bg-primary text-text-primary border-border-default"
|
||||
value={email}
|
||||
onChangeText={(text: string) => {
|
||||
setEmail(text);
|
||||
@@ -144,27 +108,11 @@ export default function LoginScreen(): React.JSX.Element {
|
||||
returnKeyType="next"
|
||||
/>
|
||||
|
||||
<Text
|
||||
style={[
|
||||
theme.typography.bodySmall,
|
||||
{
|
||||
color: theme.colors.textSecondary,
|
||||
marginBottom: theme.spacing.xs,
|
||||
marginTop: theme.spacing.md,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Text className="text-body-sm text-text-secondary mb-1 mt-4">
|
||||
Password
|
||||
</Text>
|
||||
<TextInput
|
||||
style={[
|
||||
styles.input,
|
||||
{
|
||||
backgroundColor: theme.colors.backgroundPrimary,
|
||||
borderColor: theme.colors.borderDefault,
|
||||
color: theme.colors.textPrimary,
|
||||
},
|
||||
]}
|
||||
className="h-14 border rounded-[14px] px-4 text-base bg-bg-primary text-text-primary border-border-default"
|
||||
value={password}
|
||||
onChangeText={(text: string) => {
|
||||
setPassword(text);
|
||||
@@ -179,55 +127,34 @@ export default function LoginScreen(): React.JSX.Element {
|
||||
/>
|
||||
|
||||
{error ? (
|
||||
<Text
|
||||
style={[
|
||||
theme.typography.bodySmall,
|
||||
{
|
||||
color: theme.colors.statusError,
|
||||
marginTop: theme.spacing.sm,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Text className="text-body-sm mt-2" style={{ color: theme.colors.statusError }}>
|
||||
{error}
|
||||
</Text>
|
||||
) : null}
|
||||
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.button,
|
||||
theme.shadows.md,
|
||||
{
|
||||
backgroundColor: theme.colors.actionPrimary,
|
||||
opacity: isLoading ? 0.7 : 1,
|
||||
},
|
||||
]}
|
||||
className="h-[52px] rounded-[14px] items-center justify-center mt-6 shadow-md"
|
||||
style={{
|
||||
backgroundColor: theme.colors.actionPrimary,
|
||||
opacity: isLoading ? 0.7 : 1,
|
||||
}}
|
||||
onPress={handleLogin}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? (
|
||||
<ActivityIndicator color={theme.colors.textInverse} />
|
||||
) : (
|
||||
<Text
|
||||
style={[
|
||||
theme.typography.bodyMedium,
|
||||
{ color: theme.colors.textInverse, fontWeight: "700" },
|
||||
]}
|
||||
>
|
||||
<Text className="text-body-md text-text-inverse font-bold">
|
||||
Log In
|
||||
</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
style={styles.changeServer}
|
||||
className="items-center mt-6 py-2"
|
||||
onPress={handleChangeServer}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
theme.typography.bodySmall,
|
||||
{ color: theme.colors.actionPrimary },
|
||||
]}
|
||||
>
|
||||
<Text className="text-body-sm text-action-primary">
|
||||
Change Server
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
@@ -237,52 +164,3 @@ export default function LoginScreen(): React.JSX.Element {
|
||||
</KeyboardAvoidingView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles: {
|
||||
flex: ViewStyle;
|
||||
scrollContent: ViewStyle;
|
||||
container: ViewStyle;
|
||||
header: ViewStyle;
|
||||
form: ViewStyle;
|
||||
input: TextStyle;
|
||||
button: ViewStyle;
|
||||
changeServer: ViewStyle;
|
||||
} = StyleSheet.create({
|
||||
flex: {
|
||||
flex: 1,
|
||||
},
|
||||
scrollContent: {
|
||||
flexGrow: 1,
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
paddingHorizontal: 24,
|
||||
},
|
||||
header: {
|
||||
alignItems: "center",
|
||||
marginBottom: 48,
|
||||
},
|
||||
form: {
|
||||
width: "100%",
|
||||
},
|
||||
input: {
|
||||
height: 56,
|
||||
borderWidth: 1,
|
||||
borderRadius: 14,
|
||||
paddingHorizontal: 16,
|
||||
fontSize: 16,
|
||||
},
|
||||
button: {
|
||||
height: 52,
|
||||
borderRadius: 14,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
marginTop: 24,
|
||||
},
|
||||
changeServer: {
|
||||
alignItems: "center",
|
||||
marginTop: 24,
|
||||
paddingVertical: 8,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -4,13 +4,10 @@ import {
|
||||
Text,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
StyleSheet,
|
||||
ActivityIndicator,
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
ScrollView,
|
||||
ViewStyle,
|
||||
TextStyle,
|
||||
} from "react-native";
|
||||
import { NativeStackNavigationProp } from "@react-navigation/native-stack";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
@@ -66,65 +63,34 @@ export default function ServerUrlScreen(): React.JSX.Element {
|
||||
|
||||
return (
|
||||
<KeyboardAvoidingView
|
||||
style={[styles.flex, { backgroundColor: theme.colors.backgroundPrimary }]}
|
||||
className="flex-1 bg-bg-primary"
|
||||
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
||||
>
|
||||
<ScrollView
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
contentContainerStyle={{ flexGrow: 1 }}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
>
|
||||
<View style={styles.container}>
|
||||
<View style={styles.header}>
|
||||
<Text
|
||||
style={[
|
||||
theme.typography.titleLarge,
|
||||
{
|
||||
color: theme.colors.textPrimary,
|
||||
fontWeight: "800",
|
||||
fontSize: 28,
|
||||
letterSpacing: -0.5,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<View className="flex-1 justify-center px-6">
|
||||
<View className="items-center mb-12">
|
||||
<Text className="text-text-primary font-extrabold text-[28px] tracking-tight">
|
||||
OneUptime
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
theme.typography.bodyMedium,
|
||||
{
|
||||
color: theme.colors.textSecondary,
|
||||
marginTop: theme.spacing.sm,
|
||||
textAlign: "center",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Text className="text-body-md text-text-secondary mt-2 text-center">
|
||||
Connect to your OneUptime instance
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.form}>
|
||||
<Text
|
||||
style={[
|
||||
theme.typography.bodySmall,
|
||||
{
|
||||
color: theme.colors.textSecondary,
|
||||
marginBottom: theme.spacing.xs,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<View className="w-full">
|
||||
<Text className="text-body-sm text-text-secondary mb-1">
|
||||
Server URL
|
||||
</Text>
|
||||
<TextInput
|
||||
style={[
|
||||
styles.input,
|
||||
{
|
||||
backgroundColor: theme.colors.backgroundPrimary,
|
||||
borderColor: error
|
||||
? theme.colors.statusError
|
||||
: theme.colors.borderDefault,
|
||||
color: theme.colors.textPrimary,
|
||||
},
|
||||
]}
|
||||
className="h-14 border rounded-[14px] px-4 text-base bg-bg-primary text-text-primary"
|
||||
style={{
|
||||
borderColor: error
|
||||
? theme.colors.statusError
|
||||
: theme.colors.borderDefault,
|
||||
}}
|
||||
value={url}
|
||||
onChangeText={(text: string) => {
|
||||
setUrl(text);
|
||||
@@ -140,55 +106,30 @@ export default function ServerUrlScreen(): React.JSX.Element {
|
||||
/>
|
||||
|
||||
{error ? (
|
||||
<Text
|
||||
style={[
|
||||
theme.typography.bodySmall,
|
||||
{
|
||||
color: theme.colors.statusError,
|
||||
marginTop: theme.spacing.sm,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Text className="text-body-sm mt-2" style={{ color: theme.colors.statusError }}>
|
||||
{error}
|
||||
</Text>
|
||||
) : null}
|
||||
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.button,
|
||||
theme.shadows.md,
|
||||
{
|
||||
backgroundColor: theme.colors.actionPrimary,
|
||||
opacity: isLoading ? 0.7 : 1,
|
||||
},
|
||||
]}
|
||||
className="h-[52px] rounded-[14px] items-center justify-center mt-6 shadow-md"
|
||||
style={{
|
||||
backgroundColor: theme.colors.actionPrimary,
|
||||
opacity: isLoading ? 0.7 : 1,
|
||||
}}
|
||||
onPress={handleConnect}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? (
|
||||
<ActivityIndicator color={theme.colors.textInverse} />
|
||||
) : (
|
||||
<Text
|
||||
style={[
|
||||
theme.typography.bodyMedium,
|
||||
{ color: theme.colors.textInverse, fontWeight: "700" },
|
||||
]}
|
||||
>
|
||||
<Text className="text-body-md text-text-inverse font-bold">
|
||||
Connect
|
||||
</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
|
||||
<Text
|
||||
style={[
|
||||
theme.typography.caption,
|
||||
{
|
||||
color: theme.colors.textTertiary,
|
||||
textAlign: "center",
|
||||
marginTop: theme.spacing.lg,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Text className="text-caption text-text-tertiary text-center mt-6">
|
||||
Self-hosting? Enter your OneUptime server URL above.
|
||||
</Text>
|
||||
</View>
|
||||
@@ -197,46 +138,3 @@ export default function ServerUrlScreen(): React.JSX.Element {
|
||||
</KeyboardAvoidingView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles: {
|
||||
flex: ViewStyle;
|
||||
scrollContent: ViewStyle;
|
||||
container: ViewStyle;
|
||||
header: ViewStyle;
|
||||
form: ViewStyle;
|
||||
input: TextStyle;
|
||||
button: ViewStyle;
|
||||
} = StyleSheet.create({
|
||||
flex: {
|
||||
flex: 1,
|
||||
},
|
||||
scrollContent: {
|
||||
flexGrow: 1,
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
paddingHorizontal: 24,
|
||||
},
|
||||
header: {
|
||||
alignItems: "center",
|
||||
marginBottom: 48,
|
||||
},
|
||||
form: {
|
||||
width: "100%",
|
||||
},
|
||||
input: {
|
||||
height: 56,
|
||||
borderWidth: 1,
|
||||
borderRadius: 14,
|
||||
paddingHorizontal: 16,
|
||||
fontSize: 16,
|
||||
},
|
||||
button: {
|
||||
height: 52,
|
||||
borderRadius: 14,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
marginTop: 24,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -6,11 +6,8 @@ import React, {
|
||||
useMemo,
|
||||
ReactNode,
|
||||
} from "react";
|
||||
import { useColorScheme } from "react-native";
|
||||
import { View, useColorScheme } from "react-native";
|
||||
import { ColorTokens, darkColors, lightColors } from "./colors";
|
||||
import { typography } from "./typography";
|
||||
import { spacing, radius } from "./spacing";
|
||||
import { shadows, ShadowTokens } from "./shadows";
|
||||
import {
|
||||
getThemeMode as loadThemeMode,
|
||||
setThemeMode as saveThemeMode,
|
||||
@@ -20,10 +17,6 @@ export type ThemeMode = "dark" | "light" | "system";
|
||||
|
||||
export interface Theme {
|
||||
colors: ColorTokens;
|
||||
typography: typeof typography;
|
||||
spacing: typeof spacing;
|
||||
radius: typeof radius;
|
||||
shadows: ShadowTokens;
|
||||
isDark: boolean;
|
||||
}
|
||||
|
||||
@@ -70,10 +63,6 @@ export function ThemeProvider({
|
||||
|
||||
return {
|
||||
colors: isDark ? darkColors : lightColors,
|
||||
typography,
|
||||
spacing,
|
||||
radius,
|
||||
shadows,
|
||||
isDark,
|
||||
};
|
||||
}, [themeMode, systemColorScheme]);
|
||||
@@ -87,7 +76,11 @@ export function ThemeProvider({
|
||||
}, [theme, themeMode]);
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
|
||||
<ThemeContext.Provider value={value}>
|
||||
<View className={theme.isDark ? "dark flex-1" : "flex-1"}>
|
||||
{children}
|
||||
</View>
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
export { darkColors, lightColors } from "./colors";
|
||||
export type { ColorTokens } from "./colors";
|
||||
export { typography } from "./typography";
|
||||
export { spacing, radius } from "./spacing";
|
||||
export { shadows } from "./shadows";
|
||||
export type { ShadowTokens } from "./shadows";
|
||||
export { ThemeProvider, useTheme } from "./ThemeContext";
|
||||
export type { Theme, ThemeMode } from "./ThemeContext";
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import { ViewStyle } from "react-native";
|
||||
|
||||
export interface ShadowTokens {
|
||||
sm: ViewStyle;
|
||||
md: ViewStyle;
|
||||
lg: ViewStyle;
|
||||
}
|
||||
|
||||
export const shadows: ShadowTokens = {
|
||||
sm: {
|
||||
shadowColor: "#000",
|
||||
shadowOffset: { width: 0, height: 1 },
|
||||
shadowOpacity: 0.04,
|
||||
shadowRadius: 6,
|
||||
elevation: 1,
|
||||
},
|
||||
md: {
|
||||
shadowColor: "#000",
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.06,
|
||||
shadowRadius: 12,
|
||||
elevation: 3,
|
||||
},
|
||||
lg: {
|
||||
shadowColor: "#000",
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.08,
|
||||
shadowRadius: 16,
|
||||
elevation: 5,
|
||||
},
|
||||
};
|
||||
@@ -1,28 +0,0 @@
|
||||
export const spacing: {
|
||||
readonly xs: 4;
|
||||
readonly sm: 8;
|
||||
readonly md: 16;
|
||||
readonly lg: 24;
|
||||
readonly xl: 32;
|
||||
readonly xxl: 48;
|
||||
} = {
|
||||
xs: 4,
|
||||
sm: 8,
|
||||
md: 16,
|
||||
lg: 24,
|
||||
xl: 32,
|
||||
xxl: 48,
|
||||
} as const;
|
||||
|
||||
export const radius: {
|
||||
readonly sm: 6;
|
||||
readonly md: 12;
|
||||
readonly lg: 16;
|
||||
} = {
|
||||
sm: 6,
|
||||
md: 12,
|
||||
lg: 16,
|
||||
} as const;
|
||||
|
||||
export type Spacing = typeof spacing;
|
||||
export type Radius = typeof radius;
|
||||
@@ -1,91 +0,0 @@
|
||||
import { Platform, TextStyle } from "react-native";
|
||||
|
||||
const fontFamily: string = Platform.OS === "ios" ? "System" : "Roboto";
|
||||
const monoFontFamily: string = Platform.OS === "ios" ? "Menlo" : "monospace";
|
||||
|
||||
interface TypographyStyles {
|
||||
titleLarge: TextStyle;
|
||||
titleMedium: TextStyle;
|
||||
titleSmall: TextStyle;
|
||||
bodyLarge: TextStyle;
|
||||
bodyMedium: TextStyle;
|
||||
bodySmall: TextStyle;
|
||||
monoLarge: TextStyle;
|
||||
monoMedium: TextStyle;
|
||||
monoSmall: TextStyle;
|
||||
label: TextStyle;
|
||||
caption: TextStyle;
|
||||
}
|
||||
|
||||
export const typography: TypographyStyles = {
|
||||
titleLarge: {
|
||||
fontFamily,
|
||||
fontSize: 28,
|
||||
fontWeight: "700",
|
||||
lineHeight: 34,
|
||||
},
|
||||
titleMedium: {
|
||||
fontFamily,
|
||||
fontSize: 22,
|
||||
fontWeight: "600",
|
||||
lineHeight: 28,
|
||||
},
|
||||
titleSmall: {
|
||||
fontFamily,
|
||||
fontSize: 17,
|
||||
fontWeight: "600",
|
||||
lineHeight: 22,
|
||||
},
|
||||
bodyLarge: {
|
||||
fontFamily,
|
||||
fontSize: 17,
|
||||
fontWeight: "400",
|
||||
lineHeight: 24,
|
||||
},
|
||||
bodyMedium: {
|
||||
fontFamily,
|
||||
fontSize: 15,
|
||||
fontWeight: "400",
|
||||
lineHeight: 20,
|
||||
},
|
||||
bodySmall: {
|
||||
fontFamily,
|
||||
fontSize: 13,
|
||||
fontWeight: "400",
|
||||
lineHeight: 18,
|
||||
},
|
||||
monoLarge: {
|
||||
fontFamily: monoFontFamily,
|
||||
fontSize: 17,
|
||||
fontWeight: "400",
|
||||
lineHeight: 24,
|
||||
},
|
||||
monoMedium: {
|
||||
fontFamily: monoFontFamily,
|
||||
fontSize: 15,
|
||||
fontWeight: "400",
|
||||
lineHeight: 20,
|
||||
},
|
||||
monoSmall: {
|
||||
fontFamily: monoFontFamily,
|
||||
fontSize: 13,
|
||||
fontWeight: "400",
|
||||
lineHeight: 18,
|
||||
},
|
||||
label: {
|
||||
fontFamily,
|
||||
fontSize: 12,
|
||||
fontWeight: "600",
|
||||
lineHeight: 16,
|
||||
letterSpacing: 0.5,
|
||||
textTransform: "uppercase",
|
||||
},
|
||||
caption: {
|
||||
fontFamily,
|
||||
fontSize: 12,
|
||||
fontWeight: "400",
|
||||
lineHeight: 16,
|
||||
},
|
||||
};
|
||||
|
||||
export default typography;
|
||||
80
MobileApp/tailwind.config.ts
Normal file
80
MobileApp/tailwind.config.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import type { Config } from "tailwindcss";
|
||||
|
||||
const config: Config = {
|
||||
presets: [require("nativewind/preset")],
|
||||
darkMode: "class",
|
||||
content: ["./App.tsx", "./src/**/*.{ts,tsx}"],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
"bg-primary": "var(--color-bg-primary)",
|
||||
"bg-secondary": "var(--color-bg-secondary)",
|
||||
"bg-tertiary": "var(--color-bg-tertiary)",
|
||||
"bg-elevated": "var(--color-bg-elevated)",
|
||||
"border-default": "var(--color-border-default)",
|
||||
"border-subtle": "var(--color-border-subtle)",
|
||||
"text-primary": "var(--color-text-primary)",
|
||||
"text-secondary": "var(--color-text-secondary)",
|
||||
"text-tertiary": "var(--color-text-tertiary)",
|
||||
"text-inverse": "var(--color-text-inverse)",
|
||||
"severity-critical": "var(--color-severity-critical)",
|
||||
"severity-critical-bg": "var(--color-severity-critical-bg)",
|
||||
"severity-major": "var(--color-severity-major)",
|
||||
"severity-major-bg": "var(--color-severity-major-bg)",
|
||||
"severity-minor": "var(--color-severity-minor)",
|
||||
"severity-minor-bg": "var(--color-severity-minor-bg)",
|
||||
"severity-warning": "var(--color-severity-warning)",
|
||||
"severity-warning-bg": "var(--color-severity-warning-bg)",
|
||||
"severity-info": "var(--color-severity-info)",
|
||||
"severity-info-bg": "var(--color-severity-info-bg)",
|
||||
"state-created": "var(--color-state-created)",
|
||||
"state-acknowledged": "var(--color-state-acknowledged)",
|
||||
"state-resolved": "var(--color-state-resolved)",
|
||||
"state-investigating": "var(--color-state-investigating)",
|
||||
"state-muted": "var(--color-state-muted)",
|
||||
"oncall-active": "var(--color-oncall-active)",
|
||||
"oncall-active-bg": "var(--color-oncall-active-bg)",
|
||||
"oncall-inactive": "var(--color-oncall-inactive)",
|
||||
"oncall-inactive-bg": "var(--color-oncall-inactive-bg)",
|
||||
"action-primary": "var(--color-action-primary)",
|
||||
"action-primary-pressed": "var(--color-action-primary-pressed)",
|
||||
"action-destructive": "var(--color-action-destructive)",
|
||||
"action-destructive-pressed": "var(--color-action-destructive-pressed)",
|
||||
"status-success": "var(--color-status-success)",
|
||||
"status-success-bg": "var(--color-status-success-bg)",
|
||||
"status-error": "var(--color-status-error)",
|
||||
"status-error-bg": "var(--color-status-error-bg)",
|
||||
},
|
||||
fontSize: {
|
||||
"title-lg": [
|
||||
"28px",
|
||||
{ lineHeight: "34px", fontWeight: "700" },
|
||||
],
|
||||
"title-md": [
|
||||
"22px",
|
||||
{ lineHeight: "28px", fontWeight: "600" },
|
||||
],
|
||||
"title-sm": [
|
||||
"17px",
|
||||
{ lineHeight: "22px", fontWeight: "600" },
|
||||
],
|
||||
"body-lg": ["17px", { lineHeight: "24px" }],
|
||||
"body-md": ["15px", { lineHeight: "20px" }],
|
||||
"body-sm": ["13px", { lineHeight: "18px" }],
|
||||
label: [
|
||||
"12px",
|
||||
{ lineHeight: "16px", fontWeight: "600", letterSpacing: "0.5px" },
|
||||
],
|
||||
caption: ["12px", { lineHeight: "16px" }],
|
||||
},
|
||||
boxShadow: {
|
||||
sm: "0 1px 6px rgba(0,0,0,0.04)",
|
||||
md: "0 2px 12px rgba(0,0,0,0.06)",
|
||||
lg: "0 4px 16px rgba(0,0,0,0.08)",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -5,6 +5,7 @@
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
}
|
||||
},
|
||||
"types": ["nativewind/types"]
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user