Browse Source

refactor(web): refactor and fix minor bugs

pull/4/head
isra el 2 years ago
parent
commit
24f860ad18
  1. 2
      README.md
  2. 30
      web/components/AnimatedScrollWrapper.tsx
  3. 42
      web/components/Navbar.tsx
  4. 43
      web/components/analytics/Analytics.tsx
  5. 86
      web/components/dashboard/ApiKeyList.tsx
  6. 74
      web/components/dashboard/DeviceList.tsx
  7. 17
      web/components/dashboard/GenerateApiKey.tsx
  8. 129
      web/components/dashboard/SendSMS.tsx
  9. 27
      web/components/dashboard/UserStats.tsx
  10. 2
      web/components/home/CodeSnippetSection.tsx
  11. 88
      web/components/home/DownloadAppSection.tsx
  12. 58
      web/components/home/FeaturesSection.tsx
  13. 57
      web/components/home/HowItWorksSection.tsx
  14. 124
      web/components/home/IntroSection.tsx
  15. 45
      web/components/landing/CodeSnippetSection.tsx
  16. 91
      web/components/landing/DownloadAppSection.tsx
  17. 64
      web/components/landing/FeaturesSection.tsx
  18. 60
      web/components/landing/HowItWorksSection.tsx
  19. 153
      web/components/landing/IntroSection.tsx
  20. 0
      web/components/landing/featuresContent.ts
  21. 0
      web/components/landing/howItWorksContent.ts
  22. 6
      web/lib/httpClient.ts
  23. 0
      web/pages/404.tsx
  24. 4
      web/pages/_app.tsx
  25. 23
      web/pages/dashboard.tsx
  26. 16
      web/pages/index.tsx
  27. 11
      web/pages/login.tsx
  28. 16
      web/pages/register.tsx
  29. 34
      web/services/authService.ts
  30. 34
      web/services/gatewayService.ts
  31. 66
      web/services/index.ts
  32. 6
      web/services/types.ts
  33. 61
      web/store/apiKeyListReducer.ts
  34. 85
      web/store/apiKeySlice.ts
  35. 22
      web/store/authSlice.ts
  36. 61
      web/store/deviceListReducer.ts
  37. 92
      web/store/deviceSlice.ts
  38. 10
      web/store/store.ts

2
README.md

