('http://127.0.0.1:8000/pricing', {
33 | headers: {
34 | Accept: 'application/json',
35 | },
36 | })
37 | .then((response) => {
38 | setPricing(response.data);
39 | })
40 | .catch((error) => console.log(error));
41 | };
42 |
43 | useEffect(() => {
44 | fetchPricing();
45 | }, []);
46 |
47 | return (
48 |
49 |
57 |
58 |
69 | Pricing
70 |
71 |
78 | We offer a range of pricing plans to suit everyone
79 |
80 |
81 |
82 |
83 | {pricing.map((item, i) => (
84 |
85 |
93 |
98 |
104 |
105 |
106 | {item.title}
107 |
108 |
109 |
110 |
111 |
122 | {item.currency}
123 |
124 |
125 |
126 |
136 | {item.price}
137 |
138 |
139 |
140 |
144 | Per user, per month
145 |
146 |
147 |
148 | {item.features?.map((feature, j) => (
149 |
150 |
157 | {feature.name}
158 |
159 |
160 | ))}
161 |
162 |
163 |
164 |
165 | }
169 | sx={{
170 | textTransform: 'uppercase',
171 | color:
172 | theme.palette.mode === 'dark'
173 | ? theme.palette.common.black
174 | : theme.palette.common.white,
175 | bgcolor:
176 | theme.palette.mode === 'dark'
177 | ? theme.palette.primary.main
178 | : theme.palette.success.dark,
179 | border: '2px solid',
180 | borderColor:
181 | theme.palette.mode === 'dark'
182 | ? theme.palette.primary.main
183 | : theme.palette.success.dark,
184 | '&:hover': {
185 | backgroundColor: 'transparent',
186 | color:
187 | theme.palette.mode === 'dark'
188 | ? theme.palette.primary.main
189 | : theme.palette.success.dark,
190 | border: '2px solid',
191 | borderColor:
192 | theme.palette.mode === 'dark'
193 | ? theme.palette.primary.main
194 | : theme.palette.success.dark,
195 | },
196 | }}
197 | >
198 | Get started
199 |
200 |
201 |
202 |
203 | ))}
204 |
205 |
206 |
207 |
208 | );
209 | };
210 |
211 | export default Pricing;
212 |
--------------------------------------------------------------------------------
/frontend/src/components/Products.tsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 | import axios from 'axios';
3 |
4 | // Material UI
5 | import Box from '@mui/material/Box';
6 | import Card from '@mui/material/Card';
7 | import CardMedia from '@mui/material/CardMedia';
8 | import Container from '@mui/material/Container';
9 | import Grid from '@mui/material/Grid';
10 | import Typography from '@mui/material/Typography';
11 | import { useTheme } from '@mui/material/styles';
12 |
13 | interface ProductsProps {
14 | name: number;
15 | description: string;
16 | image: string;
17 | }
18 |
19 | const Products = (): JSX.Element => {
20 | const theme = useTheme();
21 |
22 | const [products, setProducts] = useState([]);
23 |
24 | const fetchProducts = () => {
25 | axios
26 | .get('http://127.0.0.1:8000/products', {
27 | headers: {
28 | Accept: 'application/json',
29 | },
30 | })
31 | .then((response) => {
32 | setProducts(response.data);
33 | })
34 | .catch((error) => console.log(error));
35 | };
36 |
37 | useEffect(() => {
38 | fetchProducts();
39 | }, []);
40 |
41 | return (
42 |
43 |
51 |
52 |
63 | Products
64 |
65 |
72 | We offer a range of products to support your business
73 |
74 |
75 |
76 |
77 | {products.map((item, i) => (
78 |
79 |
95 |
96 |
103 | {item.name}
104 |
105 | {item.description}
106 |
107 |
108 |
119 |
134 |
135 |
136 |
137 |
138 | ))}
139 |
140 |
141 |
142 |
143 | );
144 | };
145 |
146 | export default Products;
147 |
--------------------------------------------------------------------------------
/frontend/src/components/Services.tsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 | import axios from 'axios';
3 |
4 | // Material UI
5 | import Avatar from '@mui/material/Avatar';
6 | import Box from '@mui/material/Box';
7 | import Card from '@mui/material/Card';
8 | import Container from '@mui/material/Container';
9 | import Grid from '@mui/material/Grid';
10 | import Icon from '@mui/material/Icon';
11 | import Typography from '@mui/material/Typography';
12 | import { useTheme } from '@mui/material/styles';
13 |
14 | interface ServicesProps {
15 | title: string;
16 | description: string;
17 | icon: string;
18 | }
19 |
20 | const Services = (): JSX.Element => {
21 | const theme = useTheme();
22 |
23 | const [services, setServices] = useState([]);
24 |
25 | const fetchServices = () => {
26 | axios
27 | .get('http://127.0.0.1:8000/services', {
28 | headers: {
29 | Accept: 'application/json',
30 | },
31 | })
32 | .then((response) => {
33 | setServices(response.data);
34 | })
35 | .catch((error) => console.log(error));
36 | };
37 |
38 | useEffect(() => {
39 | fetchServices();
40 | }, []);
41 |
42 | return (
43 |
44 |
52 |
53 |
64 | Services
65 |
66 |
73 | We offer a range of services to support your business
74 |
75 |
76 |
77 |
78 | {services.map((item, i) => (
79 |
80 |
81 |
82 |
94 |
95 | {item.icon}
96 |
97 |
98 |
103 | {item.title}
104 |
105 |
106 | {item.description}
107 |
108 |
109 |
110 |
111 | ))}
112 |
113 |
114 |
115 |
116 | );
117 | };
118 |
119 | export default Services;
120 |
--------------------------------------------------------------------------------
/frontend/src/components/Spacer.tsx:
--------------------------------------------------------------------------------
1 | import Box from '@mui/material/Box';
2 | import { useTheme } from '@mui/material';
3 |
4 | interface Props {
5 | [x: string]: any;
6 | }
7 |
8 | const Spacer = ({ sx = [] }: Props): JSX.Element => {
9 | const theme = useTheme();
10 |
11 | return (
12 |
20 | );
21 | };
22 |
23 | export default Spacer;
24 |
--------------------------------------------------------------------------------
/frontend/src/index.tsx:
--------------------------------------------------------------------------------
1 | import ReactDOM from 'react-dom/client';
2 | import App from './App';
3 |
4 | const root = ReactDOM.createRoot(
5 | document.getElementById('root') as HTMLElement
6 | );
7 | root.render();
8 |
--------------------------------------------------------------------------------
/frontend/src/layout/Footer.tsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 | import axios from 'axios';
3 |
4 | // Material UI
5 | import Box from '@mui/material/Box';
6 | import Grid from '@mui/material/Grid';
7 | import Typography from '@mui/material/Typography';
8 | import { useTheme } from '@mui/material/styles';
9 |
10 | interface FooterProps {
11 | copyright: string;
12 | }
13 |
14 | const Footer = (): JSX.Element => {
15 | const theme = useTheme();
16 |
17 | const [footer, setFooter] = useState([]);
18 |
19 | const fetchFooter = () => {
20 | axios
21 | .get('http://127.0.0.1:8000/footer', {
22 | headers: {
23 | Accept: 'application/json',
24 | },
25 | })
26 | .then((response) => {
27 | setFooter(response.data);
28 | })
29 | .catch((error) => console.log(error));
30 | };
31 |
32 | useEffect(() => {
33 | fetchFooter();
34 | }, []);
35 |
36 | return (
37 |
38 | {footer.slice(0, 1).map((item, i) => (
39 |
40 |
41 |
48 | Copyright © {new Date().getFullYear()} {item.copyright}.
49 |
50 |
51 |
52 | ))}
53 |
54 | );
55 | };
56 |
57 | export default Footer;
58 |
--------------------------------------------------------------------------------
/frontend/src/layout/Header.tsx:
--------------------------------------------------------------------------------
1 | import { useContext } from 'react';
2 |
3 | // Material UI
4 | import AppBar from '@mui/material/AppBar';
5 | import Button from '@mui/material/Button';
6 | import Box from '@mui/material/Box';
7 | import Divider from '@mui/material/Divider';
8 | import IconButton from '@mui/material/IconButton';
9 | import Link from '@mui/material/Link';
10 | import Toolbar from '@mui/material/Toolbar';
11 | import Tooltip from '@mui/material/Tooltip';
12 | import Typography from '@mui/material/Typography';
13 | import { alpha, useTheme } from '@mui/material/styles';
14 |
15 | // Material Icons
16 | import DarkModeIcon from '@mui/icons-material/DarkMode';
17 | import LightModeIcon from '@mui/icons-material/LightMode';
18 | import StormIcon from '@mui/icons-material/Storm';
19 | import MenuIcon from '@mui/icons-material/Menu';
20 |
21 | import CustomButton from '../components/CustomButton';
22 | import ColorModeContext from '../utils/ColorModeContext';
23 |
24 | interface Props {
25 | onSidebarOpen: () => void;
26 | }
27 |
28 | const Header = ({ onSidebarOpen }: Props): JSX.Element => {
29 | const theme = useTheme();
30 | const colorMode = useContext(ColorModeContext);
31 |
32 | return (
33 | <>
34 |
46 |
47 |
48 |
49 |
59 |
60 |
71 | Bob's Company
72 |
73 |
74 |
75 |
76 |
77 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
97 |
98 |
103 | {theme.palette.mode === 'dark' ? (
104 |
105 |
106 |
107 | ) : (
108 |
109 |
110 |
111 | )}
112 |
113 |
114 |
120 |
140 |
141 |
142 |
143 | >
144 | );
145 | };
146 |
147 | export default Header;
148 |
--------------------------------------------------------------------------------
/frontend/src/layout/Layout.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 |
3 | // Material UI
4 | import Box from '@mui/material/Box';
5 | import Fab from '@mui/material/Fab';
6 | import NoSsr from '@mui/material/NoSsr';
7 | import Zoom from '@mui/material/Zoom';
8 | import useMediaQuery from '@mui/material/useMediaQuery';
9 | import useScrollTrigger from '@mui/material/useScrollTrigger';
10 | import { useTheme } from '@mui/material/styles';
11 |
12 | // Material Icons
13 | import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
14 |
15 | import Header from './Header';
16 | import Footer from './Footer';
17 | import Sidebar from './Sidebar';
18 |
19 | interface Props {
20 | children: React.ReactNode;
21 | }
22 |
23 | const Layout = ({ children }: Props): JSX.Element => {
24 | const theme = useTheme();
25 | const isLg = useMediaQuery(theme.breakpoints.up('lg'), {
26 | defaultMatches: true,
27 | });
28 |
29 | const [openSidebar, setOpenSidebar] = useState(false);
30 |
31 | const handleSidebarOpen = (): void => {
32 | setOpenSidebar(true);
33 | };
34 |
35 | const handleSidebarClose = (): void => {
36 | setOpenSidebar(false);
37 | };
38 |
39 | const open = isLg ? false : openSidebar;
40 |
41 | const trigger = useScrollTrigger({
42 | disableHysteresis: true,
43 | threshold: 100,
44 | });
45 |
46 | const scrollTo = (id: string): void => {
47 | setTimeout(() => {
48 | const element = document.querySelector(`#${id}`) as HTMLElement;
49 | if (!element) {
50 | return;
51 | }
52 | window.scrollTo({ left: 0, top: element.offsetTop, behavior: 'smooth' });
53 | });
54 | };
55 |
56 | return (
57 |
64 |
65 |
66 |
67 | {children}
68 |
69 |
70 |
71 |
72 | scrollTo('page-top')}
74 | role='presentation'
75 | sx={{ position: 'fixed', bottom: 24, right: 32 }}
76 | >
77 |
99 |
100 |
101 |
102 |
103 |
104 |
105 | );
106 | };
107 |
108 | export default Layout;
109 |
--------------------------------------------------------------------------------
/frontend/src/layout/Sidebar.tsx:
--------------------------------------------------------------------------------
1 | import { Link } from 'react-router-dom';
2 |
3 | // Material UI
4 | import Box from '@mui/material/Box';
5 | import Drawer from '@mui/material/Drawer';
6 | import IconButton from '@mui/material/IconButton';
7 | import Typography from '@mui/material/Typography';
8 | import StormIcon from '@mui/icons-material/Storm';
9 | import { useTheme } from '@mui/material/styles';
10 |
11 | import CustomButton from '../components/CustomButton';
12 |
13 | interface Props {
14 | onClose: () => void;
15 | open: boolean;
16 | }
17 |
18 | const Sidebar = ({ open, onClose }: Props): JSX.Element => {
19 | const theme = useTheme();
20 |
21 | return (
22 | <>
23 | onClose()}
26 | open={open}
27 | variant='temporary'
28 | PaperProps={{
29 | sx: {
30 | backgroundColor: theme.palette.background.default,
31 | width: 256,
32 | },
33 | }}
34 | >
35 |
36 |
37 |
38 |
39 |
49 |
60 | Bob's Company
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 | >
85 | );
86 | };
87 |
88 | export default Sidebar;
89 |
--------------------------------------------------------------------------------
/frontend/src/pages/Home.tsx:
--------------------------------------------------------------------------------
1 | import Hero from '../components/Hero';
2 | import Products from '../components/Products';
3 | import Services from '../components/Services';
4 | import Pricing from '../components/Pricing';
5 | import About from '../components/About';
6 | import Contact from '../components/Contact';
7 |
8 | const Home = (): JSX.Element => {
9 | return (
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | );
19 | };
20 |
21 | export default Home;
22 |
--------------------------------------------------------------------------------
/frontend/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/frontend/src/tests/Home.test.tsx:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/react';
2 | import userEvent from '@testing-library/user-event';
3 |
4 | import Home from '../pages/Home';
5 |
6 | const renderApp = () => {
7 | render();
8 |
9 | return { user: userEvent.setup() };
10 | };
11 |
12 | describe('Home page', () => {
13 | it('should render the elements on the Home page', async () => {
14 | renderApp();
15 |
16 | expect(
17 | await screen.findByText(
18 | /We offer a range of products to support your business/i
19 | )
20 | ).toBeInTheDocument();
21 | expect(
22 | await screen.findByText(
23 | /We offer a range of services to support your business/i
24 | )
25 | ).toBeVisible();
26 | expect(
27 | await screen.findByText(
28 | /We offer a range of pricing plans to suit everyone/i
29 | )
30 | ).toBeVisible();
31 | expect(
32 | await screen.findByText(
33 | /We help software developers learn new skills, gain more experience and create excellent applications/i
34 | )
35 | ).toBeVisible();
36 | expect(
37 | await screen.findByText(/We would love to hear from you/i)
38 | ).toBeVisible();
39 | });
40 | });
41 |
--------------------------------------------------------------------------------
/frontend/src/tests/mocks/fileMock.css:
--------------------------------------------------------------------------------
1 | module.exports = {};
2 |
--------------------------------------------------------------------------------
/frontend/src/tests/mocks/fileMock.ts:
--------------------------------------------------------------------------------
1 | export {};
2 |
--------------------------------------------------------------------------------
/frontend/src/theme/palette.ts:
--------------------------------------------------------------------------------
1 | import { PaletteMode } from '@mui/material';
2 | import { green, orange } from '@mui/material/colors';
3 |
4 | export const light = {
5 | mode: 'light' as PaletteMode,
6 | primary: {
7 | main: 'rgb(129, 187, 89)',
8 | contrastText: 'rgb(100, 101, 98)',
9 | },
10 | success: {
11 | main: 'rgb(111, 214, 145)',
12 | light: 'rgb(131, 231, 168)',
13 | dark: green[600],
14 | },
15 | text: {
16 | primary: 'rgb(40, 40, 40)',
17 | secondary: 'rgb(103, 119, 136)',
18 | },
19 | background: {
20 | paper: 'rgb(242, 243, 245)',
21 | default: 'rgb(255, 255, 255)',
22 | },
23 | divider: 'rgba(0, 0, 0, 0.12)',
24 | };
25 |
26 | export const dark = {
27 | mode: 'dark' as PaletteMode,
28 | primary: {
29 | main: 'rgb(129, 187, 89)',
30 | contrastText: 'rgb(100, 101, 98)',
31 | },
32 | warning: {
33 | main: 'rgb(242, 175, 87)',
34 | light: 'rgb(245, 205, 130)',
35 | dark: orange[600],
36 | },
37 | text: {
38 | primary: 'rgb(255, 255, 255)',
39 | secondary: 'rgb(207, 207, 207)',
40 | },
41 | background: {
42 | default: 'rgb(0, 0, 0)',
43 | paper: 'rgb(15, 15, 15)',
44 | },
45 | divider: 'rgba(145, 158, 171, 0.24)',
46 | };
47 |
--------------------------------------------------------------------------------
/frontend/src/theme/theme.ts:
--------------------------------------------------------------------------------
1 | import { Theme, responsiveFontSizes } from '@mui/material';
2 | import { createTheme, ComponentsOverrides } from '@mui/material/styles';
3 | import { light, dark } from './palette';
4 |
5 | const getTheme = (mode: string): Theme =>
6 | responsiveFontSizes(
7 | createTheme({
8 | palette: mode === 'light' ? light : dark,
9 | typography: {
10 | fontFamily: '"Poppins", sans-serif',
11 | },
12 | components: {
13 | MuiButton: {
14 | styleOverrides: {
15 | root: {
16 | fontWeight: 600,
17 | borderRadius: 0,
18 | paddingTop: 10,
19 | paddingBottom: 10,
20 | },
21 | } as ComponentsOverrides['MuiButton'],
22 | },
23 | MuiInputBase: {
24 | styleOverrides: {
25 | root: {
26 | borderRadius: 0,
27 | },
28 | } as ComponentsOverrides['MuiInputBase'],
29 | },
30 | MuiOutlinedInput: {
31 | styleOverrides: {
32 | root: {
33 | borderRadius: 0,
34 | },
35 | input: {
36 | borderRadius: 0,
37 | },
38 | } as ComponentsOverrides['MuiOutlinedInput'],
39 | },
40 | MuiCard: {
41 | styleOverrides: {
42 | root: {
43 | borderRadius: 0,
44 | },
45 | } as ComponentsOverrides['MuiCard'],
46 | },
47 | },
48 | })
49 | );
50 |
51 | export default getTheme;
52 |
--------------------------------------------------------------------------------
/frontend/src/utils/ColorModeContext.ts:
--------------------------------------------------------------------------------
1 | import { createContext } from 'react';
2 |
3 | const ColorModeContext = createContext({
4 | toggleColorMode: () => {},
5 | });
6 |
7 | export default ColorModeContext;
8 |
--------------------------------------------------------------------------------
/frontend/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "esModuleInterop": true,
8 | "allowSyntheticDefaultImports": true,
9 | "strict": true,
10 | "forceConsistentCasingInFileNames": true,
11 | "noFallthroughCasesInSwitch": true,
12 | "module": "esnext",
13 | "moduleResolution": "node",
14 | "resolveJsonModule": true,
15 | "isolatedModules": true,
16 | "noEmit": true,
17 | "jsx": "react-jsx"
18 | },
19 | "include": ["src", "jest.config.js", "jest.setup.js", "**/*.ts", "**/*.tsx"]
20 | }
21 |
--------------------------------------------------------------------------------