1- import { useCallback } from 'react'
1+ import { useCallback , useState } from 'react'
22import {
33 View ,
44 Text ,
@@ -7,11 +7,16 @@ import {
77 StyleSheet ,
88 RefreshControl ,
99 ActivityIndicator ,
10+ Modal ,
11+ TextInput ,
12+ KeyboardAvoidingView ,
13+ Platform ,
14+ Alert ,
1015} from 'react-native'
1116import { useSafeAreaInsets } from 'react-native-safe-area-context'
1217import { useNavigation } from '@react-navigation/native'
13- import { useQuery } from '@tanstack/react-query'
14- import { api , WorkspaceInfo , HOST_WORKSPACE_NAME } from '../lib/api'
18+ import { useQuery , useMutation , useQueryClient } from '@tanstack/react-query'
19+ import { api , WorkspaceInfo , HOST_WORKSPACE_NAME , CreateWorkspaceRequest } from '../lib/api'
1520import { useNetwork , parseNetworkError } from '../lib/network'
1621
1722function StatusDot ( { status } : { status : WorkspaceInfo [ 'status' ] | 'host' } ) {
@@ -104,13 +109,43 @@ function HostSection({ onHostPress }: { onHostPress: () => void }) {
104109export function HomeScreen ( ) {
105110 const insets = useSafeAreaInsets ( )
106111 const navigation = useNavigation < any > ( )
112+ const queryClient = useQueryClient ( )
107113 const { status } = useNetwork ( )
114+ const [ showCreate , setShowCreate ] = useState ( false )
115+ const [ newName , setNewName ] = useState ( '' )
116+ const [ newRepo , setNewRepo ] = useState ( '' )
108117
109118 const { data : workspaces , isLoading, refetch, isRefetching, error } = useQuery ( {
110119 queryKey : [ 'workspaces' ] ,
111120 queryFn : api . listWorkspaces ,
112121 } )
113122
123+ const createMutation = useMutation ( {
124+ mutationFn : ( data : CreateWorkspaceRequest ) => api . createWorkspace ( data ) ,
125+ onSuccess : ( workspace ) => {
126+ queryClient . invalidateQueries ( { queryKey : [ 'workspaces' ] } )
127+ setShowCreate ( false )
128+ setNewName ( '' )
129+ setNewRepo ( '' )
130+ navigation . navigate ( 'WorkspaceDetail' , { name : workspace . name } )
131+ } ,
132+ onError : ( err ) => {
133+ Alert . alert ( 'Error' , parseNetworkError ( err ) )
134+ } ,
135+ } )
136+
137+ const handleCreate = ( ) => {
138+ const name = newName . trim ( )
139+ if ( ! name ) {
140+ Alert . alert ( 'Error' , 'Please enter a workspace name' )
141+ return
142+ }
143+ createMutation . mutate ( {
144+ name,
145+ clone : newRepo . trim ( ) || undefined ,
146+ } )
147+ }
148+
114149 const handleWorkspacePress = useCallback ( ( workspace : WorkspaceInfo ) => {
115150 navigation . navigate ( 'WorkspaceDetail' , { name : workspace . name } )
116151 } , [ navigation ] )
@@ -150,13 +185,22 @@ export function HomeScreen() {
150185 < View style = { [ styles . container , { paddingTop : insets . top } ] } >
151186 < View style = { styles . header } >
152187 < Text style = { styles . headerTitle } > Perry</ Text >
153- < TouchableOpacity
154- style = { styles . settingsBtn }
155- onPress = { ( ) => navigation . navigate ( 'Settings' ) }
156- testID = "settings-button"
157- >
158- < Text style = { styles . settingsIcon } > ⚙</ Text >
159- </ TouchableOpacity >
188+ < View style = { styles . headerButtons } >
189+ < TouchableOpacity
190+ style = { styles . headerBtn }
191+ onPress = { ( ) => setShowCreate ( true ) }
192+ testID = "add-workspace-button"
193+ >
194+ < Text style = { styles . addIcon } > +</ Text >
195+ </ TouchableOpacity >
196+ < TouchableOpacity
197+ style = { styles . headerBtn }
198+ onPress = { ( ) => navigation . navigate ( 'Settings' ) }
199+ testID = "settings-button"
200+ >
201+ < Text style = { styles . settingsIcon } > ⚙</ Text >
202+ </ TouchableOpacity >
203+ </ View >
160204 </ View >
161205
162206 < FlatList
@@ -179,6 +223,66 @@ export function HomeScreen() {
179223 }
180224 ItemSeparatorComponent = { ( ) => < View style = { styles . separator } /> }
181225 />
226+
227+ < Modal
228+ visible = { showCreate }
229+ animationType = "slide"
230+ presentationStyle = "pageSheet"
231+ onRequestClose = { ( ) => setShowCreate ( false ) }
232+ >
233+ < KeyboardAvoidingView
234+ style = { styles . modalContainer }
235+ behavior = { Platform . OS === 'ios' ? 'padding' : undefined }
236+ >
237+ < View style = { styles . modalHeader } >
238+ < TouchableOpacity onPress = { ( ) => setShowCreate ( false ) } style = { styles . modalCancelBtn } >
239+ < Text style = { styles . modalCancelText } > Cancel</ Text >
240+ </ TouchableOpacity >
241+ < Text style = { styles . modalTitle } > New Workspace</ Text >
242+ < TouchableOpacity
243+ onPress = { handleCreate }
244+ style = { styles . modalCreateBtn }
245+ disabled = { createMutation . isPending || ! newName . trim ( ) }
246+ >
247+ { createMutation . isPending ? (
248+ < ActivityIndicator size = "small" color = "#0a84ff" />
249+ ) : (
250+ < Text style = { [ styles . modalCreateText , ! newName . trim ( ) && styles . modalCreateTextDisabled ] } >
251+ Create
252+ </ Text >
253+ ) }
254+ </ TouchableOpacity >
255+ </ View >
256+ < View style = { styles . modalContent } >
257+ < View style = { styles . inputGroup } >
258+ < Text style = { styles . inputLabel } > Name</ Text >
259+ < TextInput
260+ style = { styles . modalInput }
261+ value = { newName }
262+ onChangeText = { setNewName }
263+ placeholder = "my-project"
264+ placeholderTextColor = "#636366"
265+ autoCapitalize = "none"
266+ autoCorrect = { false }
267+ autoFocus
268+ />
269+ </ View >
270+ < View style = { styles . inputGroup } >
271+ < Text style = { styles . inputLabel } > Repository (optional)</ Text >
272+ < TextInput
273+ style = { styles . modalInput }
274+ value = { newRepo }
275+ onChangeText = { setNewRepo }
276+ placeholder = "https://github.com/user/repo"
277+ placeholderTextColor = "#636366"
278+ autoCapitalize = "none"
279+ autoCorrect = { false }
280+ keyboardType = "url"
281+ />
282+ </ View >
283+ </ View >
284+ </ KeyboardAvoidingView >
285+ </ Modal >
182286 </ View >
183287 )
184288}
@@ -207,12 +311,21 @@ const styles = StyleSheet.create({
207311 fontWeight : '700' ,
208312 color : '#fff' ,
209313 } ,
210- settingsBtn : {
314+ headerButtons : {
315+ flexDirection : 'row' ,
316+ alignItems : 'center' ,
317+ } ,
318+ headerBtn : {
211319 width : 44 ,
212320 height : 44 ,
213321 alignItems : 'center' ,
214322 justifyContent : 'center' ,
215323 } ,
324+ addIcon : {
325+ fontSize : 28 ,
326+ color : '#0a84ff' ,
327+ fontWeight : '300' ,
328+ } ,
216329 settingsIcon : {
217330 fontSize : 22 ,
218331 color : '#8e8e93' ,
@@ -358,4 +471,62 @@ const styles = StyleSheet.create({
358471 fontWeight : '600' ,
359472 color : '#fff' ,
360473 } ,
474+ modalContainer : {
475+ flex : 1 ,
476+ backgroundColor : '#000' ,
477+ } ,
478+ modalHeader : {
479+ flexDirection : 'row' ,
480+ alignItems : 'center' ,
481+ justifyContent : 'space-between' ,
482+ paddingHorizontal : 16 ,
483+ paddingVertical : 12 ,
484+ borderBottomWidth : 1 ,
485+ borderBottomColor : '#1c1c1e' ,
486+ } ,
487+ modalCancelBtn : {
488+ paddingVertical : 8 ,
489+ } ,
490+ modalCancelText : {
491+ fontSize : 17 ,
492+ color : '#0a84ff' ,
493+ } ,
494+ modalTitle : {
495+ fontSize : 17 ,
496+ fontWeight : '600' ,
497+ color : '#fff' ,
498+ } ,
499+ modalCreateBtn : {
500+ paddingVertical : 8 ,
501+ minWidth : 60 ,
502+ alignItems : 'flex-end' ,
503+ } ,
504+ modalCreateText : {
505+ fontSize : 17 ,
506+ fontWeight : '600' ,
507+ color : '#0a84ff' ,
508+ } ,
509+ modalCreateTextDisabled : {
510+ color : '#636366' ,
511+ } ,
512+ modalContent : {
513+ padding : 16 ,
514+ } ,
515+ inputGroup : {
516+ marginBottom : 20 ,
517+ } ,
518+ inputLabel : {
519+ fontSize : 13 ,
520+ color : '#8e8e93' ,
521+ marginBottom : 8 ,
522+ textTransform : 'uppercase' ,
523+ letterSpacing : 0.5 ,
524+ } ,
525+ modalInput : {
526+ backgroundColor : '#1c1c1e' ,
527+ borderRadius : 10 ,
528+ padding : 14 ,
529+ fontSize : 17 ,
530+ color : '#fff' ,
531+ } ,
361532} )
0 commit comments