diff --git a/client/src/App.tsx b/client/src/App.tsx index f0e44aa9ce126dd08a77234a5a45dd71974f08e0..3099f9ed89406764e92b7108ee6bc1eca06858ca 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,124 +1,9 @@ -import React, {useEffect, useState} from "react"; -import {Route, Routes, useNavigate} from 'react-router-dom'; -import {FetchResult, useLazyQuery, useMutation, useReactiveVar} from "@apollo/client"; import "./style/App.css"; -import StartView from "./views/StartView"; -import HomeView from "./views/HomeView"; -import { LOAD_USER } from "./GraphQL/Queries"; -import { SET_USER } from "./GraphQL/Mutations"; -import { User } from "./interface/Interfaces"; -import { requestGetUser, usersEmailVar } from './index'; -import { wait } from "@testing-library/user-event/dist/utils"; - - - - - +import View from "./View"; const App = () => { - const [email, setEmail] = useState<string>(localStorage.getItem("email") || ""); - const [errorMsg, setErrorMsg] = useState<string | null>(null); - const [makeUser, setMakeUser] = useState<boolean>(false); - const usersEmail = useReactiveVar<string>(usersEmailVar); - const requestUserData = useReactiveVar<boolean>(requestGetUser); - - const navigateTo = useNavigate(); - - - //The state of 'email' is set in StartView, once a user submits an email to log in. - const [getUser, {refetch, data: getUserData, loading: getUserLoading, error: getUserError }] = useLazyQuery(LOAD_USER, { - variables: { - input: {email: email} - } - }); - - - - - - //creates user with the state of 'email', which is set in StartView. - const [createUser] = useMutation(SET_USER, - {refetchQueries: - [{query: LOAD_USER, variables: {input: {email: email}} - }], - awaitRefetchQueries: true, - }); - - - - //triggers the above mutation once called. Checks for error in the response data object and in the mutation responce as well. - //if a new user has been created sucsessfully - const createNewUser = () =>{ - createUser({variables: { - input: {email: email} - } - }).then((data) => { - if(data.errors) { - throw new Error(data.errors.toString()); - } else if(!data.data) { - throw new Error("Could not create new user: Data is empty"); - } - if(data.data.SetUser.email != ('' || null || undefined)){ //TODO her er problemet!!! Tar utgangspunkt i responsdataen fra mutasjonen og ikke - localStorage.setItem("email", data.data.SetUser.email); - usersEmailVar(data.data.SetUser.email); - navigateTo('/Home'); - } - }).catch((error)=>{ - setErrorMsg(error); - }) + return ( + <View/> + ); }; - - - - //the reactive variable that usersEmail points to, is set to '' once a user logs out (see the logout function in Header.tsx). - //usersEmail is set to something other than '' once a user has submitted an email on the correct format in the StartView. - useEffect(() => { - if ( usersEmail != ''){ - setMakeUser(false); - getUser(); - } - }, [requestUserData]) - - - - - //email is set in localStorage once the user has - useEffect(() => { - if(getUserData != undefined){ - if(getUserData.GetUser != null) { - //setEmail(usersEmail as string); - localStorage.setItem("email", getUserData.GetUser.email as string); - navigateTo('/Home'); - setMakeUser(false) - } - else{ - setMakeUser(true) - } - }else{ - return; - } - }, [getUserData]) // - - - - useEffect(() => { - if(makeUser==true){ - createNewUser(); - } - }, [makeUser]) - - - - - //Users (the one with storedList) needs to be rendered before any CountryCards - return ( - <Routes> - <Route path="/" element={<StartView setEmail={(e: string) => { setEmail(e) }} />}/> - <Route path="/Home" element={<HomeView user={getUserData && getUserData.GetUser} poll={refetch}/>}/> - </Routes> - ); -}; - - - -export default App; +export default App; \ No newline at end of file diff --git a/client/src/GraphQL/Mutations.ts b/client/src/GraphQL/Mutations.ts index 7268d33c653e676542b2f209bb0928647c5ab0c4..bfa7ce5c7fef0036198b162b3abe62a107c03d66 100644 --- a/client/src/GraphQL/Mutations.ts +++ b/client/src/GraphQL/Mutations.ts @@ -1,30 +1,61 @@ import {gql} from '@apollo/client'; -//Sets a user with an empty list -export const SET_USER = gql` - mutation SetUser($input: EmailInput) { - SetUser(input: $input){ +//Updates user with savedList +export const UPDATE_USER = gql` + mutation UpdateUser($user: UserInput) { + UpdateUser(user: $user){ email - savedCountries { + favList { countryID - isFav - isVis - description + countryName + } + visList { + countryID + countryName } } } `; -//Updates user with savedList -export const UPDATE_USER = gql` - mutation UpdateUser($input: UserInput) { - UpdateUser(input: $input){ - email - savedCountries { - countryID - isFav - isVis - description - } + +export const ADD_FAVOURITE = gql` +mutation AddFavourite($user: UserInput, $favourite: SavedCountryInput) { + AddFavourites(user: $user, favourite: $favourite) { + favList { + countryID + countryName } } +} `; + + +export const ADD_VISITED = gql` +mutation AddVisited($user: UserInput, $visited: SavedCountryInput) { + AddVisited(user: $user, visited: $visited) { + visList { + countryID + countryName + } + } +} +`; +export const REMOVE_FAVOURITE = gql` +mutation RemoveFavourite($user: UserInput, $favourite: SavedCountryInput) { + RemoveFavourite(user: $user, favourite: $favourite) { + favList { + countryID + countryName + } + } +} +`; +export const REMOVE_VISITED = gql` +mutation RemoveVisited($user: UserInput, $visited: SavedCountryInput) { + RemoveVisited(user: $user, visited: $visited) { + visList { + countryID + countryName + } + } +} +`; \ No newline at end of file diff --git a/client/src/GraphQL/Queries.ts b/client/src/GraphQL/Queries.ts index dee12aa131c47875c3ec32872a2a7ebe7f5a216f..49bfbe7aa3d143044353d61388e57de852acc0cf 100644 --- a/client/src/GraphQL/Queries.ts +++ b/client/src/GraphQL/Queries.ts @@ -1,31 +1,18 @@ import { gql } from "@apollo/client"; - //loads a single user export const LOAD_USER = gql` - query GetUser($input: EmailInput) { - GetUser(input: $input) { + query GetUser($email: String) { + GetUser(email: $email) { email - savedCountries { + favList { countryID - isFav - isVis - description + countryName + } + visList { + countryID + countryName } - } - } -`; - -//loads a collection of countries -export const LOAD_COUNTRIES_BY_ID = gql` - query GetCountriesByID($input: [Int]) { - GetCountriesByID(input: $input) { - countryID - countryName - region - population - area - gdp } } `; @@ -42,44 +29,4 @@ export const LOAD_ALL_COUNTRIES = gql` gdp } } -`; - -//loads sorted countries -export const LOAD_COUNTRIES_SORT = gql` - query GetSortedCountries($input: SortBy, $offset: Int, $limit: Int) { - GetSortedCountries(input: $input, offset: $offset, limit: $limit) { - countryID - countryName - region - population - area - gdp - } - } -`; -//loads countries based on full name search -export const LOAD_COUNTRIES_SEARCH = gql` - query GetCountriesBySearch($input: [String], $offset: Int, $limit: Int) { - GetCountriesBySearch(input: $input, offset: $offset, limit: $limit) { - countryID - countryName - region - population - area - gdp - } - } -`; -//loads countries filtered by region -export const LOAD_COUNTRIES_FILTER = gql` - query GetCountriesByRegion($input: [String], $offset: Int, $limit: Int) { - GetCountriesByRegion(input: $input, offset: $offset, limit: $limit) { - countryID - countryName - region - population - area - gdp - } - } -`; +`; \ No newline at end of file diff --git a/client/src/View.tsx b/client/src/View.tsx new file mode 100644 index 0000000000000000000000000000000000000000..2a5f5d9d77b8e45983b77af053b8d4a60d143084 --- /dev/null +++ b/client/src/View.tsx @@ -0,0 +1,83 @@ +import React, { useState } from "react"; +import "./style/View.css"; +import { useQuery } from "@apollo/client"; +import {LOAD_ALL_COUNTRIES} from "./GraphQL/Queries"; +import { Country } from "./interface/Interfaces"; +import CountryCard, { CountryCardEvent } from "./components/CountryCard"; +import SavedList from "./components/SavedList"; +import Header from "./components/Header"; +import Filter from "./components/Filter"; +import Search from "./components/Search" +import Fab from "@mui/material/Fab"; +import { Typography } from "@mui/material"; + +const View = () => { + //const [errorMessage, setErrorMessage] = useState<string>("ERROR!"); + const [isMobile, setIsMobile] = useState(false); + //const [search, setSearch] = useState<string>(''); + const regionDefault: string[] = []; + + const { error, loading, fetchMore, refetch, data } = useQuery(LOAD_ALL_COUNTRIES, { + variables: { offset: 0, limit: 10, search: "", sort: {}, region: regionDefault}, + }); + + return ( + <> + <div id="View"> + <SavedList/> + <Header/> + <div id='searchBox'> + <Typography id='searchInfo' className='searchEl' variant="h6" component="div" sx={{ flexGrow: 1 }}>Search for a country you have visited or want to visit...</Typography> + <div id="searchElements"> + <Search onChange={s => refetch({search: s})} /> + {<h4> OR </h4>} + <Filter id='Filter' setFilter={(r: string[]) => refetch({region: r})} /> + </div> + </div> + + <div id='searchResults'> + {loading || error ? ( + <> + {error && <p> Could not load countries: {error.message} </p>} + {loading && <p> Loading... Please wait.</p>} + </> + ) : ( + <> + <h2>All countries</h2> + <div className="Countries"> + {data && + data.GetAllCountries.map((country: Country) => { + return ( + <CountryCard + key={country.countryID} + country={country} + isFav={false} + isVis={false} + countryAction={function (countryID: number, event: CountryCardEvent): void { + throw new Error("Function not implemented."); + } } /> + ); + })} + {data && ( + + <Fab variant="extended" id = 'loadMore' size="medium" color="primary" aria-label="Press to load more countries" onClick={async (inView) => { + const currentLen = data.GetAllCountries.length || 0; + await fetchMore({ + variables: { + offset: currentLen, + limit: currentLen+10, + }, + }); + }} sx={{borderRadius:10, minHeight: '40px', maxHeight: '40px', minWidth: '50%'}}> + Load More Countries + </Fab> + )} + </div> + </> + )} + </div> + </div> + </> + ); +}; +export default View; \ No newline at end of file diff --git a/client/src/___test___/MockGQLData.ts b/client/src/___test___/MockGQLData.ts index 73250d879dd06383a904c276b181d6db1b1ce744..0952bd6eb39a1fa17c265b990d7244ee086cf5f9 100644 --- a/client/src/___test___/MockGQLData.ts +++ b/client/src/___test___/MockGQLData.ts @@ -1,14 +1,10 @@ -import { - LOAD_COUNTRIES_BY_ID, - LOAD_ALL_COUNTRIES - } from "../GraphQL/Queries"; - +import {LOAD_ALL_COUNTRIES} from "../GraphQL/Queries"; import { MockedProvider } from "@apollo/client/testing"; export const mockedFilteredCCountryQuery = { request: { - query: LOAD_COUNTRIES_BY_ID, + query: LOAD_ALL_COUNTRIES, variables: { countryID: 1, countryName: "Afganistan", diff --git a/client/src/components/Card.tsx b/client/src/components/Card.tsx index f18cda6d4102e5c50ea4e7d61b7c55df16951102..04c12b2bde7886cd6cfd22d62f2192adbfd4bfda 100644 --- a/client/src/components/Card.tsx +++ b/client/src/components/Card.tsx @@ -1,4 +1,3 @@ -import "../style/Card.css" import React, {useState} from 'react' interface CardProps { diff --git a/client/src/components/CountryCard.tsx b/client/src/components/CountryCard.tsx index 8a02a935fc7b1e51e905c0bee3c3342a69960501..2a163af0b40f4d0b0fde027c1c0df38db96b061c 100644 --- a/client/src/components/CountryCard.tsx +++ b/client/src/components/CountryCard.tsx @@ -13,7 +13,7 @@ import DoneOutlineIcon from '@mui/icons-material/DoneOutline'; export interface countryCardT { country: Country; isFav: boolean; - isVisited: boolean; + isVis: boolean; countryAction: (countryID: number, event: CountryCardEvent) => void; }; @@ -23,9 +23,7 @@ export enum CountryCardEvent { Visited }; - - -const CountryCard: React.FC<countryCardT> = ({ country, isFav, isVisited, countryAction }) => { +const CountryCard: React.FC<countryCardT> = ({ country, isFav, isVis, countryAction }) => { const [expand, setExpand] = useState<boolean>(false); @@ -37,6 +35,7 @@ const CountryCard: React.FC<countryCardT> = ({ country, isFav, isVisited, countr } } + @@ -80,4 +79,4 @@ const CountryCard: React.FC<countryCardT> = ({ country, isFav, isVisited, countr ); }; -export default CountryCard; \ No newline at end of file +export default CountryCard; diff --git a/client/src/components/FavouriteCountry.tsx b/client/src/components/FavouriteCountry.tsx deleted file mode 100644 index 60403593cc405cb8632fa29d5f707b35cba10de0..0000000000000000000000000000000000000000 --- a/client/src/components/FavouriteCountry.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import React, { useState } from 'react'; - -import { Country, SavedCountry, User } from "../interface/Interfaces"; -import Fab from "@mui/material/Fab"; -import FavoriteIcon from '@mui/icons-material/Favorite'; -import CheckIcon from '@mui/icons-material/Check'; -import Card from './Card'; - - - -export interface FavouriteCountryInterface { - country: Country; - onChange: (country: Country, type: string) => void; -} - - - - -const FavouriteCountry: React.FC<FavouriteCountryInterface> = ( { country, onChange} ) => { - const [editing, setEditing] = useState<boolean>(false); - //const [textarea, setTextarea] = useState<string>(''); - - - - return ( - <Card className="card favCountry" id={country.countryID.toString()}> - - <p aria-label='country name'>{ country.countryName }</p> - - <> - <Fab title='Remove favorite from travel diary' id="removeCountry" size="small" color="primary" onClick={() => onChange(country, "unfav")} aria-description="Click this button to mark country as visited" > - <FavoriteIcon /> - </Fab> - </> - - <> - <Fab title='Remove visited from travel diary' id="removeCountry" size="small" color="primary" onClick={() => onChange(country, "unvisit")} aria-description="Click this button to mark country as visited" > - <CheckIcon /> - </Fab> - </> - - - </Card> - ) -} - -export default FavouriteCountry; \ No newline at end of file diff --git a/client/src/components/Filter.tsx b/client/src/components/Filter.tsx index 32c7cf354c585274d7e1e75cd5ea4fcc3e55f0c1..3f66911d387036f8c6d96f931e048c469415ba59 100644 --- a/client/src/components/Filter.tsx +++ b/client/src/components/Filter.tsx @@ -1,8 +1,4 @@ import * as React from "react"; -import { darkThemeVar } from "../index"; -import { useReactiveVar } from "@apollo/client"; - -import { Theme, ThemeProvider, useTheme } from "@mui/material/styles"; import OutlinedInput from "@mui/material/OutlinedInput"; import InputLabel from "@mui/material/InputLabel"; import MenuItem from "@mui/material/MenuItem"; @@ -12,11 +8,8 @@ import Stack from '@mui/material/Stack'; import Button from '@mui/material/Button'; import Fab from "@mui/material/Fab"; - - - const ITEM_HEIGHT = 48; -const ITEM_PADDING_TOP = 8; //TODO Inplementer hooks i homeview +const ITEM_PADDING_TOP = 8; const MenuProps = { PaperProps: { style: { @@ -35,18 +28,7 @@ const area = [ "Oceania", ]; -function getStyles(type: any, typeName: any, theme: any) { - return { - fontWeight: - typeName.indexOf(type) === -1 - ? theme.typography.fontWeightRegular - : theme.typography.fontWeightMedium, - }; -} - export default function Filter({ setFilter }: any) { - const darkTheme = useReactiveVar<Theme>(darkThemeVar); - const theme = useTheme(); const [typeName, setTypeName] = React.useState([]); const handleChange = (event: any) => { @@ -62,8 +44,6 @@ export default function Filter({ setFilter }: any) { return ( <div id='Filter'> - - <ThemeProvider theme={darkTheme}> <FormControl sx={{ width: '100%'}} color="success"> <InputLabel id="filterLabel" @@ -87,18 +67,12 @@ export default function Filter({ setFilter }: any) { <MenuItem key={name} value={name} - style={getStyles(name, typeName, theme)} > {name} </MenuItem> ))} - </Select> - </FormControl> - - </ThemeProvider> </div> ); -} - +} \ No newline at end of file diff --git a/client/src/components/Header.tsx b/client/src/components/Header.tsx index 78db982ee1901b10fc11b7ca09cb5976152c7532..2a079231df9499c3689542f4dd2a61764921a9b3 100644 --- a/client/src/components/Header.tsx +++ b/client/src/components/Header.tsx @@ -1,22 +1,11 @@ -import React from 'react'; -import { useNavigate } from "react-router-dom"; -import { useReactiveVar } from '@apollo/client'; -import { darkThemeVar, usersEmailVar } from '../index'; -import GlobeLogo from "../resources/globe.png" - -import { createTheme, styled, useTheme } from '@mui/material/styles'; +import * as React from 'react'; +import { styled, ThemeProvider, createTheme } from '@mui/material/styles'; import Box from '@mui/material/Box'; +import CssBaseline from '@mui/material/CssBaseline'; import MuiAppBar, { AppBarProps as MuiAppBarProps } from '@mui/material/AppBar'; import Toolbar from '@mui/material/Toolbar'; import Typography from '@mui/material/Typography'; -import { ThemeProvider, Theme } from '@mui/material/styles'; -import Fab from '@mui/material/Fab'; -import ImageListItem from '@mui/material/ImageListItem'; - - - - - +import TravelExploreIcon from '@mui/icons-material/TravelExplore'; const drawerWidth = 240; interface AppBarProps extends MuiAppBarProps { @@ -25,84 +14,39 @@ interface AppBarProps extends MuiAppBarProps { const AppBar = styled(MuiAppBar, { shouldForwardProp: (prop) => prop !== 'open', - }) - - <AppBarProps>(({ theme, open }) => ({transition: theme.transitions.create(['margin', 'width'], { +})<AppBarProps>(({ theme, open }) => ({ + transition: theme.transitions.create(['margin', 'width'], { }), ...(open && { - /*width: `calc(100% - ${drawerWidth}px)`,*/ - width: `100%`, + width: `calc(100% - ${drawerWidth}px)`, marginLeft: `${drawerWidth}px`, transition: theme.transitions.create(['margin', 'width'], { }), }), })); - - - - - - - export default function PersistentDrawerLeft() { - const darkTheme = useReactiveVar<Theme>(darkThemeVar); - const usersEmail = useReactiveVar<string>(usersEmailVar); - const navigateTo = useNavigate(); - - - - const logOut = (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => { - usersEmailVar(''); - navigateTo("/"); - } - - - const greyTheme = createTheme({ - palette: { - primary: { - main: '#3b3b3b', - }, - }, - }); - - //<CssBaseline /> - + const colorTheme = createTheme({ + palette: { + primary: { + main: '#324736e2', + }, + }, + }); return ( - - <Box sx={{ flexGrow: 1 }}> - - - <ThemeProvider theme={greyTheme}> - - <AppBar color='primary'> + <Box sx={{ display: 'flex' }}> + <CssBaseline /> + <ThemeProvider theme={colorTheme}> + <AppBar color="primary"> <Toolbar> - <ImageListItem key={GlobeLogo} > - <img src={GlobeLogo} alt="Travel diary logo" loading="lazy" id='headerLogo' /> - </ImageListItem> - <Typography variant="h6" component="div"> Travel Diary </Typography> - - <Typography variant="h6" component="div" id='space' sx={{ flexGrow: 1 }}></Typography> - - <Typography variant="h6" component="div" id='usersEmailHeader' aria-label='e-mail' aria-description='email of the user that is logged in' sx={{ paddingRight:'2%'}}> - {usersEmail} + <Typography variant="h6" noWrap component="div"> + <TravelExploreIcon sx={{ fontSize: 45 }} /> Travel Diary </Typography> - - <ThemeProvider theme={darkTheme}> - <Fab variant="extended" id="logOutBtn" size="medium" color="primary" aria-label="press button to log out" onClick={(e) => logOut(e)}> - Log out - </Fab> - </ThemeProvider> </Toolbar> </AppBar> - </ThemeProvider> - - </Box> - - + </Box> ); -} - +} \ No newline at end of file diff --git a/client/src/components/SavedList.tsx b/client/src/components/SavedList.tsx index 26ec10fba03034cb4ed0b083a9d142532a0ab261..38b75ce2b7dbc4f838f80a17721a2dc46ce94378 100644 --- a/client/src/components/SavedList.tsx +++ b/client/src/components/SavedList.tsx @@ -1,62 +1,125 @@ import React, { useState, useEffect } from 'react' - -import { LOAD_COUNTRIES_BY_ID } from "../GraphQL/Queries"; -import { useQuery } from '@apollo/client'; -import { Country, SavedCountry } from '../interface/Interfaces'; -import FavouriteCountry from './FavouriteCountry'; - +import { LOAD_USER } from "../GraphQL/Queries"; +import { Country, SavedCountry, User } from '../interface/Interfaces'; +import { useLazyQuery, useMutation } from "@apollo/client"; +import FormControl from '@mui/material/FormControl'; +import InputLabel from '@mui/material/InputLabel'; +import OutlinedInput from '@mui/material/OutlinedInput'; import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp'; - - +import "../style/SavedList.css"; +import { ADD_FAVOURITE, ADD_VISITED, REMOVE_FAVOURITE, REMOVE_VISITED } from '../GraphQL/Mutations'; export interface SavedListProps { countries: SavedCountry[]; onChange: (country: Country, change: any) => void; } - - //this function displays the SavedCountry elements in the users storedList -const SavedList: React.FC<SavedListProps> = ( { countries, onChange } ) => { +const SavedList = () => { const [expand, setExpand] = useState<boolean>(false); const [editing, setEditing] = useState<boolean>(false); + const [user, setUser] = useState<String>(); + const [currentUser, setCurrentUser] = useState<User>(); + const [getUser, {data: getUserData, loading: getUserLoading, error: getUserError }] = useLazyQuery(LOAD_USER, { + variables: {email: user} + }); + const [removeVis, {data: removeVisData}] = useMutation(REMOVE_VISITED); - const {error, loading, data, refetch} = useQuery(LOAD_COUNTRIES_BY_ID, { - variables: { - input: countries.map(c => c.countryID) - } - }); + const [removeFav, {data: removeFavData}] = useMutation(REMOVE_FAVOURITE); + const [addVis, { data: addVisData}] = useMutation(ADD_VISITED); + const [addFav, { data: addFavData}] = useMutation(ADD_FAVOURITE); - useEffect(() => { - refetch({input: countries.map(c => c.countryID)}) - }, [countries, refetch]) + const updateEmail = (email: String) => { + setUser(email); + } + const handleSubmit = () => { + console.log("Fetching user"+user); + getUser(); + setCurrentUser(getUserData); + } + const handleRemoveFav = (id: Number, name: String) => { + console.log("Handle remove Fav "+id); + if (currentUser) { + removeFav({variables: { + email: currentUser?.email, + favourite: { + countryID: id, + countryName: name + } + }}); + currentUser.favList = removeFavData; + } + } + + const handleRemoveVis = (id: Number, name: String) => { + console.log("Handle remove vis "+id); + + if (currentUser) { + removeVis({variables: { + email: currentUser?.email, + favourite: { + countryID: id, + countryName: name + } + }}); + currentUser.visList = removeVisData; + } + } + + useEffect(() => { + + }, [currentUser]) + return ( - <div id='savedList' > + <div id='savedList' > + <FormControl id='loginForm' onSubmit={(e) => {e.preventDefault(); handleSubmit()}}> + <InputLabel htmlFor="inputEmail">E-mail</InputLabel> + <OutlinedInput id="inputEmail" onChange={(e) => updateEmail(e.target.value)} label="E-mail*" aria-label="email input field"/> + <button type="button" onClick={(e) => { + e.preventDefault(); + handleSubmit(); + }}>Submit</button> + </FormControl> <div className='top' onClick={() =>{setExpand(!expand)}}> - <h3>My travel diary</h3> + <h2>My travel diary</h2> <p >{expand? <KeyboardArrowUpIcon/> : <KeyboardArrowDownIcon/>}</p> </div> - <div id="savedListContent"> + <div id="favListContent"> <h3>Favorite Countries</h3> {expand? <> <> - {error && <p> Error when loading data: {error.message} </p>} - {loading && <p> Loading... Please wait. </p> } + {getUserError && <p> Error when loading data: {getUserError.message} </p>} + {getUserLoading && <p> Loading... Please wait. </p> } </> <> - {data && data.GetCountriesByID && - data.GetCountriesByID.map((country: Country) => { - return <FavouriteCountry key={country.countryID} country={country} onChange={onChange} /> - }) + {currentUser && + currentUser.favList.map((c: { countryID: number; countryName: string; }) => <li key={c.countryID}>{c.countryName}<button onClick={() => handleRemoveFav(c.countryID, c.countryName)}>Remove</button></li>) + } + </> + </> + : + null + } + </div> + <div id="visListContent"> <h3>Visited Countries</h3> + {expand? + <> + <> + {getUserError && <p> Error when loading data: {getUserError.message} </p>} + {getUserLoading && <p> Loading... Please wait. </p> } + </> + <> + {currentUser && + currentUser.visList.map((c: { countryID: number; countryName: string; }) => <li key={c.countryID}>{c.countryName}<button onClick={() => handleRemoveVis(c.countryID, c.countryName)}>Remove</button></li>) } </> </> @@ -67,6 +130,4 @@ const SavedList: React.FC<SavedListProps> = ( { countries, onChange } ) => { </div> ) }; - - export default SavedList; \ No newline at end of file diff --git a/client/src/components/Search.tsx b/client/src/components/Search.tsx index 96e5e72564e434c8e5bff282a398e9c6bad3d4e3..b774fc428f9b762cbb7fbe3124145e0973cf15d3 100644 --- a/client/src/components/Search.tsx +++ b/client/src/components/Search.tsx @@ -1,31 +1,21 @@ import React from 'react'; import TextField from '@mui/material/TextField'; -import { useReactiveVar } from "@apollo/client"; -import { darkThemeVar } from "../index"; -import { Theme, ThemeProvider } from '@mui/material/styles'; - export interface SearchProps { onChange: (search: string) => void } - export default function Search( { onChange } : SearchProps) { - const darkTheme = useReactiveVar<Theme>(darkThemeVar); - - return ( <> - <ThemeProvider theme={darkTheme}> - <TextField - color='secondary' - id="filled-basic" - label="Search for a country..." - variant="filled" - onChange={e => onChange(e.target.value)} - helperText="First letter upper-case, the rest lower-case (Example; Norway)" - /> - </ThemeProvider> + <TextField + color='secondary' + id="filled-basic" + label="Search for a country..." + variant="filled" + onChange={e => onChange(e.target.value)} + helperText="First letter upper-case, the rest lower-case (Example; Norway)" + /> </> ); } \ No newline at end of file diff --git a/client/src/index.tsx b/client/src/index.tsx index c72b50f7c877ec7c6b101b9c072c874811ff814e..a61d6f7812e17fef58400bdbbf654b0e400d4f0f 100644 --- a/client/src/index.tsx +++ b/client/src/index.tsx @@ -1,17 +1,8 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; -import { ApolloClient, InMemoryCache, ApolloProvider, makeVar} from '@apollo/client'; -import {Route, Routes, useNavigate} from 'react-router-dom'; -import {HashRouter} from 'react-router-dom'; - -import { Theme, createTheme } from '@mui/material/styles'; - - -import { SavedCountry, User } from './interface/Interfaces'; -import './style/index.css'; +import { ApolloClient, InMemoryCache, ApolloProvider} from '@apollo/client'; import App from './App'; import { offsetLimitPagination } from '@apollo/client/utilities'; -//import reportWebVitals from './reportWebVitals'; export const cache = new InMemoryCache({ typePolicies: { @@ -28,29 +19,6 @@ const client = new ApolloClient({ uri: 'http://it2810-63.idi.ntnu.no:8080/graphql' }); - - - -//dark theme for MaterialUI components; -const darkTheme = createTheme({ - palette: { - mode: 'dark', - }, -}); - - - -export const darkThemeVar = makeVar<Theme>(darkTheme); -export const usersEmailVar = makeVar<string>(''); -export const requestGetUser = makeVar<boolean>(false); - - - - - - - - const root = ReactDOM.createRoot( document.getElementById('root') as HTMLElement ); @@ -58,10 +26,7 @@ root.render( <ApolloProvider client={client}> <React.StrictMode> - <HashRouter> <App /> - </HashRouter> </React.StrictMode> </ApolloProvider> -); - +); \ No newline at end of file diff --git a/client/src/interface/Interfaces.ts b/client/src/interface/Interfaces.ts index b027a6fa839a04732804ad89cd67a315dd67cad4..a52b46adf1bd628879d90ff1a737cd8d9f5f803e 100644 --- a/client/src/interface/Interfaces.ts +++ b/client/src/interface/Interfaces.ts @@ -1,5 +1,3 @@ - - export interface Country { countryID: number, countryName: string, @@ -9,16 +7,13 @@ export interface Country { gdp: number } - export interface SavedCountry { countryID: number, - isFav: boolean, - isVis: boolean, - description: string + countryName: string } - export interface User { email: string, - savedCountries: SavedCountry[] + favList: SavedCountry[], + visList: SavedCountry[] } \ No newline at end of file diff --git a/client/src/style/App.css b/client/src/style/App.css index 91c291df8c88b81adcaa0ef73dfd102d77f4e077..b895d18f665edceb206bae163a7c160ade1eee7a 100644 --- a/client/src/style/App.css +++ b/client/src/style/App.css @@ -3,7 +3,6 @@ font-family: "Roboto","Helvetica","Arial",sans-serif; } - #logOutBtn{ right:0px; } @@ -13,8 +12,6 @@ max-width: 70px; } - - .errorMsgField{ color: red; display: flex; @@ -22,6 +19,6 @@ justify-content: center; } - - - +.body { + background-color: black; +} \ No newline at end of file diff --git a/client/src/style/Card.css b/client/src/style/Card.css deleted file mode 100644 index 7de98e14c7ccff7abbe7fd9227b113c6ffca778b..0000000000000000000000000000000000000000 --- a/client/src/style/Card.css +++ /dev/null @@ -1,32 +0,0 @@ -/* Default style for all Cards */ - -.card{ - color: white; - border-radius: 8px; - padding: 2%; - max-width: 100%; - border: 4; - contain: content; - overflow: none; - background-color: #3b3b3b; -} - - - -.card:hover{ - transform: scale(1.01); - -} - - -/* mobile */ -@media only screen and (max-width: 414px) { - .card{ - font-size: 10pt; - } - - .card h4{ - font-size: 12pt; - } - -} \ No newline at end of file diff --git a/client/src/style/CountryCard.css b/client/src/style/CountryCard.css index 5b78ef68e775bc2302dcc3ec4cdf12d0507178a3..831b21a780d750c83dfe52b960a10f6229baaefc 100644 --- a/client/src/style/CountryCard.css +++ b/client/src/style/CountryCard.css @@ -5,14 +5,12 @@ max-width: 90%; contain: content; overflow: none; - background-color: #3b3b3b; + background-color: #213630; width: 70vw; margin-bottom: 10px; position: relative; } - - #top { display: grid; grid-template-columns: 8fr 1fr 1fr; @@ -25,13 +23,11 @@ margin-bottom: 5%; } - #name{ display: grid; justify-content: left; } - p{ display: grid; grid-template-columns: auto auto; @@ -46,22 +42,10 @@ p{ margin: 1% 1%; } - - -#description { - border: 1px solid transparent; - border-radius: 10px; - box-shadow: 0 1px 8px rgba(0, 0, 0, 0.13); - padding: 1%; -} - - - - /* smaller screens */ @media only screen and (max-width: 800px) { .cardCountry{ width: 100%; } -} +} \ No newline at end of file diff --git a/client/src/style/FavouriteCountry.css b/client/src/style/FavouriteCountry.css deleted file mode 100644 index d5960cc067f7a92b990c55232afc6b76ac035f52..0000000000000000000000000000000000000000 --- a/client/src/style/FavouriteCountry.css +++ /dev/null @@ -1,8 +0,0 @@ - - - -.favList{ - width: 100%; - display: grid; - grid-template-rows: auto auto auto auto; -} \ No newline at end of file diff --git a/client/src/style/SavedList.css b/client/src/style/SavedList.css new file mode 100644 index 0000000000000000000000000000000000000000..4bcb678f8ec33fbc2459102402e4bbf3f4ca62d0 --- /dev/null +++ b/client/src/style/SavedList.css @@ -0,0 +1,40 @@ +#savedList{ + display: grid; + color: rgb(252, 252, 252); + margin-top: 50px; + width:40%; + padding: 3%; + height: fit-content; + background: #5a4523d7; + contain: content; + overflow: scroll; + border: 2px #f2f1f4 solid; + border-radius: 10px; + grid-template-rows: auto; +} + +#favListContent, #visListContent{ + display: grid; + flex-direction: column; +} + +#inputEmail.label{ + color: white; +} + +.button{ + width: min-content; + display: grid; + color: #5a4523d7; + background-color: #f1f1f1; +} + +.favoriteCountries{ + display: inline-block; + border: 1px solid rgb(194, 203, 194); +} + +.visitedCountries{ + display: inline-block; + border: 1px solid rgb(194, 203, 194); +} \ No newline at end of file diff --git a/client/src/style/StartView.css b/client/src/style/StartView.css deleted file mode 100644 index 420acdfcb15694f6190a33130345db1ed15bb9fa..0000000000000000000000000000000000000000 --- a/client/src/style/StartView.css +++ /dev/null @@ -1,73 +0,0 @@ -/* - Styling the Card containing the input field for the userID -*/ - -.App{ - padding-top: 10%; -} - - -#StartView{ - margin-top: 10vh; -} - - -#logo{ - display: flex; - justify-content: center; - margin-bottom: 10vh; -} - - -.loginCard { - width: 50%; - margin: auto; - padding: 6%; - min-width: 200px; - display: grid; - grid-template-rows: repeat(auto-fit, minmax(20%, 1fr)); - row-gap: 1em; - grid-template-columns: 1fr; -} - - -#loginForm{ - display: grid; - grid-template-rows: repeat(auto-fit, minmax(50px, 1fr)); - row-gap: 10%; - padding-bottom: 10%; -} - - - -#introText{ - font-size: 1.5em; - margin: 2%; - display: flex; - justify-content: center; -} - - - - -#submitEmailContainer, #globeLogo{ - display: flex; - flex-direction: row; - justify-content: center; -} - - - - -/* Smart phone */ -@media only screen and (max-width: 414px) { - .loginCard { - width: 80%; - margin: auto; - display: grid; - grid-template-rows: repeat(auto-fit, minmax(20%, 1fr)); - row-gap: 1em; - grid-template-columns: 1fr; - } - -} \ No newline at end of file diff --git a/client/src/style/HomeView.css b/client/src/style/View.css similarity index 62% rename from client/src/style/HomeView.css rename to client/src/style/View.css index 384d203a41f5938b9524b651761ba27e9c18a95c..521cc0d941963e40da1a254bc2f43f70a62a85f2 100644 --- a/client/src/style/HomeView.css +++ b/client/src/style/View.css @@ -1,79 +1,33 @@ .App{ padding: 6%; - color: white; + color: rgb(14, 14, 14); text-align: center; } -#HomeView { +#View { display: flex; flex-direction: column; align-items: center; text-align: center; - margin-top: 12vh; -} - - - -/* styles the SavedList component */ -#savedList{ - color: black; - align-items: center; - border-radius: 10px; - border-radius: 10px; - margin-bottom: 10px; - width: 70vw; - padding: 2%; - margin-top: 3vh; - height: fit-content; - background: #d0b4ffd7; - contain: content; - overflow: scroll; + margin-top: 11vh; + background-color: rgb(100, 96, 96); } -#savedListContent{ - display: grid; - gap: 2%; -} - -.top{ - display: grid; - grid-template-columns: 9fr 1fr; - justify-items: left; - justify-content: center; -} - - -.favCountry{ - display:flex; - flex-direction: row; - grid-template-rows: 9fr 1fr 1fr; - gap: 5%; -} - - -.favCountry > p{ - padding-right: 62%; -} - - - - - - - - - - #searchBox{ margin-top: 5vh; - width: 80%; + width: 68%; display: grid; - grid-template-rows: auto auto; - padding: 5%; - border: 3px #cfb4ff52 solid; + padding: 3%; + border: 2px #f2f1f4 solid; border-radius: 10px; - background-color: #cfb4ff23; + background-color: #324736e2; gap: 5%; + color: #f1ecfc; + flex-direction: row; +} + +#searchInfo{ + color: white; } #searchElements{ @@ -82,13 +36,6 @@ grid-template-columns: 5fr 1fr 5fr; } - - - -#searchResults{ - color: white; -} - .Countries { display: flex; flex-direction: column; @@ -98,36 +45,22 @@ width: 100%; } - - - - - - #searchResults{ display: flex; flex-direction: column; align-items: center; width: 100%; width: 100%; + color: white; } - - -.favoriteCountries { - display: inline-block; - border: 1px solid green; -} - -.visitedCountries{ - display: inline-block; - border: 1px solid green; +#savedList{ + display: grid; + flex-direction: row; + align-items: center; } - - - /* smaller screens */ @media only screen and (max-width: 800px) { #savedList{ @@ -147,6 +80,13 @@ padding: 5%; } + #loginForm{ + display: grid; + grid-template-rows: repeat(auto-fit, minmax(50px, 1fr)); + row-gap: 10%; + padding-bottom: 10%; + } + #searchBox{ gap: 5%; } @@ -160,8 +100,7 @@ } } - - + /* even smaller screens */ @media only screen and (max-width: 600px) { @@ -173,5 +112,4 @@ #usersEmailHeader{ display: none; } - } - + } \ No newline at end of file diff --git a/client/src/style/index.css b/client/src/style/index.css deleted file mode 100644 index 22971e9f9b449c116054e412fe824822d330b444..0000000000000000000000000000000000000000 --- a/client/src/style/index.css +++ /dev/null @@ -1,16 +0,0 @@ -body { - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', - 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', - sans-serif; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - background-color: #121212!important; - color: white; -} - - - -code { - font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', - monospace; -} \ No newline at end of file diff --git a/client/src/views/HomeView.tsx b/client/src/views/HomeView.tsx deleted file mode 100644 index 8e8c6b138881dddb6298e4f58176ca73930822be..0000000000000000000000000000000000000000 --- a/client/src/views/HomeView.tsx +++ /dev/null @@ -1,228 +0,0 @@ -import React from "react"; -import { useState, useEffect } from "react"; -import { useNavigate } from "react-router-dom"; -import { InView } from "react-intersection-observer"; -import "../style/HomeView.css"; -import { useQuery, useMutation, useReactiveVar } from "@apollo/client"; -import {LOAD_ALL_COUNTRIES} from "../GraphQL/Queries"; -import { UPDATE_USER } from "../GraphQL/Mutations"; -import { Country, SavedCountry, User } from "../interface/Interfaces"; - -import CountryCard, { CountryCardEvent } from "../components/CountryCard"; -import SavedList from "../components/SavedList"; -import Header from "../components/Header"; -import Filter from "../components/Filter"; -import Search from "../components/Search" - -import { darkThemeVar} from "../index"; -import Fab from "@mui/material/Fab"; -import { Theme, ThemeProvider } from '@mui/material/styles'; -import { Typography } from "@mui/material"; - - - - - - -export interface HomeViewProps { - user: User; - poll: () => void; -} - - - -const HomeView: React.FC<HomeViewProps> = ( {user, poll} ) => { - const [errorMessage, setErrorMessage] = useState<string>("ERROR!"); - const [isMobile, setIsMobile] = useState(false); - const [search, setSearch] = useState<string>(''); - const regionDefault: string[] = []; - const darkTheme = useReactiveVar<Theme>(darkThemeVar); - - - const { error, loading, fetchMore, refetch, data } = useQuery(LOAD_ALL_COUNTRIES, { - variables: { offset: 0, limit: 10, search: "", sort: {}, region: regionDefault}, - }); - - const [updateUser,{error: updateUserError,loading: updateUserLoading,data: updateUserData,}] = useMutation<User>(UPDATE_USER); - - - - const userUpdateSavedList = (savedCountry: SavedCountry) => { - let countryIndex = user.savedCountries.findIndex( - (c) => c.countryID === savedCountry.countryID - ); - let newList: SavedCountry[] = user.savedCountries; - if (countryIndex === -1) { - newList = [...newList, savedCountry] - } else { - newList[countryIndex] = savedCountry; - } - - newList = newList.map(c => { - return { - countryID: c.countryID, - isFav: c.isFav, - isVis: c.isVis, - description: c.description - } - }); - - - - updateUser({ - variables: { - input: {email: user.email, savedCountries: newList} - }, - }).then((data) => { - if(data.errors) { - throw new Error(data.errors.toString()); - } - poll() - }) - .catch((error) => { - setErrorMessage(errorMessage + " " + { error }); - }); - }; - - - - - - const cardAction = (countryID: number, event: CountryCardEvent) => { - let country = user.savedCountries.find((c) => c.countryID === countryID); - if (country === undefined) { - country = { - countryID: countryID, - isFav: false, - isVis: false, - description: "", - }; - } - switch (event) { - case CountryCardEvent.Favourited: - country.isFav = true; - break; - case CountryCardEvent.Visited: - country.isVis = true; - break; - } - - userUpdateSavedList(country) - }; - - - - - - - - - - const saveListChange = (country: Country, type: string) => { - let countryIndex = user.savedCountries.findIndex(c => c.countryID === country.countryID); - let changedCountry = { ...user.savedCountries[countryIndex] }; - - if(type === "unfav") { - changedCountry.isFav = !changedCountry.isFav; - } else if(type === "unvisit") { - changedCountry.isVis = !changedCountry.isVis; - } else { - changedCountry.description = type; - } - let newList = user.savedCountries.map(c => { - return { - countryID: c.countryID, - isFav: c.isFav, - isVis: c.isVis, - description: c.description - } - }) - if(!changedCountry.isFav && !changedCountry.isVis && !changedCountry.description) { - newList.splice(countryIndex, 1); - } - - updateUser({variables: { - input: { - email: user.email, - savedCountries: newList - } - }}).then(() => poll()) - } - - - - - - - return ( - <> - <div id="HomeView"> - <Header/> - - <SavedList countries={user.savedCountries || []} onChange={saveListChange} /> - - - - <div id='searchBox'> - <ThemeProvider theme={darkTheme}> - <Typography id='searchInfo' className='searchEl' variant="h6" component="div" sx={{ flexGrow: 1 }}>Search for a country you have visited or want to visit...</Typography> - <div id="searchElements"> - <Search onChange={s => refetch({search: s})} /> - {<h4> OR </h4>} - <Filter id='Filter' setFilter={(r: string[]) => refetch({region: r})} /> - </div> - </ThemeProvider> - </div> - - - <div id='searchResults'> - {loading || error ? ( - <> - {error && <p> Could not load countries: {error.message} </p>} - {loading && <p> Loading... Please wait.</p>} - </> - ) : ( - <> - <h2>All countries</h2> - <div className="Countries"> - {data && - data.GetAllCountries.map((country: Country) => { - return ( - <CountryCard - key={country.countryID} - country={country} - isFav={false} - isVisited={false} - countryAction={cardAction} - /> - ); - })} - {data && ( - - <Fab variant="extended" id = 'loadMore' size="medium" color="primary" aria-label="Press to load more countries" onClick={async (inView) => { - const currentLen = data.GetAllCountries.length || 0; - await fetchMore({ - variables: { - offset: currentLen, - limit: currentLen+10, - }, - }); - }} sx={{borderRadius:10, minHeight: '40px', maxHeight: '40px', minWidth: '50%'}}> - Load More Countries - </Fab> - - - )} - </div> - </> - )} - - </div> - - </div> - - </> - ); -}; - -export default HomeView; diff --git a/client/src/views/StartView.tsx b/client/src/views/StartView.tsx deleted file mode 100644 index 300304d76a6f54879eb1bcbaefd825ebedb5f9ac..0000000000000000000000000000000000000000 --- a/client/src/views/StartView.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { useReactiveVar } from '@apollo/client'; -import "../style/StartView.css"; - -import Card from '../components/Card'; - -import FormControl from '@mui/material/FormControl'; -import InputLabel from '@mui/material/InputLabel'; -import OutlinedInput from '@mui/material/OutlinedInput'; -import Fab from '@mui/material/Fab'; - -import { Theme, ThemeProvider } from '@mui/material/styles'; -import { darkThemeVar, usersEmailVar, requestGetUser } from '../index'; - -import GlobeLogo from "../resources/globe.png" - - - - - - -export interface StartViewProps { - setEmail: (email: string) => void; -} - - -const StartView: React.FC<StartViewProps> = ( { setEmail } ) => { - const [email, updateEmail] = useState<string>(localStorage.getItem("email") || ""); - const [errorMsg, setErrorMsg] = useState<string | null>(null); - const darkTheme = useReactiveVar<Theme>(darkThemeVar); - const usersEmail = useReactiveVar<string>(usersEmailVar); - const requestUserData = useReactiveVar<boolean>(requestGetUser); - - - - useEffect(()=>{ - if (email === undefined){ - updateEmail(''); - } - }, [email]) - - - - - //if the user has tries to submit an email with the right format, the mail they entered will be set - //in localStorage, and a reactive variable. A reactive variable (requestGetUser) will also be set to true. - //In App.tsx there is a useEffect that gets triggered upon the change of this variable. - const handleSubmit = (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => { - event.preventDefault(); - const emailError: string = checkMailFormat(email); - - //Validate input and check for errors - if (emailError !== '') { - setErrorMsg(emailError); - usersEmailVar('') - } else { - setErrorMsg(''); - usersEmailVar(email) - setEmail(email) - requestGetUser(true); - } - }; - - - - - - return ( - <> - <div id="StartView"> - <div id="logo"> - <img id="globeLogo" src={GlobeLogo} alt="World Explorer Logo" /> - </div> - - <Card className='card loginCard' id='loginBox'> - - <p id="introText"> - <span>Type in your email: </span> - </p> - - {Boolean(errorMsg)? - <label className='errorMsgField'> - {errorMsg} - </label> - : - null - } - - - <FormControl id='loginForm' onSubmit={(e) => {e.preventDefault(); setEmail(email)}}> - <ThemeProvider theme={darkTheme}> - <InputLabel htmlFor="inputEmail">E-mail</InputLabel> - <OutlinedInput id="inputEmail" value={email} onChange={(e) => updateEmail(e.target.value)} label="E-mail*" aria-label="email input field"/> - - <div id='submitEmailContainer'> - <Fab variant="extended" id="submitEmail" size="medium" color="primary" aria-label="enter email to log in" onClick={(e) => handleSubmit(e)} sx={{borderRadius:10, minHeight: '40px', maxHeight: '40px', minWidth: '50%'}}> - Explore the world! - </Fab> - </div> - - </ThemeProvider> - </FormControl> - - </Card> - - </div> - </> - - ); -}; - -export default StartView; - - - -//the user email can contain up to three periods -const checkMailFormat = (format: string) => { - // Validate length and characters of project ID - if (!format.match(/^(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])$/)) { - return 'Invalid email format'; - } else if (format.length < 7) { - return 'Email must be longer than 6 characters, ' + format.length + ' is too short.'; - } else{ - return '' - } -}; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index bb752da8756bc446f46d0d9864edde823aafd2b2..f05fcae1f1523644fffa0fa737a44007882e706a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "project-3", + "name": "project-4", "lockfileVersion": 2, "requires": true, "packages": { diff --git a/server/src/models/UserModel.ts b/server/src/models/UserModel.ts index 836ab351880482fe1a9b1370b144149949778ed5..64ee9baf5d1e35e0566680970ea2d2ee6906e1a1 100644 --- a/server/src/models/UserModel.ts +++ b/server/src/models/UserModel.ts @@ -2,25 +2,29 @@ import mongoose, { Schema, model } from "mongoose"; export interface IUser extends mongoose.Document { email: string, - savedCountries: [{ + favList: [{ countryID: Number, - isFav: Boolean, - isVis: Boolean, - description: String + countryName: String }], + visList: [{ + countryID: Number, + countryName: String + }] transform: () => IUser; } const schema = new Schema<IUser>({ email: { type: String }, - savedCountries: [{ + favList: [{ + countryID: {type: Number, required: true}, + countryName: {type: String, required: true}, + }], + visList: [{ countryID: {type: Number, required: true}, - isFav: {type: Boolean, required: true}, - isVis: {type: Boolean, required: true}, - description: {type: String, required: true} + countryName: {type: String, required: true}, }] -}, {collection: "users"}); +}, {collection: "savedUsers"}); -const UserModel = model<IUser>("User", schema, "users"); +const UserModel = model<IUser>("User", schema, "savedUsers"); export default UserModel \ No newline at end of file diff --git a/server/src/resolvers/CountryResolver.ts b/server/src/resolvers/CountryResolver.ts index 9a6a4af74394010efcf70698a239e48f5573e424..9d71ef5d60d8e2c6efa5849213a40261151eeb4d 100644 --- a/server/src/resolvers/CountryResolver.ts +++ b/server/src/resolvers/CountryResolver.ts @@ -23,62 +23,11 @@ export const CountryResolver = { filter["countryName"] = {$regex: `.*${args.search}.*`} } try { - return CountryModel.find(filter, null, options).exec(); + return CountryModel.find(filter, null, options).exec(); } catch (err) { return err } }, - GetCountriesByID:(parent, args, context, info)=>{ - const {input} = args; - return new Promise((resolve,reject)=>{ - CountryModel.find({countryID: {$in : input}},(err: Error, CountryModel: ICountry)=>{ - if(err) reject(err); - else resolve(CountryModel); - }) - }) - }, - GetSortedCountries:(parent, args, context, info) => { - const {input} = args; - const start = args.offset; - const end = args.limit; - if (input.order === "asc") { - return new Promise((resolve,reject)=>{ - CountryModel.find({}).sort(input.field).skip(start).limit(end).exec(function(err,CountryModel){ - if(err) reject(err); - else resolve(CountryModel); - }) - }) - } - else { - return new Promise((resolve,reject)=>{ - CountryModel.find({}).sort("-"+input.field).skip(start).limit(end).exec(function(err,CountryModel){ - if(err) reject(err); - else resolve(CountryModel); - }) - }) - } - }, - GetCountriesByRegion:(parent, args, context, info) =>{ - const start = args.offset; - const end = args.limit; - const input = args.input; - return new Promise((resolve,reject)=>{ - CountryModel.find({region: {$in : input}},(err: Error, CountryModel: ICountry)=>{ - if(err) reject(err); - else resolve(CountryModel); - }).skip(start).limit(end); - }) - }, - GetCountriesBySearch:(parent, args, context, info) =>{ - const input = args.input; - console.log(args, args.input, input) - return new Promise((resolve,reject)=>{ - CountryModel.find({countryName: {$in : input}},(err: Error, CountryModel: ICountry)=>{ - if(err) reject(err); - else resolve(CountryModel); - }) - }) - } - } + } } \ No newline at end of file diff --git a/server/src/resolvers/UserResolver.ts b/server/src/resolvers/UserResolver.ts index fcdaf996130b4736fd655b0d7860eba67070feb9..2ee6d9358854a33f492d0a9c5e7dfccb5ffedf6e 100644 --- a/server/src/resolvers/UserResolver.ts +++ b/server/src/resolvers/UserResolver.ts @@ -4,36 +4,126 @@ import UserModel, { IUser } from "../models/UserModel.js"; export const UserResolver = { Query:{ GetUser: (parent, args, context, info)=>{ - const user = args.input; - return new Promise((resolve,reject) =>{ - UserModel.findOne(user, (err:Error,UserModel)=>{ + const { email } = args; + const query = { + email: email + } + const update = { + } + const options = { + new: true, + upsert: true + }; + return new Promise((resolve,reject) => { + UserModel.findOneAndUpdate(query, update, options, (err: Error, user) => { if(err) reject(err); - else resolve(UserModel); + else resolve(user); }) }) } }, Mutation:{ - SetUser: (parent, args, context, info) => { - const userEmail = args.input.email; - const newUser = { - email: userEmail, - savedCountries: []} - return new Promise((resolve,reject)=>{ - UserModel.create(newUser,(err: Error, UserModel)=>{ - if(err) reject(err); - else resolve(UserModel); + UpdateUser:(parent, args, context, info) => { + const { user } = args; + return new Promise((resolve,reject) =>{ + UserModel.findOneAndUpdate(user.email, user, {new: true, upsert: true}, (err:Error, user)=>{ + if(err) reject(err); + else resolve(user) }) }) }, - UpdateUser:(parent, args, context, info) =>{ - const user = args.input; - return new Promise((resolve,reject) =>{ - UserModel.findOneAndReplace(user.email, user, {new: true, upsert: true}, (err:Error,UserModel)=>{ - if(err) reject(err); - else resolve(UserModel) + + AddFavourite: (parent, args, context, info) => { + const { email, favourite } = args; + + return new Promise((resolve, reject) => { + const query = { + email: email, + "favList.countryID": { + $ne: favourite.countryID + }, + }; + const update = { + $push: { + favList: favourite + } + }; + UserModel.findOneAndUpdate(query, update, { returnDocument: "after"}, (err: Error, user) => { + if(err) { + reject(err); + } + + resolve(user); + }) + }) + + }, + AddVisited: (parent, args, context, info) => { + const { email, visited } = args; + + return new Promise((resolve, reject) => { + const query = { + email: email, + "visList.countryID": { + $ne: visited.countryID + } + }; + const update = { + $push: { + visList: visited + } + }; + UserModel.findOneAndUpdate(query, update, { returnDocument: "after"}, (err: Error, user) => { + if(err) { + reject(err); + } + resolve(user); }) }) + + }, + RemoveFavourite: (parent, args, context, info) => { + const { email, favourite } = args; + + return new Promise((resolve, reject) => { + const query = { + email: email + }; + const remove = { + $pull: { + favList: { countryID: favourite.countryID } + } + }; + UserModel.findOneAndUpdate(query, remove, { returnDocument: "after"}, (err: Error, user) => { + if(err) { + reject(err); + } + resolve(user); + }) + }) + + }, + RemoveVisited: (parent, args, context, info) => { + const { email, favourite: visited } = args; + + return new Promise((resolve, reject) => { + const query = { + email: email + }; + const remove = { + $pull: { + visList: { countryID: visited.countryID } + } + }; + UserModel.findOneAndUpdate(query, remove, { returnDocument: "after"}, (err: Error, user) => { + if(err) { + reject(err); + } + + resolve(user); + }) + }) + } } } \ No newline at end of file diff --git a/server/src/schemas/CountrySchema.ts b/server/src/schemas/CountrySchema.ts index 7cee30d8fbab5c7b18f6173beac15b7b6670a518..f4c81d885a11cc2120ba52a22a9415f0a47a68cc 100644 --- a/server/src/schemas/CountrySchema.ts +++ b/server/src/schemas/CountrySchema.ts @@ -20,9 +20,5 @@ export const CountrySchema = `#graphql type Query { GetAllCountries(offset: Int, limit: Int, sort: SortBy, search: String, region: [String]): [Country] - GetCountriesByID(input: [Int]): [Country] - GetSortedCountries(input: SortBy, offset: Int, limit: Int): [Country] - GetCountriesByRegion(input: [String], offset: Int, limit: Int): [Country] - GetCountriesBySearch(input: [String]): [Country] } `; \ No newline at end of file diff --git a/server/src/schemas/UserSchema.ts b/server/src/schemas/UserSchema.ts index 3df1cf59112a10cc854ff6b4b43074708d40ec2e..d4684f2d33ba0d79565a1feb4369e99e3d0c939f 100644 --- a/server/src/schemas/UserSchema.ts +++ b/server/src/schemas/UserSchema.ts @@ -1,38 +1,35 @@ export const UserSchema = `#graphql type SavedCountry { countryID: Int - isFav: Boolean - isVis: Boolean - description: String + countryName: String } type User { email: String - savedCountries: [SavedCountry] + favList: [SavedCountry] + visList: [SavedCountry] } input SavedCountryInput { countryID: Int - isFav: Boolean - isVis: Boolean - description: String + countryName: String } input UserInput { email: String - savedCountries: [SavedCountryInput] -} - -input EmailInput { - email: String + favList: [SavedCountryInput] + visList: [SavedCountryInput] } type Query { - GetUser(input: EmailInput): User + GetUser(email: String): User } type Mutation { - SetUser(input: EmailInput): User - UpdateUser(input: UserInput): User + UpdateUser(user: UserInput): User + AddFavourite(email: String, favourite: SavedCountryInput): User + AddVisited(email: String, visited: SavedCountryInput): User + RemoveFavourite(email: String, favourite: SavedCountryInput): User + RemoveVisited(email: String, visited: SavedCountryInput): User } `; \ No newline at end of file