Initial scaffold: Expo SDK 56, expo-router, expo-sqlite, NativeWind
This commit is contained in:
+210
@@ -0,0 +1,210 @@
|
||||
import { useState } from "react";
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
TextInput,
|
||||
ScrollView,
|
||||
Pressable,
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
Alert,
|
||||
} from "react-native";
|
||||
import { router } from "expo-router";
|
||||
import { createIncident } from "@/lib/db";
|
||||
import { Colors } from "@/constants/theme";
|
||||
import type { IncidentDraft } from "@/types/incident";
|
||||
|
||||
const FIELD_STYLE = {
|
||||
backgroundColor: Colors.surface,
|
||||
borderColor: Colors.border,
|
||||
borderWidth: 1,
|
||||
borderRadius: 8,
|
||||
color: Colors.text1,
|
||||
padding: 12,
|
||||
fontSize: 14,
|
||||
marginBottom: 16,
|
||||
} as const;
|
||||
|
||||
const MONO_FIELD_STYLE = {
|
||||
...FIELD_STYLE,
|
||||
fontFamily: "monospace",
|
||||
minHeight: 80,
|
||||
textAlignVertical: "top" as const,
|
||||
} as const;
|
||||
|
||||
const LABEL_STYLE = {
|
||||
color: Colors.text2,
|
||||
fontSize: 12,
|
||||
fontWeight: "600" as const,
|
||||
textTransform: "uppercase" as const,
|
||||
letterSpacing: 0.5,
|
||||
marginBottom: 6,
|
||||
} as const;
|
||||
|
||||
export default function NewIncidentScreen() {
|
||||
const [form, setForm] = useState<IncidentDraft>({
|
||||
title: "",
|
||||
service: "",
|
||||
symptom: "",
|
||||
rootCause: "",
|
||||
fix: "",
|
||||
status: "open",
|
||||
tags: [],
|
||||
});
|
||||
const [tagInput, setTagInput] = useState("");
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
function set(key: keyof IncidentDraft) {
|
||||
return (value: string) => setForm((f) => ({ ...f, [key]: value }));
|
||||
}
|
||||
|
||||
function handleTagInputEnd() {
|
||||
const trimmed = tagInput.trim();
|
||||
if (trimmed && !form.tags.includes(trimmed)) {
|
||||
setForm((f) => ({ ...f, tags: [...f.tags, trimmed] }));
|
||||
}
|
||||
setTagInput("");
|
||||
}
|
||||
|
||||
function removeTag(tag: string) {
|
||||
setForm((f) => ({ ...f, tags: f.tags.filter((t) => t !== tag) }));
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
if (!form.title.trim()) {
|
||||
Alert.alert("Required", "Title is required.");
|
||||
return;
|
||||
}
|
||||
setSaving(true);
|
||||
try {
|
||||
await createIncident(form);
|
||||
router.back();
|
||||
} catch (e) {
|
||||
Alert.alert("Error", "Failed to save incident.");
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<KeyboardAvoidingView
|
||||
style={{ flex: 1, backgroundColor: Colors.bg }}
|
||||
behavior={Platform.OS === "ios" ? "padding" : undefined}
|
||||
>
|
||||
<ScrollView
|
||||
contentContainerStyle={{ padding: 16, paddingBottom: 32 }}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
>
|
||||
<Text style={LABEL_STYLE}>Title *</Text>
|
||||
<TextInput
|
||||
style={FIELD_STYLE}
|
||||
value={form.title}
|
||||
onChangeText={set("title")}
|
||||
placeholder="e.g. Ghost blog down after k3s node drain"
|
||||
placeholderTextColor={Colors.textDim}
|
||||
returnKeyType="next"
|
||||
/>
|
||||
|
||||
<Text style={LABEL_STYLE}>Service</Text>
|
||||
<TextInput
|
||||
style={FIELD_STYLE}
|
||||
value={form.service}
|
||||
onChangeText={set("service")}
|
||||
placeholder="e.g. ghost-blog, k3s-cluster, haproxy"
|
||||
placeholderTextColor={Colors.textDim}
|
||||
autoCapitalize="none"
|
||||
returnKeyType="next"
|
||||
/>
|
||||
|
||||
<Text style={LABEL_STYLE}>Symptom</Text>
|
||||
<TextInput
|
||||
style={MONO_FIELD_STYLE}
|
||||
value={form.symptom}
|
||||
onChangeText={set("symptom")}
|
||||
placeholder="What was observed?"
|
||||
placeholderTextColor={Colors.textDim}
|
||||
multiline
|
||||
returnKeyType="next"
|
||||
/>
|
||||
|
||||
<Text style={LABEL_STYLE}>Root Cause</Text>
|
||||
<TextInput
|
||||
style={MONO_FIELD_STYLE}
|
||||
value={form.rootCause}
|
||||
onChangeText={set("rootCause")}
|
||||
placeholder="Why did it happen?"
|
||||
placeholderTextColor={Colors.textDim}
|
||||
multiline
|
||||
returnKeyType="next"
|
||||
/>
|
||||
|
||||
<Text style={LABEL_STYLE}>Fix Applied</Text>
|
||||
<TextInput
|
||||
style={MONO_FIELD_STYLE}
|
||||
value={form.fix}
|
||||
onChangeText={set("fix")}
|
||||
placeholder="What did you do to fix it?"
|
||||
placeholderTextColor={Colors.textDim}
|
||||
multiline
|
||||
returnKeyType="done"
|
||||
/>
|
||||
|
||||
<Text style={LABEL_STYLE}>Tags</Text>
|
||||
<TextInput
|
||||
style={FIELD_STYLE}
|
||||
value={tagInput}
|
||||
onChangeText={setTagInput}
|
||||
onSubmitEditing={handleTagInputEnd}
|
||||
onBlur={handleTagInputEnd}
|
||||
placeholder="Type tag and press Enter"
|
||||
placeholderTextColor={Colors.textDim}
|
||||
autoCapitalize="none"
|
||||
returnKeyType="done"
|
||||
blurOnSubmit={false}
|
||||
/>
|
||||
{form.tags.length > 0 && (
|
||||
<View
|
||||
style={{ flexDirection: "row", flexWrap: "wrap", gap: 8, marginBottom: 16 }}
|
||||
>
|
||||
{form.tags.map((tag) => (
|
||||
<Pressable
|
||||
key={tag}
|
||||
onPress={() => removeTag(tag)}
|
||||
style={{
|
||||
backgroundColor: Colors.surface2,
|
||||
borderRadius: 4,
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 4,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 6,
|
||||
}}
|
||||
>
|
||||
<Text style={{ color: Colors.text2, fontSize: 13 }}>{tag}</Text>
|
||||
<Text style={{ color: Colors.textDim, fontSize: 12 }}>✕</Text>
|
||||
</Pressable>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
|
||||
<Pressable
|
||||
onPress={handleSave}
|
||||
disabled={saving}
|
||||
style={{
|
||||
backgroundColor: saving ? Colors.primaryDim : Colors.primary,
|
||||
borderRadius: 10,
|
||||
padding: 16,
|
||||
alignItems: "center",
|
||||
marginTop: 8,
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{ color: "#fff", fontSize: 16, fontWeight: "700" }}
|
||||
>
|
||||
{saving ? "Saving…" : "Save Incident"}
|
||||
</Text>
|
||||
</Pressable>
|
||||
</ScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user