├── .gitignore ├── LICENSE ├── README.md ├── client ├── App.tsx ├── auth │ ├── SignIn.tsx │ └── Signup.tsx ├── components │ ├── ActionPrompt │ │ └── ActionPrompt.tsx │ ├── CreateTripForm │ │ ├── CreateTripForm.module.css │ │ └── CreateTripForm.tsx │ ├── Logo │ │ ├── Logo.module.css │ │ └── Logo.tsx │ ├── Navigation │ │ ├── AppBar.tsx │ │ ├── DrawerHeader.tsx │ │ ├── Main.tsx │ │ └── Navigation.tsx │ ├── TripList │ │ ├── TripList.module.css │ │ └── TripList.tsx │ ├── TripSummary │ │ ├── TripSummary.module.css │ │ └── TripSummary.tsx │ ├── Weather │ │ ├── Weather.module.css │ │ ├── Weather.tsx │ │ └── WeatherIcon.tsx │ └── packinglist │ │ ├── PackingList.module.css │ │ ├── PackingList.tsx │ │ └── api │ │ ├── checkItem.tsx │ │ ├── config.tsx │ │ ├── createItem.ts │ │ ├── deleteItem.ts │ │ └── getItems.ts ├── hooks │ └── useFadeInOpacity.ts ├── index.tsx └── routes │ ├── CreateTrip.tsx │ ├── Error.tsx │ ├── Home.tsx │ ├── Root.tsx │ └── TripDashboard.tsx ├── package-lock.json ├── package.json ├── public ├── 30c1db502ded3d490c8f.jpg ├── 32b8cc2ef6d5b824ea05.jpg ├── 3642035e27afe222372e.jpg ├── 36d9f89eafea50d94f0f.jpg ├── 36d9f89eafea50d94f0fc765fe6ba067.jpg ├── 45c3f734296fc7749292.jpg ├── 478544d37a72e8c61a31.jpg ├── 4f9b86feedba5a4bed0b.jpg ├── 7e7f2f22ecbd00fbf2a6.jpg ├── 86a4dc730b01e6a75469.jpg ├── 8b552a59da567e947e94.jpg ├── 94b3c1881adcab5feb18.jpg ├── 94b3c1881adcab5feb183f90610ce9d3.jpg ├── a72df4dc18674d8bf0bd.jpg ├── a72df4dc18674d8bf0bd58afed0c673f.jpg ├── b09d5eda320cf9c9dbff.jpg ├── f4412f5eab5cbb97f972.jpg ├── images │ ├── clear-night-jetsetgo.jpg │ ├── cloudy-jetsetgo.jpg │ ├── foggyMist-jetsetgo.jpg │ ├── rainy-jetsetgo.jpg │ ├── snowy-jetsetgo.jpg │ ├── stormy-jetsetgo.jpg │ └── sunny-jetsetgo.jpg ├── index.html └── style.css ├── src ├── Globals.d.ts ├── SessionData.d.ts ├── controllers │ ├── authController.ts │ ├── googleAuth.ts │ ├── packingListController.ts │ ├── placesController.ts │ ├── tripsController.ts │ └── weatherController.ts ├── index.ts ├── models │ ├── f.json │ ├── trip.ts │ └── userModel.ts └── routes │ ├── authRouter.ts │ ├── packingList.ts │ ├── placesRouter.ts │ ├── tripBudget.ts │ ├── tripsRouter.ts │ └── weather.ts ├── tsconfig.json └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .eslintrc.json 3 | babel.config.json 4 | .env 5 | .env.* 6 | 7 | dist 8 | .DS_Store 9 | 10 | /public/bundle.js 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 jet-set-go 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JetSetGo 2 | 3 | A travel planning application that helps you plan for and document your upcoming trips. 4 | -------------------------------------------------------------------------------- /client/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createBrowserRouter, RouterProvider } from 'react-router-dom'; 3 | import CreateTrip from './routes/CreateTrip'; 4 | import ErrorPage from './routes/Error'; 5 | import HomePage from './routes/Home'; 6 | import SignIn from './auth/SignIn'; 7 | import SignUp from './auth/Signup'; 8 | import Root from './routes/Root'; 9 | import TripDashboard, { loader as tripLoader } from './routes/TripDashboard'; 10 | import { createTheme } from '@mui/material'; 11 | import { ThemeProvider } from '@mui/system'; 12 | 13 | const router = createBrowserRouter([ 14 | { 15 | path: '/', 16 | element: , 17 | errorElement: , 18 | children: [ 19 | { 20 | path: '/', 21 | element: , 22 | }, 23 | { 24 | path: '/trip/new', 25 | element: , 26 | }, 27 | { 28 | path: '/trip/:tripId', 29 | element: , 30 | loader: tripLoader, 31 | }, 32 | // Additional routes go here 33 | ], 34 | }, 35 | { 36 | path: '/signin', 37 | element: , 38 | errorElement: , 39 | }, 40 | { 41 | path: '/signup', 42 | element: , 43 | errorElement: , 44 | }, 45 | ]); 46 | 47 | const theme = createTheme({ 48 | palette: { 49 | primary: { 50 | main: '#F68C02', 51 | contrastText: '#FFFFFF', 52 | }, 53 | secondary: { 54 | main: '#073064', 55 | }, 56 | }, 57 | }); 58 | 59 | const App: React.FC = () => { 60 | return ( 61 | 62 | 63 | 64 | 65 | 66 | ); 67 | }; 68 | 69 | export default App; 70 | -------------------------------------------------------------------------------- /client/auth/SignIn.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Avatar from '@mui/material/Avatar'; 3 | import Button from '@mui/material/Button'; 4 | import CssBaseline from '@mui/material/CssBaseline'; 5 | import TextField from '@mui/material/TextField'; 6 | import FormControlLabel from '@mui/material/FormControlLabel'; 7 | import Checkbox from '@mui/material/Checkbox'; 8 | import Link from '@mui/material/Link'; 9 | import Paper from '@mui/material/Paper'; 10 | import Box from '@mui/material/Box'; 11 | import Grid from '@mui/material/Grid'; 12 | import LockOutlinedIcon from '@mui/icons-material/LockOutlined'; 13 | import Typography from '@mui/material/Typography'; 14 | import { createTheme, ThemeProvider } from '@mui/material/styles'; 15 | import { FcGoogle } from 'react-icons/fc'; 16 | import { blue } from '@mui/material/colors'; 17 | import { useNavigate } from 'react-router-dom'; 18 | 19 | function Copyright(props: any) { 20 | return ( 21 | 27 | {'Copyright © '} 28 | 29 | JetSetGo 30 | {' '} 31 | {new Date().getFullYear()} 32 | {'.'} 33 | 34 | ); 35 | } 36 | 37 | const theme = createTheme(); 38 | 39 | export default function SignIn() { 40 | const [email, setEmail] = React.useState(''); 41 | const [password, setPassword] = React.useState(''); 42 | 43 | const navigate = useNavigate(); 44 | 45 | const handleSubmit = async (event: React.FormEvent) => { 46 | event.preventDefault(); 47 | const body = JSON.stringify({ 48 | email, 49 | password, 50 | }); 51 | const response = await fetch('/auth/login', { 52 | method: 'POST', 53 | headers: { 'Content-Type': 'application/json' }, 54 | body, 55 | }); 56 | 57 | if (response.status === 200) { 58 | navigate('/'); 59 | } 60 | }; 61 | 62 | return ( 63 | // 64 | 65 | 66 | 76 | t.palette.mode === 'light' 77 | ? t.palette.grey[50] 78 | : t.palette.grey[900], 79 | backgroundSize: 'cover', 80 | backgroundPosition: 'center', 81 | }} 82 | /> 83 | 84 | 93 | 94 | 95 | 96 | 97 | Sign in 98 | 99 | 105 | setEmail(e.target.value)} 116 | /> 117 | setPassword(e.target.value)} 128 | /> 129 | } 131 | label="Remember me" 132 | /> 133 | 141 | 166 | 167 | 168 | 169 | 170 | Forgot password? 171 | 172 | 173 | 174 | 175 | {"Don't have an account? Sign Up"} 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | // 185 | ); 186 | } 187 | -------------------------------------------------------------------------------- /client/auth/Signup.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Avatar from '@mui/material/Avatar'; 3 | import Button from '@mui/material/Button'; 4 | import CssBaseline from '@mui/material/CssBaseline'; 5 | import TextField from '@mui/material/TextField'; 6 | import FormControlLabel from '@mui/material/FormControlLabel'; 7 | import Checkbox from '@mui/material/Checkbox'; 8 | import Link from '@mui/material/Link'; 9 | import Grid from '@mui/material/Grid'; 10 | import Box from '@mui/material/Box'; 11 | import LockOutlinedIcon from '@mui/icons-material/LockOutlined'; 12 | import Typography from '@mui/material/Typography'; 13 | import Container from '@mui/material/Container'; 14 | import { createTheme, ThemeProvider } from '@mui/material/styles'; 15 | import { useNavigate } from 'react-router-dom'; 16 | 17 | function Copyright(props: any) { 18 | return ( 19 | 25 | {'Copyright © '} 26 | 27 | JetSetGo 28 | {' '} 29 | {new Date().getFullYear()} 30 | {'.'} 31 | 32 | ); 33 | } 34 | 35 | const theme = createTheme(); 36 | 37 | export default function SignUp() { 38 | const [firstName, setFirstName] = React.useState(''); 39 | const [lastName, setLastName] = React.useState(''); 40 | const [email, setEmail] = React.useState(''); 41 | const [password, setPassword] = React.useState(''); 42 | 43 | const navigate = useNavigate(); 44 | 45 | const handleSubmit = async (event: React.FormEvent) => { 46 | event.preventDefault(); 47 | // // const data = new FormData(event.currentTarget); 48 | // // console.log(data); 49 | // console.log(firstName, lastName, email, password); 50 | const body = JSON.stringify({ 51 | firstName, 52 | lastName, 53 | email, 54 | password, 55 | }); 56 | console.log(body); 57 | const response = await fetch('/auth/register', { 58 | method: 'POST', 59 | headers: { 'Content-Type': 'application/json' }, 60 | body, 61 | }); 62 | 63 | if (response.status === 200) { 64 | navigate('/'); 65 | } 66 | }; 67 | 68 | return ( 69 | // 70 | 71 | 72 | 80 | 81 | 82 | 83 | 84 | Sign up 85 | 86 | 92 | 93 | 94 | setFirstName(e.target.value)} 104 | /> 105 | 106 | 107 | setLastName(e.target.value)} 116 | /> 117 | 118 | 119 | setEmail(e.target.value)} 128 | /> 129 | 130 | 131 | setPassword(e.target.value)} 141 | /> 142 | 143 | 144 | 147 | } 148 | label="I want to receive inspiration, marketing promotions and updates via email." 149 | /> 150 | 151 | 152 | 160 | 161 | 162 | 163 | Already have an account? Sign in 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | // 172 | ); 173 | } 174 | -------------------------------------------------------------------------------- /client/components/ActionPrompt/ActionPrompt.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Button, 3 | Dialog, 4 | DialogActions, 5 | DialogContent, 6 | DialogContentText, 7 | DialogTitle, 8 | } from '@mui/material'; 9 | import React from 'react'; 10 | 11 | export interface PromptAction { 12 | /** 13 | * The label of the action button. 14 | */ 15 | label: string; 16 | /** 17 | * A callback function that is invoked when the action button is clicked. 18 | * @param event 19 | * @returns 20 | */ 21 | onClick: (event: React.MouseEvent) => void; 22 | } 23 | 24 | interface ActionPromptProps { 25 | /** 26 | * A boolean indicating whether the prompt should be displayed 27 | */ 28 | open: boolean; 29 | /** 30 | * A callback function that is invoked when the prompt is closed by the user 31 | */ 32 | onClose: () => void; 33 | /** 34 | * The title of the prompt 35 | */ 36 | title: string; 37 | /** 38 | * The text content of the prompt 39 | */ 40 | content: string; 41 | /** 42 | * An array of actions that the user can take. Each action will be displayed as a button and must consist of a label and onClick callback function. 43 | */ 44 | actions: PromptAction[]; 45 | } 46 | 47 | /** 48 | * A MUI-styled prompt that displays a title, content, and a set of actions that the user can take. 49 | */ 50 | const ActionPrompt: React.FC = ({ 51 | open, 52 | onClose, 53 | title, 54 | content, 55 | actions, 56 | }) => { 57 | return ( 58 | 63 | {title} 64 | 65 | {content} 66 | 67 | 68 | {actions.map((action) => ( 69 | 77 | ))} 78 | 79 | 80 | ); 81 | }; 82 | 83 | export default ActionPrompt; 84 | -------------------------------------------------------------------------------- /client/components/CreateTripForm/CreateTripForm.module.css: -------------------------------------------------------------------------------- 1 | .form { 2 | display: flex; 3 | flex-wrap: wrap; 4 | align-items: center; 5 | justify-content: space-between; 6 | width: min(80vw, 520px); 7 | height: 100%; 8 | padding: 20px; 9 | row-gap: 20px; 10 | margin: 0 auto; 11 | background-color: #ffffff50; 12 | border-radius: 6px; 13 | backdrop-filter: blur(10px); 14 | } 15 | 16 | .background { 17 | position: fixed; 18 | top: 0; 19 | left: 0; 20 | width: 100%; 21 | height: 100%; 22 | z-index: -1; 23 | } 24 | 25 | .container { 26 | width: 100%; 27 | height: 100%; 28 | } 29 | -------------------------------------------------------------------------------- /client/components/CreateTripForm/CreateTripForm.tsx: -------------------------------------------------------------------------------- 1 | import { Autocomplete, Button, TextField, Typography } from '@mui/material'; 2 | import React, { useEffect } from 'react'; 3 | import { Form, useNavigate } from 'react-router-dom'; 4 | import useFadeInOpacity from '../../hooks/useFadeInOpacity'; 5 | import styles from './CreateTripForm.module.css'; 6 | 7 | interface Place { 8 | name: string; 9 | place_id: string; 10 | } 11 | 12 | const CreateTripForm = () => { 13 | const navigate = useNavigate(); 14 | 15 | const [destination, setDestination] = React.useState(''); 16 | const [debounceDestination, setDebounceDestination] = React.useState(''); 17 | const [destinationOptions, setDestinationOptions] = React.useState( 18 | [] 19 | ); 20 | 21 | const fadeIn = useFadeInOpacity(); 22 | 23 | const [tripName, setTripName] = React.useState(''); 24 | const [startDate, setStartDate] = React.useState(''); 25 | const [endDate, setEndDate] = React.useState(''); 26 | 27 | // Debounce destination to avoid too many API calls - this will fetch only after 500ms of no input 28 | useEffect(() => { 29 | const timerId = setTimeout(() => { 30 | setDebounceDestination(destination); 31 | }, 500); 32 | 33 | return () => { 34 | clearTimeout(timerId); 35 | }; 36 | }, [destination]); 37 | 38 | // Fetch autocomplete data from API 39 | useEffect(() => { 40 | if (debounceDestination) { 41 | const fetchAutocomplete = async () => { 42 | const response = await fetch( 43 | `/api/places/autocomplete?input=${debounceDestination}` 44 | ); 45 | const data = (await response.json()) as Place[]; 46 | setDestinationOptions(data); 47 | }; 48 | fetchAutocomplete(); 49 | } 50 | }, [debounceDestination]); 51 | 52 | const handleSubmit = async (event: React.FormEvent) => { 53 | if (!destination || !startDate || !endDate) { 54 | return; 55 | } 56 | 57 | // Check if dates are valid and stored as ISO string 58 | const startTimestamp = Date.parse(startDate); 59 | const endTimestamp = Date.parse(endDate); 60 | 61 | if (isNaN(startTimestamp) || isNaN(endTimestamp)) { 62 | return; 63 | } 64 | 65 | if (startTimestamp > endTimestamp) { 66 | return; 67 | } 68 | 69 | const start = new Date(startTimestamp).toISOString(); 70 | const end = new Date(endTimestamp).toISOString(); 71 | 72 | // Get place_id from destinationOptions 73 | const place_id = destinationOptions.find( 74 | (option) => option.name === destination 75 | )?.place_id; 76 | 77 | if (!place_id) { 78 | return; 79 | } 80 | 81 | const name = tripName || destination; 82 | 83 | // Create trip in database 84 | const response = await fetch('/api/trips', { 85 | method: 'POST', 86 | headers: { 87 | 'Content-Type': 'application/json', 88 | }, 89 | body: JSON.stringify({ 90 | name, 91 | place_id: place_id, 92 | startDate: start, 93 | endDate: end, 94 | }), 95 | }); 96 | 97 | // Redirect to trip page 98 | if (response.status === 201) { 99 | const data = await response.json(); 100 | navigate(`/trip/${data.id}`); 101 | } 102 | }; 103 | 104 | return ( 105 |
106 | beach 112 |
113 |
114 | 119 | Plan your next adventure today! 120 | 121 | setTripName(e.target.value)} 125 | variant="outlined" 126 | label="Trip Name (Optional)" 127 | sx={{ minWidth: 480 }} 128 | /> 129 | option.name)} 133 | sx={{ minWidth: 480 }} 134 | inputValue={destination} 135 | onInputChange={(event, newInputValue) => { 136 | setDestination(newInputValue); 137 | }} 138 | renderInput={(params) => ( 139 | 145 | )} 146 | /> 147 | setStartDate(event.target.value)} 157 | required 158 | /> 159 | setEndDate(event.target.value)} 169 | required 170 | /> 171 | 179 |
180 |
181 |
182 | ); 183 | }; 184 | 185 | export default CreateTripForm; 186 | -------------------------------------------------------------------------------- /client/components/Logo/Logo.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | height: 100%; 6 | gap: 0.25rem; 7 | cursor: pointer; 8 | } 9 | 10 | .svg { 11 | height: 32px; 12 | width: 32px; 13 | display: flex; 14 | align-items: center; 15 | justify-content: center; 16 | flex-direction: column; 17 | } 18 | -------------------------------------------------------------------------------- /client/components/Logo/Logo.tsx: -------------------------------------------------------------------------------- 1 | import { Typography } from '@mui/material'; 2 | import React from 'react'; 3 | import { Link } from 'react-router-dom'; 4 | import styles from './Logo.module.css'; 5 | 6 | const Logo = () => { 7 | return ( 8 | 9 |
10 |
11 | 18 | 19 | 23 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 |
35 | 36 | 43 | JetSetGo 44 | 45 |
46 | 47 | ); 48 | }; 49 | 50 | export default Logo; 51 | -------------------------------------------------------------------------------- /client/components/Navigation/AppBar.tsx: -------------------------------------------------------------------------------- 1 | import { styled } from '@mui/material/styles'; 2 | import MuiAppBar, { AppBarProps as MuiAppBarProps } from '@mui/material/AppBar'; 3 | 4 | const drawerWidth = 240; 5 | 6 | interface AppBarProps extends MuiAppBarProps { 7 | open?: boolean; 8 | } 9 | 10 | const AppBar = styled(MuiAppBar, { 11 | shouldForwardProp: (prop) => prop !== 'open', 12 | })(({ theme, open }) => ({ 13 | transition: theme.transitions.create(['margin', 'width'], { 14 | easing: theme.transitions.easing.sharp, 15 | duration: theme.transitions.duration.leavingScreen, 16 | }), 17 | ...(open && { 18 | width: `calc(100% - ${drawerWidth}px)`, 19 | marginLeft: `${drawerWidth}px`, 20 | transition: theme.transitions.create(['margin', 'width'], { 21 | easing: theme.transitions.easing.easeOut, 22 | duration: theme.transitions.duration.enteringScreen, 23 | }), 24 | }), 25 | })); 26 | 27 | export default AppBar; 28 | -------------------------------------------------------------------------------- /client/components/Navigation/DrawerHeader.tsx: -------------------------------------------------------------------------------- 1 | import { styled } from '@mui/material/styles'; 2 | 3 | const DrawerHeader = styled('div')(({ theme }) => ({ 4 | display: 'flex', 5 | alignItems: 'center', 6 | padding: theme.spacing(0, 1), 7 | // necessary for content to be below app bar 8 | ...theme.mixins.toolbar, 9 | justifyContent: 'flex-end', 10 | })); 11 | 12 | export default DrawerHeader; 13 | -------------------------------------------------------------------------------- /client/components/Navigation/Main.tsx: -------------------------------------------------------------------------------- 1 | import { styled } from '@mui/material/styles'; 2 | 3 | const drawerWidth = 240; 4 | 5 | const Main = styled('main', { shouldForwardProp: (prop) => prop !== 'open' })<{ 6 | open?: boolean; 7 | }>(({ theme, open }) => ({ 8 | flexGrow: 1, 9 | padding: theme.spacing(3), 10 | transition: theme.transitions.create('margin', { 11 | easing: theme.transitions.easing.sharp, 12 | duration: theme.transitions.duration.leavingScreen, 13 | }), 14 | marginLeft: `-${drawerWidth}px`, 15 | ...(open && { 16 | transition: theme.transitions.create('margin', { 17 | easing: theme.transitions.easing.easeOut, 18 | duration: theme.transitions.duration.enteringScreen, 19 | }), 20 | marginLeft: 0, 21 | }), 22 | })); 23 | 24 | export default Main; 25 | -------------------------------------------------------------------------------- /client/components/Navigation/Navigation.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { useTheme } from '@mui/material/styles'; 3 | import Box from '@mui/material/Box'; 4 | import Drawer from '@mui/material/Drawer'; 5 | import CssBaseline from '@mui/material/CssBaseline'; 6 | import Toolbar from '@mui/material/Toolbar'; 7 | import List from '@mui/material/List'; 8 | import Divider from '@mui/material/Divider'; 9 | import IconButton from '@mui/material/IconButton'; 10 | import MenuIcon from '@mui/icons-material/Menu'; 11 | import ChevronLeftIcon from '@mui/icons-material/ChevronLeft'; 12 | import ChevronRightIcon from '@mui/icons-material/ChevronRight'; 13 | import ListItem from '@mui/material/ListItem'; 14 | import ListItemButton from '@mui/material/ListItemButton'; 15 | import ListItemIcon from '@mui/material/ListItemIcon'; 16 | import ListItemText from '@mui/material/ListItemText'; 17 | import Logo from '../Logo/Logo'; 18 | import Main from './Main'; 19 | import AppBar from './AppBar'; 20 | import DrawerHeader from './DrawerHeader'; 21 | 22 | const drawerWidth = 240; 23 | 24 | export interface DrawerItem { 25 | /** 26 | * The text to display in the drawer item. 27 | */ 28 | text: string; 29 | /** 30 | * The icon to display in the drawer item. 31 | */ 32 | icon: React.ReactElement; 33 | /** 34 | * A callback function to be called when the drawer item is clicked. 35 | * @param event 36 | */ 37 | onClick: (event: React.MouseEvent) => void; 38 | } 39 | 40 | interface NavigationProps { 41 | drawerItems?: DrawerItem[]; 42 | } 43 | 44 | const Navigation: React.FC> = ({ 45 | children, 46 | drawerItems, 47 | }) => { 48 | const theme = useTheme(); 49 | const [open, setOpen] = React.useState(false); 50 | 51 | const handleDrawerOpen = () => { 52 | setOpen(true); 53 | }; 54 | 55 | const handleDrawerClose = () => { 56 | setOpen(false); 57 | }; 58 | 59 | return ( 60 | 61 | 62 | 63 | 64 | {drawerItems && ( 65 | 72 | 73 | 74 | )} 75 | 76 | 77 | 78 | {drawerItems && ( 79 | 92 | 93 | 94 | {theme.direction === 'ltr' ? ( 95 | 96 | ) : ( 97 | 98 | )} 99 | 100 | 101 | 102 | 103 | {drawerItems.map((item) => ( 104 | 105 | 106 | {item.icon} 107 | 108 | 109 | 110 | ))} 111 | 112 | 113 | )} 114 |
115 | 116 | {children} 117 |
118 |
119 | ); 120 | }; 121 | 122 | export default Navigation; 123 | -------------------------------------------------------------------------------- /client/components/TripList/TripList.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | width: 100%; 3 | } 4 | 5 | .card { 6 | padding: 0.5rem; 7 | border-radius: 4px; 8 | box-shadow: rgba(0, 0, 0, 0.16) 0px 3px 6px, rgba(0, 0, 0, 0.23) 0px 3px 6px; 9 | display: flex; 10 | /* justify-content: space-between; */ 11 | align-items: center; 12 | } 13 | 14 | .card:hover { 15 | box-shadow: rgba(0, 0, 0, 0.19) 0px 10px 20px, rgba(0, 0, 0, 0.23) 0px 6px 6px; 16 | transform: translateY(-3px); 17 | } 18 | 19 | .link { 20 | text-decoration: none; 21 | color: #000; 22 | flex-grow: 1; 23 | } 24 | -------------------------------------------------------------------------------- /client/components/TripList/TripList.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Box, 3 | Button, 4 | Divider, 5 | IconButton, 6 | List, 7 | ListItem, 8 | ListItemText, 9 | Typography, 10 | } from '@mui/material'; 11 | import React from 'react'; 12 | import AddIcon from '@mui/icons-material/Add'; 13 | import styles from './TripList.module.css'; 14 | import DeleteIcon from '@mui/icons-material/Delete'; 15 | import { Link, useNavigate } from 'react-router-dom'; 16 | import ActionPrompt, { PromptAction } from '../ActionPrompt/ActionPrompt'; 17 | import { ITrip } from '../../../src/models/trip'; 18 | 19 | const TripList = () => { 20 | const [deletePrompt, setDeletePrompt] = React.useState(null); 21 | const [trips, setTrips] = React.useState([]); 22 | 23 | React.useEffect(() => { 24 | const fetchTrips = async () => { 25 | const response = await fetch('/api/trips'); 26 | const data = await response.json(); 27 | setTrips(data); 28 | }; 29 | 30 | fetchTrips(); 31 | }, []); 32 | 33 | const navigate = useNavigate(); 34 | 35 | const handleCreateTrip = () => { 36 | navigate('/trip/new'); 37 | }; 38 | 39 | const deletePromptActions: PromptAction[] = [ 40 | { 41 | label: 'Cancel', 42 | onClick: () => setDeletePrompt(null), 43 | }, 44 | { 45 | label: 'Delete', 46 | onClick: async () => { 47 | if (!deletePrompt) return; 48 | const deleteId = deletePrompt.id; 49 | 50 | const newTrips = trips.filter((trip) => trip.id !== deleteId); 51 | setTrips(newTrips); 52 | 53 | await fetch(`/api/trips/${deleteId}`, { 54 | method: 'DELETE', 55 | }); 56 | setDeletePrompt(null); 57 | }, 58 | }, 59 | ]; 60 | 61 | return ( 62 |
63 | setDeletePrompt(null)} 66 | title="Delete Trip" 67 | content={`Are you sure you want to permanently delete your trip ${deletePrompt?.name} to ${deletePrompt?.destination.name}?`} 68 | actions={deletePromptActions} 69 | /> 70 | 71 | 78 | 79 | 80 | 81 | 82 | {trips.map((trip) => ( 83 |
84 | 85 | 86 | 95 | {new Date(trip.startDate).toLocaleDateString()} -{' '} 96 | {new Date(trip.endDate).toLocaleDateString()} 97 | 98 | } 99 | /> 100 | 101 | 102 | setDeletePrompt(trip)}> 103 | 104 | 105 |
106 | ))} 107 |
108 |
109 |
110 | ); 111 | }; 112 | 113 | export default TripList; 114 | -------------------------------------------------------------------------------- /client/components/TripSummary/TripSummary.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | height: 400px; 3 | width: 100%; 4 | background-color: aqua; 5 | border-radius: 15px 15px 15px 15px; 6 | } 7 | 8 | .content { 9 | border-radius: 15px 15px 15px 15px; 10 | display: flex; 11 | flex-direction: column; 12 | align-items: flex-end; 13 | justify-content: flex-end; 14 | height: 100%; 15 | } 16 | 17 | .media { 18 | height: 240px; 19 | width: 100%; 20 | overflow: hidden; 21 | background-color: white; 22 | } 23 | -------------------------------------------------------------------------------- /client/components/TripSummary/TripSummary.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Card, 3 | CardContent, 4 | CardMedia, 5 | Grid, 6 | Paper, 7 | Typography, 8 | } from '@mui/material'; 9 | import { relative } from 'path'; 10 | import React from 'react'; 11 | import { ITrip } from '../../../src/models/trip'; 12 | import styles from './TripSummary.module.css'; 13 | 14 | interface TripSummaryProps { 15 | trip: ITrip; 16 | } 17 | 18 | const TripSummary: React.FC = ({ trip }) => { 19 | const [currentImage, setCurrentImage] = React.useState(0); 20 | 21 | React.useEffect(() => { 22 | const interval = setInterval(() => { 23 | setCurrentImage((currentImage + 1) % trip.destination.images.length); 24 | }, 5000); 25 | return () => clearInterval(interval); 26 | }, [currentImage, trip.destination.images.length]); 27 | 28 | const imageSlideshow = trip.destination.images.map((image, index) => { 29 | const opacity = index === currentImage ? 1 : 0; 30 | return ( 31 | 45 | ); 46 | }); 47 | 48 | return ( 49 | 50 |
{imageSlideshow}
51 | 60 |
61 | {/* Only show the destination as a subtitle if it differs from the trip name */} 62 | {trip.name !== trip.destination.name ? ( 63 | 64 | {trip.destination.name} 65 | 66 | ) : null} 67 | 68 | 69 | {trip.name} 70 | 71 | {`${new Date(trip.startDate).toLocaleDateString()} - ${new Date( 76 | trip.endDate 77 | ).toLocaleDateString()}`} 78 |
79 |
80 |
81 | ); 82 | }; 83 | 84 | export default TripSummary; 85 | -------------------------------------------------------------------------------- /client/components/Weather/Weather.module.css: -------------------------------------------------------------------------------- 1 | /* layout: */ 2 | /* styling: */ 3 | 4 | * { 5 | } 6 | html { 7 | /* layout: */ 8 | box-sizing: border-box; 9 | /* styling: */ 10 | } 11 | .container { 12 | /* layout: */ 13 | overflow: hidden; 14 | display: flex; 15 | justify-content: space-between; 16 | position: relative; 17 | width: 100%; 18 | height: auto; 19 | /* min-width: 400px; 20 | max-width: 600px; */ 21 | /* min-height: 60px; */ 22 | padding: 20px; 23 | /* styling: */ 24 | background-color: #2196f3; 25 | /* background-image: url(./images/clear-night-jetsetgo.jpg); */ 26 | background-size: 100% 100%; 27 | background-repeat: no-repeat; 28 | color: white; 29 | text-shadow: -1px 1px 4px #000, 1px 1px 4px #000, 1px -1px 0 #000, 30 | -1px -1px 0 #000; 31 | box-shadow: 0px 0px 14px 1px rgba(0, 0, 0, 0.75); 32 | font-weight: bold; 33 | border-radius: 15px 15px 15px 15px; 34 | } 35 | /* .imgContainer { */ 36 | /* width: 100%; */ 37 | /* height:100%; */ 38 | /* /* z-index:-1; */ 39 | /* } */ 40 | img { 41 | object-fit: cover; 42 | width: 100%; 43 | height: 100%; 44 | position: absolute; 45 | position: absolute; 46 | top: 0; 47 | left: 0; 48 | } 49 | 50 | .containerLeft { 51 | /* layout: */ 52 | z-index: 2; 53 | display: flex; 54 | flex-direction: column; 55 | justify-content: space-around; 56 | /* styling: */ 57 | } 58 | .containerConditions { 59 | display: flex; 60 | justify-content: left; 61 | gap: 10px; 62 | } 63 | .containerTemps { 64 | /* layout: */ 65 | display: flex; 66 | gap: 10px; 67 | justify-content: space-between; 68 | align-items: center; 69 | /* styling: */ 70 | } 71 | .currentTemp { 72 | font-size: 3rem; 73 | } 74 | .containerRight { 75 | /* layout: */ 76 | z-index: 2; 77 | display: flex; 78 | flex-direction: column; 79 | justify-content: center; 80 | align-items: center; 81 | gap: 30px; 82 | 83 | /* styling: */ 84 | } 85 | .location { 86 | /* layout: */ 87 | text-align: right; 88 | /* styling: */ 89 | } 90 | .time { 91 | margin-top: 15px; 92 | } 93 | /* .scaleToggle { 94 | } */ 95 | -------------------------------------------------------------------------------- /client/components/Weather/Weather.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react' 2 | import styles from './Weather.module.css' 3 | import Switch from '@mui/material/Switch' 4 | import WeatherIcon, { icons } from './WeatherIcon' 5 | 6 | 7 | export default function WeatherSummary({ lat, lon, location, start, end }: Props): React.ReactNode { 8 | const [apiResults, setApiResults] = useState(null) 9 | const [scale, setscale] = useState('imperial') 10 | const [img, setImg] = useState(icons.get('Clear')[1]) 11 | const url = `/api/weather?lat=${lat}&lon=${lon}&scale=metric` 12 | 13 | useEffect(() => { 14 | getData(url, setApiResults, setImgState, setImg) 15 | }, [lat, lon, scale]) 16 | 17 | if (!apiResults) return
...loading
18 | 19 | const temperature = 20 | scale === 'metric' 21 | ? Math.floor(apiResults.current.temp) 22 | : Math.floor(apiResults.current.temp * 1.8) + 32 23 | 24 | return ( 25 |
26 | img 27 |
28 |
29 | 30 |
{apiResults.current.weather[0].main}
31 |
32 |
33 |
{temperature.toString()}
34 |
{scale === 'imperial' ? 'F' : 'C'}
35 |
36 | setscale((prev) => (prev === 'imperial' ? 'metric' : 'imperial'))} 38 | /> 39 |
40 |
41 |
42 | 43 |
44 |
{getDateTime(apiResults.current.dt * 1000)[1]}
45 |
{getDateTime(apiResults.current.dt * 1000)[0]}
46 |
{location}
47 |
48 |
49 | ) 50 | } 51 | 52 | 53 | function getDateTime(code: number | Date): string[] { 54 | const date = new Date(code) 55 | let [hour, minutes] = [date.getHours(), date.getMinutes().toString()] 56 | //get HH:MM AM format 57 | let amPM = hour >= 12 ? 'PM' : 'AM' 58 | hour = hour % 12 ? hour % 12 : 12 59 | minutes = parseInt(minutes) < 10 ? '0' + minutes : minutes 60 | return [date.toDateString(), `${hour}:${minutes} ${amPM}`] 61 | } 62 | 63 | async function getData( 64 | url: string, 65 | setApiResults: Function, 66 | setImgState: Function, 67 | setImg: Function 68 | ) { 69 | const response = await fetch(url) 70 | const data = await response.json() 71 | setApiResults(data) 72 | setImgState(data, setImg) 73 | } 74 | 75 | function setImgState(data: ApiResponse, setImg: Function) { 76 | if ( 77 | data.current.weather[0].main === 'Clear' && 78 | (data.current.dt >= data.current.sunset || data.current.dt <= data.current.sunrise) 79 | ) 80 | setImg(icons.get('Clear-night')[1]) 81 | else setImg(icons.get(data.current.weather[0].main)[1]) 82 | } 83 | 84 | interface Props { 85 | lat: number 86 | lon: number 87 | location: string 88 | start: Date 89 | end: Date 90 | } 91 | 92 | //interface model of response from api call 93 | interface ApiResponse { 94 | current: { 95 | dt: number 96 | sunset: number 97 | sunrise: number 98 | temp: number 99 | weather: [ 100 | { 101 | main: string 102 | description: string 103 | } 104 | ] 105 | } 106 | daily: [ 107 | { 108 | dt: number 109 | temp: { 110 | min: number 111 | max: number 112 | } 113 | } 114 | ] 115 | } 116 | //
{getDateTime(apiResults.current.dt * 1000)[1]}
117 | //
{getDateTime(apiResults.current.dt * 1000)[0]}
118 | // CSS TEST COMP 119 | // return ( 120 | //
121 | //
122 | //
123 | // 124 | //
{'Clear'}
125 | //
126 | //
127 | //
128 | // {'58' + ' ' + 'F'} 129 | // setscale((prev) => (prev === 'imperial' ? 'metric' : 'imperial'))} 131 | // /> 132 | //
133 | //
134 | //
135 | // 136 | //
137 | //
{'9:30 PM'}
138 | //
{'Wed Jan 11 2023'}
139 | //
{'Tampa, US'}
140 | //
141 | //
142 | // ) 143 | // } 144 | -------------------------------------------------------------------------------- /client/components/Weather/WeatherIcon.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react' 2 | import CloudIcon from '@mui/icons-material/Cloud' 3 | import ThunderstormIcon from '@mui/icons-material/Thunderstorm' 4 | import TornadoIcon from '@mui/icons-material/Tornado' 5 | import OpacityIcon from '@mui/icons-material/Opacity' 6 | import PriorityHighRoundedIcon from '@mui/icons-material/PriorityHighRounded' 7 | import AcUnitRoundedIcon from '@mui/icons-material/AcUnitRounded' 8 | import WbSunnyRoundedIcon from '@mui/icons-material/WbSunnyRounded' 9 | 10 | interface Props { 11 | condition: string 12 | } 13 | 14 | export const icons = new Map() 15 | icons.set('Thunderstorm', [, '/images/stormy-jetsetgo.jpg']) 16 | icons.set('Drizzle', [, '/images/rainy-jetsetgo.jpg']) 17 | icons.set('Rain', [, '/images/rainy-jetsetgo.jpg']) 18 | icons.set('Snow', [, '/images/snowy-jetsetgo.jpg']) 19 | icons.set('Mist', [, '/images/foggyMist-jetsetgo.jpg']) 20 | icons.set('Smoke', [, '/images/foggyMist-jetsetgo.jpg']) 21 | icons.set('Dust', [, '/images/foggyMist-jetsetgo.jpg']) 22 | icons.set('Fog', [, '/images/foggyMist-jetsetgo.jpg']) 23 | icons.set('Sand', [, '/images/foggyMist-jetsetgo.jpg']) 24 | icons.set('Ash', [, '/images/foggyMist-jetsetgo.jpg']) 25 | icons.set('Squall', [, '/images/foggyMist-jetsetgo.jpg']) 26 | icons.set('Tornado', [, '/images/stormy-jetsetgo.jpg']) 27 | icons.set('Clouds', [, '/images/cloudy-jetsetgo.jpg']) 28 | icons.set('Clear', [, '/images/sunny-jetsetgo.jpg']) 29 | icons.set('Clear-night', [, '/images/clear-night-jetsetgo.jpg']) 30 | const WeatherIcon: React.FC = ({ condition }) => { 31 | return icons.get(condition)[0] 32 | } 33 | 34 | export default WeatherIcon 35 | -------------------------------------------------------------------------------- /client/components/packinglist/PackingList.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | /* display: flex; */ 3 | position: fixed; 4 | border-style: solid; 5 | border-radius: 15px 15px 15px 15px; 6 | background-color: aquamarine; 7 | } 8 | .packingInput { 9 | display: flex; 10 | align-items: center; 11 | justify-content: center; 12 | gap: 6px; 13 | } 14 | 15 | .CardContent { 16 | overflow-y: auto; 17 | } 18 | -------------------------------------------------------------------------------- /client/components/packinglist/PackingList.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { deleteItem } from './api/deleteItem'; 3 | import { getItems } from './api/getItems'; 4 | import { createItem } from './api/createItem'; 5 | import { checkItem } from './api/checkItem'; 6 | import { TItem } from './api/getItems'; 7 | import styles from './PackingList.module.css'; 8 | import List from '@mui/material/List'; 9 | import ListItem from '@mui/material/ListItem'; 10 | import ListItemButton from '@mui/material/ListItemButton'; 11 | import ListItemIcon from '@mui/material/ListItemIcon'; 12 | import ListItemText from '@mui/material/ListItemText'; 13 | import Checkbox from '@mui/material/Checkbox'; 14 | import DeleteIcon from '@mui/icons-material/Delete'; 15 | import LuggageOutlinedIcon from '@mui/icons-material/LuggageOutlined'; 16 | import { 17 | Card, 18 | IconButton, 19 | Input, 20 | TextField, 21 | CardContent, 22 | CardMedia, 23 | } from '@mui/material'; 24 | // import Input from "@mui/joy/Input"; 25 | import LuggageIcon from '@mui/icons-material/Luggage'; 26 | import Grid from '@mui/material/Grid'; 27 | import Typography from '@mui/material/Typography'; 28 | import Button from '@mui/material/Button'; 29 | import { ITrip } from '../../../src/models/trip'; 30 | import { ObjectId } from 'mongoose'; 31 | import { orange, amber, cyan, lightBlue } from '@mui/material/colors'; 32 | 33 | interface PackingListProps { 34 | trip: ITrip & { _id: ObjectId }; 35 | } 36 | 37 | //oranges 38 | const primary = orange['A400']; 39 | const accent = orange['A100']; 40 | const accent2 = amber[500]; 41 | //blues 42 | const primaryBlue = cyan['A400']; 43 | const accentBlue = cyan['A100']; 44 | const accent2Blue = cyan[500]; 45 | const accent3Blue = cyan['A900']; 46 | const PackingList: React.FC = ({ trip }) => { 47 | //set input state 48 | const [input, setInput] = useState(''); 49 | //set list state 50 | const [items, setItems] = useState(trip.packingList); 51 | 52 | //check off 53 | // const [checked, setChecked] = React.useState([0]); 54 | 55 | async function handleCreateItem(e: React.MouseEvent) { 56 | //declare const list and assign the awaited result of response.json 57 | const newItems = await createItem(input, trip._id.toString()); 58 | 59 | let checked = newItems.filter((item) => item.checked === true); 60 | let notchecked = newItems.filter((item) => item.checked !== true); 61 | let packing = notchecked.concat(checked); 62 | //append to the list state from backend 63 | setItems(packing); 64 | //clear out input when done 65 | 66 | setInput(''); 67 | } 68 | 69 | //for todo list...trying to get it to save to DB 70 | async function handleCheckItem(item: TItem) { 71 | const newItems = await checkItem(trip._id.toString(), item.toString()); 72 | let checked = newItems.filter((item) => item.checked === true); 73 | let notchecked = newItems.filter((item) => item.checked !== true); 74 | let packing2 = notchecked.concat(checked); 75 | console.log('pack2', packing2); 76 | setItems(packing2); 77 | } 78 | //handleDelete gets the argument of the item._id 79 | async function handleDelete(itemId: TItem) { 80 | const remainingItems = await deleteItem( 81 | trip._id.toString(), 82 | itemId.toString() 83 | ); 84 | let rest = remainingItems; 85 | console.log('delete', rest); 86 | let checked = remainingItems.filter((item) => item.checked === true); 87 | let notchecked = remainingItems.filter((item) => item.checked !== true); 88 | let updatedList = notchecked.concat(checked); 89 | 90 | setItems(updatedList); 91 | } 92 | 93 | //will grab the items associated with the trip and update everytime it changes 94 | useEffect(() => { 95 | async function fetchItems() { 96 | const newItems = await getItems(trip.id); 97 | newItems.sort((a, b) => (a.checked ? 1 : -1)); 98 | setItems(newItems); 99 | } 100 | fetchItems(); 101 | }, []); 102 | 103 | //form is for the bottom of the packing list to add another item 104 | return ( 105 | 106 | 111 | 112 | 113 | Packing List: 114 | 115 | 116 | 127 | 134 | {items.map((item, index) => { 135 | const labelId = `checkbox-list-label-${item.name}`; 136 | return ( 137 | handleDelete(item._id)} 144 | > 145 | 146 | 147 | } 148 | disablePadding 149 | > 150 | 155 | 156 | } 161 | checkedIcon={} 162 | inputProps={{ 'aria-labelledby': labelId }} 163 | checked={item.checked} 164 | onClick={() => handleCheckItem(item._id)} 165 | sx={{ 166 | color: accent[Symbol], 167 | '&.Mui-checked': { 168 | color: accent, 169 | }, 170 | }} 171 | /> 172 | 173 | 174 | 175 | 176 | ); 177 | })} 178 | 179 |
180 | {} 181 | setInput(e.target.value)} 185 | /> 186 | 200 |
201 |
202 |
203 |
204 |
205 | ); 206 | }; 207 | 208 | // const testPack = [{ name: "Shorts" }, { name: "Shirts" }, { name: "Pants" }]; 209 | 210 | export default PackingList; 211 | -------------------------------------------------------------------------------- /client/components/packinglist/api/checkItem.tsx: -------------------------------------------------------------------------------- 1 | import { API_URL } from "./config"; 2 | import { TItem } from "./getItems"; 3 | 4 | export async function checkItem( 5 | tripId: string, 6 | packingListId: string 7 | ): Promise { 8 | const response = await fetch(`${API_URL}/${tripId}/${packingListId}`, { 9 | method: "PATCH", 10 | }); 11 | console.log("response", response); 12 | const result: TItem = await response.json(); 13 | return result; 14 | } 15 | -------------------------------------------------------------------------------- /client/components/packinglist/api/config.tsx: -------------------------------------------------------------------------------- 1 | export const API_URL = "/api/packingList"; 2 | -------------------------------------------------------------------------------- /client/components/packinglist/api/createItem.ts: -------------------------------------------------------------------------------- 1 | import { API_URL } from "./config"; 2 | import { TItem } from "./getItems"; 3 | 4 | export async function createItem( 5 | input: string, 6 | tripId: string 7 | ): Promise { 8 | const response = await fetch(`${API_URL}/${tripId}`, { 9 | method: "POST", 10 | headers: { 11 | "Content-Type": "application/json", 12 | }, 13 | body: JSON.stringify({ 14 | item: input, 15 | }), 16 | }); 17 | const result: TItem[] = await response.json(); 18 | 19 | return result; 20 | } 21 | -------------------------------------------------------------------------------- /client/components/packinglist/api/deleteItem.ts: -------------------------------------------------------------------------------- 1 | // import { API_URL } from "./config"; 2 | // import { TItem } from "./getItems"; 3 | // export async function deleteItem( 4 | // tripId: string, 5 | // packingListId: string 6 | // ): Promise { 7 | // const response = await fetch(`${API_URL}/${tripId}/${packingListId}`, { 8 | // method: "DELETE", 9 | // }); 10 | // } 11 | 12 | import { API_URL } from "./config"; 13 | import { TItem } from "./getItems"; 14 | 15 | export async function deleteItem( 16 | tripId: string, 17 | packingListId: string 18 | ): Promise { 19 | const response = await fetch(`${API_URL}/${tripId}/${packingListId}`, { 20 | method: "DELETE", 21 | }); 22 | console.log("response", response); 23 | const result: void = await response.json(); 24 | console.log("result", result); 25 | return result; 26 | } 27 | 28 | // export async function checkItem( 29 | // tripId: string, 30 | // packingListId: string 31 | // ): Promise { 32 | // const response = await fetch(`${API_URL}/${tripId}/${packingListId}`, { 33 | // method: "PATCH", 34 | // }); 35 | // console.log("response", response); 36 | // const result = await response.json(); 37 | // return result; 38 | // } 39 | -------------------------------------------------------------------------------- /client/components/packinglist/api/getItems.ts: -------------------------------------------------------------------------------- 1 | import { API_URL } from "./config"; 2 | 3 | export interface TItem { 4 | name: string; 5 | checked: boolean; 6 | _id?: string; 7 | } 8 | 9 | // export async function getItems(tripId): Promise { 10 | // const response = await fetch(`${API_URL}`); 11 | // console.log("respones", response); 12 | // return response.json(); 13 | // } 14 | 15 | export async function getItems(tripId: string): Promise { 16 | const response = await fetch(`${API_URL}/${tripId}`); 17 | console.log("respones", response); 18 | return response.json(); 19 | } 20 | -------------------------------------------------------------------------------- /client/hooks/useFadeInOpacity.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | /** 4 | * A custom hook that will wait for a short period of time and then fade-in an element. Returns an object with the opacity and transition properties that can be passed or spread into the style prop of a component. 5 | * @param wait The amount of time to wait before fading in the element, in milliseconds. @default 200 6 | * @param duration The duration of the fade-in animation, in milliseconds. @default 700 7 | * @returns 8 | */ 9 | const useFadeInOpacity = (wait: number = 200, duration: number = 700) => { 10 | const [opacity, setOpacity] = React.useState(0); 11 | 12 | React.useEffect(() => { 13 | const timerId = setTimeout(() => { 14 | setOpacity(1); 15 | }, wait); 16 | 17 | return () => { 18 | clearTimeout(timerId); 19 | }; 20 | }, []); 21 | 22 | return { 23 | opacity, 24 | transition: `opacity ${duration}ms ease-in-out`, 25 | }; 26 | }; 27 | 28 | export default useFadeInOpacity; 29 | -------------------------------------------------------------------------------- /client/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | import App from './App'; 4 | 5 | const rootNode: HTMLElement = document.getElementById('app')!; 6 | const root = createRoot(rootNode); 7 | root.render(); 8 | -------------------------------------------------------------------------------- /client/routes/CreateTrip.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import CreateTripForm from '../components/CreateTripForm/CreateTripForm'; 3 | 4 | const CreateTrip = () => { 5 | return ( 6 |
7 | 8 |
9 | ); 10 | }; 11 | 12 | export default CreateTrip; 13 | -------------------------------------------------------------------------------- /client/routes/Error.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useNavigate, useRouteError } from 'react-router-dom'; 3 | import { AuthorizationStatus } from './Root'; 4 | 5 | interface ErrorResponse { 6 | data: AuthorizationStatus; 7 | } 8 | 9 | const ErrorPage: React.FC = () => { 10 | const { data: error } = useRouteError() as ErrorResponse; 11 | console.error('Error in navigating to desired route: ', error); 12 | // const navigate = useNavigate(); 13 | 14 | // React.useEffect(() => { 15 | // if (error.status === 401) { 16 | // console.log('Redirecting...'); 17 | // navigate('/signin', { replace: true }); 18 | // } 19 | // }, [error, navigate]); 20 | 21 | return ( 22 |
23 |

Oops!

24 |

Sorry, an unexpected error has occurred.

25 |

{error && {error.statusText || error.status}}

26 |
27 | ); 28 | }; 29 | 30 | export default ErrorPage; 31 | -------------------------------------------------------------------------------- /client/routes/Home.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TripList from '../components/TripList/TripList'; 3 | 4 | const HomePage = () => { 5 | return ; 6 | }; 7 | 8 | export default HomePage; 9 | -------------------------------------------------------------------------------- /client/routes/Root.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { json, Outlet, useNavigate } from 'react-router-dom'; 3 | import Navigation, { DrawerItem } from '../components/Navigation/Navigation'; 4 | import AddCircleOutlineIcon from '@mui/icons-material/AddCircleOutline'; 5 | import CardTravelIcon from '@mui/icons-material/CardTravel'; 6 | import AccountBoxIcon from '@mui/icons-material/AccountBox'; 7 | import LogoutIcon from '@mui/icons-material/Logout'; 8 | 9 | export interface AuthorizationStatus { 10 | status: number; 11 | statusText: string; 12 | } 13 | 14 | const Root = () => { 15 | const navigate = useNavigate(); 16 | 17 | const drawerItems: DrawerItem[] = React.useMemo( 18 | () => [ 19 | { 20 | text: 'New Trip', 21 | icon: , 22 | onClick: () => navigate('trip/new'), 23 | }, 24 | { 25 | text: 'My Trips', 26 | icon: , 27 | onClick: () => navigate('/'), 28 | }, 29 | { 30 | text: 'My Profile', 31 | icon: , 32 | onClick: () => navigate('profile'), 33 | }, 34 | 35 | { 36 | text: 'Logout', 37 | icon: , 38 | onClick: async () => { 39 | await fetch('/auth/signout'); 40 | navigate('/signin'); 41 | }, 42 | }, 43 | ], 44 | [navigate] 45 | ); 46 | return ( 47 | 48 | 49 | 50 | ); 51 | }; 52 | 53 | export default Root; 54 | -------------------------------------------------------------------------------- /client/routes/TripDashboard.tsx: -------------------------------------------------------------------------------- 1 | import { Box } from '@mui/material'; 2 | import { ObjectId } from 'mongoose'; 3 | import React from 'react'; 4 | import { useLoaderData } from 'react-router-dom'; 5 | import { ITrip } from '../../src/models/trip'; 6 | import PackingList from '../components/PackingList/PackingList'; 7 | import TripSummary from '../components/TripSummary/TripSummary'; 8 | import WeatherSummary from '../components/Weather/Weather'; 9 | export const loader = async ({ params }: { params: any }) => { 10 | const { tripId } = params; 11 | const response = await fetch(`/api/trips/${tripId}`); 12 | const data = await response.json(); 13 | return data; 14 | }; 15 | 16 | const TripDashboard = () => { 17 | const trip = useLoaderData() as ITrip & { _id: ObjectId }; 18 | 19 | return ( 20 | 26 | 27 | 28 | 29 | 30 | 37 | 38 | 39 | 40 | 41 | 42 | ); 43 | }; 44 | 45 | export default TripDashboard; 46 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jetsetgo", 3 | "version": "1.0.0", 4 | "description": "A travel planning application that helps you plan for and document your upcoming trips.", 5 | "main": "index.js", 6 | "scripts": { 7 | "build:dev": "tsc --watch --preserveWatchOutput", 8 | "start:dev": "nodemon dist/src/index.js & webpack -w", 9 | "start": "concurrently \"npm run build:dev\" \"npm run start:dev\"" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/jet-set-go/jetsetgo.git" 14 | }, 15 | "keywords": [], 16 | "author": "", 17 | "license": "ISC", 18 | "bugs": { 19 | "url": "https://github.com/jet-set-go/jetsetgo/issues" 20 | }, 21 | "homepage": "https://github.com/jet-set-go/jetsetgo#readme", 22 | "dependencies": { 23 | "@emotion/react": "^11.10.5", 24 | "@emotion/styled": "^11.10.5", 25 | "@googlemaps/google-maps-services-js": "^3.3.26", 26 | "@mui/icons-material": "^5.11.0", 27 | "@mui/joy": "^5.0.0-alpha.61", 28 | "@mui/material": "^5.11.3", 29 | "connect-mongo": "^4.6.0", 30 | "dotenv": "^16.0.3", 31 | "express": "^4.18.2", 32 | "express-session": "^1.17.3", 33 | "mongoose": "^6.8.2", 34 | "node-fetch": "^2.6.7", 35 | "passport": "^0.6.0", 36 | "passport-google-oauth2": "^0.2.0", 37 | "passport-google-oidc": "^0.1.0", 38 | "passport-local": "^1.0.0", 39 | "passport-oauth": "^1.0.0", 40 | "react": "^18.2.0", 41 | "react-dom": "^18.2.0", 42 | "react-google-button": "^0.7.2", 43 | "react-icons": "^4.7.1", 44 | "react-router-dom": "^6.6.1" 45 | }, 46 | "devDependencies": { 47 | "@babel/eslint-parser": "^7.19.1", 48 | "@babel/preset-env": "^7.20.2", 49 | "@babel/preset-react": "^7.18.6", 50 | "@types/bcrypt": "^5.0.0", 51 | "@types/express": "^4.17.15", 52 | "@types/express-session": "^1.17.5", 53 | "@types/node": "^18.11.18", 54 | "@types/node-fetch": "^2.6.2", 55 | "@types/passport": "^1.0.11", 56 | "@types/passport-google-oauth2": "^0.1.5", 57 | "@types/react": "^18.0.26", 58 | "@types/react-dom": "^18.0.10", 59 | "bcrypt": "^5.1.0", 60 | "concurrently": "^7.6.0", 61 | "css-loader": "^6.7.3", 62 | "nodemon": "^2.0.20", 63 | "prettier": "^2.8.2", 64 | "style-loader": "^3.3.1", 65 | "ts-loader": "^9.4.2", 66 | "typescript": "^4.9.4", 67 | "webpack": "^5.75.0", 68 | "webpack-cli": "^5.0.1" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /public/30c1db502ded3d490c8f.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmarrow1/jetsetgo/f48e556162b16127ba73e7753017fafe724afddc/public/30c1db502ded3d490c8f.jpg -------------------------------------------------------------------------------- /public/32b8cc2ef6d5b824ea05.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmarrow1/jetsetgo/f48e556162b16127ba73e7753017fafe724afddc/public/32b8cc2ef6d5b824ea05.jpg -------------------------------------------------------------------------------- /public/3642035e27afe222372e.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmarrow1/jetsetgo/f48e556162b16127ba73e7753017fafe724afddc/public/3642035e27afe222372e.jpg -------------------------------------------------------------------------------- /public/36d9f89eafea50d94f0f.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmarrow1/jetsetgo/f48e556162b16127ba73e7753017fafe724afddc/public/36d9f89eafea50d94f0f.jpg -------------------------------------------------------------------------------- /public/36d9f89eafea50d94f0fc765fe6ba067.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmarrow1/jetsetgo/f48e556162b16127ba73e7753017fafe724afddc/public/36d9f89eafea50d94f0fc765fe6ba067.jpg -------------------------------------------------------------------------------- /public/45c3f734296fc7749292.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmarrow1/jetsetgo/f48e556162b16127ba73e7753017fafe724afddc/public/45c3f734296fc7749292.jpg -------------------------------------------------------------------------------- /public/4f9b86feedba5a4bed0b.jpg: -------------------------------------------------------------------------------- 1 | export default "./dist/36d9f89eafea50d94f0fc765fe6ba067.jpg"; -------------------------------------------------------------------------------- /public/7e7f2f22ecbd00fbf2a6.jpg: -------------------------------------------------------------------------------- 1 | export default "./dist/a72df4dc18674d8bf0bd58afed0c673f.jpg"; -------------------------------------------------------------------------------- /public/8b552a59da567e947e94.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmarrow1/jetsetgo/f48e556162b16127ba73e7753017fafe724afddc/public/8b552a59da567e947e94.jpg -------------------------------------------------------------------------------- /public/94b3c1881adcab5feb18.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmarrow1/jetsetgo/f48e556162b16127ba73e7753017fafe724afddc/public/94b3c1881adcab5feb18.jpg -------------------------------------------------------------------------------- /public/94b3c1881adcab5feb183f90610ce9d3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmarrow1/jetsetgo/f48e556162b16127ba73e7753017fafe724afddc/public/94b3c1881adcab5feb183f90610ce9d3.jpg -------------------------------------------------------------------------------- /public/a72df4dc18674d8bf0bd.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmarrow1/jetsetgo/f48e556162b16127ba73e7753017fafe724afddc/public/a72df4dc18674d8bf0bd.jpg -------------------------------------------------------------------------------- /public/a72df4dc18674d8bf0bd58afed0c673f.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmarrow1/jetsetgo/f48e556162b16127ba73e7753017fafe724afddc/public/a72df4dc18674d8bf0bd58afed0c673f.jpg -------------------------------------------------------------------------------- /public/b09d5eda320cf9c9dbff.jpg: -------------------------------------------------------------------------------- 1 | export default "./dist/94b3c1881adcab5feb183f90610ce9d3.jpg"; -------------------------------------------------------------------------------- /public/f4412f5eab5cbb97f972.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmarrow1/jetsetgo/f48e556162b16127ba73e7753017fafe724afddc/public/f4412f5eab5cbb97f972.jpg -------------------------------------------------------------------------------- /public/images/clear-night-jetsetgo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmarrow1/jetsetgo/f48e556162b16127ba73e7753017fafe724afddc/public/images/clear-night-jetsetgo.jpg -------------------------------------------------------------------------------- /public/images/cloudy-jetsetgo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmarrow1/jetsetgo/f48e556162b16127ba73e7753017fafe724afddc/public/images/cloudy-jetsetgo.jpg -------------------------------------------------------------------------------- /public/images/foggyMist-jetsetgo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmarrow1/jetsetgo/f48e556162b16127ba73e7753017fafe724afddc/public/images/foggyMist-jetsetgo.jpg -------------------------------------------------------------------------------- /public/images/rainy-jetsetgo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmarrow1/jetsetgo/f48e556162b16127ba73e7753017fafe724afddc/public/images/rainy-jetsetgo.jpg -------------------------------------------------------------------------------- /public/images/snowy-jetsetgo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmarrow1/jetsetgo/f48e556162b16127ba73e7753017fafe724afddc/public/images/snowy-jetsetgo.jpg -------------------------------------------------------------------------------- /public/images/stormy-jetsetgo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmarrow1/jetsetgo/f48e556162b16127ba73e7753017fafe724afddc/public/images/stormy-jetsetgo.jpg -------------------------------------------------------------------------------- /public/images/sunny-jetsetgo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmarrow1/jetsetgo/f48e556162b16127ba73e7753017fafe724afddc/public/images/sunny-jetsetgo.jpg -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | -------------------------------------------------------------------------------- /public/style.css: -------------------------------------------------------------------------------- 1 | /* Global styles */ 2 | *, 3 | *:before, 4 | *:after { 5 | box-sizing: border-box; 6 | margin: 0; 7 | padding: 0; 8 | } 9 | -------------------------------------------------------------------------------- /src/Globals.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.css' { 2 | const content: any; 3 | export default content; 4 | } 5 | -------------------------------------------------------------------------------- /src/SessionData.d.ts: -------------------------------------------------------------------------------- 1 | import 'express-session'; 2 | 3 | declare module 'express-session' { 4 | export interface SessionData { 5 | userId: string; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/controllers/authController.ts: -------------------------------------------------------------------------------- 1 | import { Email } from '@mui/icons-material'; 2 | import { NextFunction, Request, Response } from 'express'; 3 | import User, { IUser } from '../models/userModel'; 4 | import bcrypt from 'bcrypt'; 5 | 6 | /** 7 | * This middleware function will log a user in with the provided email and password. It will then set the user's id on the session and set the user on res.locals.user. It will then call next() to allow the request to continue. If the email or password are not provided, this function will throw an error. If the email is not found in the database, or if the password does not match the password in the database, this function will throw an error. 8 | * @param req 9 | * @param res 10 | * @param next 11 | * @returns 12 | */ 13 | export const loginWithEmailAndPw = async ( 14 | req: Request, 15 | res: Response, 16 | next: NextFunction 17 | ) => { 18 | try { 19 | const { email, password } = req.body; // Abc123 20 | if (!email || !password) { 21 | throw new Error('Email and password are required'); 22 | } 23 | 24 | const user = await User.findOne({ email }); 25 | if (!user) { 26 | throw new Error('Invalid email or password'); 27 | } 28 | 29 | const userAuthenticated = await bcrypt.compare(password, user.password); 30 | if (!userAuthenticated) { 31 | throw new Error('Invalid email or password'); 32 | } 33 | 34 | req.session.userId = user.id; 35 | res.locals.user = user; 36 | 37 | return next(); 38 | } catch (error) { 39 | return next(error); 40 | } 41 | }; 42 | 43 | /** 44 | * This middleware function will register a new user with the provided email and password. It will hash the password before saving it to the database. It will then set the user's id on the session and set the user on res.locals.user. It will then call next() to allow the request to continue. 45 | * @param req 46 | * @param res 47 | * @param next 48 | * @returns 49 | */ 50 | export const registerWithEmailAndPw = async ( 51 | req: Request, 52 | res: Response, 53 | next: NextFunction 54 | ) => { 55 | try { 56 | const { email, password } = req.body; 57 | if (!email || !password) { 58 | throw new Error('Email and password are required'); 59 | } 60 | 61 | // TODO: Validate email is in correct format and password is long enough with required characters 62 | 63 | // TODO: Check if email is already in use 64 | 65 | const hashedPassword = await bcrypt.hash(password, 10); //Abc123 -> 83hrg9824hg2h34f89132h4f9h 66 | 67 | const user = new User({ 68 | email, 69 | password: hashedPassword, 70 | }); 71 | 72 | await user.save(); 73 | 74 | res.locals.user = user; 75 | 76 | return next(); 77 | } catch (error) { 78 | return next(error); 79 | } 80 | }; 81 | 82 | /** 83 | * This middleware function closes the gap between OAuth and traditional login. If the user is not using OAuth, the the req.user will be undefined. The req.user will then be populated with the user's information by using the userID property on req.session. If there is no req.session.userID or there is not matching user in the database, this function will not error. It will simply call next() and allow the request to continue. Use this middleware with each request before using any other middleware that accesses user data on the req.user property. 84 | * @param req 85 | * @param res 86 | * @param next 87 | * @returns 88 | */ 89 | export const getUser = async ( 90 | req: Request, 91 | res: Response, 92 | next: NextFunction 93 | ) => { 94 | try { 95 | const { userId } = req.session; 96 | if (!userId) { 97 | return next(); 98 | } 99 | const user = await User.findById(userId); 100 | if (!user) { 101 | return next(); 102 | } 103 | req.user = user; 104 | return next(); 105 | } catch (error) { 106 | return next(error); 107 | } 108 | }; 109 | 110 | /** 111 | * This middleware function will redirect the user to the login page if they are not authenticated. If they are authenticated, it will call next() and allow the request to continue. Note that if the route using this middleware is called by an AJAX request, the user will not be redirected. Instead, the request will fail with a 401 status code. 112 | * @param req 113 | * @param res 114 | * @param next 115 | * @returns 116 | */ 117 | export const authenticateUser = async ( 118 | req: Request, 119 | res: Response, 120 | next: NextFunction 121 | ) => { 122 | if (req.user) { 123 | console.log('Authenticated user: ', req.user); 124 | return next(); 125 | } 126 | res.status(401).redirect('/signin'); 127 | }; 128 | -------------------------------------------------------------------------------- /src/controllers/googleAuth.ts: -------------------------------------------------------------------------------- 1 | import User from '../models/userModel'; 2 | require('dotenv').config(); 3 | import passport from 'passport'; 4 | import oauth from 'passport-google-oauth2'; 5 | 6 | const GoogleStrategy = oauth.Strategy; 7 | 8 | passport.use( 9 | new GoogleStrategy( 10 | { 11 | clientID: process.env.GOOGLE_CLIENT_ID || '', 12 | clientSecret: process.env.GOOGLE_CLIENT_SECRET || '', 13 | callbackURL: 'http://localhost:3000/auth/google/callback', 14 | passReqToCallback: true, 15 | }, 16 | function ( 17 | request: any, 18 | accessToken: any, 19 | refreshToken: any, 20 | profile: any, 21 | done: any 22 | ) { 23 | done(null, profile); 24 | } 25 | ) 26 | ); 27 | 28 | // used to serialize the user for the session 29 | passport.serializeUser(function (user: any, done) { 30 | done(null, user); 31 | }); 32 | 33 | // used to deserialize the user 34 | passport.deserializeUser(function (user: any, done) { 35 | done(null, user); 36 | }); 37 | -------------------------------------------------------------------------------- /src/controllers/packingListController.ts: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | import { ConstructionOutlined } from "@mui/icons-material"; 4 | import { Request, Response, NextFunction } from "express"; 5 | import { ObjectId } from "mongoose"; 6 | import { TItem } from "../../client/components/PackingList/api/getItems"; 7 | import Trip, { ITrip } from "../models/trip"; 8 | 9 | export const getPackingList = async ( 10 | req: Request, 11 | res: Response, 12 | next: NextFunction 13 | ) => { 14 | //get the specific id of the trip 15 | console.log("res.params", req.params); 16 | try { 17 | const trip = res.locals.trip; 18 | res.locals.currentPackingList = trip.packingList; 19 | next(); 20 | } catch (e) { 21 | return next(e); 22 | } 23 | }; 24 | 25 | export const createItem = async ( 26 | req: Request, 27 | res: Response, 28 | next: NextFunction 29 | ) => { 30 | try { 31 | const item: TItem = { 32 | name: req.body.item, 33 | checked: false, 34 | }; 35 | const trip = res.locals.trip; 36 | trip.packingList.push(item); 37 | console.log("trip", trip); 38 | const result = await trip.save(); 39 | res.locals.packingList = result.packingList; 40 | next(); 41 | } catch (e) { 42 | return next(e); 43 | } 44 | }; 45 | 46 | // export const checkOff = async ( 47 | // req: Request, 48 | // res: Response, 49 | // next: NextFunction 50 | // ) => { 51 | // const { itemId } = req.params; 52 | // try { 53 | // const trip = res.locals.trip; 54 | // trip._id; 55 | // const packingList = trip.packingList; 56 | // const itemIdx = packingList.findIndex( 57 | // (item: TItem & { _id: ObjectId }) => item._id.toString() === itemId 58 | // ); 59 | // if (packingList[itemIdx].checked === true) { 60 | // packingList[itemIdx].checked = false; 61 | // } else { 62 | // packingList[itemIdx].checked = true; 63 | // } 64 | // trip.save(); 65 | // res.locals.packingList = trip.packingList; 66 | // next(); 67 | // } catch (e) { 68 | // return next(e); 69 | // } 70 | // }; 71 | 72 | export const checkOff = async ( 73 | req: Request, 74 | res: Response, 75 | next: NextFunction 76 | ) => { 77 | const { itemId } = req.params; 78 | try { 79 | const trip = res.locals.trip; 80 | trip._id; 81 | const packingList = trip.packingList; 82 | const itemIdx = packingList.findIndex( 83 | (item: TItem & { _id: ObjectId }) => item._id.toString() === itemId 84 | ); 85 | if (packingList[itemIdx].checked === true) { 86 | packingList[itemIdx].checked = false; 87 | } else { 88 | packingList[itemIdx].checked = true; 89 | } 90 | let checked = packingList.filter((item) => item.checked === true); 91 | let notchecked = packingList.filter((item) => item.checked !== true); 92 | let packing2 = notchecked.concat(checked); 93 | console.log("combo", packing2); 94 | trip.packingList = packing2; 95 | trip.save(); 96 | res.locals.packingList = trip.packingList; 97 | next(); 98 | } catch (e) { 99 | return next(e); 100 | } 101 | }; 102 | 103 | export const deleteItem = async ( 104 | req: Request, 105 | res: Response, 106 | next: NextFunction 107 | ) => { 108 | const { itemId } = req.params; 109 | 110 | try { 111 | //get current trip 112 | const trip = res.locals.trip; 113 | //filter the packingList of each trip 114 | const deletedItem = trip.packingList.filter( 115 | (item: TItem) => item._id.toString() == itemId 116 | ); 117 | const packingList = trip.packingList.filter( 118 | (item: TItem) => item._id.toString() !== itemId 119 | ); 120 | console.log("deletedItem", deletedItem); 121 | console.log("packingLIst", packingList); 122 | //set the packingList to the new packing list 123 | trip.packingList = packingList; 124 | //save the trip 125 | trip.save(); 126 | //return the deleted itm 127 | res.locals.packingList = trip.packingList; 128 | 129 | next(); 130 | } catch (e) { 131 | return next(e); 132 | } 133 | }; 134 | -------------------------------------------------------------------------------- /src/controllers/placesController.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from 'express'; 2 | import { 3 | Client, 4 | PlaceAutocompleteType, 5 | PlacePhoto, 6 | } from '@googlemaps/google-maps-services-js'; 7 | 8 | const client = new Client({}); 9 | 10 | /** 11 | * Returns of list of the ten most likely destinations based on the current input string. Expects an input parameter in the request query, which is the current input string. The list of autocomplete predictions will be attached to the response object as res.locals.places. 12 | * @param req 13 | * @param res 14 | * @param next 15 | * @returns 16 | */ 17 | export const getPlacesAutocomplete = async ( 18 | req: Request, 19 | res: Response, 20 | next: NextFunction 21 | ) => { 22 | try { 23 | const input = req.query.input as string; 24 | if (!input) throw new Error('Must provide input in request query.'); 25 | const { data } = await client.placeAutocomplete({ 26 | params: { 27 | input, 28 | types: PlaceAutocompleteType.regions, 29 | key: process.env.GOOGLE_MAPS_API_KEY || '', 30 | }, 31 | timeout: 1000, // milliseconds 32 | }); 33 | 34 | const matches = data.predictions.map((prediction) => { 35 | return { 36 | name: prediction.description, 37 | place_id: prediction.place_id, 38 | }; 39 | }); 40 | 41 | res.locals.places = matches; 42 | return next(); 43 | } catch (error) { 44 | console.log(error); 45 | return next(error); 46 | } 47 | }; 48 | 49 | /** 50 | * A middleware function that fetches place information from the Google Places API. Expects a place_id parameter in the request body. The place information will be attached to the response object as res.locals.place. 51 | * @param req 52 | * @param res 53 | * @param next 54 | * @returns 55 | */ 56 | export const getPlaceDetails = async ( 57 | req: Request, 58 | res: Response, 59 | next: NextFunction 60 | ) => { 61 | try { 62 | const { place_id } = req.body; 63 | if (!place_id) throw new Error('Must provide place_id in request body.'); 64 | 65 | const { data } = await client.placeDetails({ 66 | params: { 67 | place_id, 68 | key: process.env.GOOGLE_MAPS_API_KEY || '', 69 | }, 70 | }); 71 | 72 | res.locals.place = data.result; 73 | return next(); 74 | } catch (error) { 75 | return next(error); 76 | } 77 | }; 78 | 79 | /** 80 | * A middleware function that fetches place photos from the Google Places API. This must be invoked after middleware function getPlaceDetails. The photo urls will be attached to the response object as res.locals.photos. 81 | * @param req 82 | * @param res 83 | * @param next 84 | * @returns 85 | */ 86 | export const getPlacePhotos = async ( 87 | req: Request, 88 | res: Response, 89 | next: NextFunction 90 | ) => { 91 | try { 92 | const placeDetails = res.locals.place; 93 | if (!placeDetails) 94 | throw new Error( 95 | 'Middleware function getPlacePhotos must be invoked after getPlaceDetails.' 96 | ); 97 | 98 | const photos = []; 99 | 100 | for (let i = 0; i < 3; i++) { 101 | const photo = placeDetails.photos[i]; 102 | if (!photo) break; 103 | const response = await client.placePhoto({ 104 | params: { 105 | photoreference: photo.photo_reference, 106 | maxwidth: 400, 107 | key: process.env.GOOGLE_MAPS_API_KEY || '', 108 | }, 109 | responseType: 'arraybuffer', 110 | }); 111 | 112 | // Extracts the URL from the repsonse, ignoring the actual image data 113 | const url = response.request._redirectable._options.href as string; 114 | photos.push(url); 115 | } 116 | 117 | res.locals.photos = photos; 118 | 119 | return next(); 120 | } catch (error) { 121 | return next(error); 122 | } 123 | }; 124 | -------------------------------------------------------------------------------- /src/controllers/tripsController.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express'; 2 | import Trip from '../models/trip'; 3 | import { IUser } from '../models/userModel'; 4 | 5 | /** 6 | * A middleware function for creating a new trip. Must be called after the getPlaceDetails middleware. Expects name, startDate, and endDate in the request body. The resulting trip will be attached to the response object as res.locals.trip. 7 | * @param req 8 | * @param res 9 | * @param next 10 | * @returns 11 | */ 12 | export const createTrip = async ( 13 | req: Request, 14 | res: Response, 15 | next: NextFunction 16 | ) => { 17 | try { 18 | const { name, startDate, endDate } = req.body; 19 | if (!name || !startDate || !endDate) 20 | throw new Error('Must provide name, startDate, and endDate.'); 21 | 22 | const placeData = res.locals.place; 23 | console.log(placeData); 24 | if (!placeData) 25 | throw new Error( 26 | 'Middleware function createTrip must be invoked after getPlaceDetails.' 27 | ); 28 | 29 | const user = req.user as IUser; 30 | if (!user) throw new Error('Must be logged in to create trips.'); 31 | 32 | if (new Date(startDate) > new Date(endDate)) { 33 | throw new Error('Start date must be before end date.'); 34 | } 35 | 36 | const trip = { 37 | name, 38 | 39 | destination: { 40 | name: placeData.name, 41 | place_id: placeData.place_id, 42 | location: { 43 | lat: placeData.geometry.location.lat, 44 | lng: placeData.geometry.location.lng, 45 | }, 46 | images: res.locals.photos || [], 47 | }, 48 | login: { 49 | email: user.email, 50 | userId: user.id, 51 | }, 52 | startDate, 53 | endDate, 54 | }; 55 | 56 | const createdTrip = new Trip(trip); 57 | const result = await createdTrip.save(); 58 | 59 | res.locals.trip = result; 60 | return next(); 61 | } catch (error) { 62 | return next(error); 63 | } 64 | }; 65 | 66 | /** 67 | * This middleware function will fetch a trip from the database and attach it to the response object as res.locals.trip. It will also check that the trip belongs to the user. Expects an id parameter in the request params. 68 | * @param req 69 | * @param res 70 | * @param next 71 | * @returns 72 | */ 73 | export const getTrip = async ( 74 | req: Request, 75 | res: Response, 76 | next: NextFunction 77 | ) => { 78 | try { 79 | const { id } = req.params; 80 | if (!id) throw new Error('Must provide id in request params.'); 81 | 82 | const user = req.user as IUser; 83 | if (!user) throw new Error('Must be logged in to get trip information.'); 84 | 85 | const trip = await Trip.findById(id); 86 | if (!trip) throw new Error('Trip not found.'); 87 | 88 | if (trip.login.userId !== user.id) 89 | throw new Error('Trip belongs to another user.'); 90 | 91 | res.locals.trip = trip; 92 | return next(); 93 | } catch (error) { 94 | return next(error); 95 | } 96 | }; 97 | 98 | /** 99 | * This middleware function will delete a trip from the database if it belongs to the current user. Must be called after the getTrip middleware (expects a trip to already exist on the response object). 100 | * @param req 101 | * @param res 102 | * @param next 103 | * @returns 104 | */ 105 | export const deleteTrip = async ( 106 | req: Request, 107 | res: Response, 108 | next: NextFunction 109 | ) => { 110 | try { 111 | const user = req.user as IUser; 112 | if (!user) throw new Error('Must be logged in to delete trips.'); 113 | 114 | const trip = res.locals.trip; 115 | if (!trip) 116 | throw new Error( 117 | 'Middleware function deleteTrip must be invoked after getTrip.' 118 | ); 119 | if (trip.login.userId !== user.id) 120 | throw new Error('Trip belongs to another user.'); 121 | await trip.remove(); 122 | return next(); 123 | } catch (error) { 124 | return next(error); 125 | } 126 | }; 127 | 128 | /** 129 | * Gets all trips for the current user and attaches them as an array to the response object as res.locals.trips. 130 | * @param req 131 | * @param res 132 | * @param next 133 | * @returns 134 | */ 135 | export const getAllTrips = async ( 136 | req: Request, 137 | res: Response, 138 | next: NextFunction 139 | ) => { 140 | try { 141 | const user = req.user as IUser; 142 | if (!user) throw new Error('Must be logged in to get trip information.'); 143 | 144 | const trips = await Trip.find({ 145 | login: { userId: user.id, email: user.email }, 146 | }).exec(); 147 | res.locals.trips = trips; 148 | 149 | return next(); 150 | } catch (error) { 151 | return next(error); 152 | } 153 | }; 154 | -------------------------------------------------------------------------------- /src/controllers/weatherController.ts: -------------------------------------------------------------------------------- 1 | import { Response, Request, NextFunction } from 'express' 2 | import fetch from 'node-fetch' 3 | 4 | const controller = async (req: Request, res: Response, next: NextFunction) => { 5 | const { lat, lon, scale } = req.query 6 | const key = process.env.WEATHER_API_KEY2 7 | const apiURL = `https://api.openweathermap.org/data/3.0/onecall?lat=${lat}&lon=${lon}&units=${scale}&exclude=minutely,hourly&appid=${key}` 8 | 9 | const response = await fetch(apiURL) 10 | const data = await response.json() 11 | res.locals.data = data 12 | return next() 13 | } 14 | 15 | export default controller 16 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import placesRouter from './routes/placesRouter'; 2 | import tripsRouter from './routes/tripsRouter'; 3 | import weatherRouter from './routes/weather'; 4 | import packingListRouter from './routes/packingList'; 5 | import authRouter from './routes/authRouter'; 6 | import dotenv from 'dotenv'; 7 | import express, { NextFunction, Request, Response } from 'express'; 8 | import passport from 'passport'; 9 | import session from 'express-session'; 10 | import path from 'path'; 11 | import './controllers/googleAuth'; 12 | import mongoose from 'mongoose'; 13 | import MongoStore from 'connect-mongo'; 14 | import { authenticateUser, getUser } from './controllers/authController'; 15 | 16 | dotenv.config(); 17 | 18 | mongoose 19 | .connect(process.env.MONGODB_URI || '') 20 | .then(() => { 21 | console.log('Connection established!'); 22 | }) 23 | .catch(() => { 24 | console.log('Connection failed :('); 25 | }); 26 | 27 | const app = express(); 28 | 29 | //session and passport initialization 30 | app.use( 31 | session({ 32 | secret: 'sessionJetSetGo', 33 | resave: false, 34 | saveUninitialized: false, 35 | store: MongoStore.create({ mongoUrl: process.env.MONGODB_URI || '' }), 36 | }) 37 | ); 38 | app.use(passport.initialize()); 39 | app.use(passport.session()); 40 | 41 | app.use(express.json()); 42 | 43 | app.use(getUser); 44 | 45 | app.get('/', authenticateUser, (req: Request, res: Response) => { 46 | res.sendFile(path.join(__dirname, '../../public/index.html')); 47 | }); 48 | 49 | app.use(express.static(path.join(__dirname, '../../public'))); 50 | 51 | app.use('/auth', authRouter); 52 | app.use('/api/places', placesRouter); 53 | app.use('/api/trips', tripsRouter); 54 | app.use('/api/packingList', packingListRouter); 55 | app.use('/api/weather', weatherRouter); 56 | 57 | app.get('/signin', (req: Request, res: Response) => { 58 | res.sendFile(path.join(__dirname, '../../public/index.html')); 59 | }); 60 | 61 | app.get('/signup', (req: Request, res: Response) => { 62 | res.sendFile(path.join(__dirname, '../../public/index.html')); 63 | }); 64 | 65 | // This will catch all the routes and return index.html, and React Router will handle serving the correct page 66 | app.get('*', authenticateUser, (req: Request, res: Response) => { 67 | res.sendFile(path.join(__dirname, '../../public/index.html')); 68 | }); 69 | 70 | const PORT = 3000; 71 | 72 | app.listen(PORT, () => { 73 | console.log(`App listening on port ${PORT}`); 74 | }); 75 | -------------------------------------------------------------------------------- /src/models/f.json: -------------------------------------------------------------------------------- 1 | { 2 | "cookie": { 3 | "originalMaxAge": null, 4 | "expires": null, 5 | "httpOnly": true, 6 | "path": "/" 7 | }, 8 | "passport": { 9 | "user": { 10 | "provider": "google", 11 | "sub": "102784215597793508888", 12 | "id": "102784215597793508888", 13 | "displayName": "Alexander Durham", 14 | "name": { "givenName": "Alexander", "familyName": "Durham" }, 15 | "given_name": "Alexander", 16 | "family_name": "Durham", 17 | "email_verified": true, 18 | "verified": true, 19 | "language": "en", 20 | "email": "wvaviator@gmail.com", 21 | "emails": [{ "value": "wvaviator@gmail.com", "type": "account" }], 22 | "photos": [ 23 | { 24 | "value": "https://lh3.googleusercontent.com/a/AEdFTp5SpQ_yYDzBOAjf-cwvyusqrtk_0dau_tmkuB1q88E=s96-c", 25 | "type": "default" 26 | } 27 | ], 28 | "picture": "https://lh3.googleusercontent.com/a/AEdFTp5SpQ_yYDzBOAjf-cwvyusqrtk_0dau_tmkuB1q88E=s96-c", 29 | "_raw": "{\n \"sub\": \"102784215597793508888\",\n \"name\": \"Alexander Durham\",\n \"given_name\": \"Alexander\",\n \"family_name\": \"Durham\",\n \"picture\": \"https://lh3.googleusercontent.com/a/AEdFTp5SpQ_yYDzBOAjf-cwvyusqrtk_0dau_tmkuB1q88E\\u003ds96-c\",\n \"email\": \"wvaviator@gmail.com\",\n \"email_verified\": true,\n \"locale\": \"en\"\n}", 30 | "_json": { 31 | "sub": "102784215597793508888", 32 | "name": "Alexander Durham", 33 | "given_name": "Alexander", 34 | "family_name": "Durham", 35 | "picture": "https://lh3.googleusercontent.com/a/AEdFTp5SpQ_yYDzBOAjf-cwvyusqrtk_0dau_tmkuB1q88E=s96-c", 36 | "email": "wvaviator@gmail.com", 37 | "email_verified": true, 38 | "locale": "en" 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/models/trip.ts: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | import { TItem } from "../../client/components/PackingList/api/getItems"; 3 | export interface ITrip { 4 | name: string; 5 | id: string; 6 | destination: { 7 | name: string; 8 | location: { 9 | lat: number; 10 | lng: number; 11 | }; 12 | place_id: string; 13 | images: string[]; 14 | }; 15 | packingList: TItem[]; 16 | startDate: Date; 17 | endDate: Date; 18 | login: { 19 | userId: string; 20 | email: string; 21 | }; 22 | } 23 | 24 | export const tripSchema = new mongoose.Schema( 25 | { 26 | name: { type: String, required: true }, 27 | destination: { 28 | name: { type: String, required: true }, 29 | location: { 30 | lat: { type: Number, required: true }, 31 | lng: { type: Number, required: true }, 32 | }, 33 | place_id: { type: String, required: true }, 34 | images: [{ type: String }], 35 | }, 36 | packingList: [ 37 | { 38 | name: { type: String, required: true }, 39 | checked: { type: Boolean, required: true }, 40 | }, 41 | ], 42 | startDate: { type: Date, required: true }, 43 | endDate: { type: Date, required: true }, 44 | login: { 45 | userId: { type: String }, 46 | email: { type: String }, 47 | }, 48 | }, 49 | { 50 | toObject: { virtuals: true }, 51 | toJSON: { virtuals: true }, 52 | } 53 | ); 54 | 55 | export const testSchema = new mongoose.Schema({ 56 | name: { type: String }, 57 | number: { type: Number }, 58 | }); 59 | 60 | const Trip = 61 | (mongoose.models.Trip as mongoose.Model) || 62 | mongoose.model("Trip", tripSchema); 63 | 64 | export default Trip; 65 | -------------------------------------------------------------------------------- /src/models/userModel.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | const Schema = mongoose.Schema; 3 | 4 | export interface IUser { 5 | id: string; 6 | email: string; 7 | session: string; 8 | sso: boolean; 9 | firstName: string; 10 | lastName: string; 11 | username: string; 12 | password: string; 13 | displayName: string; 14 | photos: { value: string }[]; 15 | } 16 | 17 | const userSchema = new Schema( 18 | { 19 | email: { type: String, required: true, unique: true }, 20 | session: { type: String }, 21 | sso: { type: Boolean }, 22 | firstName: { type: String }, 23 | lastName: { type: String }, 24 | username: { type: String }, 25 | password: { type: String }, 26 | displayName: { type: String }, 27 | photos: [{ value: String }], 28 | }, 29 | { 30 | toObject: { virtuals: true }, 31 | toJSON: { virtuals: true }, 32 | } 33 | ); 34 | 35 | const User = 36 | (mongoose.models.User as mongoose.Model) || 37 | mongoose.model('User', userSchema); 38 | 39 | export default User; 40 | -------------------------------------------------------------------------------- /src/routes/authRouter.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, Router } from 'express'; 2 | import passport from 'passport'; 3 | import { 4 | authenticateUser, 5 | loginWithEmailAndPw, 6 | registerWithEmailAndPw, 7 | } from '../controllers/authController'; 8 | 9 | const router = Router(); 10 | 11 | // Google Auth Routes 12 | 13 | router.get( 14 | '/google', 15 | passport.authenticate('google', { scope: ['email', 'profile'] }) 16 | ); 17 | 18 | router.get( 19 | '/google/callback', 20 | passport.authenticate('google', { 21 | failureRedirect: '/auth/google/failure', 22 | }), 23 | (req: Request, res: Response) => { 24 | res.redirect('/'); 25 | } 26 | ); 27 | 28 | router.get('/google/failure', (req: Request, res: Response) => { 29 | res.send('Failure'); 30 | }); 31 | 32 | 33 | // Manual Auth Routes 34 | 35 | router.post( 36 | '/register', 37 | registerWithEmailAndPw, 38 | loginWithEmailAndPw, 39 | (req: Request, res: Response) => { 40 | res.status(200).json(res.locals.user); 41 | } 42 | ); 43 | 44 | router.post('/login', loginWithEmailAndPw, (req: Request, res: Response) => { 45 | res.status(200).json(res.locals.user); 46 | }); 47 | 48 | // Other Routes 49 | 50 | router.get('/user', authenticateUser, (req: Request, res: Response) => { 51 | return res.status(200).json(req.user); 52 | }); 53 | 54 | router.get('/signout', (req: Request, res: Response) => { 55 | req.logout((err: any) => { 56 | return res.status(500).send(err); 57 | }); 58 | res.status(200).send(); 59 | }); 60 | 61 | export default router; 62 | -------------------------------------------------------------------------------- /src/routes/packingList.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, Router } from "express"; 2 | 3 | import { 4 | createItem, 5 | deleteItem, 6 | getPackingList, 7 | checkOff, 8 | } from "../controllers/packingListController"; 9 | import { getTrip } from "../controllers/tripsController"; 10 | 11 | const router = Router(); 12 | 13 | //get individual packing list that is in the specific trip data 14 | router.get("/:id", getTrip, getPackingList, (req, res) => 15 | res.status(200).json(res.locals.currentPackingList) 16 | ); 17 | 18 | //add to packing list 19 | router.post("/:id", getTrip, createItem, (req, res) => 20 | res.status(200).json(res.locals.packingList) 21 | ); 22 | 23 | router.patch("/:id/:itemId", getTrip, checkOff, (req, res) => 24 | res.status(200).json(res.locals.packingList) 25 | ); 26 | 27 | router.delete("/:id/:itemId", getTrip, deleteItem, (req, res) => 28 | //return the deleted Item 29 | res.status(200).json(res.locals.packingList) 30 | ); 31 | 32 | export default router; 33 | -------------------------------------------------------------------------------- /src/routes/placesRouter.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, Router } from 'express'; 2 | import { getPlacesAutocomplete } from '../controllers/placesController'; 3 | 4 | const router = Router(); 5 | 6 | // Get a list of autocomplete predictions based on an input string. 7 | router.get( 8 | '/autocomplete', 9 | getPlacesAutocomplete, 10 | (req: Request, res: Response) => { 11 | return res.json(res.locals.places); 12 | } 13 | ); 14 | 15 | export default router; 16 | -------------------------------------------------------------------------------- /src/routes/tripBudget.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, Router } from "express"; 2 | 3 | // import { 4 | // createexpense, 5 | // deleteItem, 6 | // getPackingList, 7 | // checkOff, 8 | // } from "../controllers/packingListController"; 9 | // import { getTrip } from "../controllers/tripsController"; 10 | 11 | const router = Router(); 12 | 13 | // //get individual packing list that is in the specific trip data 14 | // router.get("/:id", getTrip, getPackingList, (req, res) => 15 | // res.status(200).json(res.locals.currentPackingList) 16 | // ); 17 | 18 | // //add to packing list 19 | // router.post("/:id", getTrip, createItem, (req, res) => 20 | // res.status(200).json(res.locals.packingList) 21 | // ); 22 | 23 | // router.patch("/:id/:itemId", getTrip, checkOff, (req, res) => 24 | // res.status(200).json(res.locals.packingList) 25 | // ); 26 | 27 | // router.delete("/:id/:itemId", getTrip, deleteItem, (req, res) => 28 | // //return the deleted Item 29 | // res.status(200).json(res.locals.deletedItem) 30 | // ); 31 | 32 | export default router; 33 | -------------------------------------------------------------------------------- /src/routes/tripsRouter.ts: -------------------------------------------------------------------------------- 1 | import { 2 | getPlaceDetails, 3 | getPlacePhotos, 4 | } from '../controllers/placesController'; 5 | import { Request, Response, Router } from 'express'; 6 | import { 7 | createTrip, 8 | deleteTrip, 9 | getAllTrips, 10 | getTrip, 11 | } from '../controllers/tripsController'; 12 | import { authenticateUser } from '../controllers/authController'; 13 | 14 | const router = Router(); 15 | 16 | // Get a complete list of trips for the current user 17 | router.get( 18 | '/', 19 | authenticateUser, 20 | getAllTrips, 21 | (req: Request, res: Response) => { 22 | return res.status(200).json(res.locals.trips); 23 | } 24 | ); 25 | 26 | // Create a new trip 27 | router.post( 28 | '/', 29 | authenticateUser, 30 | getPlaceDetails, 31 | getPlacePhotos, 32 | createTrip, 33 | (req: Request, res: Response) => { 34 | return res.status(201).json(res.locals.trip); 35 | } 36 | ); 37 | 38 | // Get a single trip by id 39 | router.get('/:id', authenticateUser, getTrip, (req: Request, res: Response) => { 40 | return res.status(200).json(res.locals.trip); 41 | }); 42 | 43 | // Delete a trip by id 44 | router.delete( 45 | '/:id', 46 | authenticateUser, 47 | getTrip, 48 | deleteTrip, 49 | (req: Request, res: Response) => { 50 | return res.status(200).json(res.locals.trip); 51 | } 52 | ); 53 | 54 | export default router; 55 | -------------------------------------------------------------------------------- /src/routes/weather.ts: -------------------------------------------------------------------------------- 1 | import { Router, Response, Request, NextFunction } from 'express' 2 | import weatherController from '../controllers/weatherController' 3 | const router = Router() 4 | router.get('/', weatherController, (req:Request, res:Response) => { 5 | res.status(200).json(res.locals.data) 6 | }) 7 | export default router 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | "jsx": "react" /* Specify what JSX code is generated. */, 17 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 26 | 27 | /* Modules */ 28 | "module": "commonjs" /* Specify what module code is generated. */, 29 | // "rootDir": "./", /* Specify the root folder within your source files. */ 30 | // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 31 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 34 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 35 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 37 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 38 | // "resolveJsonModule": true, /* Enable importing .json files. */ 39 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 40 | 41 | /* JavaScript Support */ 42 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 43 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 44 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 45 | 46 | /* Emit */ 47 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 48 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 49 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 50 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 51 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 52 | "outDir": "./dist" /* Specify an output folder for all emitted files. */, 53 | // "removeComments": true, /* Disable emitting comments. */ 54 | // "noEmit": true, /* Disable emitting files from a compilation. */ 55 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 56 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 57 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 58 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 59 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 60 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 61 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 62 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 63 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 64 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 65 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 66 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 67 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 68 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 69 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 70 | 71 | /* Interop Constraints */ 72 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 73 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 74 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, 75 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 76 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 77 | 78 | /* Type Checking */ 79 | "strict": true /* Enable all strict type-checking options. */, 80 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 81 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 82 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 83 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 84 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 85 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 86 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 87 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 88 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 89 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 90 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 91 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 92 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 93 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 94 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 95 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 96 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 97 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 98 | 99 | /* Completeness */ 100 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 101 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 102 | }, 103 | "include": ["src/", "client/"] 104 | } 105 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | entry: './client/index.tsx', 5 | module: { 6 | rules: [ 7 | { 8 | test: /\.tsx?$/, 9 | use: 'ts-loader', 10 | exclude: /node_modules/, 11 | }, 12 | { 13 | test: /\.css$/, 14 | use: [ 15 | 'style-loader', 16 | { 17 | loader: 'css-loader', 18 | options: { 19 | modules: true, 20 | }, 21 | }, 22 | ], 23 | }, 24 | ], 25 | }, 26 | mode: 'development', 27 | resolve: { 28 | extensions: ['.tsx', '.ts', '.js', '.css'], 29 | }, 30 | output: { 31 | filename: 'bundle.js', 32 | path: path.resolve(__dirname, 'public'), 33 | }, 34 | } 35 | --------------------------------------------------------------------------------