('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 |
116 | {item.currency}
117 |
118 |
119 |
120 |
121 | {item.price}
122 |
123 |
124 |
125 |
129 | Per user, per month
130 |
131 |
132 |
133 | {item.features?.map((feature, j) => (
134 |
135 |
142 | {feature.name}
143 |
144 |
145 | ))}
146 |
147 |
148 |
149 |
150 | }
154 | sx={{
155 | textTransform: 'uppercase',
156 | color: theme.palette.common.black,
157 | border: `2px solid ${theme.palette.primary.main}`,
158 | '&:hover': {
159 | backgroundColor: 'transparent',
160 | color: theme.palette.primary.main,
161 | border: `2px solid ${theme.palette.primary.main}`,
162 | },
163 | }}
164 | >
165 | Get started
166 |
167 |
168 |
169 |
170 | ))}
171 |
172 |
173 |
174 |
175 | );
176 | };
177 |
178 | export default Pricing;
179 |
--------------------------------------------------------------------------------
/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 |
92 |
93 |
100 | {item.name}
101 |
102 | {item.description}
103 |
104 |
105 |
116 |
128 |
129 |
130 |
131 |
132 | ))}
133 |
134 |
135 |
136 |
137 | );
138 | };
139 |
140 | export default Products;
141 |
--------------------------------------------------------------------------------
/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 |
90 |
91 | {item.icon}
92 |
93 |
94 |
99 | {item.title}
100 |
101 |
102 | {item.description}
103 |
104 |
105 |
106 |
107 | ))}
108 |
109 |
110 |
111 |
112 | );
113 | };
114 |
115 | export default Services;
116 |
--------------------------------------------------------------------------------
/frontend/src/components/Spacer.tsx:
--------------------------------------------------------------------------------
1 | // Material UI
2 | import Box from '@mui/material/Box';
3 | import { useTheme } from '@mui/material';
4 |
5 | interface Props {
6 | [x: string]: any;
7 | }
8 |
9 | const Spacer = ({ sx = [] }: Props): JSX.Element => {
10 | const theme = useTheme();
11 |
12 | return (
13 |
21 | );
22 | };
23 |
24 | export default Spacer;
25 |
--------------------------------------------------------------------------------
/frontend/src/index.tsx:
--------------------------------------------------------------------------------
1 | import ReactDOM from 'react-dom/client';
2 |
3 | import App from './App';
4 | import reportWebVitals from './reportWebVitals';
5 |
6 | const root = ReactDOM.createRoot(
7 | document.getElementById('root') as HTMLElement
8 | );
9 | root.render();
10 |
11 | // If you want to start measuring performance in your app, pass a function
12 | // to log results (for example: reportWebVitals(console.log))
13 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
14 | reportWebVitals();
15 |
--------------------------------------------------------------------------------
/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 | // Material UI
2 | import AppBar from '@mui/material/AppBar';
3 | import Button from '@mui/material/Button';
4 | import Box from '@mui/material/Box';
5 | import IconButton from '@mui/material/IconButton';
6 | import Link from '@mui/material/Link';
7 | import Toolbar from '@mui/material/Toolbar';
8 | import Typography from '@mui/material/Typography';
9 | import { alpha, useTheme } from '@mui/material/styles';
10 |
11 | // Material Icons
12 | import StormIcon from '@mui/icons-material/Storm';
13 | import MenuIcon from '@mui/icons-material/Menu';
14 |
15 | import CustomButton from '../components/CustomButton';
16 |
17 | interface HeaderProps {
18 | onSidebarOpen: () => void;
19 | }
20 |
21 | const Header = ({ onSidebarOpen }: HeaderProps): JSX.Element => {
22 | const theme = useTheme();
23 |
24 | return (
25 | <>
26 |
38 |
39 |
40 |
41 |
48 |
49 |
60 | Bob's Company
61 |
62 |
63 |
64 |
65 |
66 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
84 |
97 |
98 |
99 |
100 | >
101 | );
102 | };
103 |
104 | export default Header;
105 |
--------------------------------------------------------------------------------
/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 LayoutProps {
20 | children: React.ReactNode;
21 | }
22 |
23 | const Layout = ({ children }: LayoutProps): 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 |
90 |
91 |
92 |
93 |
94 |
95 |
96 | );
97 | };
98 |
99 | export default Layout;
100 |
--------------------------------------------------------------------------------
/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 SidebarProps {
14 | onClose: () => void;
15 | open: boolean;
16 | }
17 |
18 | const Sidebar = ({ open, onClose }: SidebarProps): 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 |
46 |
57 | Bob's Company
58 |
59 |
60 |
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 | export default Sidebar;
86 |
--------------------------------------------------------------------------------
/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/reportWebVitals.ts:
--------------------------------------------------------------------------------
1 | import { ReportHandler } from 'web-vitals';
2 |
3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => {
4 | if (onPerfEntry && onPerfEntry instanceof Function) {
5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
6 | getCLS(onPerfEntry);
7 | getFID(onPerfEntry);
8 | getFCP(onPerfEntry);
9 | getLCP(onPerfEntry);
10 | getTTFB(onPerfEntry);
11 | });
12 | }
13 | };
14 |
15 | export default reportWebVitals;
16 |
--------------------------------------------------------------------------------
/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/theme.ts:
--------------------------------------------------------------------------------
1 | import { Theme, PaletteMode, responsiveFontSizes } from '@mui/material';
2 | import { createTheme, ComponentsOverrides } from '@mui/material/styles';
3 |
4 | const getTheme = (): Theme =>
5 | responsiveFontSizes(
6 | createTheme({
7 | palette: {
8 | mode: 'dark' as PaletteMode,
9 | background: {
10 | default: 'rgb(0, 0, 0)',
11 | paper: 'rgb(15, 15, 15)',
12 | },
13 | text: {
14 | primary: 'rgb(255, 255, 255)',
15 | secondary: 'rgb(207, 207, 207)',
16 | },
17 | primary: {
18 | main: 'rgb(129, 187, 89)',
19 | contrastText: 'rgb(100, 101, 98)',
20 | },
21 | divider: 'rgba(145, 158, 171, 0.24)',
22 | },
23 | typography: {
24 | fontFamily: '"Poppins", sans-serif',
25 | },
26 | components: {
27 | MuiButton: {
28 | styleOverrides: {
29 | root: {
30 | fontWeight: 600,
31 | borderRadius: 0,
32 | paddingTop: 10,
33 | paddingBottom: 10,
34 | },
35 | } as ComponentsOverrides['MuiButton'],
36 | },
37 | MuiInputBase: {
38 | styleOverrides: {
39 | root: {
40 | borderRadius: 0,
41 | },
42 | } as ComponentsOverrides['MuiInputBase'],
43 | },
44 | MuiOutlinedInput: {
45 | styleOverrides: {
46 | root: {
47 | borderRadius: 0,
48 | },
49 | input: {
50 | borderRadius: 0,
51 | },
52 | } as ComponentsOverrides['MuiOutlinedInput'],
53 | },
54 | MuiCard: {
55 | styleOverrides: {
56 | root: {
57 | borderRadius: 0,
58 | },
59 | } as ComponentsOverrides['MuiCard'],
60 | },
61 | },
62 | })
63 | );
64 |
65 | export default getTheme;
66 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------