mirror of
https://github.com/vernu/textbee.git
synced 2026-05-24 08:19:13 -04:00
refactor(web): refactor state mgmnt
This commit is contained in:
@@ -18,7 +18,7 @@ import Link from 'next/link'
|
||||
import { MoonIcon, SunIcon } from '@chakra-ui/icons'
|
||||
import Router from 'next/router'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { logout, selectAuth } from '../store/authSlice'
|
||||
import { logout, selectAuth } from '../store/authReducer'
|
||||
|
||||
export default function Navbar() {
|
||||
const dispatch = useDispatch()
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { DeleteIcon } from '@chakra-ui/icons'
|
||||
import {
|
||||
Spinner,
|
||||
Table,
|
||||
TableContainer,
|
||||
Tbody,
|
||||
@@ -11,29 +12,33 @@ import {
|
||||
useToast,
|
||||
} from '@chakra-ui/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, setApiKeyList] = useState([])
|
||||
const toast = useToast()
|
||||
const dispatch = useDispatch()
|
||||
const { data, loading } = useSelector(selectApiKeyList)
|
||||
|
||||
const { user, accessToken } = useSelector(selectAuth)
|
||||
useEffect(() => {
|
||||
if (user && accessToken) {
|
||||
getApiKeyListRequest().then((apiKeys) => {
|
||||
setApiKeyList(apiKeys)
|
||||
})
|
||||
dispatch(fetchApiKeyList())
|
||||
}
|
||||
}, [user, accessToken])
|
||||
}, [dispatch, user, accessToken])
|
||||
|
||||
const onDelete = (apiKeyId: string) => {
|
||||
deleteApiKeyRequest(apiKeyId)
|
||||
setApiKeyList(apiKeyList.filter((apiKey) => apiKey._id !== apiKeyId))
|
||||
dispatch(fetchApiKeyList())
|
||||
toast({
|
||||
title: 'Success',
|
||||
description: 'API Key deleted',
|
||||
isClosable: true,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -48,21 +53,37 @@ const ApiKeyList = () => {
|
||||
</Tr>
|
||||
</Thead>
|
||||
<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>
|
||||
</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>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
95
web/components/dashboard/DeviceList.tsx
Normal file
95
web/components/dashboard/DeviceList.tsx
Normal file
@@ -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
|
||||
@@ -43,7 +43,10 @@ const NewApiKeyGeneratedModal = ({
|
||||
Open the SMS Gateway App and scan this QR to get started
|
||||
</chakra.h1>
|
||||
|
||||
<Flex justifyContent='center'>
|
||||
<Flex
|
||||
justifyContent='center'
|
||||
style={{ backgroundColor: '#fff', padding: '5px' }}
|
||||
>
|
||||
<QRCode value={generatedApiKey} />{' '}
|
||||
</Flex>
|
||||
</>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Box, SimpleGrid, chakra } from '@chakra-ui/react'
|
||||
import React from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { selectAuth } from '../../store/authSlice'
|
||||
import { selectAuth } from '../../store/authReducer'
|
||||
import UserStatsCard from './UserStatsCard'
|
||||
|
||||
const UserStats = () => {
|
||||
|
||||
@@ -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 UserStats from '../../components/dashboard/UserStats'
|
||||
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() {
|
||||
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 (
|
||||
<>
|
||||
<UserStats />
|
||||
<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 }}>
|
||||
<div>
|
||||
<GenerateApiKey />
|
||||
<Box backdropBlur='2xl' borderWidth='1px' borderRadius='lg'>
|
||||
<ApiKeyList />
|
||||
</div>
|
||||
</Box>
|
||||
<Box backdropBlur='2xl' borderWidth='1px' borderRadius='lg'>
|
||||
<DeviceList />
|
||||
</Box>
|
||||
</SimpleGrid>
|
||||
</Box>
|
||||
</>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useEffect } from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
import FeaturesSection from '../components/home/FeaturesSection'
|
||||
import IntroSection from '../components/home/IntroSection'
|
||||
import { selectAuth } from '../store/authSlice'
|
||||
import { selectAuth } from '../store/authReducer'
|
||||
|
||||
export default function HomePage() {
|
||||
const { accessToken, user } = useSelector(selectAuth)
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
import Link from 'next/link'
|
||||
import { useState } from 'react'
|
||||
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 { LoginRequestPayload } from '../services/types'
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
import Link from 'next/link'
|
||||
import { useState } from 'react'
|
||||
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 { RegisterRequestPayload } from '../services/types'
|
||||
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import axios from 'axios'
|
||||
import { LOCAL_STORAGE_KEY } from '../shared/constants'
|
||||
import {
|
||||
LoginRequestPayload,
|
||||
LoginResponse,
|
||||
RegisterRequestPayload,
|
||||
RegisterResponse,
|
||||
SendSMSRequestPayload,
|
||||
} from './types'
|
||||
const BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL
|
||||
|
||||
if (typeof localStorage !== 'undefined')
|
||||
axios.defaults.headers.common[
|
||||
'Authorization'
|
||||
] = `Bearer ${localStorage.accessToken}`
|
||||
] = `Bearer ${localStorage.getItem(LOCAL_STORAGE_KEY.TOKEN)}`
|
||||
|
||||
export const loginRequest = async (
|
||||
payload: LoginRequestPayload
|
||||
@@ -40,3 +42,19 @@ export const deleteApiKeyRequest = async (id: string) => {
|
||||
const res = await axios.delete(`${BASE_URL}/auth/api-keys/${id}`)
|
||||
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
|
||||
}
|
||||
|
||||
@@ -35,3 +35,24 @@ export interface LoginResponse extends BaseResponse {
|
||||
}
|
||||
|
||||
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
Normal file
61
web/store/apiKeyListReducer.ts
Normal file
@@ -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
|
||||
@@ -33,7 +33,7 @@ export const login = createAsyncThunk(
|
||||
const res = await loginRequest(payload)
|
||||
const { accessToken, user } = res
|
||||
saveUserAndToken(user, accessToken)
|
||||
Router.push('/dashboard')
|
||||
Router.push('/')
|
||||
return res
|
||||
} catch (e) {
|
||||
toast({
|
||||
@@ -52,7 +52,7 @@ export const register = createAsyncThunk(
|
||||
const res = await registerRequest(payload)
|
||||
const { accessToken, user } = res
|
||||
saveUserAndToken(user, accessToken)
|
||||
Router.push('/dashboard')
|
||||
Router.push('/')
|
||||
return res
|
||||
} catch (e) {
|
||||
toast({
|
||||
61
web/store/deviceListReducer.ts
Normal file
61
web/store/deviceListReducer.ts
Normal file
@@ -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
|
||||
@@ -1,9 +1,13 @@
|
||||
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({
|
||||
reducer: {
|
||||
auth: authSlice,
|
||||
auth: authReducer,
|
||||
apiKeyList: apiKeyListReducer,
|
||||
deviceList: deviceListReducer,
|
||||
},
|
||||
enhancers: [],
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user