Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: add mapeo core react and state manager #59

Closed
wants to merge 20 commits into from
Closed
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 47 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
"valibot": "^0.42.1"
},
"devDependencies": {
"@comapeo/core-react": "0.1.1",
"@electron-forge/cli": "^7.5.0",
"@electron-forge/maker-deb": "^7.5.0",
"@electron-forge/maker-dmg": "^7.5.0",
Expand Down Expand Up @@ -116,7 +117,8 @@
"typescript-eslint": "^8.15.0",
"vite": "^5.4.11",
"vite-plugin-svgr": "^4.3.0",
"vitest": "2.1.8"
"vitest": "2.1.8",
"zustand": "5.0.2"
},
"overrides": {
"better-sqlite3": "11.5.0"
Expand Down
30 changes: 30 additions & 0 deletions src/renderer/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { useOwnDeviceInfo } from '@comapeo/core-react'
import { RouterProvider, createRouter } from '@tanstack/react-router'

import { usePersistedProjectIdStore } from './contexts/persistedState/PersistedProjectId'
import { routeTree } from './routeTree.gen'

const router = createRouter({
routeTree,
context: { hasDeviceName: undefined!, persistedProjectId: undefined! },
})

declare module '@tanstack/react-router' {
interface Register {
router: typeof router
}
}

export const App = () => {
const { data } = useOwnDeviceInfo()
const hasDeviceName = data?.name !== undefined
const persistedProjectId = !!usePersistedProjectIdStore(
(store) => store.projectId,
)
return (
<RouterProvider
router={router}
context={{ hasDeviceName, persistedProjectId }}
/>
)
}
28 changes: 28 additions & 0 deletions src/renderer/src/AppWrapper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { ThemeProvider } from '@emotion/react'
import { CssBaseline } from '@mui/material'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'

import { App } from './App'
import { theme } from './Theme'
import { ApiProvider } from './contexts/ApiContext'
import { IntlProvider } from './contexts/IntlContext'
import { PersistedProjectIdProvider } from './contexts/persistedState/PersistedProjectId'

const queryClient = new QueryClient()

export const AppWrapper = () => {
return (
<ThemeProvider theme={theme}>
<CssBaseline />
<IntlProvider>
<QueryClientProvider client={queryClient}>
<ApiProvider>
<PersistedProjectIdProvider>
<App />
</PersistedProjectIdProvider>
</ApiProvider>
</QueryClientProvider>
</IntlProvider>
</ThemeProvider>
)
}
8 changes: 4 additions & 4 deletions src/renderer/src/components/Tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export const Tabs = () => {
<MapTabStyled
data-testid="tab-observation"
icon={<PostAddIcon />}
value={'/tab1'}
value={'/Tab1'}
/>
{/* This is needed to properly space the items. Originally used a div, but was causing console errors as the Parent component passes it props, which were invalid for non-tab components */}
<Tab disabled={true} sx={{ flex: 1 }} />
Expand All @@ -59,7 +59,7 @@ export const Tabs = () => {
{formatMessage(m.setting)}
</Text>
}
value={'/tab2'}
value={'/Tab2'}
/>
<MapTabStyled
icon={<InfoOutlinedIcon />}
Expand All @@ -68,7 +68,7 @@ export const Tabs = () => {
{formatMessage(m.about)}
</Text>
}
value={'/tab2'}
value={'/Tab2'}
/>
</MuiTabs>
)
Expand All @@ -77,7 +77,7 @@ export const Tabs = () => {
type TabProps = React.ComponentProps<typeof Tab>

type MapTabRoute = {
[K in keyof FileRoutesById]: K extends `${'/(MapTabs)/_Map'}${infer Rest}`
[K in keyof FileRoutesById]: K extends `${'/_Map'}${infer Rest}`
? Rest extends ''
? never
: `${Rest}`
Expand Down
32 changes: 32 additions & 0 deletions src/renderer/src/contexts/ActiveProjectIdStore.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { createContext, useContext, type ReactNode } from 'react'

import { usePersistedProjectIdStore } from './persistedState/PersistedProjectId'

const ActiveProjectContext = createContext<string | null>(null)

/**
* This provider guarantees a projectId in persisted state
*/
export const ActiveProjectContextProvider = ({
children,
}: {
children: ReactNode
}) => {
const projectId = usePersistedProjectIdStore((store) => store.projectId)
if (!projectId) {
throw new Error('No Project Id set')
}
return (
<ActiveProjectContext.Provider value={projectId}>
{children}
</ActiveProjectContext.Provider>
)
}

export function useActiveProjectId() {
const context = useContext(ActiveProjectContext)
if (!context) {
throw new Error('no active project')
}
return context
}
21 changes: 3 additions & 18 deletions src/renderer/src/contexts/ApiContext.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,7 @@
import {
createContext,
useContext,
useEffect,
useState,
type PropsWithChildren,
} from 'react'
import { useEffect, useState, type PropsWithChildren } from 'react'
import { ClientApiProvider } from '@comapeo/core-react'
import { createMapeoClient, type MapeoClientApi } from '@comapeo/ipc'

const ApiContext = createContext<MapeoClientApi | null>(null)

export function ApiProvider({ children }: PropsWithChildren) {
const [api, setApi] = useState<MapeoClientApi | null>(null)

Expand Down Expand Up @@ -44,13 +37,5 @@ export function ApiProvider({ children }: PropsWithChildren) {

if (!api) return null

return <ApiContext.Provider value={api}>{children}</ApiContext.Provider>
}

export function useApi() {
const api = useContext(ApiContext)

if (!api) throw new Error('MapeoApiContext provider needs to be set up')

return api
return <ClientApiProvider clientApi={api}>{children}</ClientApiProvider>
}
18 changes: 18 additions & 0 deletions src/renderer/src/contexts/persistedState/PersistedProjectId.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { type StateCreator } from 'zustand'

import { createPersistedStoreWithProvider } from './createPersistedState'

type ProjectIdSlice = {
projectId: string | undefined
setProjectId: (id?: string) => void
}

const projectIdSlice: StateCreator<ProjectIdSlice> = (set) => ({
projectId: undefined,
setProjectId: (projectId) => set({ projectId }),
})

export const {
Provider: PersistedProjectIdProvider,
useStoreHook: usePersistedProjectIdStore,
} = createPersistedStoreWithProvider(projectIdSlice, 'ActiveProjectId')
55 changes: 55 additions & 0 deletions src/renderer/src/contexts/persistedState/createPersistedState.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { createContext, useContext, useState, type ReactNode } from 'react'
import { createStore, useStore, type StateCreator } from 'zustand'
import { persist } from 'zustand/middleware'

type PersistedStoreKey = 'ActiveProjectId'

export function createPersistedStoreWithProvider<T>(
slice: StateCreator<T>,
persistedStoreKey: PersistedStoreKey,
) {
const store = createPersistedStore(slice, persistedStoreKey)
const Context = createContext<typeof store | null>(null)

const Provider = ({ children }: { children: ReactNode }) => {
const [storeInstance] = useState(() => store)

return <Context.Provider value={storeInstance}>{children}</Context.Provider>
}

const useStoreHook = <Selected,>(
selector: (state: T) => Selected,
): Selected => {
const contextStore = useContext(Context)
if (!contextStore) {
throw new Error(
`Missing provider for persisted store: ${persistedStoreKey}`,
)
}

return useStore(contextStore, selector)
}

return { Provider, useStoreHook }
}

function createPersistedStore<T>(
...args: Parameters<typeof createPersistMiddleware<T>>
) {
const store = createStore<T>()(createPersistMiddleware(...args))
store.setState((state) => ({
...state,
...args[0],
}))

return store
}

function createPersistMiddleware<State>(
slice: StateCreator<State>,
persistedStoreKey: PersistedStoreKey,
) {
return persist(slice, {
name: persistedStoreKey,
})
}
20 changes: 20 additions & 0 deletions src/renderer/src/hooks/mutations/deviceInfo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { getDeviceInfoQueryKey, useClientApi } from '@comapeo/core-react'
import { useMutation, useQueryClient } from '@tanstack/react-query'

export const useEditDeviceInfo = () => {
const clientApi = useClientApi()
const queryClient = useQueryClient()

return useMutation({
mutationKey: ['device'],
mutationFn: async (name: string) => {
return clientApi.setDeviceInfo({
name,
deviceType: 'desktop',
})
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: getDeviceInfoQueryKey() })
},
})
}
21 changes: 21 additions & 0 deletions src/renderer/src/hooks/mutations/projects.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { getProjectsQueryKey, useClientApi } from '@comapeo/core-react'
import { useMutation, useQueryClient } from '@tanstack/react-query'

export const CREATE_PROJECT_KEY = 'create_project'

export function useCreateProject() {
const api = useClientApi()
const queryClient = useQueryClient()

return useMutation({
mutationKey: [CREATE_PROJECT_KEY],
mutationFn: (name?: string) => {
return api.createProject({ name })
},
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: getProjectsQueryKey(),
})
},
})
}
13 changes: 2 additions & 11 deletions src/renderer/src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,9 @@
import { RouterProvider, createRouter } from '@tanstack/react-router'
import { createRoot } from 'react-dom/client'

import { routeTree } from './routeTree.gen'

import './index.css'

const router = createRouter({ routeTree })

declare module '@tanstack/react-router' {
interface Register {
router: typeof router
}
}
import { AppWrapper } from './AppWrapper'

const root = createRoot(document.getElementById('app') as HTMLElement)

root.render(<RouterProvider router={router} />)
root.render(<AppWrapper />)
Loading
Loading