diff --git a/src/App.scss b/src/App.scss index 1988af9..ce676c3 100644 --- a/src/App.scss +++ b/src/App.scss @@ -67,3 +67,8 @@ a { .fas:hover { color: #c92e1e; } + +.main { + display: flex; + justify-content: center; +} diff --git a/src/Utils/apiUtils.ts b/src/Utils/apiUtils.ts index 71a4b63..4316838 100644 --- a/src/Utils/apiUtils.ts +++ b/src/Utils/apiUtils.ts @@ -46,7 +46,7 @@ export const updateCafeDetailData = async (data: CafeDetailResponse, url: string export const getLocationsData = async () => { try { - const response = await httpGet<{_id: string, locations: string[]}[]>('/locations'); + const response = await httpGet<{_id: string, locations: string[]}[]>('/api/locations'); const fetchData = await response.data; return fetchData; } catch (error) { diff --git a/src/api/index.tsx b/src/api/index.tsx deleted file mode 100644 index e69de29..0000000 diff --git a/src/components/Cafe/AddCafeForm.tsx b/src/components/Cafe/AddCafeForm.tsx index 3bbec95..8d44839 100644 --- a/src/components/Cafe/AddCafeForm.tsx +++ b/src/components/Cafe/AddCafeForm.tsx @@ -12,9 +12,9 @@ import { RootState } from '../../store'; import { FormValues, CafeDetailResponse } from '../../types/cafe'; import { styled } from '@mui/material/styles'; -import FormTextField from '../Form/FormComponets/FormTextField'; -import FormSelect from '../Form/FormComponets/FormSelect'; -import FormTimeField from '../Form/FormComponets/FormTimeField'; +import FormTextField from '../common/FormComponets/FormTextField'; +import FormSelect from '../common/FormComponets/FormSelect'; +import FormTimeField from '../common/FormComponets/FormTimeField'; type FormNewCafeType ={ openDialog: boolean; diff --git a/src/components/Cafe/EditCafeForm.tsx b/src/components/Cafe/EditCafeForm.tsx index bd173d5..f745db2 100644 --- a/src/components/Cafe/EditCafeForm.tsx +++ b/src/components/Cafe/EditCafeForm.tsx @@ -16,8 +16,8 @@ import { getLocationsData } from '../../Utils/apiUtils'; export { daysOfWeek } from '../../constants'; import { styled } from '@mui/material/styles'; -import FormTextField from '../Form/FormComponets/FormTextField'; -import FormSelect from '../Form/FormComponets/FormSelect'; +import FormTextField from '../common/FormComponets/FormTextField'; +import FormSelect from '../common/FormComponets/FormSelect'; import OpenedTimeCard from './OpenedTimeCard'; import { daysOfWeek } from '../../constants'; diff --git a/src/components/Cafe/OpenedTimeCard.tsx b/src/components/Cafe/OpenedTimeCard.tsx index 1e0d5ef..45e8478 100644 --- a/src/components/Cafe/OpenedTimeCard.tsx +++ b/src/components/Cafe/OpenedTimeCard.tsx @@ -6,7 +6,7 @@ import { DevTool } from '@hookform/devtools'; import { Divider, Grid, Typography } from '@mui/material'; import { TOpeningHours } from '../../types/cafe'; -import FormTimeField from '../Form/FormComponets/FormTimeField'; +import FormTimeField from '../common/FormComponets/FormTimeField'; type TOpeningHoursProps = { openingHours: TOpeningHours; diff --git a/src/components/Header/AppHeader.tsx b/src/components/Header/AppHeader.tsx index bb59742..985dfa2 100644 --- a/src/components/Header/AppHeader.tsx +++ b/src/components/Header/AppHeader.tsx @@ -3,26 +3,24 @@ import { useTranslation } from 'react-i18next'; import { Link } from 'react-router-dom'; import { Grid } from '@mui/material'; -import Login from './Login'; +import User from './User'; +import LanguageSwitch from '../common/LanguageSwitch'; const Header: React.FC = () => { const { t } = useTranslation(); return ( -
- - - - -

{ t('mapCafes') }

- -
- - - -
+ + + +

{ t('mapCafes') }

+
-
+ + + + + ); }; diff --git a/src/components/Header/Login.tsx b/src/components/Header/Login.tsx deleted file mode 100644 index 34cd5dd..0000000 --- a/src/components/Header/Login.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import React, { useState } from 'react'; -import { useTranslation } from 'react-i18next'; - -import { Avatar, Divider, IconButton, Menu, MenuItem, ListItemIcon, Tooltip } from '@mui/material'; -import { Logout } from '@mui/icons-material'; - -import LanguageSwitch from '../common/LanguageSwitch'; - -const Login: React.FC = () => { - const { t } = useTranslation(); - const [anchorEl, setAnchorEl] = useState(null); - - const open = Boolean(anchorEl); - - const handleClick = (event: React.MouseEvent) => { - setAnchorEl(event.currentTarget); - }; - - const handleClose = () => { - setAnchorEl(null); - }; - - return ( - <> - - - - - - - - { t('profile') } - - - { t('account') } - - - - - - - - - - { t('logout') } - - - - ) -} - -export default Login; \ No newline at end of file diff --git a/src/components/Header/User.tsx b/src/components/Header/User.tsx new file mode 100644 index 0000000..689d74b --- /dev/null +++ b/src/components/Header/User.tsx @@ -0,0 +1,118 @@ +import React, { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useHistory } from 'react-router-dom'; +import { useDispatch, useSelector } from 'react-redux'; + +import { Avatar, Divider, IconButton, Menu, MenuItem, ListItemIcon, Tooltip } from '@mui/material'; +import { Logout, Login } from '@mui/icons-material'; + +import { UserLogin } from '../../constants'; +import { setLogin, checkLoginUser } from '../../store/settings'; +import { RootState } from '../../store'; + +const User: React.FC = () => { + const { t } = useTranslation(); + const history = useHistory(); + const dispatch = useDispatch(); + + const [anchorEl, setAnchorEl] = useState(null); + + const isOpenMenu = Boolean(anchorEl); + const isLogin = useSelector((state: RootState) => state.settings.isLogin); + + const handleClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(null); + }; + + const handleLogout = () => { + localStorage.removeItem(UserLogin.LOGIN); + dispatch(setLogin(false)); + handleClose(); + } + + useEffect(() => { + const login = localStorage.getItem(UserLogin.LOGIN); + dispatch(checkLoginUser(login === 'true')); + }, []); + + return ( + <> + { isLogin ? + ( + <> + + + + + + + + { t('profile') } + + + + + + + { t('logout') } + + + + ) : ( + + history.push('/login')} + size='small' + sx={{ mr: 2 }} + > + + + + )} + + ) +} + +export default User; \ No newline at end of file diff --git a/src/common/Map/AppMap.tsx b/src/components/Map/AppMap.tsx similarity index 97% rename from src/common/Map/AppMap.tsx rename to src/components/Map/AppMap.tsx index 202cf89..520125d 100644 --- a/src/common/Map/AppMap.tsx +++ b/src/components/Map/AppMap.tsx @@ -2,7 +2,7 @@ import { GoogleMap, LoadScript } from '@react-google-maps/api'; import React, { useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import MarkerComponent from '../../components/Marker/AppMarker'; +import MarkerComponent from '../Marker/AppMarker'; import coffeePin from '../../img/newPin.svg'; diff --git a/src/components/Navigation/AppNavigation.tsx b/src/components/Navigation/AppNavigation.tsx index d79663c..9b947f0 100644 --- a/src/components/Navigation/AppNavigation.tsx +++ b/src/components/Navigation/AppNavigation.tsx @@ -118,7 +118,7 @@ const Navigation: React.FC = () => { const addCreateCafe = async (data: CafeDetailResponse) => { try { - await addNewData(data, '/create'); + await addNewData(data, '/api/create'); } catch (error) { setShowError(true); return null; diff --git a/src/components/Form/FormComponets/FormSelect.tsx b/src/components/common/FormComponets/FormSelect.tsx similarity index 100% rename from src/components/Form/FormComponets/FormSelect.tsx rename to src/components/common/FormComponets/FormSelect.tsx diff --git a/src/components/Form/FormComponets/FormTextField.tsx b/src/components/common/FormComponets/FormTextField.tsx similarity index 100% rename from src/components/Form/FormComponets/FormTextField.tsx rename to src/components/common/FormComponets/FormTextField.tsx diff --git a/src/components/Form/FormComponets/FormTimeField.tsx b/src/components/common/FormComponets/FormTimeField.tsx similarity index 100% rename from src/components/Form/FormComponets/FormTimeField.tsx rename to src/components/common/FormComponets/FormTimeField.tsx diff --git a/src/components/common/LanguageSwitch.tsx b/src/components/common/LanguageSwitch.tsx index 0d75c5e..37ee4a5 100644 --- a/src/components/common/LanguageSwitch.tsx +++ b/src/components/common/LanguageSwitch.tsx @@ -23,11 +23,11 @@ const LanguageSwitch: React.FC = () => { return (
{currentLanguage === 'cz' ? ( - ) : ( - )} diff --git a/src/constants.ts b/src/constants.ts index 246d150..6c03b9e 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1 +1,2 @@ export const daysOfWeek = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday']; +export const UserLogin: Readonly<{LOGIN: string}> = { LOGIN: 'login' }; \ No newline at end of file diff --git a/src/i18n/locales/cz.json b/src/i18n/locales/cz.json index 80f8ce2..26f214b 100644 --- a/src/i18n/locales/cz.json +++ b/src/i18n/locales/cz.json @@ -63,6 +63,16 @@ "somethingWrong": "Něco je špatně.", "404": "Tato stránka neexistuje!" }, + "loginUser": { + "username": "Uživatelské jméno", + "password": "Heslo", + "registrationInfo": "Nemáš zatím účet?", + "registrationAction": "Zaregistrovat se můžeš zde" + }, + "registration": { + "createAccount": "Vytvořit účet", + "submit":"Registrovat" + }, "notExist": "Tato kavárna pravděpodobně neexistuje!", "profile": "Profil", "account": "Můj účet", diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 298be21..baf8d4f 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -53,6 +53,16 @@ "somethingWrong": "Something is wrong.", "404": "This page is doesn't exist!" }, + "loginUser": { + "username": "Username", + "password": "Password", + "registrationInfo": "Don't have an account?", + "registrationAction": "Sign up" + }, + "registration": { + "createAccount": "Create your account", + "submit":"Sign up" + }, "notExist": "This cafe probably doesn't exist!", "profile": "Profil", "account": "My account", diff --git a/src/components/Cafe/CafeDetail.tsx b/src/pages/CafeDetail.tsx similarity index 92% rename from src/components/Cafe/CafeDetail.tsx rename to src/pages/CafeDetail.tsx index 0266c8f..a1b75cc 100644 --- a/src/components/Cafe/CafeDetail.tsx +++ b/src/pages/CafeDetail.tsx @@ -7,14 +7,14 @@ import dayjs from 'dayjs'; import { Typography, Grid, Skeleton, Tooltip, IconButton } from '@mui/material'; import { LocalCafeOutlined, EditOutlined } from '@mui/icons-material'; -import { getCafeDetailData, updateCafeDetailData } from '../../Utils/apiUtils'; -import { setCafeDetail } from '../../store/cafeDetail'; -import { RootState } from '../../store'; +import { getCafeDetailData, updateCafeDetailData } from '../Utils/apiUtils'; +import { setCafeDetail } from '../store/cafeDetail'; +import { RootState } from '../store'; import 'tailwindcss/tailwind.css'; -import Map from '../../common/Map/AppMap'; -import { openTime, CafeDetailResponse } from '../../types/cafe'; -import EditCafeForm from './EditCafeForm'; +import Map from '../components/Map/AppMap'; +import { openTime, CafeDetailResponse } from '../types/cafe'; +import EditCafeForm from '../components/Cafe/EditCafeForm'; type ParamsType = { id: string; @@ -33,7 +33,7 @@ const CafeDetail: React.FC = () => { const getCafeDetail = useCallback(async() => { setIsLoading(true); try { - const detail = await getCafeDetailData(`/cafe/${id}`); + const detail = await getCafeDetailData(`/api/cafe/${id}`); if (detail) { dispatch(setCafeDetail(detail)); } @@ -55,7 +55,7 @@ const CafeDetail: React.FC = () => { const submitEditCafe = async(data: CafeDetailResponse) => { setIsLoading(true); try { - await updateCafeDetailData(data, `/cafe/${id}`); + await updateCafeDetailData(data, `/api/cafe/${id}`); } catch (err) { console.error(err); } finally { diff --git a/src/components/Home/index.tsx b/src/pages/Home/index.tsx similarity index 70% rename from src/components/Home/index.tsx rename to src/pages/Home/index.tsx index f1acdaa..7922642 100644 --- a/src/components/Home/index.tsx +++ b/src/pages/Home/index.tsx @@ -1,9 +1,9 @@ import React, { useEffect } from 'react'; import { useDispatch } from 'react-redux'; -import Map from '../../common/Map/AppMap'; -import CafeList from '../Cafe/CafeList'; -import Navigation from '../Navigation/AppNavigation'; +import Map from '../../components/Map/AppMap'; +import CafeList from '../../components/Cafe/CafeList'; +import Navigation from '../../components/Navigation/AppNavigation'; import { getCafeList } from '../../Utils/apiUtils'; import { setCafes } from '../../store/cafeList'; @@ -18,7 +18,7 @@ const Home: React.FC = () => { useEffect(() => { const setCafeList = async () => { try { - const response = await getCafeList('/cafe/list'); + const response = await getCafeList('/api/cafe/list'); if (response) { dispatch(setCafes(response)); } @@ -32,11 +32,11 @@ const Home: React.FC = () => { return ( - <> +
- +
) } diff --git a/src/pages/Login/Login.tsx b/src/pages/Login/Login.tsx new file mode 100644 index 0000000..0ee925c --- /dev/null +++ b/src/pages/Login/Login.tsx @@ -0,0 +1,98 @@ +import { FC } from 'react'; +import { useForm } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import { useHistory } from 'react-router-dom'; +import { useDispatch } from 'react-redux'; + +import FormTextField from '../../components/common/FormComponets/FormTextField'; +import { Button, Card, CardActions, CardContent, Grid, Link, Typography } from '@mui/material'; +import { httpPost } from '../../Utils/axiosService'; +import { UserLogin } from '../../constants'; +import { setLogin } from '../../store/settings'; +import { FieldErrors } from '../../types/cafe'; + +type FormValues = { + username: string; + password: string; +}; + +const Login: FC = () => { + const { t } = useTranslation(); + const { control, formState: { errors }, handleSubmit, reset } = useForm(); + const history = useHistory(); + const dispatch = useDispatch(); + + const fieldErrors = { + username: errors.username, + password: errors.password, + } + + const onSubmit = (values: FormValues) => { + createNewUser(values); + reset(); + } + + const createNewUser = async (data: FormValues) => { + try { + await httpPost('/api/user/login', data); + localStorage.setItem(UserLogin.LOGIN, 'true') + dispatch(setLogin(true)); + history.push('/'); + } catch (error) { + console.error(error.response.data.message); + fieldErrors.username = error.response.data.message; + console.log(fieldErrors) + } + }; + + return ( + <> + +
+ + + { t('login') } + + + + + + + + + + + + + +
+ + { t('loginUser.registrationInfo') } + history.push('/registration')} + > + { t('loginUser.registrationAction')} + + +
+
+
+ + ); +}; + +export default Login; diff --git a/src/pages/Registration/Registration.tsx b/src/pages/Registration/Registration.tsx new file mode 100644 index 0000000..f0a4b63 --- /dev/null +++ b/src/pages/Registration/Registration.tsx @@ -0,0 +1,75 @@ +import { FC, useEffect, useState } from 'react'; +import { useForm } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; + +import FormTextField from '../../components/common/FormComponets/FormTextField'; +import { Button, Card, CardActions, CardContent, Grid, Typography } from '@mui/material'; +import { httpPost } from '../../Utils/axiosService'; + +type FormValues = { + username: string; + password: string; +}; + +const Registration: FC = () => { + const { t } = useTranslation(); + const { control, formState: { errors }, handleSubmit, reset } = useForm(); + + const fieldErrors = { + username: errors.username, + password: errors.password, + } + + const onSubmit = (values: FormValues) => { + console.log(values); + createNewUser(values); + reset(); + } + + const createNewUser = async (data: FormValues) => { + try { + await httpPost('/api/user/registration', data); + } catch (error) { + console.error(error); + } + }; + + return ( + <> + +
+ + + { t('registration.createAccount') } + + + + + + + + + + + + + +
+
+ + ); +}; + +export default Registration; diff --git a/src/routes/index.jsx b/src/routes/index.jsx index 125bea0..3a4b38d 100644 --- a/src/routes/index.jsx +++ b/src/routes/index.jsx @@ -1,6 +1,7 @@ -import CafeDetail from '../components/Cafe/CafeDetail'; -import Home from '../components/Home'; -import AppRegistration from '../components/Registration/AppRegistration'; +import CafeDetail from '../pages/CafeDetail'; +import Home from '../pages/Home'; +import Registration from '../pages/Registration/Registration'; +import Login from '../pages/Login/Login'; /** * Sorts routes @@ -28,13 +29,6 @@ const sortRoutes = (routes, sortBy, sortOrder) => { * @prop {string} label - label pro routu - da se pouzit jako title h1 atp. * @prop {order} number - pozice napriklad pro menu, pokud chces treba pouzit pro linky v nejake navigaci- Sortuji se dole v exportu * @prop {JSXElement | Element} route - Element nebo Komponent ktery se ma renderovat - * @prop {boolean?} inFooter - pouziva se pro filter exportu routes pro footer, napr. navigace ve footeru - nepovinne - * @prop {boolean?} inHeader - pouziva se pro filter exportu routes pro header, napr. navigace v headeru - nepovinne - * @prop {JSXElement | Element | SVGElement | null} icon - muze se pouzit jako ikona pro navigaci napriklad - nepovinne - * @prop {boolean?} disabled - zamezí exportu routy a ta bude nedosupná - nepovinne - * @prop {string?} additionalClass - doplňkové classy pro danou routu jako jeden string napr.: 'class-1 class-2 ...' - nepovinne - */ - /** * Routes * pole Route @@ -49,25 +43,31 @@ const routes = [ order: 1, route: , }, + { + exact: false, + path: '/cafe/:id', + label: 'Detail', + order: 4, + route: + }, { exact: false, path: '/registration', label: 'Registrace', - order: 3, - route: + order: 5, + route: }, { exact: false, - path: '/cafe/:id', - label: 'Detail', - order: 4, - route: + path: '/login', + label: 'Login', + order: 5, + route: }, ]; -// zde se meguji a sortuji vsechn pole s routami export const AllRoutes = sortRoutes(routes.filter(route => !route.disabled)); -// toto je treba pro navigaci v headeru + export const HeaderNavItems = AllRoutes.filter(route => route.inHeader); -// toto je treba pro navigaci ve footeru + export const FooterNavItems = AllRoutes.filter(route => route.inFooter); diff --git a/src/store/settings.ts b/src/store/settings.ts index 2cd0744..0af7844 100644 --- a/src/store/settings.ts +++ b/src/store/settings.ts @@ -2,10 +2,12 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; interface SettingsState { language: string; + isLogin: boolean; } const initialState: SettingsState = { - language: 'en' + language: 'en', + isLogin: false, }; export const settingsSlice = createSlice({ @@ -14,9 +16,15 @@ export const settingsSlice = createSlice({ reducers: { setLanguage: (state, action: PayloadAction) => { state.language = action.payload; + }, + setLogin: (state, action: PayloadAction) => { + state.isLogin = action.payload; + }, + checkLoginUser: (state, action: PayloadAction) => { + state.isLogin = action.payload; } } }); -export const { setLanguage } = settingsSlice.actions; +export const { setLanguage, setLogin, checkLoginUser } = settingsSlice.actions; export default settingsSlice.reducer;