Commit bba25780 authored by Цой Данил's avatar Цой Данил 💬

Added basic front to show all data

parent e3b30264
......@@ -49,11 +49,6 @@ export default db.once('open', async () => {
title: 'Commander Ledros',
photo: 'ledros.jpg'
},
{
user: userOne._id,
title: 'Commander Ledros',
photo: 'ledros.jpg'
},
{
user: userTwo._id,
title: 'Quinn and Valor',
......
This diff is collapsed.
VITE_BASE_URL=http://localhost:8000/
\ No newline at end of file
import { useState } from 'react'
import React, { useState } from 'react'
import reactLogo from './assets/react.svg'
import viteLogo from '/vite.svg'
import './App.css'
import { Route, Routes } from 'react-router-dom'
import Layout from './components/Layout/Layout'
import PrivateRoute from './utils/PrivateRoute'
import ErrorPage from './containers/ErrorPage/ErrorPage'
import AuthorizeForm from './containers/AuthorizeForm/AuthorizeForm'
import MainPage from './containers/MainPage/MainPage'
import UserGallery from './containers/UserGallery/UserGallery'
import AddForm from './containers/AddForm/AddForm'
function App() {
const [count, setCount] = useState(0)
const App: React.FunctionComponent = (): React.ReactElement => {
return (
<div className="App">
<div>
<a href="https://vitejs.dev" target="_blank">
<img src={viteLogo} className="logo" alt="Vite logo" />
</a>
<a href="https://reactjs.org" target="_blank">
<img src={reactLogo} className="logo react" alt="React logo" />
</a>
</div>
<h1>Vite + React</h1>
<div className="card">
<button onClick={() => setCount((count) => count + 1)}>
count is {count}
</button>
<p>
Edit <code>src/App.tsx</code> and save to test HMR
</p>
</div>
<p className="read-the-docs">
Click on the Vite and React logos to learn more
</p>
</div>
<Routes>
<Route element={<Layout/>}>
<Route path='/' element={<MainPage/>}/>
<Route path='/user/:id' element={<UserGallery/>}/>
<Route path='/authorize' element={<AuthorizeForm/>}/>
<Route element={<PrivateRoute/>}>
<Route path='/add-photo' element={<AddForm/>}/>
</Route>
<Route path='*' element={<ErrorPage/>}/>
</Route>
</Routes>
)
}
......
......@@ -71,48 +71,6 @@ const Header: React.FunctionComponent = (): React.ReactElement => {
<h4 style={{margin: 0, color: 'black', marginRight: '40px'}}>
<span style={{fontWeight: 'normal', marginRight: '10px'}}>Hello,</span><NavLink to={`/user/${user?._id}`}>{user?.username}</NavLink>
</h4>
<Button
ref={anchorRef}
id="composition-button"
aria-controls={open ? 'composition-menu' : undefined}
aria-expanded={open ? 'true' : undefined}
aria-haspopup="true"
onClick={handleToggle}
>
Navigate menu
</Button>
{/* <Popper
open={open}
anchorEl={anchorRef.current}
role={undefined}
placement="bottom-start"
transition
>
{({ TransitionProps, placement }) => (
<Grow
{...TransitionProps}
style={{
transformOrigin:
placement === 'bottom-start' ? 'left top' : 'left bottom',
}}
>
<Paper
style={{position: 'relative', zIndex: 1000, fontWeight: 'bold'}}
>
<ClickAwayListener onClickAway={handleClose}>
<MenuList
autoFocusItem={open}
id="composition-menu"
aria-labelledby="composition-button"
onKeyDown={handleListKeyDown}
>
<MenuItem onClick={()=>{navigateToPage('/add-photo')}}>Add new photo</MenuItem>
</MenuList>
</ClickAwayListener>
</Paper>
</Grow>
)}
</Popper> */}
<button className={styles.Header_button} onClick={logoutHandler}>Logout</button>
</div> :
<div>
......
import React from "react";
import { Outlet } from "react-router-dom";
import Header from "../Header/Header";
import styles from './Layout.module.css'
const Layout: React.FunctionComponent = (): React.ReactElement => {
return (
<div className={styles.Layout}>
<div>
<Header/>
<main>
<Outlet />
......
import { MouseEventHandler } from "react";
import IPhoto from "../../interfaces/IPhoto";
export default interface IPhotoBlockProps {
photo: IPhoto
showFullImage: MouseEventHandler<any>
goToUserPage: MouseEventHandler<HTMLButtonElement>
}
\ No newline at end of file
import * as React from 'react';
import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
import CardMedia from '@mui/material/CardMedia';
import Typography from '@mui/material/Typography';
import { Button, CardActionArea, CardActions } from '@mui/material';
import IPhotoBlockProps from './IPhotoBlockProps';
import image_not_found from '../../assets/image_not_found.png'
import { shallowEqual, useSelector } from 'react-redux';
import { AppState } from '../../store/store';
const PhotoBlock: React.FunctionComponent<IPhotoBlockProps> = (props): React.ReactElement => {
const {user} = useSelector((state: AppState) => state.users, shallowEqual)
return (
<div style={{margin: '10px'}}>
<Card sx={{ maxWidth: 400 }}>
<CardActionArea
onClick={props.showFullImage}
>
<CardMedia
component="img"
width="350"
image={import.meta.env.VITE_BASE_URL + 'uploads/' + props.photo.photo}
onError = {(e) => {
e.currentTarget.src = image_not_found
}}
alt={props.photo.title + 'image'}
/>
<CardContent style={{margin: '5px', padding: '5px', marginBottom: 0}}>
<Typography gutterBottom variant="h5" component="div" style={{marginBottom: 0}}>
{props.photo.title}
</Typography>
</CardContent>
</CardActionArea>
<CardActions>
<Button onClick={props.goToUserPage} size="small" color="primary">
{
user?.username === props.photo.user.username ? 'My Photo' : props.photo.user.username
}
</Button>
</CardActions>
</Card>
</div>
);
}
export default PhotoBlock
\ No newline at end of file
import { MouseEventHandler } from "react";
import IPhoto from "../../interfaces/IPhoto";
export default interface IPhotoUserGalleryBlockProps {
photo: IPhoto
showFullImage: MouseEventHandler<any>
deletePhoto: MouseEventHandler<HTMLButtonElement>
}
\ No newline at end of file
import * as React from 'react';
import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
import CardMedia from '@mui/material/CardMedia';
import Typography from '@mui/material/Typography';
import { Button, CardActionArea, CardActions } from '@mui/material';
import image_not_found from '../../assets/image_not_found.png'
import { shallowEqual, useSelector } from 'react-redux';
import { AppState } from '../../store/store';
import IPhotoUserGalleryBlockProps from './IPhotoUserGalleryBlockProps';
const PhotoUserGalleryBlock: React.FunctionComponent<IPhotoUserGalleryBlockProps> = (props): React.ReactElement => {
const {user} = useSelector((state: AppState) => state.users, shallowEqual)
return (
<div style={{margin: '10px'}}>
<Card sx={{ maxWidth: 350 }}>
<CardActionArea
onClick={props.showFullImage}
>
<CardMedia
component="img"
width="200"
image={import.meta.env.VITE_BASE_URL + 'uploads/' + props.photo.photo}
onError = {(e) => {
e.currentTarget.src = image_not_found
}}
alt={props.photo.title + 'image'}
/>
<CardContent style={{margin: '5px', padding: '5px', marginBottom: 0}}>
<Typography gutterBottom variant="h5" component="div" style={{marginBottom: 0}}>
{props.photo.title}
</Typography>
</CardContent>
</CardActionArea>
{
user?._id === props.photo.user._id
?
<CardActions>
<Button onClick={props.deletePhoto} variant="outlined" color="error">Delete</Button>
</CardActions>
:
null
}
</Card>
</div>
);
}
export default PhotoUserGalleryBlock
\ No newline at end of file
.file_input{
color: white;
border-bottom: 1px solid red;
max-width: 200px;
cursor: pointer;
transition: 0.2s;
border-radius: 5px;
color: black;
}
.file_input:hover{
color: rgb(255, 255, 255);
background-color: rgb(0, 0, 0);
color: white;
}
.filename{
background-color: black;
color: white;
}
.add_btn{
max-width: 200px;
width: 100%;
height: 50px;
border-radius: 7px;
margin-top: 35px;
transition: 0.2s;
cursor: pointer;
font-weight: bold;
}
.add_btn:hover{
background: rgb(38, 164, 38);
}
\ No newline at end of file
import { shallowEqual } from "react-redux"
import { Button, TextField } from '@mui/material';
import Box from '@mui/material/Box';
import Modal from '@mui/material/Modal';
import { AppDispatch, AppState, useAppDispatch } from "../../store/store"
import { useSelector } from "react-redux"
import Preloader from "../../components/UI/Preloader/Preloader"
import { ChangeEvent, FormEvent, useEffect, useState } from "react"
import IPhotoDto from "../../interfaces/IPhotoDto"
import { addPhoto } from "../../store/photos/photos.slice"
import styles from './AddForm.module.css'
const AddForm: React.FunctionComponent = (): React.ReactElement => {
const {loadingPhotos} = useSelector((state: AppState) => state.photos, shallowEqual)
const [buttonDisabled, setButtonDisabled] = useState<boolean>(true)
const [fileName, setFileName] = useState<string>('')
const [photoDto, setPhotoDto] = useState<IPhotoDto>({
title: '',
photo: undefined
})
const dispatch: AppDispatch = useAppDispatch()
const inputHandler = (e: ChangeEvent<HTMLInputElement>): void => {
setPhotoDto(prevState => {
return {...prevState, [e.target.name]: e.target.value}
})
}
const inputFileHandler = (e: ChangeEvent<HTMLInputElement>): void => {
setPhotoDto(prevState => {
return {...prevState,
photo: e.target.files ? e.target.files[0] : undefined}
})
setFileName(e.target.files && e.target.files[0] ? e.target.files[0].name : '')
}
const checkButton = () => {
if(photoDto.title.trim() === '' || !photoDto.photo){
setButtonDisabled(true)
} else{
setButtonDisabled(false)
}
}
const submitHandler = (e: FormEvent): void => {
e.preventDefault()
const formData = new FormData()
Object.keys(photoDto).forEach((key: string) => {
//@ts-ignore
formData.append(key, photoDto[key])
})
dispatch(addPhoto(formData))
setPhotoDto({
title: '',
photo: undefined
})
setFileName('')
}
useEffect(() => {
checkButton()
},[photoDto])
return(
<div
>
{
loadingPhotos ? <Preloader/> : null
}
<h1>Add new photo</h1>
<form
onSubmit={submitHandler}
style={{margin: '10px', padding: '20px', background: '#e0e0e0', borderRadius: '5px', display: 'flex', flexDirection: 'column'}}
>
<TextField
id="outlined-basic"
label="Photo title"
variant="outlined"
style={{marginBottom: '20px'}}
value={photoDto.title}
name='title'
onChange={inputHandler}
autoComplete='off'
/>
<label style={{height: 'auto', margin: '10px', maxWidth: '30%', display: 'flex', flexDirection: 'column'}}>
<input
style={{display: "none"}}
name={'photo'}
type="file"
placeholder="Image"
accept=".png, .jpg, .jpeg"
onChange={inputFileHandler}
/>
<h1 className={styles.file_input} style={{margin: '0'}}>
Choose file
</h1>
<span className={styles.filename}>{fileName}</span>
</label>
<button
disabled={buttonDisabled}
className={styles.add_btn}>SEND</button>
</form>
</div>
)
}
export default AddForm
\ No newline at end of file
.AuthorizeForm{
max-width: 600px;
margin: 0 auto;
display: flex;
justify-content: center;
align-items: center;
margin-top: 100px;
}
.AuthorizeForm p {
margin: 10px 0;
}
.AuthorizeForm form{
width: 90%;
min-height: 300px;
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
background: rgba(83, 26, 136, 0.659);
border-radius: 5px;
}
.login_input{
width: 80%;
height: 40px;
padding: 5px;
border-radius: 7px;
border: none;
margin-bottom: 20px;
font-family: 'Kanit', sans-serif;
}
.toggle {
cursor: pointer;
display: inline-block;
}
.toggleSwitch {
margin-top: 10px;
display: inline-block;
background: #ccc;
border-radius: 16px;
width: 58px;
height: 32px;
position: relative;
vertical-align: middle;
transition: background 0.25s;
cursor: pointer;
}
.toggleSwitch:before, .toggleSwitch:after {
content: "";
}
.toggleSwitch:before {
display: block;
background: linear-gradient(to bottom, #fff 0%, #eee 100%);
border-radius: 50%;
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.25);
width: 24px;
height: 24px;
position: absolute;
top: 4px;
left: 4px;
transition: left 0.25s;
}
.toggle:hover .toggleSwitch:before {
background: linear-gradient(to bottom, #fff 0%, #fff 100%);
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.5);
}
.toggleCheckbox:checked + .toggleSwitch {
background: #0d893f;
}
.toggleCheckbox:checked + .toggleSwitch:before {
left: 30px;
}
.toggleCheckbox {
position: absolute;
visibility: hidden;
}
.toggleLabel {
margin-left: 5px;
position: relative;
top: 2px;
}
.login_btn{
margin: 0 auto;
margin-top: 30px;
width: 170px;
height: 50px;
}
.login_btn:hover{
background-color: #56c080;
border: none;
}
import { FormEvent, useEffect, useState } from 'react'
import styles from './AuthorizeForm.module.css'
import IUserCreateDto from '../../interfaces/IUserCreateDto'
import { AppDispatch, AppState, useAppDispatch } from '../../store/store'
import { useLocation, useNavigate } from 'react-router-dom'
import { shallowEqual, useSelector } from 'react-redux'
import Preloader from '../../components/UI/Preloader/Preloader'
import { createUser, hideMessage, loginUser } from '../../store/user/user.slice'
import Alert from '@mui/material/Alert';
const AuthorizeForm: React.FunctionComponent = (): React.ReactElement => {
const navigate = useNavigate()
const dispatch: AppDispatch = useAppDispatch()
const { isAuth, messageUser, loadingUser } = useSelector((state: AppState) => state.users, shallowEqual)
const location = useLocation()
const [userValues, setUserValues] = useState<IUserCreateDto>({
username: '',
password: ''
})
const [buttonDisabled, setButtonDisabled] = useState<boolean>(true)
const [isLoginUser, setIsLoginUser] = useState<boolean>(true)
const inputHandler = (e: React.ChangeEvent<HTMLInputElement>): void => {
setUserValues(prevState => {
return {...prevState, [e.target.name]: e.target.value}
})
}
const toggleChangeHandler = () => {
setIsLoginUser(!isLoginUser)
}
const submitHandler = async (e: FormEvent) => {
e.preventDefault()
if (isLoginUser){
await dispatch(createUser(userValues))
} else{
await dispatch(loginUser(userValues))
}
}
useEffect(() => {
checkButton()
},[userValues])
useEffect(() => {
if (isAuth){
navigate(location.state?.from ? location.state.from : '/')
}
}, [isAuth])
const checkButton = () => {
if (userValues.username.trim() === '' || userValues.password.trim() === ''){
setButtonDisabled(true)
} else{
setButtonDisabled(false)
}
}
useEffect(() => {
dispatch(hideMessage())
}, [])
return (
<div className={styles.AuthorizeForm}>
{
loadingUser ? <Preloader/> : null
}
<form onSubmit={submitHandler}>
{
messageUser.trim() !== '' ?
<Alert variant="filled" severity="error">
{messageUser}
</Alert> : null
}
<p>{isLoginUser ? 'Login user' : 'Authorize user'}</p>
<p>Username:</p>
<input
className={styles.login_input}
placeholder='username...'
name={'username'}
autoComplete='off'
value={userValues.username}
onChange={inputHandler}
/>
<p>Password:</p>
<input
className={styles.login_input}
placeholder='password...'
name={'password'}
type='password'
autoComplete='off'
value={userValues.password}
onChange={inputHandler}
/>
<label style={{display: 'flex', alignItems: 'center'}}>
<input
className={styles.toggleCheckbox}
type="checkbox"
checked={isLoginUser}
onChange={toggleChangeHandler}
/>
<div className={styles.toggleSwitch}></div>
<span className={styles.toggleLabel}>New user?</span>
</label>
<button
className={styles.login_btn}
disabled={buttonDisabled}
>Authorize</button>
</form>
</div>
)
}
export default AuthorizeForm
import React from 'react';
import { Box, Button, Typography } from '@mui/material';
import { useNavigate } from 'react-router-dom';
const ErrorPage: React.FunctionComponent = (): React.ReactElement => {
const navigate = useNavigate()
const goToHome = () => {
navigate('/')
}
return (
<Box
sx={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
flexDirection: 'column',
minHeight: '100vh'
}}
>
<Typography variant="h1" style={{ color: 'white' }}>
404
</Typography>
<Typography variant="h6" style={{ color: 'white' }}>
The page you’re looking for doesn’t exist.
</Typography>
<Button onClick={goToHome} variant="contained">Back Home</Button>
</Box>
);
}
export default ErrorPage
\ No newline at end of file
.main_page_container{
max-width: 1400px;
width: 100%;
margin: 0 auto;
padding: 20px;
margin-top: 100px;
display: flex;
justify-content: space-between;
flex-wrap: wrap;
}
\ No newline at end of file
import { useEffect, useState } from 'react'
import { AppDispatch, AppState, useAppDispatch } from '../../store/store'
import styles from './MainPage.module.css'
import { getAllPhotos, setTargetedUser } from '../../store/photos/photos.slice'
import { shallowEqual, useSelector } from 'react-redux'
import IPhoto from '../../interfaces/IPhoto'
import PhotoBlock from '../../components/PhotoBlock/PhotoBlock'
import Backdrop from '@mui/material/Backdrop';
import Box from '@mui/material/Box';
import Modal from '@mui/material/Modal';
import Fade from '@mui/material/Fade';
import { useNavigate } from 'react-router-dom'
import { CardMedia } from '@mui/material'
import image_not_found from '../../assets/image_not_found.png'
import IUser from '../../interfaces/IUser'
import Preloader from '../../components/UI/Preloader/Preloader'
const modalStyles = {
position: 'absolute' as 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: 700,
bgcolor: 'background.paper',
border: '2px solid #000',
boxShadow: 24,
p: 1,
};
const MainPage: React.FunctionComponent = (): React.ReactElement => {
const [open, setOpen] = useState<boolean>(false);
const [fullImageSrc, setFullImageSrc] = useState<string>('')
const handleClose = () => setOpen(false);
const dispatch: AppDispatch = useAppDispatch()
const {photos, loadingPhotos} = useSelector((state: AppState) => state.photos, shallowEqual)
const navigate = useNavigate()
const goToUserPageHandler = (user: IUser) => {
dispatch(setTargetedUser(user))
navigate(`/user/${user._id}`)
}
const showFullImageHandler = (src: string) => {
setFullImageSrc(src)
setOpen(true)
}
useEffect(() => {
dispatch(getAllPhotos())
}, [])
return(
<div className={styles.main_page_container}>
{
loadingPhotos ? <Preloader/> : null
}
<Modal
aria-labelledby="transition-modal-title"
aria-describedby="transition-modal-description"
open={open}
onClose={handleClose}
closeAfterTransition
slots={{ backdrop: Backdrop }}
slotProps={{
backdrop: {
timeout: 500,
},
}}
>
<Fade in={open}>
<Box sx={modalStyles}>
<CardMedia
component="img"
style={{
maxWidth: '100%',
height: 'auto',
maxHeight: '70vh'
}}
image={import.meta.env.VITE_BASE_URL + 'uploads/' + fullImageSrc}
alt="full image"
onError = {(e) => {
e.currentTarget.src = image_not_found
}}
/>
</Box>
</Fade>
</Modal>
{
photos.length ?
photos.map((photo: IPhoto) => {
return <PhotoBlock
key={photo._id}
photo={photo}
goToUserPage={()=>{goToUserPageHandler(photo.user)}}
showFullImage={()=>{showFullImageHandler(photo.photo)}}
/>
}): <h1>Gallery is empty</h1>
}
</div>
)
}
export default MainPage
\ No newline at end of file
import { useEffect, useState } from 'react'
import { AppDispatch, AppState, useAppDispatch } from '../../store/store'
import styles from './MainPage.module.css'
import { deletePhotoById, getAllPhotos, getPhotosByUserId, setTargetedUser } from '../../store/photos/photos.slice'
import { shallowEqual, useSelector } from 'react-redux'
import IPhoto from '../../interfaces/IPhoto'
import PhotoBlock from '../../components/PhotoBlock/PhotoBlock'
import Backdrop from '@mui/material/Backdrop';
import Box from '@mui/material/Box';
import Modal from '@mui/material/Modal';
import Fade from '@mui/material/Fade';
import { useNavigate, useParams } from 'react-router-dom'
import { Button, CardMedia } from '@mui/material'
import image_not_found from '../../assets/image_not_found.png'
import IUser from '../../interfaces/IUser'
import PhotoUserGalleryBlock from '../../components/PhotoUserGalleryBlock/PhotoUserGalleryBlock'
const modalStyles = {
position: 'absolute' as 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: 700,
bgcolor: 'background.paper',
border: '2px solid #000',
boxShadow: 24,
p: 1,
};
export const UserGallery: React.FunctionComponent = (): React.ReactElement => {
const [open, setOpen] = useState<boolean>(false);
const [fullImageSrc, setFullImageSrc] = useState<string>('')
const handleClose = () => setOpen(false);
const params = useParams()
const dispatch: AppDispatch = useAppDispatch()
const {photosByUser, targetedUser} = useSelector((state: AppState) => state.photos, shallowEqual)
const {user} = useSelector((state: AppState) => state.users, shallowEqual)
const navigate = useNavigate()
const showFullImageHandler = (src: string) => {
setFullImageSrc(src)
setOpen(true)
}
const deletePhotoHandler = (id: string) => {
dispatch(deletePhotoById(id))
}
const goToAddForm = () => {
navigate('/add-photo')
}
useEffect(() => {
if (params.id) dispatch(getPhotosByUserId(params.id))
}, [])
return (
<div
style={{
maxWidth: '1400px',
width: '100%',
margin: '0 auto'
}}
>
<Modal
aria-labelledby="transition-modal-title"
aria-describedby="transition-modal-description"
open={open}
onClose={handleClose}
closeAfterTransition
slots={{ backdrop: Backdrop }}
slotProps={{
backdrop: {
timeout: 500,
},
}}
>
<Fade in={open}>
<Box sx={modalStyles}>
<CardMedia
component="img"
style={{
maxWidth: '100%',
height: 'auto',
maxHeight: '70vh'
}}
image={import.meta.env.VITE_BASE_URL + 'uploads/' + fullImageSrc}
alt="full image"
onError = {(e) => {
e.currentTarget.src = image_not_found
}}
/>
</Box>
</Fade>
</Modal>
<div style={{display: 'flex', justifyContent: 'space-between', alignItems: 'center'}}>
<h1>{targetedUser._id === user?._id ? 'This is your page' : `${targetedUser.username}'s gallery`}</h1>
{targetedUser._id === user?._id ?
<Button variant="contained" size='medium' onClick={goToAddForm}>
Add Photo
</Button> : null}
</div>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
flexWrap: 'wrap'
}}
>
{
photosByUser && photosByUser.length ?
photosByUser.map((photo: IPhoto) => {
return <PhotoUserGalleryBlock
key={photo._id}
photo={photo}
showFullImage={()=>showFullImageHandler(photo.photo)}
deletePhoto={()=>{deletePhotoHandler(photo._id)}}
/>
}): <h1>Gallery is empty</h1>
}
</div>
</div>
)
}
export default UserGallery
\ No newline at end of file
export enum EStatuses {
SUCCESS = 'Success',
FAILURE = 'Failure'
SUCCESS = 'SUCCESS',
FAILURE = 'FAILURE'
}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment