Browse Source

refactor(web): refactor state mgmnt

pull/1/head
isra el 3 years ago
parent
commit
8e2609cbb3
  1. 2
      web/components/Navbar.tsx
  2. 65
      web/components/dashboard/ApiKeyList.tsx
  3. 95
      web/components/dashboard/DeviceList.tsx
  4. 5
      web/components/dashboard/GenerateApiKey.tsx
  5. 2
      web/components/dashboard/UserStats.tsx
  6. 29
      web/pages/dashboard/index.tsx
  7. 2
      web/pages/index.tsx
  8. 2
      web/pages/login.tsx
  9. 2
      web/pages/register.tsx
  10. 20
      web/services/index.ts
  11. 21
      web/services/types.ts
  12. 61
      web/store/apiKeyListReducer.ts
  13. 4
      web/store/authReducer.ts
  14. 61
      web/store/deviceListReducer.ts
  15. 8
      web/store/store.ts

2
web/components/Navbar.tsx

@ -18,7 +18,7 @@ import Link from 'next/link'
import { MoonIcon, SunIcon } from '@chakra-ui/icons' import { MoonIcon, SunIcon } from '@chakra-ui/icons'
import Router from 'next/router' import Router from 'next/router'
import { useDispatch, useSelector } from 'react-redux' import { useDispatch, useSelector } from 'react-redux'
import { logout, selectAuth } from '../store/authSlice'
import { logout, selectAuth } from '../store/authReducer'
export default function Navbar() { export default function Navbar() {
const dispatch = useDispatch() const dispatch = useDispatch()

65
web/components/dashboard/ApiKeyList.tsx

@ -1,5 +1,6 @@
import { DeleteIcon } from '@chakra-ui/icons' import { DeleteIcon } from '@chakra-ui/icons'
import { import {
Spinner,
Table, Table,
TableContainer, TableContainer,
Tbody, Tbody,
@ -11,29 +12,33 @@ import {
useToast, useToast,
} from '@chakra-ui/react' } from '@chakra-ui/react'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useSelector } from 'react-redux'
import { deleteApiKeyRequest, getApiKeyListRequest } from '../../services'
import { selectAuth } from '../../store/authSlice'
import { useDispatch, useSelector } from 'react-redux'
import { deleteApiKeyRequest } from '../../services'
import {
fetchApiKeyList,
selectApiKeyList,
} from '../../store/apiKeyListReducer'
import { selectAuth } from '../../store/authReducer'
const ApiKeyList = () => { const ApiKeyList = () => {
const [apiKeyList, setApiKeyList] = useState([])
const toast = useToast() const toast = useToast()
const dispatch = useDispatch()
const { data, loading } = useSelector(selectApiKeyList)
const { user, accessToken } = useSelector(selectAuth) const { user, accessToken } = useSelector(selectAuth)
useEffect(() => { useEffect(() => {
if (user && accessToken) { if (user && accessToken) {
getApiKeyListRequest().then((apiKeys) => {
setApiKeyList(apiKeys)
})
dispatch(fetchApiKeyList())
} }
}, [user, accessToken])
}, [dispatch, user, accessToken])
const onDelete = (apiKeyId: string) => { const onDelete = (apiKeyId: string) => {
deleteApiKeyRequest(apiKeyId) deleteApiKeyRequest(apiKeyId)
setApiKeyList(apiKeyList.filter((apiKey) => apiKey._id !== apiKeyId))
dispatch(fetchApiKeyList())
toast({ toast({
title: 'Success', title: 'Success',
description: 'API Key deleted', description: 'API Key deleted',
isClosable: true,
}) })
} }
@ -48,21 +53,37 @@ const ApiKeyList = () => {
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
{apiKeyList.map((apiKey) => (
<Tr key={apiKey}>
<Td>{apiKey.apiKey}</Td>
<Td>{apiKey.status}</Td>
<Td>
<Tooltip label='Double Click to delete'>
<DeleteIcon
onDoubleClick={(e) => {
onDelete(apiKey._id)
}}
/>
</Tooltip>
{loading ? (
<Tr>
<Td colSpan={3} textAlign='center'>
<Spinner size='lg' />
</Td> </Td>
</Tr> </Tr>
))}
) : (
<>
{data.length == 0 ? (
<Td colSpan={3} textAlign='center'>
No API Keys
</Td>
) : (
data.map(({ _id, apiKey, status }) => (
<Tr key={_id}>
<Td>{apiKey}</Td>
<Td>{status}</Td>
<Td>
<Tooltip label='Double Click to delete'>
<DeleteIcon
onDoubleClick={(e) => {
onDelete(_id)
}}
/>
</Tooltip>
</Td>
</Tr>
))
)}
</>
)}
</Tbody> </Tbody>
</Table> </Table>
</TableContainer> </TableContainer>

95
web/components/dashboard/DeviceList.tsx

@ -0,0 +1,95 @@
import { DeleteIcon, EmailIcon } from '@chakra-ui/icons'
import {
IconButton,
Spinner,
Table,
TableContainer,
Tbody,
Td,
Th,
Thead,
Tooltip,
Tr,
} from '@chakra-ui/react'
import { useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { sendSMSRequest } from '../../services'
import { selectAuth } from '../../store/authReducer'
import {
fetchDeviceList,
selectDeviceList,
} from '../../store/deviceListReducer'
const DeviceList = () => {
const dispatch = useDispatch()
const { user, accessToken } = useSelector(selectAuth)
useEffect(() => {
if (user && accessToken) {
dispatch(fetchDeviceList())
}
}, [user, accessToken, dispatch])
const { data, loading } = useSelector(selectDeviceList)
const onDelete = (apiKeyId: string) => {}
return (
<TableContainer>
<Table variant='simple'>
<Thead>
<Tr>
<Th>Your Devices</Th>
<Th>Status</Th>
<Th colSpan={2}>Actions</Th>
</Tr>
</Thead>
<Tbody>
{loading ? (
<Tr>
<Td colSpan={3} textAlign='center'>
<Spinner size='lg' />
</Td>
</Tr>
) : (
<>
{data.length === 0 ? (
<Tr>
<Td colSpan={3} textAlign='center'>
No Devices
</Td>
</Tr>
) : (
data.map(({ _id, brand, model, enabled, createdAt }) => (
<Tr key={_id}>
<Td>{`${brand}/ ${model}`}</Td>
<Td>{enabled ? 'enabled' : 'disabled'}</Td>
<Td>
<EmailIcon onDoubleClick={(e) => {}} />
</Td>
<Td>
<Tooltip label='Double Click to delete'>
<IconButton
aria-label='Delete'
icon={<DeleteIcon />}
onDoubleClick={(e) => {
sendSMSRequest(_id, {
receivers: ['+251912657519'],
smsBody: 'Hello World',
})
}}
/>
</Tooltip>
</Td>
</Tr>
))
)}
</>
)}
</Tbody>
</Table>
</TableContainer>
)
}
export default DeviceList

5
web/components/dashboard/GenerateApiKey.tsx

@ -43,7 +43,10 @@ const NewApiKeyGeneratedModal = ({
Open the SMS Gateway App and scan this QR to get started Open the SMS Gateway App and scan this QR to get started
</chakra.h1> </chakra.h1>
<Flex justifyContent='center'>
<Flex
justifyContent='center'
style={{ backgroundColor: '#fff', padding: '5px' }}
>
<QRCode value={generatedApiKey} />{' '} <QRCode value={generatedApiKey} />{' '}
</Flex> </Flex>
</> </>

2
web/components/dashboard/UserStats.tsx

@ -1,7 +1,7 @@
import { Box, SimpleGrid, chakra } from '@chakra-ui/react' import { Box, SimpleGrid, chakra } from '@chakra-ui/react'
import React from 'react' import React from 'react'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
import { selectAuth } from '../../store/authSlice'
import { selectAuth } from '../../store/authReducer'
import UserStatsCard from './UserStatsCard' import UserStatsCard from './UserStatsCard'
const UserStats = () => { const UserStats = () => {

29
web/pages/dashboard/index.tsx

@ -1,19 +1,40 @@
import { Box, SimpleGrid } from '@chakra-ui/react'
import { Box, SimpleGrid, useToast } from '@chakra-ui/react'
import ApiKeyList from '../../components/dashboard/ApiKeyList' import ApiKeyList from '../../components/dashboard/ApiKeyList'
import UserStats from '../../components/dashboard/UserStats' import UserStats from '../../components/dashboard/UserStats'
import GenerateApiKey from '../../components/dashboard/GenerateApiKey' import GenerateApiKey from '../../components/dashboard/GenerateApiKey'
import DeviceList from '../../components/dashboard/DeviceList'
import { useSelector } from 'react-redux'
import { selectAuth } from '../../store/authReducer'
import Router from 'next/router'
import { useEffect } from 'react'
export default function Dashboard() { export default function Dashboard() {
const { user: currentUser } = useSelector(selectAuth)
const toast = useToast()
useEffect(() => {
if (!currentUser) {
toast({
title: 'You are not logged in',
description: 'Please login to access this page',
status: 'warning',
})
Router.push('/login')
}
}, [currentUser, toast])
return ( return (
<> <>
<UserStats /> <UserStats />
<Box maxW='7xl' mx={'auto'} pt={5} px={{ base: 2, sm: 12, md: 17 }}> <Box maxW='7xl' mx={'auto'} pt={5} px={{ base: 2, sm: 12, md: 17 }}>
<GenerateApiKey />
<br />
<SimpleGrid columns={{ base: 1, md: 2 }} spacing={{ base: 5, lg: 8 }}> <SimpleGrid columns={{ base: 1, md: 2 }} spacing={{ base: 5, lg: 8 }}>
<div>
<GenerateApiKey />
<Box backdropBlur='2xl' borderWidth='1px' borderRadius='lg'>
<ApiKeyList /> <ApiKeyList />
</div>
</Box>
<Box backdropBlur='2xl' borderWidth='1px' borderRadius='lg'>
<DeviceList />
</Box>
</SimpleGrid> </SimpleGrid>
</Box> </Box>
</> </>

2
web/pages/index.tsx

@ -4,7 +4,7 @@ import { useEffect } from 'react'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
import FeaturesSection from '../components/home/FeaturesSection' import FeaturesSection from '../components/home/FeaturesSection'
import IntroSection from '../components/home/IntroSection' import IntroSection from '../components/home/IntroSection'
import { selectAuth } from '../store/authSlice'
import { selectAuth } from '../store/authReducer'
export default function HomePage() { export default function HomePage() {
const { accessToken, user } = useSelector(selectAuth) const { accessToken, user } = useSelector(selectAuth)

2
web/pages/login.tsx

@ -17,7 +17,7 @@ import {
import Link from 'next/link' import Link from 'next/link'
import { useState } from 'react' import { useState } from 'react'
import { ViewIcon, ViewOffIcon } from '@chakra-ui/icons' import { ViewIcon, ViewOffIcon } from '@chakra-ui/icons'
import { login, selectAuth } from '../store/authSlice'
import { login, selectAuth } from '../store/authReducer'
import { useDispatch, useSelector } from 'react-redux' import { useDispatch, useSelector } from 'react-redux'
import { LoginRequestPayload } from '../services/types' import { LoginRequestPayload } from '../services/types'

2
web/pages/register.tsx

@ -16,7 +16,7 @@ import {
import Link from 'next/link' import Link from 'next/link'
import { useState } from 'react' import { useState } from 'react'
import { ViewIcon, ViewOffIcon } from '@chakra-ui/icons' import { ViewIcon, ViewOffIcon } from '@chakra-ui/icons'
import { register, selectAuth } from '../store/authSlice'
import { register, selectAuth } from '../store/authReducer'
import { useDispatch, useSelector } from 'react-redux' import { useDispatch, useSelector } from 'react-redux'
import { RegisterRequestPayload } from '../services/types' import { RegisterRequestPayload } from '../services/types'

20
web/services/index.ts

@ -1,16 +1,18 @@
import axios from 'axios' import axios from 'axios'
import { LOCAL_STORAGE_KEY } from '../shared/constants'
import { import {
LoginRequestPayload, LoginRequestPayload,
LoginResponse, LoginResponse,
RegisterRequestPayload, RegisterRequestPayload,
RegisterResponse, RegisterResponse,
SendSMSRequestPayload,
} from './types' } from './types'
const BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL const BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL
if (typeof localStorage !== 'undefined') if (typeof localStorage !== 'undefined')
axios.defaults.headers.common[ axios.defaults.headers.common[
'Authorization' 'Authorization'
] = `Bearer ${localStorage.accessToken}`
] = `Bearer ${localStorage.getItem(LOCAL_STORAGE_KEY.TOKEN)}`
export const loginRequest = async ( export const loginRequest = async (
payload: LoginRequestPayload payload: LoginRequestPayload
@ -40,3 +42,19 @@ export const deleteApiKeyRequest = async (id: string) => {
const res = await axios.delete(`${BASE_URL}/auth/api-keys/${id}`) const res = await axios.delete(`${BASE_URL}/auth/api-keys/${id}`)
return res.data.data return res.data.data
} }
export const getDeviceListRequest = async () => {
const res = await axios.get(`${BASE_URL}/gateway/devices`)
return res.data.data
}
export const sendSMSRequest = async (
deviceId: string,
payload: SendSMSRequestPayload
) => {
const res = await axios.post(
`${BASE_URL}/gateway/devices/${deviceId}/sendSMS`,
payload
)
return res.data.data
}

21
web/services/types.ts

@ -35,3 +35,24 @@ export interface LoginResponse extends BaseResponse {
} }
export type RegisterResponse = LoginResponse export type RegisterResponse = LoginResponse
export interface SendSMSRequestPayload {
receivers: string[]
smsBody: string
}
export interface ApiKeyEntity {
_id: string
apiKey: string
user: UserEntity
}
export interface DeviceEntity {
_id: string
user: UserEntity
enabled: boolean
fcmToken: string
brand: string
manufacturer: string
model: string
}

61
web/store/apiKeyListReducer.ts

@ -0,0 +1,61 @@
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
import type { PayloadAction } from '@reduxjs/toolkit'
import { getApiKeyListRequest } from '../services'
import { createStandaloneToast } from '@chakra-ui/react'
import { RootState } from './store'
const toast = createStandaloneToast()
const initialState = {
loading: false,
data: [],
}
export const fetchApiKeyList = createAsyncThunk(
'apiKeyList/fetchApiKeys',
async (payload, thunkAPI) => {
try {
const res = await getApiKeyListRequest()
return res
} catch (e) {
toast({
title: e.response.data.error || 'Failed to Fetch apiKeys',
status: 'error',
})
return thunkAPI.rejectWithValue(e.response.data)
}
}
)
export const apiKeyListSlice = createSlice({
name: 'apiKeyList',
initialState,
reducers: {
clearApiKeyList: (state) => {
state.loading = false
state.data = []
},
},
extraReducers: (builder) => {
builder
.addCase(fetchApiKeyList.pending, (state) => {
state.loading = true
})
.addCase(
fetchApiKeyList.fulfilled,
(state, action: PayloadAction<any>) => {
state.loading = false
state.data = action.payload
}
)
.addCase(fetchApiKeyList.rejected, (state) => {
state.loading = false
})
},
})
export const { clearApiKeyList } = apiKeyListSlice.actions
export const selectApiKeyList = (state: RootState) => state.apiKeyList
export default apiKeyListSlice.reducer

4
web/store/authSlice.ts → web/store/authReducer.ts

@ -33,7 +33,7 @@ export const login = createAsyncThunk(
const res = await loginRequest(payload) const res = await loginRequest(payload)
const { accessToken, user } = res const { accessToken, user } = res
saveUserAndToken(user, accessToken) saveUserAndToken(user, accessToken)
Router.push('/dashboard')
Router.push('/')
return res return res
} catch (e) { } catch (e) {
toast({ toast({
@ -52,7 +52,7 @@ export const register = createAsyncThunk(
const res = await registerRequest(payload) const res = await registerRequest(payload)
const { accessToken, user } = res const { accessToken, user } = res
saveUserAndToken(user, accessToken) saveUserAndToken(user, accessToken)
Router.push('/dashboard')
Router.push('/')
return res return res
} catch (e) { } catch (e) {
toast({ toast({

61
web/store/deviceListReducer.ts

@ -0,0 +1,61 @@
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
import type { PayloadAction } from '@reduxjs/toolkit'
import { getDeviceListRequest } from '../services'
import { createStandaloneToast } from '@chakra-ui/react'
import { RootState } from './store'
const toast = createStandaloneToast()
const initialState = {
loading: false,
data: [],
}
export const fetchDeviceList = createAsyncThunk(
'deviceList/fetchDevices',
async (payload, thunkAPI) => {
try {
const res = await getDeviceListRequest()
return res
} catch (e) {
toast({
title: e.response.data.error || 'Failed to Fetch devices',
status: 'error',
})
return thunkAPI.rejectWithValue(e.response.data)
}
}
)
export const deviceListSlice = createSlice({
name: 'deviceList',
initialState,
reducers: {
clearDeviceList: (state) => {
state.loading = false
state.data = []
},
},
extraReducers: (builder) => {
builder
.addCase(fetchDeviceList.pending, (state) => {
state.loading = true
})
.addCase(
fetchDeviceList.fulfilled,
(state, action: PayloadAction<any>) => {
state.loading = false
state.data = action.payload
}
)
.addCase(fetchDeviceList.rejected, (state) => {
state.loading = false
})
},
})
export const { clearDeviceList } = deviceListSlice.actions
export const selectDeviceList = (state: RootState) => state.deviceList
export default deviceListSlice.reducer

8
web/store/store.ts

@ -1,9 +1,13 @@
import { configureStore } from '@reduxjs/toolkit' import { configureStore } from '@reduxjs/toolkit'
import authSlice from './authSlice'
import apiKeyListReducer from './apiKeyListReducer'
import authReducer from './authReducer'
import deviceListReducer from './deviceListReducer'
export const store = configureStore({ export const store = configureStore({
reducer: { reducer: {
auth: authSlice,
auth: authReducer,
apiKeyList: apiKeyListReducer,
deviceList: deviceListReducer,
}, },
enhancers: [], enhancers: [],
}) })

Loading…
Cancel
Save