diff --git a/web/components/Navbar.tsx b/web/components/Navbar.tsx index 9e45abd..170f27e 100644 --- a/web/components/Navbar.tsx +++ b/web/components/Navbar.tsx @@ -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() diff --git a/web/components/dashboard/ApiKeyList.tsx b/web/components/dashboard/ApiKeyList.tsx index e8f0e63..db46c54 100644 --- a/web/components/dashboard/ApiKeyList.tsx +++ b/web/components/dashboard/ApiKeyList.tsx @@ -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 = () => { - {apiKeyList.map((apiKey) => ( - - {apiKey.apiKey} - {apiKey.status} - - - { - onDelete(apiKey._id) - }} - /> - + {loading ? ( + + + - ))} + ) : ( + <> + {data.length == 0 ? ( + + No API Keys + + ) : ( + data.map(({ _id, apiKey, status }) => ( + + {apiKey} + {status} + + + { + onDelete(_id) + }} + /> + + + + )) + )} + + )} diff --git a/web/components/dashboard/DeviceList.tsx b/web/components/dashboard/DeviceList.tsx new file mode 100644 index 0000000..fb85546 --- /dev/null +++ b/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 ( + + + + + + + + + + + {loading ? ( + + + + ) : ( + <> + {data.length === 0 ? ( + + + + ) : ( + data.map(({ _id, brand, model, enabled, createdAt }) => ( + + + + + + + )) + )} + + )} + +
Your DevicesStatusActions
+ +
+ No Devices +
{`${brand}/ ${model}`}{enabled ? 'enabled' : 'disabled'} + {}} /> + + + } + onDoubleClick={(e) => { + sendSMSRequest(_id, { + receivers: ['+251912657519'], + smsBody: 'Hello World', + }) + }} + /> + +
+
+ ) +} + +export default DeviceList diff --git a/web/components/dashboard/GenerateApiKey.tsx b/web/components/dashboard/GenerateApiKey.tsx index 16c6d52..aeb48fe 100644 --- a/web/components/dashboard/GenerateApiKey.tsx +++ b/web/components/dashboard/GenerateApiKey.tsx @@ -43,7 +43,10 @@ const NewApiKeyGeneratedModal = ({ Open the SMS Gateway App and scan this QR to get started - + {' '} diff --git a/web/components/dashboard/UserStats.tsx b/web/components/dashboard/UserStats.tsx index 466a806..c731787 100644 --- a/web/components/dashboard/UserStats.tsx +++ b/web/components/dashboard/UserStats.tsx @@ -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 = () => { diff --git a/web/pages/dashboard/index.tsx b/web/pages/dashboard/index.tsx index 955e114..3176d4a 100644 --- a/web/pages/dashboard/index.tsx +++ b/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 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 ( <> + +
-
- + -
+
+ + + diff --git a/web/pages/index.tsx b/web/pages/index.tsx index 1cf187d..327083d 100644 --- a/web/pages/index.tsx +++ b/web/pages/index.tsx @@ -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) diff --git a/web/pages/login.tsx b/web/pages/login.tsx index c5d3cab..5763139 100644 --- a/web/pages/login.tsx +++ b/web/pages/login.tsx @@ -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' diff --git a/web/pages/register.tsx b/web/pages/register.tsx index b097670..7e95b22 100644 --- a/web/pages/register.tsx +++ b/web/pages/register.tsx @@ -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' diff --git a/web/services/index.ts b/web/services/index.ts index 694f7a8..2d38a57 100644 --- a/web/services/index.ts +++ b/web/services/index.ts @@ -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 +} diff --git a/web/services/types.ts b/web/services/types.ts index ede8ba3..babc67f 100644 --- a/web/services/types.ts +++ b/web/services/types.ts @@ -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 +} diff --git a/web/store/apiKeyListReducer.ts b/web/store/apiKeyListReducer.ts new file mode 100644 index 0000000..5f286d9 --- /dev/null +++ b/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) => { + 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 diff --git a/web/store/authSlice.ts b/web/store/authReducer.ts similarity index 97% rename from web/store/authSlice.ts rename to web/store/authReducer.ts index 9e66d57..d43aa08 100644 --- a/web/store/authSlice.ts +++ b/web/store/authReducer.ts @@ -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({ diff --git a/web/store/deviceListReducer.ts b/web/store/deviceListReducer.ts new file mode 100644 index 0000000..6dc7f19 --- /dev/null +++ b/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) => { + 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 diff --git a/web/store/store.ts b/web/store/store.ts index aa27354..7d18a32 100644 --- a/web/store/store.ts +++ b/web/store/store.ts @@ -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: [], })