Skip to content

Commit a8c898c

Browse files
committed
feat(expo): wizard sheet with form, annotate, and submit steps
1 parent 715e63f commit a8c898c

4 files changed

Lines changed: 215 additions & 0 deletions

File tree

packages/expo/src/wizard/sheet.tsx

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import React, { useEffect, useRef, useState } from "react"
2+
import { Modal, SafeAreaView, Text, View } from "react-native"
3+
import { FlattenView, type FlattenHandle } from "../capture/flatten"
4+
import { StepForm } from "./step-form"
5+
import { StepAnnotate } from "./step-annotate"
6+
import { StepSubmit } from "./step-submit"
7+
import { createAnnotationStore } from "../annotation/store"
8+
9+
export interface WizardArgs {
10+
initialTitle?: string
11+
initialDescription?: string
12+
screenshot: { uri: string; width: number; height: number } | null
13+
onSubmit: (result: {
14+
title: string
15+
description: string
16+
annotatedUri: string | null
17+
rawUri: string | null
18+
}) => Promise<void>
19+
onClose: () => void
20+
}
21+
22+
export function WizardSheet({
23+
initialTitle,
24+
initialDescription,
25+
screenshot,
26+
onSubmit,
27+
onClose,
28+
}: WizardArgs) {
29+
const [step, setStep] = useState<"form" | "annotate" | "submit">("form")
30+
const [title, setTitle] = useState(initialTitle ?? "")
31+
const [description, setDescription] = useState(initialDescription ?? "")
32+
const [submitting, setSubmitting] = useState(false)
33+
const [error, setError] = useState<string | null>(null)
34+
const store = useRef(createAnnotationStore()).current
35+
const flattenRef = useRef<FlattenHandle | null>(null)
36+
37+
useEffect(() => {
38+
setStep("form")
39+
}, [screenshot])
40+
41+
async function handleSubmit() {
42+
setSubmitting(true)
43+
setError(null)
44+
try {
45+
let annotated: string | null = null
46+
if (screenshot && store.snapshot().length > 0 && flattenRef.current) {
47+
const flat = await flattenRef.current.flatten()
48+
annotated = flat.uri
49+
}
50+
await onSubmit({
51+
title,
52+
description,
53+
annotatedUri: annotated,
54+
rawUri: screenshot?.uri ?? null,
55+
})
56+
} catch (e) {
57+
setError((e as Error).message)
58+
} finally {
59+
setSubmitting(false)
60+
}
61+
}
62+
63+
return (
64+
<Modal visible animationType="slide" onRequestClose={onClose}>
65+
<SafeAreaView style={{ flex: 1 }}>
66+
<View style={{ padding: 12, borderBottomWidth: 1, borderBottomColor: "#eee" }}>
67+
<Text style={{ fontSize: 18, fontWeight: "600" }}>Report a bug</Text>
68+
</View>
69+
{step === "form" && (
70+
<StepForm
71+
title={title}
72+
description={description}
73+
onTitleChange={setTitle}
74+
onDescriptionChange={setDescription}
75+
/>
76+
)}
77+
{step === "annotate" && screenshot && (
78+
<StepAnnotate
79+
imageUri={screenshot.uri}
80+
width={screenshot.width}
81+
height={screenshot.height}
82+
store={store}
83+
/>
84+
)}
85+
{step === "submit" && (
86+
<StepSubmit
87+
submitting={submitting}
88+
error={error}
89+
onSubmit={handleSubmit}
90+
onCancel={onClose}
91+
/>
92+
)}
93+
{screenshot && (
94+
<FlattenView
95+
ref={flattenRef}
96+
uri={screenshot.uri}
97+
width={screenshot.width}
98+
height={screenshot.height}
99+
shapes={store.snapshot()}
100+
/>
101+
)}
102+
<View style={{ flexDirection: "row", padding: 12, gap: 8 }}>
103+
{step !== "form" && (
104+
<Text onPress={() => setStep(step === "submit" ? "annotate" : "form")}>Back</Text>
105+
)}
106+
{step !== "submit" && (
107+
<Text onPress={() => setStep(step === "form" ? "annotate" : "submit")}>Next</Text>
108+
)}
109+
</View>
110+
</SafeAreaView>
111+
</Modal>
112+
)
113+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import React, { useState } from "react"
2+
import { View } from "react-native"
3+
import { AnnotationCanvas } from "../annotation/canvas"
4+
import { AnnotationToolbar } from "../annotation/toolbar"
5+
import type { AnnotationStore } from "../annotation/store"
6+
import type { Tool } from "@reprojs/sdk-utils"
7+
8+
interface Props {
9+
imageUri: string
10+
width: number
11+
height: number
12+
store: AnnotationStore
13+
}
14+
15+
export function StepAnnotate({ imageUri: _imageUri, width, height, store }: Props) {
16+
const [tool, setTool] = useState<Tool>("pen")
17+
return (
18+
<View style={{ flex: 1 }}>
19+
<AnnotationToolbar tool={tool} onToolChange={setTool} store={store} />
20+
<View style={{ width, height, position: "relative" }}>
21+
<AnnotationCanvas
22+
width={width}
23+
height={height}
24+
tool={tool}
25+
color="#ef4444"
26+
strokeWidth={3}
27+
store={store}
28+
/>
29+
</View>
30+
</View>
31+
)
32+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import React from "react"
2+
import { TextInput, View, Text } from "react-native"
3+
4+
interface Props {
5+
title: string
6+
description: string
7+
onTitleChange: (v: string) => void
8+
onDescriptionChange: (v: string) => void
9+
}
10+
11+
export function StepForm({ title, description, onTitleChange, onDescriptionChange }: Props) {
12+
return (
13+
<View style={{ padding: 16, gap: 12 }}>
14+
<Text style={{ fontWeight: "600" }}>Title</Text>
15+
<TextInput
16+
value={title}
17+
onChangeText={onTitleChange}
18+
maxLength={120}
19+
placeholder="Short description of the issue"
20+
style={{ borderWidth: 1, borderColor: "#ccc", padding: 8, borderRadius: 6 }}
21+
/>
22+
<Text style={{ fontWeight: "600" }}>Details (optional)</Text>
23+
<TextInput
24+
value={description}
25+
onChangeText={onDescriptionChange}
26+
maxLength={10000}
27+
multiline
28+
placeholder="Steps to reproduce, expected vs actual"
29+
style={{
30+
borderWidth: 1,
31+
borderColor: "#ccc",
32+
padding: 8,
33+
borderRadius: 6,
34+
minHeight: 120,
35+
}}
36+
/>
37+
</View>
38+
)
39+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import React from "react"
2+
import { Pressable, Text, View, ActivityIndicator } from "react-native"
3+
4+
interface Props {
5+
submitting: boolean
6+
error: string | null
7+
onSubmit: () => void
8+
onCancel: () => void
9+
}
10+
11+
export function StepSubmit({ submitting, error, onSubmit, onCancel }: Props) {
12+
return (
13+
<View style={{ padding: 16, gap: 12 }}>
14+
{error && <Text style={{ color: "#dc2626" }}>{error}</Text>}
15+
<Pressable
16+
onPress={onSubmit}
17+
disabled={submitting}
18+
style={{ backgroundColor: "#6366f1", padding: 12, borderRadius: 6, alignItems: "center" }}
19+
>
20+
{submitting ? (
21+
<ActivityIndicator color="white" />
22+
) : (
23+
<Text style={{ color: "white" }}>Submit</Text>
24+
)}
25+
</Pressable>
26+
<Pressable onPress={onCancel} style={{ padding: 12, alignItems: "center" }}>
27+
<Text>Cancel</Text>
28+
</Pressable>
29+
</View>
30+
)
31+
}

0 commit comments

Comments
 (0)