mirror of
				https://github.com/khairul169/vaulterm.git
				synced 2025-10-31 03:39:37 +07:00 
			
		
		
		
	feat: update ui
This commit is contained in:
		
							parent
							
								
									ca3fe7150b
								
							
						
					
					
						commit
						887cb64878
					
				
							
								
								
									
										42110
									
								
								frontend/.tamagui/tamagui.config.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42110
									
								
								frontend/.tamagui/tamagui.config.json
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										36
									
								
								frontend/app.config.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								frontend/app.config.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,36 @@ | ||||
| import { ExpoConfig, ConfigContext } from "expo/config"; | ||||
| 
 | ||||
| export default ({ config }: ConfigContext): ExpoConfig => ({ | ||||
|   ...config, | ||||
|   name: "frontend", | ||||
|   slug: "frontend", | ||||
|   version: "1.0.0", | ||||
|   orientation: "portrait", | ||||
|   icon: "./assets/images/icon.png", | ||||
|   scheme: "myapp", | ||||
|   userInterfaceStyle: "automatic", | ||||
|   newArchEnabled: true, | ||||
|   splash: { | ||||
|     image: "./assets/images/splash.png", | ||||
|     resizeMode: "contain", | ||||
|     backgroundColor: "#ffffff", | ||||
|   }, | ||||
|   ios: { | ||||
|     supportsTablet: true, | ||||
|   }, | ||||
|   android: { | ||||
|     adaptiveIcon: { | ||||
|       foregroundImage: "./assets/images/adaptive-icon.png", | ||||
|       backgroundColor: "#ffffff", | ||||
|     }, | ||||
|   }, | ||||
|   web: { | ||||
|     bundler: "metro", | ||||
|     output: "static", | ||||
|     favicon: "./assets/images/favicon.png", | ||||
|   }, | ||||
|   plugins: ["expo-router"], | ||||
|   experiments: { | ||||
|     typedRoutes: true, | ||||
|   }, | ||||
| }); | ||||
| @ -1,37 +0,0 @@ | ||||
| { | ||||
|   "expo": { | ||||
|     "name": "frontend", | ||||
|     "slug": "frontend", | ||||
|     "version": "1.0.0", | ||||
|     "orientation": "portrait", | ||||
|     "icon": "./assets/images/icon.png", | ||||
|     "scheme": "myapp", | ||||
|     "userInterfaceStyle": "automatic", | ||||
|     "newArchEnabled": true, | ||||
|     "splash": { | ||||
|       "image": "./assets/images/splash.png", | ||||
|       "resizeMode": "contain", | ||||
|       "backgroundColor": "#ffffff" | ||||
|     }, | ||||
|     "ios": { | ||||
|       "supportsTablet": true | ||||
|     }, | ||||
|     "android": { | ||||
|       "adaptiveIcon": { | ||||
|         "foregroundImage": "./assets/images/adaptive-icon.png", | ||||
|         "backgroundColor": "#ffffff" | ||||
|       } | ||||
|     }, | ||||
|     "web": { | ||||
|       "bundler": "metro", | ||||
|       "output": "static", | ||||
|       "favicon": "./assets/images/favicon.png" | ||||
|     }, | ||||
|     "plugins": [ | ||||
|       "expo-router" | ||||
|     ], | ||||
|     "experiments": { | ||||
|       "typedRoutes": true | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @ -1,20 +1,15 @@ | ||||
| import { | ||||
|   DarkTheme, | ||||
|   DefaultTheme, | ||||
|   ThemeProvider, | ||||
| } from "@react-navigation/native"; | ||||
| import { useFonts } from "expo-font"; | ||||
| import { Stack } from "expo-router"; | ||||
| import * as SplashScreen from "expo-splash-screen"; | ||||
| import { StatusBar } from "expo-status-bar"; | ||||
| import { useEffect } from "react"; | ||||
| import "react-native-reanimated"; | ||||
| import Providers from "./_providers"; | ||||
| 
 | ||||
| // Prevent the splash screen from auto-hiding before asset loading is complete.
 | ||||
| SplashScreen.preventAutoHideAsync(); | ||||
| 
 | ||||
| export default function RootLayout() { | ||||
|   const colorScheme: string = "light"; | ||||
|   const [loaded] = useFonts({ | ||||
|     SpaceMono: require("../assets/fonts/SpaceMono-Regular.ttf"), | ||||
|   }); | ||||
| @ -30,11 +25,11 @@ export default function RootLayout() { | ||||
|   } | ||||
| 
 | ||||
|   return ( | ||||
|     <ThemeProvider value={colorScheme === "dark" ? DarkTheme : DefaultTheme}> | ||||
|     <Providers> | ||||
|       <Stack> | ||||
|         <Stack.Screen name="+not-found" /> | ||||
|       </Stack> | ||||
|       <StatusBar style="auto" /> | ||||
|     </ThemeProvider> | ||||
|     </Providers> | ||||
|   ); | ||||
| } | ||||
|  | ||||
							
								
								
									
										48
									
								
								frontend/app/_providers.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								frontend/app/_providers.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,48 @@ | ||||
| import React, { PropsWithChildren, useMemo, useState } from "react"; | ||||
| import tamaguiConfig from "@/tamagui.config"; | ||||
| import { | ||||
|   DarkTheme, | ||||
|   DefaultTheme, | ||||
|   ThemeProvider, | ||||
| } from "@react-navigation/native"; | ||||
| import { TamaguiProvider, Theme } from "@tamagui/core"; | ||||
| import useThemeStore from "@/stores/theme"; | ||||
| import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; | ||||
| 
 | ||||
| type Props = PropsWithChildren; | ||||
| 
 | ||||
| const Providers = ({ children }: Props) => { | ||||
|   const colorScheme = useThemeStore((i) => i.theme); | ||||
|   const [queryClient] = useState(() => new QueryClient()); | ||||
| 
 | ||||
|   const theme = useMemo(() => { | ||||
|     return colorScheme === "dark" | ||||
|       ? tamaguiConfig.themes.dark_blue | ||||
|       : tamaguiConfig.themes.light_blue; | ||||
|   }, [colorScheme]); | ||||
| 
 | ||||
|   const navTheme = useMemo(() => { | ||||
|     const base = colorScheme === "dark" ? DarkTheme : DefaultTheme; | ||||
|     return { | ||||
|       ...base, | ||||
|       colors: { | ||||
|         ...base.colors, | ||||
|         background: theme.background.val, | ||||
|       }, | ||||
|     }; | ||||
|   }, [theme, colorScheme]); | ||||
| 
 | ||||
|   return ( | ||||
|     <ThemeProvider value={navTheme}> | ||||
|       <TamaguiProvider config={tamaguiConfig} defaultTheme={colorScheme}> | ||||
|         <Theme name="blue"> | ||||
|           <QueryClientProvider client={queryClient}> | ||||
|             {children} | ||||
|           </QueryClientProvider> | ||||
|         </Theme> | ||||
|       </TamaguiProvider> | ||||
|     </ThemeProvider> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| export default Providers; | ||||
							
								
								
									
										69
									
								
								frontend/app/hosts/_comp/host-form.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								frontend/app/hosts/_comp/host-form.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,69 @@ | ||||
| import Icons from "@/components/ui/icons"; | ||||
| import Select, { SelectItem } from "@/components/ui/select"; | ||||
| import api from "@/lib/api"; | ||||
| import { useQuery } from "@tanstack/react-query"; | ||||
| import React from "react"; | ||||
| import { Button, Input, Label, ScrollView, Text, View, XStack } from "tamagui"; | ||||
| 
 | ||||
| type Props = {}; | ||||
| 
 | ||||
| const typeOptions: SelectItem[] = [ | ||||
|   { label: "SSH", value: "ssh" }, | ||||
|   { label: "Proxmox VE", value: "pve" }, | ||||
|   { label: "Incus", value: "incus" }, | ||||
| ]; | ||||
| 
 | ||||
| const HostForm = (props: Props) => { | ||||
|   const keys = useQuery({ | ||||
|     queryKey: ["keychains"], | ||||
|     queryFn: () => api("/keychains"), | ||||
|     select: (i) => i.rows, | ||||
|   }); | ||||
| 
 | ||||
|   return ( | ||||
|     <> | ||||
|       <ScrollView contentContainerStyle={{ padding: "$4" }}> | ||||
|         <Label>Hostname</Label> | ||||
|         <Input placeholder="IP or hostname..." /> | ||||
| 
 | ||||
|         <Label>Type</Label> | ||||
|         <Select items={typeOptions} /> | ||||
| 
 | ||||
|         <Label>Port</Label> | ||||
|         <Input keyboardType="number-pad" placeholder="SSH Port" /> | ||||
| 
 | ||||
|         <Label>Label</Label> | ||||
|         <Input placeholder="Label..." /> | ||||
| 
 | ||||
|         <XStack gap="$3" mt="$3" mb="$1"> | ||||
|           <Label flex={1}>Credentials</Label> | ||||
|           <Button size="$3" icon={<Icons size={16} name="plus" />}> | ||||
|             Add | ||||
|           </Button> | ||||
|         </XStack> | ||||
| 
 | ||||
|         <Select | ||||
|           placeholder="Username & Password" | ||||
|           items={keys.data?.map((key: any) => ({ | ||||
|             label: key.label, | ||||
|             value: key.id, | ||||
|           }))} | ||||
|         /> | ||||
|         <Select | ||||
|           mt="$3" | ||||
|           placeholder="Private Key" | ||||
|           items={keys.data?.map((key: any) => ({ | ||||
|             label: key.label, | ||||
|             value: key.id, | ||||
|           }))} | ||||
|         /> | ||||
|       </ScrollView> | ||||
| 
 | ||||
|       <View p="$4"> | ||||
|         <Button icon={<Icons name="content-save" size={18} />}>Save</Button> | ||||
|       </View> | ||||
|     </> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| export default HostForm; | ||||
							
								
								
									
										12
									
								
								frontend/app/hosts/create.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								frontend/app/hosts/create.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,12 @@ | ||||
| import React from "react"; | ||||
| import { Stack } from "expo-router"; | ||||
| import HostForm from "./_comp/host-form"; | ||||
| 
 | ||||
| export default function CreateHostPage() { | ||||
|   return ( | ||||
|     <> | ||||
|       <Stack.Screen options={{ title: "Add Host" }} /> | ||||
|       <HostForm /> | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										12
									
								
								frontend/app/hosts/edit.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								frontend/app/hosts/edit.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,12 @@ | ||||
| import React from "react"; | ||||
| import { Stack } from "expo-router"; | ||||
| import HostForm from "./_comp/host-form"; | ||||
| 
 | ||||
| export default function EditHostPage() { | ||||
|   return ( | ||||
|     <> | ||||
|       <Stack.Screen options={{ title: "Edit Host" }} /> | ||||
|       <HostForm /> | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										80
									
								
								frontend/app/hosts/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								frontend/app/hosts/index.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,80 @@ | ||||
| import { View, Text, Button, ScrollView, Spinner, Card, XStack } from "tamagui"; | ||||
| import React from "react"; | ||||
| import useThemeStore from "@/stores/theme"; | ||||
| import { useQuery } from "@tanstack/react-query"; | ||||
| import api from "@/lib/api"; | ||||
| import { Stack } from "expo-router"; | ||||
| import Pressable from "@/components/ui/pressable"; | ||||
| import Icons from "@/components/ui/icons"; | ||||
| 
 | ||||
| export default function Hosts() { | ||||
|   const { toggle } = useThemeStore(); | ||||
| 
 | ||||
|   const hosts = useQuery({ | ||||
|     queryKey: ["hosts"], | ||||
|     queryFn: () => api("/hosts"), | ||||
|   }); | ||||
| 
 | ||||
|   return ( | ||||
|     <> | ||||
|       <Stack.Screen | ||||
|         options={{ | ||||
|           title: "Hosts", | ||||
|           headerRight: () => ( | ||||
|             <Button onPress={() => toggle()} mr="$2"> | ||||
|               Toggle Theme | ||||
|             </Button> | ||||
|           ), | ||||
|         }} | ||||
|       /> | ||||
| 
 | ||||
|       {hosts.isLoading ? ( | ||||
|         <View alignItems="center" justifyContent="center" flex={1}> | ||||
|           <Spinner size="large" /> | ||||
|           <Text mt="$4">Loading...</Text> | ||||
|         </View> | ||||
|       ) : ( | ||||
|         <ScrollView | ||||
|           contentContainerStyle={{ | ||||
|             padding: "$2", | ||||
|             flexDirection: "row", | ||||
|             flexWrap: "wrap", | ||||
|             // gap: "$4",
 | ||||
|           }} | ||||
|         > | ||||
|           {hosts.data.rows?.map((host: any) => ( | ||||
|             <Pressable | ||||
|               key={host.id} | ||||
|               flexBasis="100%" | ||||
|               $gtXs={{ flexBasis: "50%" }} | ||||
|               $gtSm={{ flexBasis: "33.3%" }} | ||||
|               $gtMd={{ flexBasis: "25%" }} | ||||
|               $gtLg={{ flexBasis: "20%" }} | ||||
|               p="$2" | ||||
|               group | ||||
|             > | ||||
|               <Card elevate bordered p="$4"> | ||||
|                 <XStack> | ||||
|                   <View flex={1}> | ||||
|                     <Text>{host.label}</Text> | ||||
|                     <Text fontSize="$3" mt="$2"> | ||||
|                       {host.host} | ||||
|                     </Text> | ||||
|                   </View> | ||||
| 
 | ||||
|                   <Button | ||||
|                     circular | ||||
|                     display="none" | ||||
|                     $group-hover={{ display: "block" }} | ||||
|                   > | ||||
|                     <Icons name="pencil" size={16} /> | ||||
|                   </Button> | ||||
|                 </XStack> | ||||
|               </Card> | ||||
|             </Pressable> | ||||
|           ))} | ||||
|         </ScrollView> | ||||
|       )} | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										6
									
								
								frontend/app/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								frontend/app/index.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,6 @@ | ||||
| import React from "react"; | ||||
| import { Redirect } from "expo-router"; | ||||
| 
 | ||||
| export default function index() { | ||||
|   return <Redirect href="/hosts" />; | ||||
| } | ||||
							
								
								
									
										20
									
								
								frontend/babel.config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								frontend/babel.config.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,20 @@ | ||||
| module.exports = function (api) { | ||||
|   api.cache(true); | ||||
|   return { | ||||
|     presets: ["babel-preset-expo"], | ||||
|     plugins: [ | ||||
|       [ | ||||
|         "@tamagui/babel-plugin", | ||||
|         { | ||||
|           components: ["tamagui"], | ||||
|           config: "./tamagui.config.ts", | ||||
|           logTimings: true, | ||||
|           disableExtraction: process.env.NODE_ENV === "development", | ||||
|         }, | ||||
|       ], | ||||
| 
 | ||||
|       // NOTE: this is only necessary if you are using reanimated for animations
 | ||||
|       "react-native-reanimated/plugin", | ||||
|     ], | ||||
|   }; | ||||
| }; | ||||
							
								
								
									
										8
									
								
								frontend/components/ui/icons.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								frontend/components/ui/icons.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,8 @@ | ||||
| import MaterialCommunityIcons from "@expo/vector-icons/MaterialCommunityIcons"; | ||||
| import { styled } from "tamagui"; | ||||
| 
 | ||||
| export const Icons = styled(MaterialCommunityIcons, { | ||||
|   color: "$color", | ||||
| }); | ||||
| 
 | ||||
| export default Icons; | ||||
							
								
								
									
										20
									
								
								frontend/components/ui/pressable.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								frontend/components/ui/pressable.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,20 @@ | ||||
| import { Pressable as BasePressable } from "react-native"; | ||||
| import { GetProps, styled, ViewStyle } from "tamagui"; | ||||
| 
 | ||||
| const StyledPressable = styled(BasePressable); | ||||
| export type PressableProps = GetProps<typeof StyledPressable> & { | ||||
|   $hover?: ViewStyle; | ||||
|   $pressed?: ViewStyle; | ||||
| }; | ||||
| 
 | ||||
| const Pressable = ({ | ||||
|   $hover, | ||||
|   $pressed = { opacity: 0.5 }, | ||||
|   ...props | ||||
| }: PressableProps) => { | ||||
|   return ( | ||||
|     <StyledPressable pressStyle={$pressed} hoverStyle={$hover} {...props} /> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| export default Pressable; | ||||
							
								
								
									
										65
									
								
								frontend/components/ui/select.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								frontend/components/ui/select.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,65 @@ | ||||
| import React, { forwardRef } from "react"; | ||||
| import { Select as BaseSelect } from "tamagui"; | ||||
| 
 | ||||
| export type SelectItem = { | ||||
|   label: string; | ||||
|   value: string; | ||||
| }; | ||||
| 
 | ||||
| type SelectProps = React.ComponentPropsWithoutRef<typeof BaseSelect.Trigger> & { | ||||
|   items?: SelectItem[] | null; | ||||
|   value?: string; | ||||
|   defaultValue?: string; | ||||
|   onChange?: (value: string) => void; | ||||
|   placeholder?: string; | ||||
| }; | ||||
| 
 | ||||
| type SelectRef = React.ElementRef<typeof BaseSelect.Trigger>; | ||||
| 
 | ||||
| const Select = forwardRef<SelectRef, SelectProps>( | ||||
|   ( | ||||
|     { | ||||
|       items, | ||||
|       value, | ||||
|       defaultValue, | ||||
|       onChange, | ||||
|       placeholder = "Select...", | ||||
|       ...props | ||||
|     }, | ||||
|     ref | ||||
|   ) => { | ||||
|     return ( | ||||
|       <BaseSelect | ||||
|         defaultValue={defaultValue} | ||||
|         value={value} | ||||
|         onValueChange={onChange} | ||||
|       > | ||||
|         <BaseSelect.Trigger ref={ref} {...props}> | ||||
|           <BaseSelect.Value placeholder={placeholder} /> | ||||
|         </BaseSelect.Trigger> | ||||
| 
 | ||||
|         <BaseSelect.Content> | ||||
|           <BaseSelect.ScrollUpButton /> | ||||
|           <BaseSelect.Viewport> | ||||
|             <BaseSelect.Item value="" index={0}> | ||||
|               <BaseSelect.ItemText>{placeholder}</BaseSelect.ItemText> | ||||
|             </BaseSelect.Item> | ||||
| 
 | ||||
|             {items?.map((item, idx) => ( | ||||
|               <BaseSelect.Item | ||||
|                 key={item.value} | ||||
|                 value={item.value} | ||||
|                 index={idx + 1} | ||||
|               > | ||||
|                 <BaseSelect.ItemText>{item.label}</BaseSelect.ItemText> | ||||
|               </BaseSelect.Item> | ||||
|             ))} | ||||
|           </BaseSelect.Viewport> | ||||
|           <BaseSelect.ScrollDownButton /> | ||||
|         </BaseSelect.Content> | ||||
|       </BaseSelect> | ||||
|     ); | ||||
|   } | ||||
| ); | ||||
| 
 | ||||
| export default Select; | ||||
| @ -1,2 +1,10 @@ | ||||
| import { ofetch } from "ofetch"; | ||||
| 
 | ||||
| export const BASE_API_URL = process.env.EXPO_PUBLIC_API_URL || ""; //"http://10.0.0.100:3000";
 | ||||
| export const BASE_WS_URL = BASE_API_URL.replace("http", "ws"); | ||||
| 
 | ||||
| const api = ofetch.create({ | ||||
|   baseURL: BASE_API_URL, | ||||
| }); | ||||
| 
 | ||||
| export default api; | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| { | ||||
|   "name": "frontend", | ||||
|   "license": "0BSD", | ||||
|   "license": "MIT", | ||||
|   "main": "expo-router/entry", | ||||
|   "version": "1.0.0", | ||||
|   "scripts": { | ||||
| @ -18,8 +18,11 @@ | ||||
|   "dependencies": { | ||||
|     "@expo/vector-icons": "^14.0.2", | ||||
|     "@novnc/novnc": "^1.5.0", | ||||
|     "@react-native-async-storage/async-storage": "1.23.1", | ||||
|     "@react-navigation/bottom-tabs": "7.0.0-rc.36", | ||||
|     "@react-navigation/native": "7.0.0-rc.21", | ||||
|     "@tamagui/config": "^1.116.14", | ||||
|     "@tanstack/react-query": "^5.59.20", | ||||
|     "@xterm/addon-attach": "^0.11.0", | ||||
|     "@xterm/addon-fit": "^0.10.0", | ||||
|     "@xterm/xterm": "^5.5.0", | ||||
| @ -35,6 +38,7 @@ | ||||
|     "expo-symbols": "~0.2.0", | ||||
|     "expo-system-ui": "~4.0.2", | ||||
|     "expo-web-browser": "~14.0.0", | ||||
|     "ofetch": "^1.4.1", | ||||
|     "react": "18.3.1", | ||||
|     "react-dom": "18.3.1", | ||||
|     "react-native": "0.76.1", | ||||
| @ -44,10 +48,13 @@ | ||||
|     "react-native-safe-area-context": "4.12.0", | ||||
|     "react-native-screens": "4.0.0-beta.16", | ||||
|     "react-native-web": "~0.19.13", | ||||
|     "react-native-webview": "^13.12.2" | ||||
|     "react-native-webview": "^13.12.2", | ||||
|     "tamagui": "^1.116.14", | ||||
|     "zustand": "^5.0.1" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@babel/core": "^7.25.2", | ||||
|     "@tamagui/babel-plugin": "^1.116.14", | ||||
|     "@types/jest": "^29.5.12", | ||||
|     "@types/novnc__novnc": "^1.5.0", | ||||
|     "@types/react": "~18.3.12", | ||||
|  | ||||
							
								
								
									
										2664
									
								
								frontend/pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2664
									
								
								frontend/pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										29
									
								
								frontend/stores/theme.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								frontend/stores/theme.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,29 @@ | ||||
| import { create } from "zustand"; | ||||
| import { persist, createJSONStorage } from "zustand/middleware"; | ||||
| import AsyncStorage from "@react-native-async-storage/async-storage"; | ||||
| 
 | ||||
| type Store = { | ||||
|   theme: "light" | "dark"; | ||||
|   setTheme: (theme: "light" | "dark") => void; | ||||
|   toggle: () => void; | ||||
| }; | ||||
| 
 | ||||
| const useThemeStore = create( | ||||
|   persist<Store>( | ||||
|     (set) => ({ | ||||
|       theme: "light", | ||||
|       setTheme: (theme: "light" | "dark") => { | ||||
|         set({ theme }); | ||||
|       }, | ||||
|       toggle: () => { | ||||
|         set((state) => ({ theme: state.theme === "light" ? "dark" : "light" })); | ||||
|       }, | ||||
|     }), | ||||
|     { | ||||
|       name: "theme", | ||||
|       storage: createJSONStorage(() => AsyncStorage), | ||||
|     } | ||||
|   ) | ||||
| ); | ||||
| 
 | ||||
| export default useThemeStore; | ||||
							
								
								
									
										13
									
								
								frontend/tamagui.config.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								frontend/tamagui.config.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,13 @@ | ||||
| import { createTamagui } from "@tamagui/core"; | ||||
| import { config } from "@tamagui/config/v3"; | ||||
| 
 | ||||
| // you usually export this from a tamagui.config.ts file
 | ||||
| const tamaguiConfig = createTamagui(config); | ||||
| 
 | ||||
| // TypeScript types across all Tamagui APIs
 | ||||
| type Conf = typeof tamaguiConfig; | ||||
| declare module "@tamagui/core" { | ||||
|   interface TamaguiCustomConfig extends Conf {} | ||||
| } | ||||
| 
 | ||||
| export default tamaguiConfig; | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user