@ -24,7 +24,7 @@ from their application via a REST API. It utilizes android phones as SMS gateway
const API_KEY = 'YOUR_API_KEY';
const DEVICE_ID = 'YOUR_DEVICE_ID';
await axios.post(`https://api.textbee.vernu.dev/api/v1/devices/${DEVICE_ID}/sendSMS?apiKey=${API_KEY}`, {
await axios.post(`https://api.textbee.vernu.dev/api/v1/gateway/devices/${DEVICE_ID}/sendSMS?apiKey=${API_KEY}`, {
receivers: [ '+251912345678' ],
smsBody: 'Hello World!',
})

30
web/components/AnimatedScrollWrapper.tsx

@ -0,0 +1,30 @@
import React, { ReactNode } from 'react'
import { motion } from 'framer-motion'
interface AnimatedScrollWrapperProps {
children: ReactNode
}
const AnimatedScrollWrapper = ({ children }: AnimatedScrollWrapperProps) => {
return (
<motion.div
variants={{
hidden: { opacity: 0, y: 10 },
visible: {
opacity: 1,
y: 0,
transition: {
duration: 0.5,
ease: 'easeInOut',
// delay: 0.25,
},
},
}}
initial='hidden'
whileInView='visible'
>
{children}
</motion.div>
)
}
export default AnimatedScrollWrapper

42
web/components/Navbar.tsx

@ -11,19 +11,19 @@ import {
useColorModeValue,
Stack,
useColorMode,
Center,
Image,
SimpleGrid,
} from '@chakra-ui/react'
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/authReducer'
import { logout, selectAuthUser } from '../store/authSlice'
export default function Navbar() {
const dispatch = useDispatch()
const { colorMode, toggleColorMode } = useColorMode()
const { user } = useSelector(selectAuth)
const authUser = useSelector(selectAuthUser)
return (
<>
@ -62,8 +62,7 @@ export default function Navbar() {
</Link>
</Menu>
{!user ? (
<>
{!authUser && (
<Menu>
<Link href='/login' passHref>
<MenuButton>Login</MenuButton>
@ -72,8 +71,9 @@ export default function Navbar() {
<MenuButton>Register</MenuButton>
</Link>
</Menu>
</>
) : (
)}
{authUser && (
<Menu>
<MenuButton
as={Button}
@ -84,28 +84,22 @@ export default function Navbar() {
>
<Avatar
size={'sm'}
src={
user?.avatar ??
'https://avatars.dicebear.com/api/male/username.svg'
}
name={authUser.name}
src={authUser?.avatar}
/>
</MenuButton>
<MenuList alignItems={'center'}>
<br />
<Center>
<MenuItem>
<SimpleGrid columns={2} spacing={3}>
<Avatar
size={'xl'}
src={
user?.avatar ??
'https://avatars.dicebear.com/api/male/username.svg'
}
size={'sm'}
name={authUser.name}
src={authUser?.avatar}
/>
</Center>
<br />
<Center>
<p>{user?.name}</p>
</Center>
<br />
{authUser?.name}
</SimpleGrid>
</MenuItem>
<MenuDivider />
<MenuItem
onClick={() => {

43
web/components/analytics/Analytics.tsx

@ -0,0 +1,43 @@
import Script from 'next/script'
const Analytics = () => {
return (
<>
{/* Global Site Tag (gtag.js) - Google Analytics */}
<Script
id='gtag1'
strategy='afterInteractive'
src={`https:www.googletagmanager.com/gtag/js?id=G-MLD1JPRQZ`}
/>
<Script
id='gtag2'
strategy='afterInteractive'
dangerouslySetInnerHTML={{
__html: `
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-MLD1JPRQZ', {
page_path: window.location.pathname,
});
`,
}}
/>
<Script
id='ms-clarity1'
strategy='afterInteractive'
dangerouslySetInnerHTML={{
__html: `
(function(c,l,a,r,i,t,y){
c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};
t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i;
y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);
})(window, document, "clarity", "script", "iacr7j4ozh");
`,
}}
/>
</>
)
}
export default Analytics

86
web/components/dashboard/ApiKeyList.tsx

@ -9,38 +9,48 @@ import {
Thead,
Tooltip,
Tr,
useToast,
} from '@chakra-ui/react'
import { useEffect, useState } from 'react'
import { useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { deleteApiKeyRequest } from '../../services'
import {
fetchApiKeyList,
deleteApiKey,
fetchApiKeys,
selectApiKeyList,
} from '../../store/apiKeyListReducer'
import { selectAuth } from '../../store/authReducer'
selectApiKeyLoading,
} from '../../store/apiKeySlice'
import { selectAuthUser } from '../../store/authSlice'
const ApiKeyList = () => {
const toast = useToast()
const ApiKeyRow = ({ apiKey }: any) => {
const dispatch = useDispatch()
const { data, loading } = useSelector(selectApiKeyList)
const { user, accessToken } = useSelector(selectAuth)
useEffect(() => {
if (user && accessToken) {
dispatch(fetchApiKeyList())
const handleDelete = async () => {
dispatch(deleteApiKey(apiKey._id))
}
}, [dispatch, user, accessToken])
const onDelete = (apiKeyId: string) => {
deleteApiKeyRequest(apiKeyId)
dispatch(fetchApiKeyList())
toast({
title: 'Success',
description: 'API Key deleted',
isClosable: true,
})
return (
<Tr>
<Td>{apiKey.apiKey}</Td>
<Td>{apiKey.status}</Td>
<Td>
<Tooltip label='Double Click to delete'>
<DeleteIcon onDoubleClick={handleDelete} />
</Tooltip>
</Td>
</Tr>
)
}
const ApiKeyList = () => {
const dispatch = useDispatch()
const loading = useSelector(selectApiKeyLoading)
const apiKeyList = useSelector(selectApiKeyList)
const authUser = useSelector(selectAuthUser)
useEffect(() => {
if (authUser) {
dispatch(fetchApiKeys())
}
}, [dispatch, authUser])
return (
<TableContainer>
@ -53,37 +63,25 @@ const ApiKeyList = () => {
</Tr>
</Thead>
<Tbody>
{loading ? (
{loading && (
<Tr>
<Td colSpan={3} textAlign='center'>
<Spinner size='lg' />
</Td>
</Tr>
) : (
<>
{data.length == 0 ? (
)}
{!loading && apiKeyList.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>
))
)}
</>
)}
{!loading &&
apiKeyList.length > 0 &&
apiKeyList.map((apiKey) => (
<ApiKeyRow key={apiKey._id} apiKey={apiKey} />
))}
</Tbody>
</Table>
</TableContainer>

74
web/components/dashboard/DeviceList.tsx

@ -1,4 +1,4 @@
import { DeleteIcon, EmailIcon } from '@chakra-ui/icons'
import { DeleteIcon } from '@chakra-ui/icons'
import {
IconButton,
Spinner,
@ -13,24 +13,41 @@ import {
} 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'
import { selectAuthUser } from '../../store/authSlice'
import { fetchDevices, selectDeviceList, selectDeviceLoading } from '../../store/deviceSlice'
const DeviceRow = ({ device }: any) => {
const { enabled, model, brand, _id, createdAt } = device
return (
<Tr>
<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) => {}}
/>
</Tooltip>
</Td>
</Tr>
)
}
const DeviceList = () => {
const dispatch = useDispatch()
const { user, accessToken } = useSelector(selectAuth)
const authUser = useSelector(selectAuthUser)
useEffect(() => {
if (user && accessToken) {
dispatch(fetchDeviceList())
if (authUser) {
dispatch(fetchDevices())
}
}, [user, accessToken, dispatch])
}, [authUser, dispatch])
const { data, loading } = useSelector(selectDeviceList)
const deviceList = useSelector(selectDeviceList)
const loading = useSelector(selectDeviceLoading)
const onDelete = (apiKeyId: string) => {}
@ -45,40 +62,27 @@ const DeviceList = () => {
</Tr>
</Thead>
<Tbody>
{loading ? (
{loading && (
<Tr>
<Td colSpan={3} textAlign='center'>
<Spinner size='lg' />
</Td>
</Tr>
) : (
<>
{data.length === 0 ? (
)}
{!loading && deviceList.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) => {}}
/>
</Tooltip>
</Td>
</Tr>
))
)}
</>
)}
{!loading &&
deviceList.length > 0 &&
deviceList.map((device) => (
<DeviceRow key={device._id} device={device} />
))}
</Tbody>
</Table>
</TableContainer>

17
web/components/dashboard/GenerateApiKey.tsx

@ -14,9 +14,10 @@ import {
useToast,
} from '@chakra-ui/react'
import { useState } from 'react'
import { useDispatch } from 'react-redux'
import QRCode from 'react-qr-code'
import { generateApiKeyRequest } from '../../services'
import { fetchApiKeys } from '../../store/apiKeySlice'
import { gatewayService } from '../../services/gatewayService'
const NewApiKeyGeneratedModal = ({
isOpen = false,
@ -104,16 +105,24 @@ export default function GenerateApiKey() {
const [showGeneratedApiKeyModal, setShowGeneratedApiKeyModal] =
useState(false)
const dispatch = useDispatch()
const generateApiKey = async () => {
setGeneratingApiKey(true)
const newApiKey = await generateApiKeyRequest()
const newApiKey = await gatewayService.generateApiKey()
setGeneratedApiKey(newApiKey)
setShowGeneratedApiKeyModal(true)
setGeneratingApiKey(false)
dispatch(fetchApiKeys())
}
return (
<>
<Box padding={5} border='1px solid gray' marginBottom={10} borderRadius='2xl'>
<Box
padding={5}
border='1px solid gray'
marginBottom={10}
borderRadius='2xl'
>
<Flex direction='row' justifyContent='space-between'>
{' '}
<chakra.h1

129
web/components/dashboard/SendSMS.tsx

@ -12,19 +12,68 @@ import {
ModalHeader,
ModalOverlay,
Select,
Spinner,
Textarea,
useDisclosure,
useToast,
} from '@chakra-ui/react'
import { useState } from 'react'
import { useSelector } from 'react-redux'
import { sendSMSRequest } from '../../services'
import { selectDeviceList } from '../../store/deviceListReducer'
import { useSelector, useDispatch } from 'react-redux'
import {
selectDeviceList,
selectSendingSMS,
sendSMS,
} from '../../store/deviceSlice'
export const SendSMSForm = ({ deviceList, formData, handleChange }) => {
return (
<>
<Box>
<FormLabel htmlFor='device'>Select Device</FormLabel>
<Select
id='device'
name='device'
placeholder='Select Device'
onChange={handleChange}
value={formData.device}
>
{deviceList.map((device) => (
<option key={device._id} value={device._id}>
{device.model}
</option>
))}
</Select>
</Box>
<Box>
<FormLabel htmlFor='receivers'>Receiver</FormLabel>
<Input
placeholder='receiver'
name='receivers'
onChange={handleChange}
value={formData.receivers}
type='tel'
/>
</Box>
<Box>
<FormLabel htmlFor='smsBody'>SMS Body</FormLabel>
<Textarea
id='smsBody'
name='smsBody'
onChange={handleChange}
value={formData.smsBody}
/>
</Box>
</>
)
}
export default function SendSMS() {
const { isOpen, onOpen, onClose } = useDisclosure()
const deviceList = useSelector(selectDeviceList)
const toast = useToast()
const dispatch = useDispatch()
const sendingSMS = useSelector(selectSendingSMS)
const [formData, setFormData] = useState({
device: '',
@ -34,15 +83,30 @@ export default function SendSMS() {
const handSend = (e) => {
e.preventDefault()
sendSMSRequest(formData.device, {
receivers: formData.receivers.replace(' ', '').split(','),
smsBody: formData.smsBody,
})
const { device: deviceId, receivers, smsBody } = formData
const receiversArray = receivers.replace(' ', '').split(',')
if (!deviceId || !receivers || !smsBody) {
toast({
title: 'Sending SMS...',
title: 'Please fill all fields',
status: 'error',
})
onClose()
return
}
for (let receiver of receiversArray) {
// TODO: validate phone numbers
}
dispatch(
sendSMS({
deviceId,
payload: {
receivers: receiversArray,
smsBody,
},
})
)
}
const handleChange = (e) => {
@ -66,48 +130,23 @@ export default function SendSMS() {
<ModalHeader>Send SMS</ModalHeader>
<ModalCloseButton />
<ModalBody>
<Box>
<FormLabel htmlFor='device'>Select Device</FormLabel>
<Select
id='device'
name='device'
placeholder='Select Device'
onChange={handleChange}
value={formData.smsBody}
>
{deviceList.data.map((device) => (
<option key={device._id} value={device._id}>
{device.model}
</option>
))}
</Select>
</Box>
<Box>
<FormLabel htmlFor='receivers'>Receiver</FormLabel>
<Input
placeholder='receiver'
name='receivers'
onChange={handleChange}
value={formData.receivers}
type='tel'
/>
</Box>
<Box>
<FormLabel htmlFor='smsBody'>SMS Body</FormLabel>
<Textarea
id='smsBody'
name='smsBody'
onChange={handleChange}
value={formData.smsBody}
<SendSMSForm
deviceList={deviceList}
formData={formData}
handleChange={handleChange}
/>
</Box>
</ModalBody>
<ModalFooter>
<Button variant='ghost' mr={3} onClick={onClose}>
Close
</Button>
<Button variant='outline' colorScheme='blue' onClick={handSend}>
Send
<Button
variant='outline'
colorScheme='blue'
onClick={handSend}
disabled={sendingSMS}
>
{sendingSMS ? <Spinner size='md' /> : 'Send'}
</Button>
</ModalFooter>
</ModalContent>

27
web/components/dashboard/UserStats.tsx

@ -1,32 +1,37 @@
import { Box, SimpleGrid, chakra } from '@chakra-ui/react'
import React from 'react'
import { useSelector } from 'react-redux'
import { selectApiKeyList } from '../../store/apiKeyListReducer'
import { selectAuth } from '../../store/authReducer'
import { selectDeviceList } from '../../store/deviceListReducer'
import { selectApiKeyList } from '../../store/apiKeySlice'
import { selectAuthUser } from '../../store/authSlice'
import { selectDeviceList } from '../../store/deviceSlice'
import UserStatsCard from './UserStatsCard'
const UserStats = () => {
const { user: currentUser } = useSelector(selectAuth)
const { data: deviceListData } = useSelector(selectDeviceList)
const { data: apiKeyListData } = useSelector(selectApiKeyList)
const authUser = useSelector(selectAuthUser)
const deviceList = useSelector(selectDeviceList)
const apiKeyList = useSelector(selectApiKeyList)
return (
<>
<Box maxW='7xl' mx={'auto'} pt={5} px={{ base: 2, sm: 12, md: 17 }}>
<SimpleGrid columns={{ base: 1, md: 2 }} >
<SimpleGrid columns={{ base: 1, md: 2 }}>
<chakra.h1
textAlign={'center'}
fontSize={'4xl'}
py={10}
fontWeight={'bold'}
>
Welcome {currentUser?.name}
Welcome {authUser?.name}
</chakra.h1>
<SimpleGrid columns={{ base: 3 }} spacing={{ base: 5, lg: 8 }}>
<UserStatsCard title={'Registered '} stat={`${deviceListData?.length || '-:-'} Devices`} />
<UserStatsCard title={'Generated'} stat={`${apiKeyListData?.length || '-:-'} API Keys`} />
<UserStatsCard
title={'Registered '}
stat={`${deviceList?.length || '-:-'} Devices`}
/>
<UserStatsCard
title={'Generated'}
stat={`${apiKeyList?.length || '-:-'} API Keys`}
/>
<UserStatsCard title={'Sent'} stat={'-:- SMS'} />
</SimpleGrid>
</SimpleGrid>

2
web/components/home/CodeSnippetSection.tsx

@ -31,7 +31,7 @@ export default function CodeSnippetSection() {
align={'center'}
// h={'100%'}
src={
'https://ik.imagekit.io/vernu/textbee/Screenshot_2023-06-18_at_11.30.25_AM.png?updatedAt=1687077054749'
'https://ik.imagekit.io/vernu/textbee/Screenshot%202023-09-25%20at%2011.13.30%20AM.png?updatedAt=1695629672884'
}
borderRadius={'lg'}
/>

88
web/components/home/DownloadAppSection.tsx

@ -1,88 +0,0 @@
import {
Box,
Button,
chakra,
Flex,
Image,
useColorModeValue,
} from '@chakra-ui/react'
import React from 'react'
export default function DownloadAppSection() {
return (
<Box my={16}>
<Flex
padding={5}
background={useColorModeValue('gray.100', 'gray.700')}
borderRadius='2xl'
>
<Flex
borderRadius='2xl'
m={{ base: 5, md: 8 }}
p={{ base: 5, md: 8 }}
width='100%'
border='1px solid gray'
direction='row'
justifyContent='center'
>
<Box>
<Image
alt={'Hero Image'}
fit={'cover'}
align={'center'}
w={'180px'}
// h={'100%'}
src={'/images/smsgatewayandroid.png'}
/>
</Box>
<Box>
<Flex
height='100%'
direction='column'
justifyContent='center'
alignItems='center'
>
<chakra.h1
fontSize='md'
fontWeight='bold'
my={4}
color={useColorModeValue('gray.800', 'white')}
>
Download the App to get started!
</chakra.h1>
<chakra.p
fontSize='sm'
color={useColorModeValue('gray.600', 'gray.400')}
mb={4}
>
Unlock the power of messaging with our open-source Android SMS
Gateway.
</chakra.p>
<a href='/android' target='_blank'>
<Button
/* flex={1} */
px={4}
fontSize={'sm'}
rounded={'full'}
bg={'blue.400'}
color={'white'}
boxShadow={
'0px 1px 25px -5px rgb(66 153 225 / 48%), 0 10px 10px -5px rgb(66 153 225 / 43%)'
}
_hover={{
bg: 'blue.500',
}}
_focus={{
bg: 'blue.500',
}}
>
Download App
</Button>
</a>
</Flex>
</Box>
</Flex>
</Flex>
</Box>
)
}

58
web/components/home/FeaturesSection.tsx

@ -1,58 +0,0 @@
import { CheckIcon } from '@chakra-ui/icons'
import {
Box,
Container,
Heading,
HStack,
Icon,
SimpleGrid,
Stack,
Text,
useColorModeValue,
VStack,
} from '@chakra-ui/react'
import React from 'react'
import { featuresContent } from './featuresContent'
export default function FeaturesSection() {
const boxBgColor = useColorModeValue('gray.100', 'gray.800')
return (
<Box p={4} my={16} maxW={'6xl'}>
<Heading fontSize={'3xl'} textAlign={'center'} pb={0}>
Features
</Heading>
<Text color={'gray.600'} fontSize={'lg'} textAlign={'center'}>
The ultimate solution for your messaging needs! Our free open-source
Android-based SMS Gateway provides you with all the features you need to
effectively manage your SMS communications. From sending messages and
automating messaging workflows via API, our SMS Gateway is the perfect
tool for any small/mid business or individual.
</Text>
<Container maxW={'6xl'} mt={0}>
<SimpleGrid columns={{ base: 1, md: 2, lg: 4 }} spacing={3} pt={16}>
{featuresContent.map((feature, i) => (
<HStack
key={i}
align={'top'}
borderWidth='1px'
borderRadius='sm'
p={2}
shadow='lg'
background={boxBgColor}
>
<Box color={'green.400'} px={1}>
<Icon as={CheckIcon} />
</Box>
<VStack align={'start'}>
<Text fontWeight={800}>{feature.title}</Text>
<Text fontWeight='normal'>{feature.description}</Text>
</VStack>
</HStack>
))}
</SimpleGrid>
</Container>
</Box>
)
}

57
web/components/home/HowItWorksSection.tsx

@ -1,57 +0,0 @@
import { AddIcon, MinusIcon } from '@chakra-ui/icons'
import {
Accordion,
AccordionButton,
AccordionItem,
AccordionPanel,
Box,
Container,
Heading,
Text,
} from '@chakra-ui/react'
import React from 'react'
import { howItWorksContent } from './howItWorksContent'
export default function HowItWorksSection() {
return (
<Box px={4} my={24} maxW={'6xl'}>
{/* @ts-ignore */}
<a name='how-it-works'>
<Heading fontSize={'3xl'} textAlign={'center'}>
How It Works
</Heading>
</a>
<Text color={'gray.600'} fontSize={'lg'} textAlign={'center'}>
How it works is simple. You install the app on your Android device, and
it will turn your device into a SMS Gateway. You can then use the API to
send SMS messages from your own applications.
</Text>
<Container maxW={'6xl'} mt={10} pt={0}>
<Accordion allowMultiple defaultIndex={[]}>
{howItWorksContent.map(({ title, description }) => (
<AccordionItem key={title}>
{({ isExpanded }) => (
<>
<h2>
<AccordionButton>
<Box as='span' flex='1' textAlign='left'>
{title}
</Box>
{isExpanded ? (
<MinusIcon fontSize='12px' />
) : (
<AddIcon fontSize='12px' />
)}
</AccordionButton>
</h2>
<AccordionPanel pb={4}>{description}</AccordionPanel>
</>
)}
</AccordionItem>
))}
</Accordion>
</Container>
</Box>
)
}

124
web/components/home/IntroSection.tsx

@ -1,124 +0,0 @@
import {
Container,
Stack,
Flex,
Box,
Heading,
Text,
Button,
Image,
createIcon,
} from '@chakra-ui/react'
import Link from 'next/link'
import Router from 'next/router'
import { selectAuth } from '../../store/authReducer'
import { useSelector } from 'react-redux'
import { ChatIcon } from '@chakra-ui/icons'
export default function IntroSection() {
const { currentUser } = useSelector(selectAuth)
const handleGetStarted = () => {
if (!currentUser) {
Router.push('/register')
} else {
Router.push('/dashboard')
}
}
return (
<Container maxW={'7xl'} py={8}>
<Stack
align={'center'}
spacing={{ base: 8, md: 10 }}
py={{ base: 20, md: 28 }}
direction={{ base: 'column', md: 'row' }}
>
<Stack flex={1} spacing={{ base: 5, md: 10 }}>
<Heading
lineHeight={1.1}
fontWeight={600}
fontSize={{ base: '3xl', sm: '4xl', lg: '5xl' }}
>
<Text as={'span'} position={'relative'} fontWeight={600}>
<ChatIcon /> Text
<Text as={'span'} color={'blue.400'} decoration='underline'>
Bee
</Text>
</Text>
<br />
<Text as={'span'} color={'blue.400'} fontWeight={300}>
Make your android device a portable SMS Gateway!
</Text>
</Heading>
<Text
color={'gray.500'}
fontSize={{ base: 'md', sm: 'lg', lg: 'xl' }}
>
Unlock the power of messaging with our open-source Android SMS
Gateway.
</Text>
<Stack
spacing={{ base: 4, sm: 6 }}
direction={{ base: 'column', sm: 'row' }}
>
<Button
rounded={'full'}
size={'lg'}
fontWeight={'normal'}
px={6}
colorScheme={'blue'}
bg={'blue.400'}
_hover={{ bg: 'blue.500' }}
onClick={handleGetStarted}
>
Get Started
</Button>
<Link href={'#how-it-works'} passHref>
<Button
rounded={'full'}
size={'lg'}
fontWeight={'normal'}
px={6}
leftIcon={<PlayIcon h={4} w={4} color={'gray.300'} />}
>
How It Works
</Button>
</Link>
</Stack>
</Stack>
<Flex
flex={1}
justify={'center'}
align={'center'}
position={'relative'}
w={'full'}
>
<Box
position={'relative'}
height={'400px'}
rounded={'2xl'}
boxShadow={'xs'}
width={'full'}
overflow={'hidden'}
>
<Image
alt={'Hero Image'}
fit={'cover'}
align={'center'}
// w={'100%'}
// h={'100%'}
src={'/images/smsgatewayandroid.png'}
/>
</Box>
</Flex>
</Stack>
</Container>
)
}
const PlayIcon = createIcon({
displayName: 'PlayIcon',
viewBox: '0 0 58 58',
d: 'M28.9999 0.562988C13.3196 0.562988 0.562378 13.3202 0.562378 29.0005C0.562378 44.6808 13.3196 57.438 28.9999 57.438C44.6801 57.438 57.4374 44.6808 57.4374 29.0005C57.4374 13.3202 44.6801 0.562988 28.9999 0.562988ZM39.2223 30.272L23.5749 39.7247C23.3506 39.8591 23.0946 39.9314 22.8332 39.9342C22.5717 39.9369 22.3142 39.8701 22.0871 39.7406C21.86 39.611 21.6715 39.4234 21.5408 39.1969C21.4102 38.9705 21.3421 38.7133 21.3436 38.4519V19.5491C21.3421 19.2877 21.4102 19.0305 21.5408 18.8041C21.6715 18.5776 21.86 18.3899 22.0871 18.2604C22.3142 18.1308 22.5717 18.064 22.8332 18.0668C23.0946 18.0696 23.3506 18.1419 23.5749 18.2763L39.2223 27.729C39.4404 27.8619 39.6207 28.0486 39.7458 28.2713C39.8709 28.494 39.9366 28.7451 39.9366 29.0005C39.9366 29.2559 39.8709 29.507 39.7458 29.7297C39.6207 29.9523 39.4404 30.1391 39.2223 30.272Z',
})

45
web/components/landing/CodeSnippetSection.tsx

@ -0,0 +1,45 @@
import { Box, Flex, Heading, Image, Text } from '@chakra-ui/react'
import React from 'react'
import AnimatedScrollWrapper from '../AnimatedScrollWrapper'
export default function CodeSnippetSection() {
return (
<AnimatedScrollWrapper>
<Box m={{ base: 0, md: 8 }} p={{ base: 0, md: 8 }}>
<Flex
height='100%'
direction='column'
justifyContent='center'
alignItems='center'
>
<Heading fontSize={'3xl'} textAlign={'center'} py={8}>
Code Snippet
</Heading>
<Text color={'gray.600'} fontSize={'lg'} textAlign={'center'} pb='4'>
Send SMS messages from your web application using our REST API. You
can use any programming language to interact with our API. Here is a
sample code snippet in JavaScript using axios library.
</Text>
<Box
borderRadius={'lg'}
padding={{ base: 0, md: 8 }}
border={'1px solid #E2E8F0'}
w={{ base: '100%', md: '70%' }}
>
<Image
alt={'Hero Image'}
fit={'cover'}
align={'center'}
// h={'100%'}
src={
'https://ik.imagekit.io/vernu/textbee/Screenshot_2023-06-18_at_11.30.25_AM.png?updatedAt=1687077054749'
}
borderRadius={'lg'}
/>
</Box>
</Flex>
</Box>
</AnimatedScrollWrapper>
)
}

91
web/components/landing/DownloadAppSection.tsx

@ -0,0 +1,91 @@
import {
Box,
Button,
chakra,
Flex,
Image,
useColorModeValue,
} from '@chakra-ui/react'
import React from 'react'
import AnimatedScrollWrapper from '../AnimatedScrollWrapper'
export default function DownloadAppSection() {
return (
<AnimatedScrollWrapper>
<Box my={16}>
<Flex
padding={5}
background={useColorModeValue('gray.100', 'gray.700')}
borderRadius='2xl'
>
<Flex
borderRadius='2xl'
m={{ base: 5, md: 8 }}
p={{ base: 5, md: 8 }}
width='100%'
border='1px solid gray'
direction='row'
justifyContent='center'
>
<Box>
<Image
alt={'Hero Image'}
fit={'cover'}
align={'center'}
w={'180px'}
// h={'100%'}
src={'/images/smsgatewayandroid.png'}
/>
</Box>
<Box>
<Flex
height='100%'
direction='column'
justifyContent='center'
alignItems='center'
>
<chakra.h1
fontSize='md'
fontWeight='bold'
my={4}
color={useColorModeValue('gray.800', 'white')}
>
Download the App to get started!
</chakra.h1>
<chakra.p
fontSize='sm'
color={useColorModeValue('gray.600', 'gray.400')}
mb={4}
>
Unlock the power of messaging with our open-source Android SMS
Gateway.
</chakra.p>
<a href='/android' target='_blank'>
<Button
/* flex={1} */
px={4}
fontSize={'sm'}
rounded={'full'}
bg={'blue.400'}
color={'white'}
boxShadow={
'0px 1px 25px -5px rgb(66 153 225 / 48%), 0 10px 10px -5px rgb(66 153 225 / 43%)'
}
_hover={{
bg: 'blue.500',
}}
_focus={{
bg: 'blue.500',
}}
>
Download App
</Button>
</a>
</Flex>
</Box>
</Flex>
</Flex>
</Box>
</AnimatedScrollWrapper>
)
}

64
web/components/landing/FeaturesSection.tsx

@ -0,0 +1,64 @@
import { CheckIcon } from '@chakra-ui/icons'
import {
Box,
Container,
Heading,
HStack,
Icon,
SimpleGrid,
Text,
useColorModeValue,
VStack,
} from '@chakra-ui/react'
import React from 'react'
import { featuresContent } from './featuresContent'
import AnimatedScrollWrapper from '../AnimatedScrollWrapper'
const FeatureCard = ({ feature }) => {
const boxBgColor = useColorModeValue('gray.100', 'gray.800')
return (
<HStack
align={'top'}
borderWidth='1px'
borderRadius='sm'
p={2}
shadow='lg'
background={boxBgColor}
>
<Box color={'green.400'} px={1}>
<Icon as={CheckIcon} />
</Box>
<VStack align={'start'}>
<Text fontWeight={800}>{feature.title}</Text>
<Text fontWeight='normal'>{feature.description}</Text>
</VStack>
</HStack>
)
}
export default function FeaturesSection() {
return (
<AnimatedScrollWrapper>
<Box p={4} my={16} maxW={'6xl'}>
<Heading fontSize={'3xl'} textAlign={'center'} pb={0}>
Features
</Heading>
<Text color={'gray.600'} fontSize={'lg'} textAlign={'center'}>
The ultimate solution for your messaging needs! Our free open-source
Android-based SMS Gateway provides you with all the features you need
to effectively manage your SMS communications. From sending messages
and automating messaging workflows via API, our SMS Gateway is the
perfect tool for any small/mid business or individual.
</Text>
<Container maxW={'6xl'} mt={0}>
<SimpleGrid columns={{ base: 1, md: 2, lg: 4 }} spacing={3} pt={16}>
{featuresContent.map((feature, i) => (
<FeatureCard key={feature.title} feature={feature} />
))}
</SimpleGrid>
</Container>
</Box>
</AnimatedScrollWrapper>
)
}

60
web/components/landing/HowItWorksSection.tsx

@ -0,0 +1,60 @@
import { AddIcon, MinusIcon } from '@chakra-ui/icons'
import {
Accordion,
AccordionButton,
AccordionItem,
AccordionPanel,
Box,
Container,
Heading,
Text,
} from '@chakra-ui/react'
import React from 'react'
import { howItWorksContent } from './howItWorksContent'
import AnimatedScrollWrapper from '../AnimatedScrollWrapper'
export default function HowItWorksSection() {
return (
<AnimatedScrollWrapper>
<Box px={4} my={24} maxW={'6xl'}>
{/* @ts-ignore */}
<a name='how-it-works'>
<Heading fontSize={'3xl'} textAlign={'center'}>
How It Works
</Heading>
</a>
<Text color={'gray.600'} fontSize={'lg'} textAlign={'center'}>
How it works is simple. You install the app on your Android device,
and it will turn your device into a SMS Gateway. You can then use the
API to send SMS messages from your own applications.
</Text>
<Container maxW={'6xl'} mt={10} pt={0}>
<Accordion allowMultiple defaultIndex={[]}>
{howItWorksContent.map(({ title, description }) => (
<AccordionItem key={title}>
{({ isExpanded }) => (
<>
<h2>
<AccordionButton>
<Box as='span' flex='1' textAlign='left'>
{title}
</Box>
{isExpanded ? (
<MinusIcon fontSize='12px' />
) : (
<AddIcon fontSize='12px' />
)}
</AccordionButton>
</h2>
<AccordionPanel pb={4}>{description}</AccordionPanel>
</>
)}
</AccordionItem>
))}
</Accordion>
</Container>
</Box>
</AnimatedScrollWrapper>
)
}

153
web/components/landing/IntroSection.tsx

@ -0,0 +1,153 @@
import {
Container,
Stack,
Flex,
Box,
Heading,
Text,
Button,
Image,
createIcon,
} from '@chakra-ui/react'
import Link from 'next/link'
import Router from 'next/router'
import { selectAuthUser } from '../../store/authSlice'
import { useSelector } from 'react-redux'
import { ChatIcon } from '@chakra-ui/icons'
import AnimatedScrollWrapper from '../AnimatedScrollWrapper'
import { motion } from 'framer-motion'
const AnimatedScreenshotImage = () => {
const animateVariants = {
hidden: {
opacity: 0,
y: Math.floor(Math.random() * 100) + -50,
x: Math.floor(Math.random() * 100) + 50,
},
visible: {
opacity: 1,
y: 0,
x: 0,
transition: {
duration: 2.0,
},
},
}
return (
<motion.div
variants={animateVariants}
initial='hidden'
whileInView='visible'
>
<Box
position={'relative'}
height={'400px'}
rounded={'2xl'}
boxShadow={'xs'}
width={'full'}
overflow={'hidden'}
>
<Image
alt={'TextBee App Screenshot'}
fit={'cover'}
align={'center'}
src={'/images/smsgatewayandroid.png'}
/>
</Box>
</motion.div>
)
}
export default function IntroSection() {
const authUser = useSelector(selectAuthUser)
const handleGetStarted = () => {
if (!authUser) {
Router.push('/register')
} else {
Router.push('/dashboard')
}
}
return (
<AnimatedScrollWrapper>
<Container maxW={'7xl'} py={8}>
<Stack
align={'center'}
spacing={{ base: 8, md: 10 }}
py={{ base: 20, md: 28 }}
direction={{ base: 'column', md: 'row' }}
>
<Stack flex={1} spacing={{ base: 5, md: 10 }}>
<Heading
lineHeight={1.1}
fontWeight={600}
fontSize={{ base: '3xl', sm: '4xl', lg: '5xl' }}
>
<Text as={'span'} position={'relative'} fontWeight={600}>
<ChatIcon /> Text
<Text as={'span'} color={'blue.400'} decoration='underline'>
Bee
</Text>
</Text>
<br />
<Text as={'span'} color={'blue.400'} fontWeight={300}>
Make your android device a portable SMS Gateway!
</Text>
</Heading>
<Text
color={'gray.500'}
fontSize={{ base: 'md', sm: 'lg', lg: 'xl' }}
>
Unlock the power of messaging with our open-source Android SMS
Gateway.
</Text>
<Stack
spacing={{ base: 4, sm: 6 }}
direction={{ base: 'column', sm: 'row' }}
>
<Button
rounded={'full'}
size={'lg'}
fontWeight={'normal'}
px={6}
colorScheme={'blue'}
bg={'blue.400'}
_hover={{ bg: 'blue.500' }}
onClick={handleGetStarted}
>
Get Started
</Button>
<Link href={'#how-it-works'} passHref>
<Button
rounded={'full'}
size={'lg'}
fontWeight={'normal'}
px={6}
leftIcon={<PlayIcon h={4} w={4} color={'gray.300'} />}
>
How It Works
</Button>
</Link>
</Stack>
</Stack>
<Flex
flex={1}
justify={'center'}
align={'center'}
position={'relative'}
w={'full'}
>
<AnimatedScreenshotImage />
</Flex>
</Stack>
</Container>
</AnimatedScrollWrapper>
)
}
const PlayIcon = createIcon({
displayName: 'PlayIcon',
viewBox: '0 0 58 58',
d: 'M28.9999 0.562988C13.3196 0.562988 0.562378 13.3202 0.562378 29.0005C0.562378 44.6808 13.3196 57.438 28.9999 57.438C44.6801 57.438 57.4374 44.6808 57.4374 29.0005C57.4374 13.3202 44.6801 0.562988 28.9999 0.562988ZM39.2223 30.272L23.5749 39.7247C23.3506 39.8591 23.0946 39.9314 22.8332 39.9342C22.5717 39.9369 22.3142 39.8701 22.0871 39.7406C21.86 39.611 21.6715 39.4234 21.5408 39.1969C21.4102 38.9705 21.3421 38.7133 21.3436 38.4519V19.5491C21.3421 19.2877 21.4102 19.0305 21.5408 18.8041C21.6715 18.5776 21.86 18.3899 22.0871 18.2604C22.3142 18.1308 22.5717 18.064 22.8332 18.0668C23.0946 18.0696 23.3506 18.1419 23.5749 18.2763L39.2223 27.729C39.4404 27.8619 39.6207 28.0486 39.7458 28.2713C39.8709 28.494 39.9366 28.7451 39.9366 29.0005C39.9366 29.2559 39.8709 29.507 39.7458 29.7297C39.6207 29.9523 39.4404 30.1391 39.2223 30.272Z',
})

0
web/components/home/featuresContent.ts → web/components/landing/featuresContent.ts

0
web/components/home/howItWorksContent.ts → web/components/landing/howItWorksContent.ts

6
web/lib/customAxios.ts → web/lib/httpClient.ts

@ -1,11 +1,11 @@
import axios from 'axios'
import { LOCAL_STORAGE_KEY } from '../shared/constants'
const customAxios = axios.create({
const httpClient = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_BASE_URL,
})
customAxios.interceptors.request.use((config) => {
httpClient.interceptors.request.use((config) => {
const token = localStorage.getItem(LOCAL_STORAGE_KEY.TOKEN)
if (token) {
config.headers.Authorization = `Bearer ${token}`
@ -13,4 +13,4 @@ customAxios.interceptors.request.use((config) => {
return config
})
export default customAxios
export default httpClient

0
web/pages/404.js → web/pages/404.tsx

4
web/pages/_app.tsx

@ -1,4 +1,3 @@
import '../styles/globals.css'
import type { AppProps } from 'next/app'
import { Provider } from 'react-redux'
import { store } from '../store/store'
@ -8,10 +7,13 @@ import Meta from '../components/meta/Meta'
import { GoogleOAuthProvider } from '@react-oauth/google'
import ErrorBoundary from '../components/ErrorBoundary'
import Footer from '../components/Footer'
import Analytics from '../components/analytics/Analytics'
function MyApp({ Component, pageProps }: AppProps) {
return (
<ErrorBoundary>
<Provider store={store}>
<Analytics />
<GoogleOAuthProvider
clientId={process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID}
>

23
web/pages/dashboard/index.tsx → web/pages/dashboard.tsx

@ -1,21 +1,21 @@
import { Box, Flex, 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 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 { selectAuthUser } from '../store/authSlice'
import Router from 'next/router'
import { useEffect } from 'react'
import SendSMS from '../../components/dashboard/SendSMS'
import ErrorBoundary from '../../components/ErrorBoundary'
import SendSMS from '../components/dashboard/SendSMS'
import ErrorBoundary from '../components/ErrorBoundary'
export default function Dashboard() {
const { user: currentUser } = useSelector(selectAuth)
const authUser = useSelector(selectAuthUser)
const toast = useToast()
useEffect(() => {
if (!currentUser) {
if (!authUser) {
toast({
title: 'You are not logged in',
description: 'Please login to access this page',
@ -23,14 +23,11 @@ export default function Dashboard() {
})
Router.push('/login')
}
}, [currentUser, toast])
}, [authUser, toast])
return (
<>
<UserStats />
<Box maxW='7xl' mx={'auto'} pt={5} px={{ base: 2, sm: 12, md: 17 }}>
<Flex justifyContent='space-eve nly'></Flex>
<br />
<SimpleGrid columns={{ base: 1, md: 2 }} spacing={{ base: 5, lg: 8 }}>
<Box backdropBlur='2xl' borderWidth='0px' borderRadius='lg'>
<GenerateApiKey />

16
web/pages/index.tsx

@ -2,16 +2,16 @@ import { Container } from '@chakra-ui/react'
import { useGoogleOneTapLogin } from '@react-oauth/google'
import { useDispatch, useSelector } from 'react-redux'
import FeaturesSection from '../components/home/FeaturesSection'
import HowItWorksSection from '../components/home/HowItWorksSection'
import IntroSection from '../components/home/IntroSection'
import { loginWithGoogle, selectAuth } from '../store/authReducer'
import FeaturesSection from '../components/landing/FeaturesSection'
import HowItWorksSection from '../components/landing/HowItWorksSection'
import IntroSection from '../components/landing/IntroSection'
import { loginWithGoogle, selectAuthUser } from '../store/authSlice'
import DownloadAppSection from '../components/home/DownloadAppSection'
import CodeSnippetSection from '../components/home/CodeSnippetSection'
import DownloadAppSection from '../components/landing/DownloadAppSection'
import CodeSnippetSection from '../components/landing/CodeSnippetSection'
export default function HomePage() {
const { user } = useSelector(selectAuth)
const authUser = useSelector(selectAuthUser)
const dispatch = useDispatch()
@ -24,7 +24,7 @@ export default function HomePage() {
)
},
onError: () => {},
disabled: !!user,
disabled: !!authUser,
})
return (

11
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, loginWithGoogle, selectAuth } from '../store/authReducer'
import { login, loginWithGoogle, selectAuthLoading, selectAuthUser } from '../store/authSlice'
import { useDispatch, useSelector } from 'react-redux'
import { LoginRequestPayload } from '../services/types'
import { GoogleLogin } from '@react-oauth/google'
@ -32,7 +32,8 @@ export default function LoginPage() {
const dispatch = useDispatch()
const toast = useToast()
const authState = useSelector(selectAuth)
const authUser = useSelector(selectAuthUser)
const loading = useSelector(selectAuthLoading)
const handleSubmit = async (e) => {
e.preventDefault()
@ -107,9 +108,9 @@ export default function LoginPage() {
bg: 'blue.500',
}}
onClick={handleSubmit}
disabled={authState.loading}
disabled={loading}
>
{authState.loading ? 'Please Wait...' : 'Login'}
{loading ? 'Please Wait...' : 'Login'}
</Button>
</Stack>
@ -125,7 +126,7 @@ export default function LoginPage() {
status: 'error',
})
}}
useOneTap={!authState.user}
useOneTap={!authUser}
width={300}
size='large'
shape='pill'

16
web/pages/register.tsx

@ -16,7 +16,12 @@ import {
import Link from 'next/link'
import { useState } from 'react'
import { ViewIcon, ViewOffIcon } from '@chakra-ui/icons'
import { loginWithGoogle, register, selectAuth } from '../store/authReducer'
import {
loginWithGoogle,
register,
selectAuthUser,
selectAuthLoading,
} from '../store/authSlice'
import { useDispatch, useSelector } from 'react-redux'
import { RegisterRequestPayload } from '../services/types'
import { GoogleLogin } from '@react-oauth/google'
@ -30,7 +35,8 @@ export default function RegisterPage() {
})
const toast = useToast()
const dispatch = useDispatch()
const authState = useSelector(selectAuth)
const authUser = useSelector(selectAuthUser)
const loading = useSelector(selectAuthLoading)
const handleSubmit = async (e) => {
e.preventDefault()
@ -109,9 +115,9 @@ export default function RegisterPage() {
bg: 'blue.500',
}}
onClick={handleSubmit}
disabled={authState.loading}
disabled={loading}
>
{authState.loading ? 'Please wait...' : 'Register'}
{loading ? 'Please wait...' : 'Register'}
</Button>
</Stack>
@ -130,7 +136,7 @@ export default function RegisterPage() {
status: 'error',
})
}}
useOneTap={!authState.user}
useOneTap={!authUser}
width={300}
size='large'
shape='pill'

34
web/services/authService.ts

@ -0,0 +1,34 @@
import httpClient from '../lib/httpClient'
import {
GoogleLoginRequestPayload,
LoginRequestPayload,
LoginResponse,
RegisterRequestPayload,
RegisterResponse,
} from './types'
class AuthService {
async login(payload: LoginRequestPayload): Promise<LoginResponse> {
const res = await httpClient.post(`/auth/login`, payload)
return res.data.data
}
async loginWithGoogle(
payload: GoogleLoginRequestPayload
): Promise<LoginResponse> {
const res = await httpClient.post(`/auth/google-login`, payload)
return res.data.data
}
async register(payload: RegisterRequestPayload): Promise<RegisterResponse> {
const res = await httpClient.post(`/auth/register`, payload)
return res.data.data
}
async getCurrentUser() {
const res = await httpClient.get(`/auth/who-am-i`)
return res.data.data
}
}
export const authService = new AuthService()

34
web/services/gatewayService.ts

@ -0,0 +1,34 @@
import httpClient from '../lib/httpClient'
import { SendSMSRequestPayload } from './types'
class GatewayService {
async generateApiKey() {
const res = await httpClient.post(`/auth/api-keys`, {})
return res.data.data
}
async getApiKeyList() {
const res = await httpClient.get(`/auth/api-keys`)
return res.data.data
}
async deleteApiKey(id: string) {
const res = await httpClient.delete(`/auth/api-keys/${id}`)
return res.data.data
}
async getDeviceList() {
const res = await httpClient.get(`/gateway/devices`)
return res.data.data
}
async sendSMS(deviceId: string, payload: SendSMSRequestPayload) {
const res = await httpClient.post(
`/gateway/devices/${deviceId}/sendSMS`,
payload
)
return res.data.data
}
}
export const gatewayService = new GatewayService()

66
web/services/index.ts

@ -1,66 +0,0 @@
import axios from '../lib/customAxios'
import {
GoogleLoginRequestPayload,
LoginRequestPayload,
LoginResponse,
RegisterRequestPayload,
RegisterResponse,
SendSMSRequestPayload,
} from './types'
export const loginRequest = async (
payload: LoginRequestPayload
): Promise<LoginResponse> => {
const res = await axios.post(`/auth/login`, payload)
return res.data.data
}
export const loginWithGoogleRequest = async (
payload: GoogleLoginRequestPayload
): Promise<LoginResponse> => {
const res = await axios.post(`/auth/google-login`, payload)
return res.data.data
}
export const registerRequest = async (
payload: RegisterRequestPayload
): Promise<RegisterResponse> => {
const res = await axios.post(`/auth/register`, payload)
return res.data.data
}
export const getCurrentUserRequest = async () => {
const res = await axios.get(`/auth/who-am-i`)
return res.data.data
}
export const generateApiKeyRequest = async () => {
const res = await axios.post(`/auth/api-keys`, {})
return res.data.data
}
export const getApiKeyListRequest = async () => {
const res = await axios.get(`/auth/api-keys`)
return res.data.data
}
export const deleteApiKeyRequest = async (id: string) => {
const res = await axios.delete(`/auth/api-keys/${id}`)
return res.data.data
}
export const getDeviceListRequest = async () => {
const res = await axios.get(`/gateway/devices`)
return res.data.data
}
export const sendSMSRequest = async (
deviceId: string,
payload: SendSMSRequestPayload
) => {
const res = await axios.post(
`/gateway/devices/${deviceId}/sendSMS`,
payload
)
return res.data.data
}

6
web/services/types.ts

@ -36,12 +36,14 @@ export interface BaseResponse {
error?: string
message?: string
}
export interface LoginResponse extends BaseResponse {
export interface BaseAuthResponse extends BaseResponse {
accessToken: string
user: UserEntity
}
export interface LoginResponse extends BaseAuthResponse {}
export type RegisterResponse = LoginResponse
export interface RegisterResponse extends BaseAuthResponse {}
export interface CurrentUserResponse extends BaseResponse {
data: UserEntity

61
web/store/apiKeyListReducer.ts

@ -1,61 +0,0 @@
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

85
web/store/apiKeySlice.ts

@ -0,0 +1,85 @@
import { createAsyncThunk, createSlice, isAnyOf } from '@reduxjs/toolkit'
import type { PayloadAction } from '@reduxjs/toolkit'
import { createStandaloneToast } from '@chakra-ui/react'
import { RootState } from './store'
import { gatewayService } from '../services/gatewayService'
const toast = createStandaloneToast()
const initialState = {
loading: false,
item: null,
list: [],
}
export const fetchApiKeys = createAsyncThunk(
'apiKey/fetchApiKeys',
async (payload, { rejectWithValue }) => {
try {
const res = await gatewayService.getApiKeyList()
return res
} catch (e) {
toast({
title: e.response.data.error || 'Failed to Fetch apiKeys',
status: 'error',
})
return rejectWithValue(e.response.data)
}
}
)
export const deleteApiKey = createAsyncThunk(
'apiKey/deleteApiKey',
async (apiKeyId: string, { dispatch, rejectWithValue }) => {
try {
const res = await gatewayService.deleteApiKey(apiKeyId)
dispatch(fetchApiKeys())
toast({
title: 'ApiKey deleted successfully',
status: 'success',
})
return res
} catch (e) {
toast({
title: e.response.data.error || 'Failed to delete ApiKey',
status: 'error',
})
return rejectWithValue(e.response.data)
}
}
)
export const apiKeySlice = createSlice({
name: 'apiKey',
initialState,
reducers: {
clearApiKeyList: (state) => {
state.loading = false
state.list = []
},
},
extraReducers: (builder) => {
builder
.addCase(fetchApiKeys.fulfilled, (state, action: PayloadAction<any>) => {
state.loading = false
state.list = action.payload
})
.addCase(fetchApiKeys.rejected, (state) => {
state.loading = false
})
.addMatcher(
isAnyOf(fetchApiKeys.pending, deleteApiKey.pending),
(state) => {
state.loading = true
}
)
},
})
export const { clearApiKeyList } = apiKeySlice.actions
export const selectApiKeyLoading = (state: RootState) => state.apiKey.loading
export const selectApiKeyList = (state: RootState) => state.apiKey.list
export const selectApiKeyItem = (state: RootState) => state.apiKey.item
export default apiKeySlice.reducer

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

@ -1,10 +1,5 @@
import { createAsyncThunk, createSlice, isAnyOf } from '@reduxjs/toolkit'
import type { PayloadAction } from '@reduxjs/toolkit'
import {
loginRequest,
loginWithGoogleRequest,
registerRequest,
} from '../services'
import { createStandaloneToast } from '@chakra-ui/react'
import Router from 'next/router'
import { RootState } from './store'
@ -17,7 +12,7 @@ import {
import { removeUserAndToken, saveUserAndToken } from '../shared/utils'
import { LOCAL_STORAGE_KEY } from '../shared/constants'
import { googleLogout } from '@react-oauth/google'
import { authService } from '../services/authService'
const toast = createStandaloneToast()
const initialState: AuthState = {
@ -34,9 +29,9 @@ const initialState: AuthState = {
export const login = createAsyncThunk(
'auth/login',
async (payload: LoginRequestPayload, thunkAPI) => {
async (payload: LoginRequestPayload, { rejectWithValue }) => {
try {
const res = await loginRequest(payload)
const res = await authService.login(payload)
const { accessToken, user } = res
saveUserAndToken(user, accessToken)
Router.push('/')
@ -46,7 +41,7 @@ export const login = createAsyncThunk(
title: e.response.data.error || 'Login failed',
status: 'error',
})
return thunkAPI.rejectWithValue(e.response.data)
return rejectWithValue(e.response.data)
}
}
)
@ -55,7 +50,7 @@ export const loginWithGoogle = createAsyncThunk(
'auth/google-login',
async (payload: GoogleLoginRequestPayload, thunkAPI) => {
try {
const res = await loginWithGoogleRequest(payload)
const res = await authService.loginWithGoogle(payload)
const { accessToken, user } = res
saveUserAndToken(user, accessToken)
Router.push('/')
@ -74,7 +69,7 @@ export const register = createAsyncThunk(
'auth/register',
async (payload: RegisterRequestPayload, thunkAPI) => {
try {
const res = await registerRequest(payload)
const res = await authService.register(payload)
const { accessToken, user } = res
saveUserAndToken(user, accessToken)
Router.push('/')
@ -124,6 +119,9 @@ export const authSlice = createSlice({
export const { logout } = authSlice.actions
export const selectAuth = (state: RootState) => state.auth
export const selectAuthLoading = (state: RootState) => state.auth.loading
export const selectAuthUser = (state: RootState) => state.auth.user
export const selectAuthAccessToken = (state: RootState) =>
state.auth.accessToken
export default authSlice.reducer

61
web/store/deviceListReducer.ts

@ -1,61 +0,0 @@
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

92
web/store/deviceSlice.ts

@ -0,0 +1,92 @@
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
import type { PayloadAction } from '@reduxjs/toolkit'
import { createStandaloneToast } from '@chakra-ui/react'
import { RootState } from './store'
import { gatewayService } from '../services/gatewayService'
const toast = createStandaloneToast()
const initialState = {
loading: false,
item: null,
list: [],
sendingSMS: false,
}
export const fetchDevices = createAsyncThunk(
'device/fetchDevices',
async (payload, { rejectWithValue }) => {
try {
const res = await gatewayService.getDeviceList()
return res
} catch (e) {
toast({
title: e.response.data.error || 'Failed to Fetch devices',
status: 'error',
})
return rejectWithValue(e.response.data)
}
}
)
export const sendSMS = createAsyncThunk(
'device/sendSMS',
async ({ deviceId, payload }: any, { rejectWithValue }) => {
try {
const res = await gatewayService.sendSMS(deviceId, payload)
toast({
title: 'SMS sent successfully',
status: 'success',
})
return res
} catch (e) {
toast({
title: e.response.data.error || 'Failed to send SMS',
status: 'error',
})
return rejectWithValue(e.response.data)
}
}
)
export const deviceSlice = createSlice({
name: 'device',
initialState,
reducers: {
clearDeviceList: (state) => {
state.loading = false
state.list = []
},
},
extraReducers: (builder) => {
builder
.addCase(fetchDevices.pending, (state) => {
state.loading = true
})
.addCase(fetchDevices.fulfilled, (state, action: PayloadAction<any>) => {
state.loading = false
state.list = action.payload
})
.addCase(fetchDevices.rejected, (state) => {
state.loading = false
})
.addCase(sendSMS.pending, (state) => {
state.sendingSMS = true
})
.addCase(sendSMS.fulfilled, (state) => {
state.sendingSMS = false
})
.addCase(sendSMS.rejected, (state) => {
state.sendingSMS = false
})
},
})
export const { clearDeviceList } = deviceSlice.actions
export const selectDeviceList = (state: RootState) => state.device.list
export const selectDeviceItem = (state: RootState) => state.device.item
export const selectDeviceLoading = (state: RootState) => state.device.loading
export const selectSendingSMS = (state: RootState) => state.device.sendingSMS
export default deviceSlice.reducer

10
web/store/store.ts

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

Loading…
Cancel
Save