Compare commits

..

9 Commits

Author SHA1 Message Date
00c8831dc9 adjust location detail response with reviews 2023-09-27 21:55:51 +07:00
cd680de68b add spinner loading component 2023-09-27 21:55:21 +07:00
19f7f6f07b add logout service and adjust axios client 2023-09-27 21:54:58 +07:00
5e13d85ff8 add not found page 2023-09-27 21:53:28 +07:00
ba1ceb1af3 responsive login buttton 2023-09-25 10:02:20 +07:00
ade7e71d74 add interweave 2023-09-24 18:06:57 +07:00
c869ce47c5 add new color for error 2023-09-24 18:06:41 +07:00
3e810c80e9 add login 2023-09-23 15:20:47 +07:00
d86738dccf add redux 2023-09-23 15:19:22 +07:00
39 changed files with 1203 additions and 431 deletions

View File

@ -9,9 +9,20 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@reduxjs/toolkit": "^1.9.5",
"@types/react-redux": "^7.1.26",
"axios": "^1.5.0", "axios": "^1.5.0",
"emojibase": "^15.0.0",
"interweave": "^13.1.0",
"interweave-autolink": "^5.1.0",
"interweave-emoji": "^7.0.0",
"moment": "^2.29.4",
"preact": "^10.16.0", "preact": "^10.16.0",
"react": "^18.2.0",
"react-redux": "^8.1.2",
"react-router-dom": "^6.16.0", "react-router-dom": "^6.16.0",
"redux-persist": "^6.0.0",
"redux-thunk": "^2.4.2",
"yet-another-react-lightbox": "^3.12.2" "yet-another-react-lightbox": "^3.12.2"
}, },
"devDependencies": { "devDependencies": {

View File

@ -0,0 +1,5 @@
import { LOGOUT } from "../constants/actions";
export const logout = () => ({
type: LOGOUT
});

1
src/actions/index.ts Normal file
View File

@ -0,0 +1 @@
export * from "./LogoutAction";

View File

@ -4,26 +4,36 @@ import './app.css'
import { DefaultLayout } from './layouts' import { DefaultLayout } from './layouts'
import routes from './routes' import routes from './routes'
import "yet-another-react-lightbox/styles.css"; import "yet-another-react-lightbox/styles.css";
import { Login, NotFound } from './pages'
import { Provider } from 'react-redux'
import { persistore, store } from './store/config'
import { PersistGate } from 'redux-persist/integration/react'
export function App() { export function App() {
return ( return (
<> <>
<Router> <Provider store={store}>
<Routes> <PersistGate persistor={persistore}>
<Route element={<DefaultLayout />}> <Router>
{routes.map(({ path, name, element}) => ( <Routes>
<> <Route path='/login' element={<Login />} />
<Route <Route element={<DefaultLayout />}>
path={path} {routes.map(({ path, name, element }) => (
id={name} <>
element={element} <Route
/> path={path}
</> id={name}
))} element={element}
</Route> />
</Routes> </>
</Router> ))}
</Route>
<Route path="*" element={<NotFound />} />
</Routes>
</Router>
</PersistGate>
</Provider>
</> </>
) )
} }

View File

@ -0,0 +1,64 @@
import { stripHexcode } from 'emojibase';
import { InterweaveProps, FilterInterface, MatcherInterface, Interweave } from 'interweave';
import { IpMatcher, UrlMatcher, EmailMatcher, HashtagMatcher } from 'interweave-autolink';
import { EmojiMatcher, PathConfig } from 'interweave-emoji';
const globalFilters: FilterInterface[] = [];
const globalMatchers: MatcherInterface[] = [
new EmailMatcher('email'),
new IpMatcher('ip'),
new UrlMatcher('url'),
new HashtagMatcher('hashtag'),
new EmojiMatcher('emoji', {
convertEmoticon: true,
convertShortcode: true,
convertUnicode: true,
}),
];
function getEmojiPath(hexcode: string, { enlarged }: PathConfig): string {
return `//cdn.jsdelivr.net/emojione/assets/3.1/png/${enlarged ? 64 : 32}/${stripHexcode(
hexcode,
).toLowerCase()}.png`;
}
interface Props extends InterweaveProps {
instagram?: boolean;
twitter?: boolean;
}
export default function CustomInterweave({
filters = [],
matchers = [],
twitter,
instagram,
...props
}: Props) {
let hashtagUrl = '';
if (twitter) {
hashtagUrl = 'https://twitter.com/hashtag/{{hashtag}}';
} else if (instagram) {
hashtagUrl = 'https://instagram.com/explore/tags/{{hashtag}}';
}
return (
<Interweave
filters={[...globalFilters, ...filters]}
matchers={[...globalMatchers, ...matchers]}
// hashtagUrl={hashtagUrl}
emojiSource={getEmojiPath}
// newWindow
{...props}
/>
// <BaseInterweave
// filters={[...globalFilters, ...filters]}
// matchers={[...globalMatchers, ...matchers]}
// hashtagUrl={hashtagUrl}
// emojiPath={getEmojiPath}
// newWindow
// {...props}
// />
);
}

View File

@ -1,26 +1,45 @@
import React from "preact/compat"; import React, { TargetedEvent } from "preact/compat";
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
import { useSelector, useDispatch } from "react-redux";
import { UserRootState } from "../../store/type";
import { logout } from '../../actions';
import './style.css'; import './style.css';
import { logoutService } from "../../services";
function Header() { function Header() {
const [searchVal, setSearchVal] = useState(''); const [searchVal, setSearchVal] = useState('');
const [dropdown, setDropdown] = useState(false); const [dropdown, setDropdown] = useState(false);
const [pageState, setPageState] = useState({
profileMenu: false
})
const dispatch = useDispatch();
const user = useSelector((state: UserRootState) => state.auth)
const onInput = (e: React.ChangeEvent<HTMLInputElement>): void => { const onInput = (e: React.ChangeEvent<HTMLInputElement>): void => {
const val = e.target as HTMLInputElement; const val = e.target as HTMLInputElement;
setSearchVal(val.value) setSearchVal(val.value)
} }
const handleLogout = async (): Promise<void> => {
try {
await logoutService()
dispatch(logout())
location.reload()
} catch (error) {
alert(error)
}
}
const onSearchSubmit = (e: React.TargetedEvent<HTMLFormElement>): void => { const onSearchSubmit = (e: React.TargetedEvent<HTMLFormElement>): void => {
e.preventDefault(); e.preventDefault();
} }
const onDropdown = (): void => { const onDropdown = (): void => {
document.body.style.overflow = "hidden" document.body.style.overflow = "hidden"
if(dropdown) { if (dropdown) {
document.body.style.overflowX = "hidden"; document.body.style.overflowX = "hidden";
document.body.style.overflowY = "scroll"; document.body.style.overflowY = "scroll";
} }
@ -33,6 +52,25 @@ function Header() {
<a href={"/"}> <a href={"/"}>
<h1 className={`title ${dropdown ? 'title-dropdown' : ""}`}>Hilingin</h1> <h1 className={`title ${dropdown ? 'title-dropdown' : ""}`}>Hilingin</h1>
</a> </a>
<div className={'user-img self-center mr-5'} style={dropdown ? { display: 'none' } : ''}>
<a href={user.username ? '#' : '/login'} onClick={() => user.username ? setPageState({ ...pageState, profileMenu: !pageState.profileMenu }) : ''}>
<img
loading={'lazy'}
style={{ width: 40, borderRadius: 15 }}
src={'https://cdn.discordapp.com/attachments/743422487882104837/1153985664849805392/421-4212617_person-placeholder-image-transparent-hd-png-download.png'}
/>
</a>
{user.username &&
<div className={'profile-dropdown-img bg-secondary text-left'} style={pageState.profileMenu ? { display: 'block'} : { display: 'none'}}>
<a href={'#'}><div className={'p-2'}>Profile</div></a>
<a href={'#'}><div className={'p-2'}>Feed</div></a>
<a href={'#'}><div className={'p-2'}>Add location</div></a>
<a href={'#'} onClick={handleLogout}><div className={'p-2'}>Logout</div></a>
{/* <div className={'p-2'}><a href={'#'}>Halo</a></div> */}
{/* <div className={'p-2'}><a href={'#'}>Halo</a></div> */}
</div>
}
</div>
<form onSubmit={onSearchSubmit} className={`search-input ${dropdown ? "search-input-dropdown" : ""}`}> <form onSubmit={onSearchSubmit} className={`search-input ${dropdown ? "search-input-dropdown" : ""}`}>
<label> <label>
<input <input
@ -44,7 +82,7 @@ function Header() {
/> />
</label> </label>
</form> </form>
<button onClick={onDropdown} className="dropdown-menu bg-secondary" style={{ padding: 5, margin: 'auto 0', borderRadius: 10, marginLeft: 'auto' }}> <button onClick={onDropdown} className={`dropdown-menu bg-secondary ${dropdown ? 'ml-auto' : ''}`} style={{ padding: 5, borderRadius: 10 }}>
<svg xmlns="http://www.w3.org/2000/svg" height="30" fill="white" viewBox="0 -960 960 960" width="30"><path d="M120-240v-80h720v80H120Zm0-200v-80h720v80H120Zm0-200v-80h720v80H120Z" /></svg> <svg xmlns="http://www.w3.org/2000/svg" height="30" fill="white" viewBox="0 -960 960 960" width="30"><path d="M120-240v-80h720v80H120Zm0-200v-80h720v80H120Zm0-200v-80h720v80H120Z" /></svg>
</button> </button>
</div> </div>
@ -54,11 +92,22 @@ function Header() {
} }
<a href="/best-places" className={`navLink ${!dropdown ? "navLink-disabled" : ""}`}>Top Places</a> <a href="/best-places" className={`navLink ${!dropdown ? "navLink-disabled" : ""}`}>Top Places</a>
<a href="/discover" className={`navLink ${!dropdown ? "navLink-disabled" : ""}`}>Discover</a> <a href="/discover" className={`navLink ${!dropdown ? "navLink-disabled" : ""}`}>Discover</a>
<a href="#" className={`navLink ${!dropdown ? "navLink-disabled" : ""}`}>Trending Places</a>
<a href="/stories" className={`navLink ${!dropdown ? "navLink-disabled" : ""}`}>Stories</a> <a href="/stories" className={`navLink ${!dropdown ? "navLink-disabled" : ""}`}>Stories</a>
<a href="/news-events" className={`navLink ${!dropdown ? "navLink-disabled" : ""}`}>News / Events</a> <a href="/news-events" className={`navLink ${!dropdown ? "navLink-disabled" : ""}`}>News / Events</a>
<a href="#" className={`navLink ${!dropdown ? "navLink-disabled" : ""}`}>Forum</a> <a href="#" className={`navLink ${!dropdown ? "navLink-disabled" : ""}`}>Forum</a>
<a href="#" className={`navLink ${!dropdown ? "navLink-disabled" : ""}`}>Sign in</a> <div className={'profile-container'}>
<a href={user.username ? '#' : '/login'} onClick={() => user.username ? setPageState({ ...pageState, profileMenu: !pageState.profileMenu }) : ''} className={`navLink ${!dropdown ? "navLink-disabled" : ""}`}>{user.username ? user.username : 'Sign in'}</a>
{user && screen.width > 600 &&
<div className={'profile-dropdown bg-secondary ml-6'} style={pageState.profileMenu ? { display: 'block' } : {display: 'none'}}>
<a href={'#'}><div className={'p-1'}>Profile</div></a>
<a href={'#'}><div className={'p-1'}>Feed</div></a>
<a href={'#'}><div className={'p-1'}>Add location</div></a>
<a href={'#'} onClick={handleLogout}><div className={'p-1'}>Logout</div></a>
{/* <div className={'p-2'}><a href={'#'}>Halo</a></div> */}
{/* <div className={'p-2'}><a href={'#'}>Halo</a></div> */}
</div>
}
</div>
</div> </div>
</header> </header>
) )

View File

@ -1,74 +1,7 @@
@media screen and (max-width: 768px) {
a.navLink {
font-size: 12px;
padding: 0px 20px;
}
}
@media screen and (max-width: 488px) {
a.navLink{
/* display: none; */
margin-top: 10px;
width: 100%;
padding: 0;
border-bottom: 1px solid white;
padding: 10px 0
}
div.nav-container {
padding: 0 20px;
height: 100vh;
/* align-items: start; */
justify-content: start;
flex-direction: column;
}
div.nav-container-disabled {
padding: 4px;
height: 0;
}
a.navLink-disabled {
display: none;
}
form.search-input {
display: none;
margin-left: 0;
}
button.dropdown-menu {
display: block;
}
.search-input-dropdown {
margin-right: 10px;
display: block !important;
}
h1.title-dropdown {
display: none;
}
a.navLink:hover{
border-bottom-width: 1px;
margin-bottom: 0px;
}
}
@media screen and (max-width: 390px){
input.text-input-search {
width: 230px;
}
}
.dropdown-menu { .dropdown-menu {
display: none; display: none;
} }
.search-input {
margin-left: auto;
}
label { label {
position: relative; position: relative;
@ -102,6 +35,33 @@ label:before {
font-size: 0.8em; font-size: 0.8em;
} }
.profile-dropdown {
display: block;
position: absolute;
padding: 5px;
width: 135px;
font-size: 13px;
}
.profile-dropdown a div:hover {
background-color: #a8adb3;
color: white;
}
.profile-dropdown-img {
position: absolute;
font-size: 13px;
padding: 5px;
margin-top: 5px;
right: 70px;
width: 130px;
z-index: 9999;
}
.profile-dropdown-img a div:hover {
background-color: #a8adb3;
color: white;
}
.nav-container { .nav-container {
background-color: #2f3136; background-color: #2f3136;
@ -114,4 +74,91 @@ label:before {
/* display: 'inline-block'; /* display: 'inline-block';
max-width: '100%'; max-width: '100%';
text-align: 'center'; */ text-align: 'center'; */
} }
.search-input {
margin-left: auto;
}
.user-img {
display: none;
}
@media screen and (max-width: 768px) {
a.navLink {
font-size: 12px;
padding: 0px 20px;
}
}
@media screen and (max-width: 488px) {
a.navLink{
/* display: none; */
margin-top: 10px;
width: 100%;
padding: 0;
border-bottom: 1px solid white;
padding: 10px 0
}
.profile-container {
display: none;
}
div.nav-container {
padding: 0 20px;
height: 100vh;
/* align-items: start; */
justify-content: start;
flex-direction: column;
}
div.nav-container-disabled {
padding: 4px;
height: 0;
}
a.navLink-disabled {
display: none;
}
form.search-input {
display: none;
margin-left: 0;
}
div.user-img {
display: block;
margin-left: auto;
}
button.dropdown-menu {
display: block;
}
.search-input-dropdown {
margin-right: 10px;
display: block !important;
}
h1.title-dropdown {
display: none;
}
a.navLink:hover{
border-bottom-width: 1px;
margin-bottom: 0px;
}
.user-img .profile-dropdown-img {
display: block;
}
}
@media screen and (max-width: 390px){
input.text-input-search {
width: 230px;
}
}

View File

@ -0,0 +1,6 @@
import './style.css'
export default function SpinnerLoading() {
return (
<div className={'spinner'} style={{ display: 'inline-block'}}></div>
)
}

View File

@ -0,0 +1,16 @@
@keyframes spinner {
to {transform: rotate(360deg);}
}
.spinner:before {
content: '';
box-sizing: border-box;
position: relative;
display: inline-block;
width: 16px;
height: 16px;
border-radius: 50%;
border: 2px solid #ccc;
border-top-color: #000;
animation: spinner .7s linear infinite;
}

View File

@ -2,10 +2,16 @@ import Header from "./Header";
import SeparatorWithAnchor from "./Separator/WithAnchor"; import SeparatorWithAnchor from "./Separator/WithAnchor";
import DefaultSeparator from "./Separator/Default"; import DefaultSeparator from "./Separator/Default";
import Footer from './Footer/'; import Footer from './Footer/';
import CustomInterweave from "./CustomInterweave";
import SpinnerLoading from "./Loading/Spinner";
export { export {
Header, Header,
SeparatorWithAnchor, SeparatorWithAnchor,
DefaultSeparator, DefaultSeparator,
Footer Footer,
CustomInterweave,
SpinnerLoading,
} }

1
src/constants/actions.ts Normal file
View File

@ -0,0 +1 @@
export const LOGOUT = 'LOGOUT';

View File

@ -1,7 +1,8 @@
const BASE_URL = "http://localhost:8888" const BASE_URL = "http://localhost:8888"
const SIGNUP_URI = `${BASE_URL}/user/signup` const SIGNUP_URI = `${BASE_URL}/user/signup`
const LOGIN_URI = `${BASE_URL}/user/login`
const LOGOUT_URI = `${BASE_URL}/user/logout`
const GET_LIST_LOCATIONS_URI = `${BASE_URL}/locations`; const GET_LIST_LOCATIONS_URI = `${BASE_URL}/locations`;
const GET_LIST_TOP_LOCATIONS = `${BASE_URL}/locations/top-ratings` const GET_LIST_TOP_LOCATIONS = `${BASE_URL}/locations/top-ratings`
@ -11,13 +12,18 @@ const GET_LOCATION_TAGS_URI = `${BASE_URL}/location/tags`
const GET_IMAGES_BY_LOCATION_URI = `${BASE_URL}/images/location` const GET_IMAGES_BY_LOCATION_URI = `${BASE_URL}/images/location`
const POST_REVIEW_LOCATION_URI = `${BASE_URL}/review/location`
export { export {
BASE_URL, BASE_URL,
SIGNUP_URI, SIGNUP_URI,
LOGIN_URI,
LOGOUT_URI,
GET_LIST_RECENT_LOCATIONS_RATING_URI, GET_LIST_RECENT_LOCATIONS_RATING_URI,
GET_LIST_TOP_LOCATIONS, GET_LIST_TOP_LOCATIONS,
GET_LIST_LOCATIONS_URI, GET_LIST_LOCATIONS_URI,
GET_LOCATION_URI, GET_LOCATION_URI,
GET_LOCATION_TAGS_URI, GET_LOCATION_TAGS_URI,
GET_IMAGES_BY_LOCATION_URI, GET_IMAGES_BY_LOCATION_URI,
POST_REVIEW_LOCATION_URI
} }

2
src/constants/default.ts Normal file
View File

@ -0,0 +1,2 @@
export const DEFAULT_AVATAR_IMG = 'https://cdn.discordapp.com/attachments/743422487882104837/1153985664849805392/421-4212617_person-placeholder-image-transparent-hd-png-download.png';

View File

@ -0,0 +1,20 @@
import { createSlice } from "@reduxjs/toolkit";
const initialState = {}
const authSlice = createSlice({
name: 'auth',
initialState,
reducers: {
authAdded(state, action) {
const current_user = action.payload;
let newState = { ...state }
newState = current_user
return newState
}
}
})
export const { authAdded } = authSlice.actions;
export default authSlice.reducer;

View File

@ -0,0 +1,14 @@
export interface IUser {
id: Number,
email: String,
username: String,
avatar_picture: String,
banned_at: NullValueRes<"Time", String>,
banned_until: NullValueRes<"Time", String>,
ban_reason: String,
is_permaban: boolean,
is_admin: boolean,
is_critics: boolean,
is_verfied: boolean,
social_media: NullValueRes<"RawMessage", any>
}

5
src/features/index.ts Normal file
View File

@ -0,0 +1,5 @@
import authSlice from "./auth/authSlice/authSlice";
export {
authSlice
}

View File

@ -18,6 +18,13 @@
-webkit-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;
} }
input:focus {
outline: none;
}
a:hover {
cursor: pointer;
}
/* a { /* a {
font-weight: 500; font-weight: 500;
color: #646cff; color: #646cff;

View File

@ -3,8 +3,6 @@ import { getListTopLocationsService } from "../../services";
import { DefaultSeparator } from "../../components"; import { DefaultSeparator } from "../../components";
import { TargetedEvent } from "preact/compat"; import { TargetedEvent } from "preact/compat";
import './style.css'; import './style.css';
import { useNavigate } from "react-router-dom";
interface TopLocation { interface TopLocation {
row_number: Number, row_number: Number,
@ -57,8 +55,6 @@ function BestLocation() {
filterRegionType: 0, filterRegionType: 0,
}) })
const navigate = useNavigate()
async function getTopLocations() { async function getTopLocations() {
try { try {
const res = await getListTopLocationsService({ page: page, page_size: 20, order_by: pageState.filterScoreTypeidx, region_type: pageState.filterRegionType }) const res = await getListTopLocationsService({ page: page, page_size: 20, order_by: pageState.filterScoreTypeidx, region_type: pageState.filterRegionType })
@ -79,18 +75,6 @@ function BestLocation() {
setPageState({ ...pageState, filterRegionTypeName: region_name, filterRegionType: type}) setPageState({ ...pageState, filterRegionTypeName: region_name, filterRegionType: type})
} }
function onNavigateToDetail(
id: Number,
critic_count: Number,
critic_score: Number,
user_count: Number,
user_score: Number,
) {
navigate(`/location/${id}`, { state: { user_score: user_score, user_count: user_count, critic_score: critic_score, critic_count: critic_count }})
}
useEffect(() => { useEffect(() => {
getTopLocations() getTopLocations()
}, [pageState]) }, [pageState])
@ -152,10 +136,10 @@ function BestLocation() {
</a> </a>
</div> */} </div> */}
<div className={'mb-2 best-locations-title'}> <div className={'mb-2 best-locations-title'}>
<a className={'text-xl'} href={`/location/${x.id}`} onClick={() => onNavigateToDetail(x.id, x.critic_count, x.critic_score, x.user_count, x.user_score)}>{x.row_number}.{x.name}</a> <a className={'text-xl'} href={`/location/${x.id}`}>{x.row_number}.{x.name}</a>
</div> </div>
<div style={{ maxWidth: 200, maxHeight: 200, margin: '0 30px 30px 10px', float: 'left' }}> <div style={{ maxWidth: 200, maxHeight: 200, margin: '0 30px 30px 10px', float: 'left' }}>
<a href={`/location/${x.id}`} onClick={() => onNavigateToDetail(x.id, x.critic_count, x.critic_score, x.user_count, x.user_score)}> <a href={`/location/${x.id}`} >
<img src={x.thumbnail.String.toString()} loading={'lazy'} style={{ width: '100%', objectFit: 'cover', height: '100%', aspectRatio: '1/1' }} /> <img src={x.thumbnail.String.toString()} loading={'lazy'} style={{ width: '100%', objectFit: 'cover', height: '100%', aspectRatio: '1/1' }} />
</a> </a>
</div> </div>

View File

@ -67,6 +67,11 @@ img {
outline: none; outline: none;
} }
.text-area-button {
background-color: gray;
letter-spacing: 1px;
}
.criticSortFilter { .criticSortFilter {
float: right; float: right;
} }

View File

@ -1,22 +1,43 @@
import { useLocation, useParams } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
import { ChangeEvent, TargetedEvent } from 'preact/compat'; import { ChangeEvent, TargetedEvent } from 'preact/compat';
import { useEffect, useRef, useState } from 'preact/hooks'; import { useEffect, useRef, useState } from 'preact/hooks';
import './index.css';
import Lightbox from 'yet-another-react-lightbox'; import Lightbox from 'yet-another-react-lightbox';
import useCallbackState from '../../types/state-callback'; import useCallbackState from '../../types/state-callback';
import { EmptyLocationDetailResponse, LocationDetailResponse, LocationResponse, emptyLocationResponse } from './types'; import {
import { useAutosizeTextArea } from '../../utils'; EmptyLocationDetailResponse,
import { getImagesByLocationService, getLocationService } from "../../services"; LocationDetailResponse,
import { DefaultSeparator, SeparatorWithAnchor } from '../../components'; LocationResponse,
emptyLocationResponse,
CurrentUserLocationReviews,
} from './types';
import { handleAxiosError, useAutosizeTextArea } from '../../utils';
import { getImagesByLocationService, getLocationService, postReviewLocation } from "../../services";
import { DefaultSeparator, SeparatorWithAnchor, CustomInterweave, SpinnerLoading } from '../../components';
import { useSelector } from 'react-redux';
import { UserRootState } from '../../store/type';
import { DEFAULT_AVATAR_IMG } from '../../constants/default';
import './index.css';
import { AxiosError } from 'axios';
const SORT_TYPE = [
'highest rated',
'lowest rated',
'newest',
'oldest'
]
function LocationDetail() { function LocationDetail() {
const [locationDetail, setLocationDetail] = useCallbackState<LocationDetailResponse>(EmptyLocationDetailResponse) const [locationDetail, setLocationDetail] = useCallbackState<LocationDetailResponse>(EmptyLocationDetailResponse)
const [locationImages, setLocationImages] = useState<LocationResponse>(emptyLocationResponse()) const [locationImages, setLocationImages] = useState<LocationResponse>(emptyLocationResponse())
const [currentUserReview, setCurrentUserReview] = useState<CurrentUserLocationReviews>()
const [lightboxOpen, setLightboxOpen] = useState<boolean>(false) const [lightboxOpen, setLightboxOpen] = useState<boolean>(false)
const [pageState, setPageState] = useState({ const [pageState, setPageState] = useState({
critic_filter_name: 'highest rated', critic_filter_name: 'highest rated',
critic_filter_type: 0, critic_filter_type: 0,
show_sort: false show_sort: false,
enable_post: true,
on_submit_loading: false,
is_score_rating_panic_msg: '',
}) })
const [reviewValue, setReviewValue] = useState({ const [reviewValue, setReviewValue] = useState({
review_textArea: '', review_textArea: '',
@ -24,28 +45,27 @@ function LocationDetail() {
}) })
const [isLoading, setIsLoading] = useState<boolean>(true) const [isLoading, setIsLoading] = useState<boolean>(true)
const navigate = useNavigate();
const user = useSelector((state: UserRootState) => state.auth)
const textAreaRef = useRef<HTMLTextAreaElement>(null); const textAreaRef = useRef<HTMLTextAreaElement>(null);
useAutosizeTextArea(textAreaRef.current, reviewValue.review_textArea); useAutosizeTextArea(textAreaRef.current, reviewValue.review_textArea);
const { state } = useLocation();
const { id } = useParams() const { id } = useParams()
const SORT_TYPE = [
'highest rated',
'lowest rated',
'newest',
'oldest'
]
async function getLocationDetail(): Promise<void> { async function getLocationDetail(): Promise<void> {
try { try {
const res = await getLocationService(Number(id)) const res = await getLocationService(Number(id))
setLocationDetail(res.data, (val) => { setLocationDetail(res.data, (val) => {
getImage(val.detail.thumbnail.String.toString()) getImage(val.detail.thumbnail.String.toString())
}) })
} catch (err) { } catch (error) {
console.log(err) let err = error as AxiosError;
if (err.response?.status == 404) {
navigate("/")
}
alert(error)
} }
} }
@ -76,11 +96,75 @@ function LocationDetail() {
...reviewValue, ...reviewValue,
score_input: val.value score_input: val.value
}) })
setPageState({
...pageState,
is_score_rating_panic_msg: ''
})
} }
function onChangeCriticsSort(e: TargetedEvent<HTMLAnchorElement>, sort_name: string, sort_type: number): void { function onChangeCriticsSort(e: TargetedEvent<HTMLAnchorElement>, sort_name: string, sort_type: number): void {
e.preventDefault() e.preventDefault()
setPageState({ show_sort: false, critic_filter_name: sort_name, critic_filter_type: sort_type }) setPageState({ ...pageState, show_sort: false, critic_filter_name: sort_name, critic_filter_type: sort_type })
}
async function handleSubmitReview(e: TargetedEvent<HTMLAnchorElement>) {
e.preventDefault();
setPageState({ ...pageState, on_submit_loading: true })
if (isNaN(Number(reviewValue.score_input))) {
setPageState({ ...pageState, is_score_rating_panic_msg: "SCORE MUST BE A NUMBER" })
return
}
if (Number(reviewValue.score_input) > 100) {
setPageState({ ...pageState, is_score_rating_panic_msg: "SCORE MUST BE LESS OR EQUAL THAN 100" })
return
}
if (reviewValue.score_input === '') {
setPageState({ ...pageState, is_score_rating_panic_msg: "SCORE MUSTN'T BE EMPTY" })
return
}
try {
const { data } = await postReviewLocation({
is_hided: false,
location_id: Number(id),
score: Number(reviewValue.score_input),
submitted_by: Number(user.id),
is_from_critic: user.is_critics,
comments: reviewValue.review_textArea,
})
setPageState({ ...pageState, enable_post: false, on_submit_loading: false })
setReviewValue({ review_textArea: '', score_input: '' })
setCurrentUserReview({
id: data.id,
comments: data.comments,
is_from_critic: data.is_from_critic,
is_hided: data.is_hided,
location_id: data.location_id,
score: data.score,
submitted_by: data.submitted_by,
created_at: data.created_at,
updated_at: data.updated_at
})
} catch (error) {
let err = error as AxiosError;
console.log(err)
const str = handleAxiosError(err)
alert(str)
setPageState({ ...pageState, on_submit_loading: false })
}
}
function handleSignInNavigation(e: TargetedEvent<HTMLAnchorElement>) {
e.preventDefault();
navigate('/login', { state: { from: `/location/${id}` } })
} }
useEffect(() => { useEffect(() => {
@ -137,36 +221,36 @@ function LocationDetail() {
<div className={'p-4 bg-secondary mt-3'} style={{ width: '100%', height: 120, borderTopLeftRadius: 10, borderTopRightRadius: 10 }}> <div className={'p-4 bg-secondary mt-3'} style={{ width: '100%', height: 120, borderTopLeftRadius: 10, borderTopRightRadius: 10 }}>
<div className={'font-bold ml-1 text-xs'}>CRITICS SCORE</div> <div className={'font-bold ml-1 text-xs'}>CRITICS SCORE</div>
<div className={'text-4xl text-center mt-2 mr-4'} style={{ width: 95, float: 'left' }}> <div className={'text-4xl text-center mt-2 mr-4'} style={{ width: 95, float: 'left' }}>
{state.critic_count !== 0 ? state.critic_score : "NR"} {locationDetail.detail.critic_count !== 0 ? Math.floor(Number(locationDetail.detail.critic_score) / Number(locationDetail.detail.critic_count)) : "NR"}
<div className={"items-center p-2"}> <div className={"items-center p-2"}>
<div className={'mr-3 users-score-bar'}> <div className={'mr-3 users-score-bar'}>
<div className={"mt-1"} style={{ height: 4, width: 80, backgroundColor: "#72767d" }}> <div className={"mt-1"} style={{ height: 4, width: 80, backgroundColor: "#72767d" }}>
<div style={{ height: 4, width: ` ${state.critic_count !== 0 ? Number(state.critic_score) / Number(state.critic_count) * 10 : 0}%`, backgroundColor: 'green' }} /> <div style={{ height: 4, width: ` ${locationDetail.detail.critic_count !== 0 ? Number(locationDetail.detail.critic_score) : 0}%`, backgroundColor: 'green' }} />
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{state.critic_count !== 0 && {locationDetail.detail.critic_count !== 0 &&
<div className={'ml-14 text-sm'}> <div className={'ml-14 text-sm'}>
Based on {state.critic_count} reviews Based on {locationDetail.detail.critic_count} reviews
</div> </div>
} }
</div> </div>
<div className={'p-4 bg-secondary mt-1 inline-block'} style={{ width: '100%', height: 120, borderBottomLeftRadius: 10, borderBottomRightRadius: 10 }}> <div className={'p-4 bg-secondary mt-1 inline-block'} style={{ width: '100%', height: 120, borderBottomLeftRadius: 10, borderBottomRightRadius: 10 }}>
<div className={'font-bold ml-2 text-xs'}>USERS SCORE</div> <div className={'font-bold ml-2 text-xs'}>USERS SCORE</div>
<div className={'text-4xl text-center mt-2'} style={{ width: 95, float: 'left' }}> <div className={'text-4xl text-center mt-2 mr-4'} style={{ width: 95, float: 'left' }}>
{state.user_count !== 0 ? state.user_score : "NR"} {locationDetail.detail.user_count !== 0 ? Math.floor(Number(locationDetail.detail.user_score) / Number(locationDetail.detail.user_count)) : "NR"}
<div className={"items-center p-2"}> <div className={"items-center p-2"}>
<div className={'mr-3 users-score-bar'}> <div className={'mr-3 users-score-bar'}>
<div className={"mt-1"} style={{ height: 4, width: 80, backgroundColor: "#72767d" }}> <div className={"mt-1"} style={{ height: 4, width: 80, backgroundColor: "#72767d" }}>
<div style={{ height: 4, width: ` ${state.user_count !== 0 ? Number(state.user_score) / Number(state.user_score) * 10 : 0}%`, backgroundColor: 'green' }} /> <div style={{ height: 4, width: ` ${locationDetail.detail.user_count !== 0 ? Number(locationDetail.detail.user_score) / Number(locationDetail.detail.user_count) : 0}%`, backgroundColor: 'green' }} />
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{state.user_count !== 0 && {locationDetail.detail.user_count !== 0 &&
<div className={'ml-4'}> <div className={'ml-14 text-sm'}>
Based on {state.user_count} reviews Based on {locationDetail.detail.user_count} reviews
</div> </div>
} }
</div> </div>
@ -212,61 +296,87 @@ function LocationDetail() {
<section name={'REVIEWS SECTION'}> <section name={'REVIEWS SECTION'}>
<div className={'mt-5'} style={{ tableLayout: 'fixed', display: 'table', width: '100%' }}> <div className={'mt-5'} style={{ tableLayout: 'fixed', display: 'table', width: '100%' }}>
<div className={'wideLeft'} style={{ textAlign: 'left', paddingRight: 20, maxWidth: 1096, minWidth: 680, display: 'table-cell', position: 'relative', verticalAlign: 'top', width: '100%', boxSizing: 'border-box' }}> <div className={'wideLeft'} style={{ textAlign: 'left', paddingRight: 20, maxWidth: 1096, minWidth: 680, display: 'table-cell', position: 'relative', verticalAlign: 'top', width: '100%', boxSizing: 'border-box' }}>
{!user.username ?
<div className={'bg-secondary text-center p-3'} style={{ width: '100%' }}><a href={'#'} onClick={handleSignInNavigation} style={{ borderBottom: '1px solid white' }}>SIGN IN</a> TO REVIEW</div>
:
<div name="REVIEW INPUT TEXTAREA" className={'reviewContainer p-4'} style={{ backgroundColor: '#2f3136' }}>
<div className={'reviewBoxContent'} style={{ width: '75%', margin: '0 auto' }}>
{/* <div className={'bg-secondary text-center p-3'} style={{ width: '100%'}}><a href={'#'}>SIGN IN</a> TO REVIEW</div> */} <div className={'userImage mr-3'} style={{ width: 55, float: 'left' }}>
<div name="REVIEW INPUT TEXTAREA" className={'reviewContainer p-4'} style={{ backgroundColor: '#2f3136' }}>
<div className={'reviewBoxContent'} style={{ width: '75%', margin: '0 auto' }}>
<div className={'userImage mr-3'} style={{ width: 55, float: 'left' }}>
<a href={'#'}>
<img loading={'lazy'} src={'https://cdn.discordapp.com/attachments/743422487882104837/1153985664849805392/421-4212617_person-placeholder-image-transparent-hd-png-download.png'} />
</a>
</div>
<div style={{ display: 'block' }}>
<a href={'#'}>user</a>
</div>
<div className={'ratingInput'} style={{ margin: '5px 0 10px' }}>
<div style={{ float: 'left' }}>
<input
type={'text'}
pattern={"\d*"}
style={{ fontSize: 12, backgroundColor: '#40444b', textAlign: 'center', width: 40, height: 20, lineHeight: 18, border: '1px solid #38444d' }}
maxLength={3}
value={reviewValue.score_input}
onChange={handleScoreInputChange}
placeholder={"0-100"}
autoComplete={'off'}
/>
<div className={'inline-block text-xs ml-2 text-tertiary'}>/ score</div>
</div>
<div style={{ clear: 'both' }} />
</div>
<div className={'mt-3'} style={{ width: '100%' }}>
<textarea
onChange={handleTextAreaChange}
ref={textAreaRef}
className={'p-2'}
value={reviewValue.review_textArea}
style={{ border: 'none', overflow: 'auto', outline: 'none', boxShadow: 'none', backgroundColor: '#40444b', width: '100%', minHeight: 100, overflowY: 'hidden' }}
/>
</div>
<div style={{ textAlign: 'right', width: "100%" }}>
<div style={{ display: 'inline-block', fontSize: 11, verticalAlign: 'middle', margin: '0 10px 0 0', letterSpacing: .5 }}>
<a>Review Guidelines</a>
</div>
<span className={'text-xxs p-1'} style={{ backgroundColor: 'gray', letterSpacing: 1 }}>
<a href={'#'}> <a href={'#'}>
POST <img loading={'lazy'} src={user.avatar_picture != '' ? user.avatar_picture.toString() : DEFAULT_AVATAR_IMG} style={{ aspectRatio: '1/1' }} />
</a> </a>
</span> </div>
<div style={{ display: 'block' }}>
<a href={'#'}>{user.username}</a>
</div>
<div className={'ratingInput'} style={currentUserReview ? { margin: '0 0 10px' } : { margin: '5px 0 10px' }}>
{currentUserReview ?
<div style={{ display: 'inline-block' }}>
{console.log(currentUserReview)}
<p className={'ml-2'}>{currentUserReview.score}</p>
<div style={{ height: 4, width: 35, backgroundColor: "#72767d" }}>
<div style={{ height: 4, width: `${currentUserReview.score}%`, backgroundColor: 'green' }} />
</div>
</div>
:
<>
<input
type={'text'}
pattern={"\d*"}
style={{ fontSize: 12, backgroundColor: '#40444b', textAlign: 'center', width: 40, height: 20, lineHeight: 18, border: '1px solid #38444d' }}
maxLength={3}
value={reviewValue.score_input}
onChange={handleScoreInputChange}
placeholder={"0-100"}
autoComplete={'off'}
/>
<div className={'inline-block text-xs ml-2 text-tertiary'}>/ score</div>
{pageState.is_score_rating_panic_msg &&
<div className={'inline-block text-xs ml-2 text-error'}>{pageState.is_score_rating_panic_msg}</div>
}
</>
}
<div style={{ clear: 'both' }} />
</div>
<div className={'mt-3'} style={{ width: '100%' }}>
{currentUserReview ?
<CustomInterweave
content={currentUserReview.comments}
/>
:
<textarea
onChange={handleTextAreaChange}
ref={textAreaRef}
className={'p-2'}
value={reviewValue.review_textArea}
style={{ border: 'none', overflow: 'auto', outline: 'none', boxShadow: 'none', backgroundColor: '#40444b', width: '100%', minHeight: 100, overflowY: 'hidden' }}
/>
}
</div>
<div style={{ textAlign: 'right', width: "100%" }}>
<div style={{ display: 'inline-block', fontSize: 11, verticalAlign: 'middle', margin: '0 10px 0 0', letterSpacing: .5 }}>
<a>Review Guidelines</a>
</div>
{pageState.on_submit_loading ?
<SpinnerLoading />
:
<span className={'text-xxs p-1 text-area-button'} style={pageState.enable_post ? '' : { display: 'none'}}>
<a href={'#'} onClick={handleSubmitReview}>
POST
</a>
</span>
}
</div>
</div> </div>
</div> </div>
</div>
}
<div name={'CRTICITS REVIEW'} style={{ margin: '50px 0', textAlign: 'left' }}> <div name={'CRTICITS REVIEW'} style={{ margin: '50px 0', textAlign: 'left' }}>
<SeparatorWithAnchor pageName={"critic's review"} pageLink='#' /> <SeparatorWithAnchor pageName={"critic's review"} pageLink='#' />
<div className={'criticSortFilter'}> <div className={'criticSortFilter'}>
@ -283,236 +393,121 @@ function LocationDetail() {
</div> </div>
<div style={{ clear: 'both' }} /> <div style={{ clear: 'both' }} />
<div className={''} style={{ padding: '15px 0' }}> {locationDetail.critics_review.map(x => (
<div style={{ float: 'left' }}> <div className={''} style={{ padding: '15px 0' }}>
<div style={{ fontSize: 20, marginRight: 20, textAlign: 'center', width: 55, marginBottom: 3 }}> <div style={{ float: 'left' }}>
90 <div style={{ fontSize: 20, marginRight: 20, textAlign: 'center', width: 55, marginBottom: 3 }}>
{x.score}
</div>
<div style={{ height: 4, width: 55, position: 'relative', backgroundColor: '#d8d8d8' }}>
<div style={{ height: 4, backgroundColor: '#85ce73', width: `${x.score}%` }} />
</div>
</div> </div>
<div style={{ height: 4, width: 55, position: 'relative', backgroundColor: '#d8d8d8' }}> <div className={'mr-3'} style={{ display: 'inline-block', width: 40 }}>
<div style={{ height: 4, backgroundColor: '#85ce73', width: '90%' }} /> <a href="#">
<img
loading={'lazy'}
style={{ width: '100%' }}
src={x.user_avatar.Valid ? x.user_avatar.String.toString() : 'https://cdn.discordapp.com/attachments/743422487882104837/1153985664849805392/421-4212617_person-placeholder-image-transparent-hd-png-download.png'}
/>
</a>
</div> </div>
</div> <div style={{ display: 'inline-block', verticalAlign: 'top' }}>
<div className={'mr-3'} style={{ display: 'inline-block', width: 40 }}> <div style={{ fontWeight: 700, fontSize: 16, lineHeight: 'initial' }}>
<a href="#"> <a>
<img <span>{x.username}</span>
loading={'lazy'} </a>
style={{ width: '100%' }} </div>
src={'https://cdn.discordapp.com/attachments/743422487882104837/1153985664849805392/421-4212617_person-placeholder-image-transparent-hd-png-download.png'} </div>
<div style={{ fontSize: 15, lineHeight: '24px', margin: '5px 75px 1px' }}>
<CustomInterweave
content={x.comments}
/> />
</a> </div>
</div> <div className={'reviewLinks'} style={{ marginLeft: 72 }}>
<div style={{ display: 'inline-block', verticalAlign: 'top' }}> <div className={'mr-2'} style={{ minWidth: 55, display: 'inline-block', verticalAlign: 'middle' }}>
<div style={{ fontWeight: 700, fontSize: 16, lineHeight: 'initial' }}> <a className={'text-sm'} href={'#'}>
<a> <svg className={'inline-block mr-1'} fill={'currentColor'} xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 -960 960 960"><path d="M200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h280v80H200v560h560v-280h80v280q0 33-23.5 56.5T760-120H200Zm188-212-56-56 372-372H560v-80h280v280h-80v-144L388-332Z" /></svg>
<span>Benito Mussolini</span> <div className={'inline-block'}>Video</div>
</a> </a>
</div>
<div style={{ minWidth: 55, display: 'inline-block', verticalAlign: 'middle' }}>
<a className={'text-sm'} href={'#'}>
<svg className={'inline-block mr-1'} fill={'currentColor'} xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 -960 960 960"><path d="M200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h280v80H200v560h560v-280h80v280q0 33-23.5 56.5T760-120H200Zm188-212-56-56 372-372H560v-80h280v280h-80v-144L388-332Z" /></svg>
<div className={'inline-block'}>Instagram</div>
</a>
</div>
</div> </div>
</div> </div>
<div style={{ fontSize: 15, lineHeight: '24px', margin: '5px 75px 1px', wordWrap: 'break-word' }}> ))}
<p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Porro nihil dolor delectus ex minima aliquid quidem veniam officiis temporibus ipsum ea incidunt voluptatum a, repellat illum, cumque consequatur saepe assumenda.</p>
</div>
<div className={'reviewLinks'} style={{ marginLeft: 72 }}>
<div className={'mr-2'} style={{ minWidth: 55, display: 'inline-block', verticalAlign: 'middle' }}>
<a className={'text-sm'} href={'#'}>
<svg className={'inline-block mr-1'} fill={'currentColor'} xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 -960 960 960"><path d="M200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h280v80H200v560h560v-280h80v280q0 33-23.5 56.5T760-120H200Zm188-212-56-56 372-372H560v-80h280v280h-80v-144L388-332Z" /></svg>
<div className={'inline-block'}>Video</div>
</a>
</div>
<div style={{ minWidth: 55, display: 'inline-block', verticalAlign: 'middle' }}>
<a className={'text-sm'} href={'#'}>
<svg className={'inline-block mr-1'} fill={'currentColor'} xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 -960 960 960"><path d="M200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h280v80H200v560h560v-280h80v280q0 33-23.5 56.5T760-120H200Zm188-212-56-56 372-372H560v-80h280v280h-80v-144L388-332Z" /></svg>
<div className={'inline-block'}>Instagram</div>
</a>
</div>
</div>
</div>
<div className={''} style={{ padding: '15px 0', borderTop: '1px solid #38444d' }}>
<div style={{ float: 'left' }}>
<div style={{ fontSize: 20, marginRight: 20, textAlign: 'center', width: 55, marginBottom: 3 }}>
90
</div>
<div style={{ height: 4, width: 55, position: 'relative', backgroundColor: '#d8d8d8' }}>
<div style={{ height: 4, backgroundColor: '#85ce73', width: '90%' }} />
</div>
</div>
<div className={'mr-3'} style={{ display: 'inline-block', width: 40 }}>
<a href="#">
<img
loading={'lazy'}
style={{ width: '100%' }}
src={'https://cdn.discordapp.com/attachments/743422487882104837/1153985664849805392/421-4212617_person-placeholder-image-transparent-hd-png-download.png'}
/>
</a>
</div>
<div style={{ display: 'inline-block', verticalAlign: 'top' }}>
<div style={{ fontWeight: 700, fontSize: 16, lineHeight: 'initial' }}>
<a>
<span>Benito Mussolini</span>
</a>
</div>
</div>
<div style={{ fontSize: 15, lineHeight: '24px', margin: '5px 75px 1px', wordWrap: 'break-word' }}>
<p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Porro nihil dolor delectus ex minima aliquid quidem veniam officiis temporibus ipsum ea incidunt voluptatum a, repellat illum, cumque consequatur saepe assumenda.</p>
</div>
<div className={'reviewLinks'} style={{ marginLeft: 72 }}>
<div className={'mr-2'} style={{ minWidth: 55, display: 'inline-block', verticalAlign: 'middle' }}>
<a className={'text-sm'} href={'#'}>
<svg className={'inline-block mr-1'} fill={'currentColor'} xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 -960 960 960"><path d="M200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h280v80H200v560h560v-280h80v280q0 33-23.5 56.5T760-120H200Zm188-212-56-56 372-372H560v-80h280v280h-80v-144L388-332Z" /></svg>
<div className={'inline-block'}>Video</div>
</a>
</div>
<div style={{ minWidth: 55, display: 'inline-block', verticalAlign: 'middle' }}>
<a className={'text-sm'} href={'#'}>
<svg className={'inline-block mr-1'} fill={'currentColor'} xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 -960 960 960"><path d="M200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h280v80H200v560h560v-280h80v280q0 33-23.5 56.5T760-120H200Zm188-212-56-56 372-372H560v-80h280v280h-80v-144L388-332Z" /></svg>
<div className={'inline-block'}>Instagram</div>
</a>
</div>
</div>
</div>
<div className={''} style={{ padding: '15px 0', borderTop: '1px solid #38444d' }}>
<div style={{ float: 'left' }}>
<div style={{ fontSize: 20, marginRight: 20, textAlign: 'center', width: 55, marginBottom: 3 }}>
90
</div>
<div style={{ height: 4, width: 55, position: 'relative', backgroundColor: '#d8d8d8' }}>
<div style={{ height: 4, backgroundColor: '#85ce73', width: '90%' }} />
</div>
</div>
<div className={'mr-3'} style={{ display: 'inline-block', width: 40 }}>
<a href="#">
<img
loading={'lazy'}
style={{ width: '100%' }}
src={'https://cdn.discordapp.com/attachments/743422487882104837/1153985664849805392/421-4212617_person-placeholder-image-transparent-hd-png-download.png'}
/>
</a>
</div>
<div style={{ display: 'inline-block', verticalAlign: 'top' }}>
<div style={{ fontWeight: 700, fontSize: 16, lineHeight: 'initial' }}>
<a>
<span>Benito Mussolini</span>
</a>
</div>
</div>
<div style={{ fontSize: 15, lineHeight: '24px', margin: '5px 75px 1px', wordWrap: 'break-word' }}>
<p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Porro nihil dolor delectus ex minima aliquid quidem veniam officiis temporibus ipsum ea incidunt voluptatum a, repellat illum, cumque consequatur saepe assumenda.</p>
</div>
<div className={'reviewLinks'} style={{ marginLeft: 72 }}>
<div className={'mr-2'} style={{ minWidth: 55, display: 'inline-block', verticalAlign: 'middle' }}>
<a className={'text-sm'} href={'#'}>
<svg className={'inline-block mr-1'} fill={'currentColor'} xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 -960 960 960"><path d="M200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h280v80H200v560h560v-280h80v280q0 33-23.5 56.5T760-120H200Zm188-212-56-56 372-372H560v-80h280v280h-80v-144L388-332Z" /></svg>
<div className={'inline-block'}>Video</div>
</a>
</div>
<div style={{ minWidth: 55, display: 'inline-block', verticalAlign: 'middle' }}>
<a className={'text-sm'} href={'#'}>
<svg className={'inline-block mr-1'} fill={'currentColor'} xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 -960 960 960"><path d="M200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h280v80H200v560h560v-280h80v280q0 33-23.5 56.5T760-120H200Zm188-212-56-56 372-372H560v-80h280v280h-80v-144L388-332Z" /></svg>
<div className={'inline-block'}>Instagram</div>
</a>
</div>
</div>
</div>
</div> </div>
<div name={'USERS REVIEW'} style={{ margin: '50px 0', textAlign: 'left' }}> <div name={'USERS REVIEW'} style={{ margin: '50px 0', textAlign: 'left' }}>
<SeparatorWithAnchor pageName={"Popular User's review"} pageLink='#' secondLink='#'/> <SeparatorWithAnchor pageName={"User's review"} pageLink='#' secondLink={locationDetail.users_review.length > 0 ? '#' : ''} />
{ locationDetail.users_review.length > 0 ?
<>
{locationDetail.users_review.map(x => (
<div style={{ padding: '15px 0' }}>
<div className={'mr-5'} style={{ width: 45, float: 'left' }}>
<a href="#">
<img
loading={'lazy'}
style={{ width: '100%' }}
src={x.user_avatar.Valid ? x.user_avatar.String.toString() : 'https://cdn.discordapp.com/attachments/743422487882104837/1153985664849805392/421-4212617_person-placeholder-image-transparent-hd-png-download.png'}
/>
</a>
</div>
<div>
<div style={{ fontWeight: 700, fontSize: 16, lineHeight: 'initial' }}>
<a>
<span>{x.username}</span>
</a>
</div>
</div>
<div className={'inline-block'}>
<div className={'text-sm text-center'} >{x.score}</div>
<div style={{ height: 4, width: 25, position: 'relative', backgroundColor: '#d8d8d8' }}>
<div style={{ height: 4, backgroundColor: '#85ce73', width: `${x.score}%` }} />
</div>
</div>
<div style={{ fontSize: 15, lineHeight: '24px', margin: '10px 65px 1px', wordWrap: 'break-word' }}>
<CustomInterweave
content={x.comments}
/>
</div>
<div className={'reviewLinks'} style={{ marginLeft: 63 }}>
<div className={'mr-2'} style={{ minWidth: 55, display: 'inline-block', verticalAlign: 'middle' }}>
<a className={'text-sm'} href={'#'}>
<svg className={'inline-block mr-1'} fill={'currentColor'} xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 -960 960 960"><path d="M200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h280v80H200v560h560v-280h80v280q0 33-23.5 56.5T760-120H200Zm188-212-56-56 372-372H560v-80h280v280h-80v-144L388-332Z" /></svg>
<div className={'inline-block'}>Video</div>
</a>
</div>
<div style={{ minWidth: 55, display: 'inline-block', verticalAlign: 'middle' }}>
<a className={'text-sm'} href={'#'}>
<svg className={'inline-block mr-1'} fill={'currentColor'} xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 -960 960 960"><path d="M200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h280v80H200v560h560v-280h80v280q0 33-23.5 56.5T760-120H200Zm188-212-56-56 372-372H560v-80h280v280h-80v-144L388-332Z" /></svg>
<div className={'inline-block'}>Instagram</div>
</a>
</div>
</div>
</div>
))}
<div className={''} style={{ padding: '15px 0' }}> <div className={'review-more-button text-center text-sm mt-5'}>
<div className={'mr-5'} style={{ width: 45, float: 'left' }}> <a style={{ borderRadius: 15, padding: '8px 32px', border: '1px solid #d8d8d8' }}>
<a href="#"> More
<img
loading={'lazy'}
style={{ width: '100%' }}
src={'https://cdn.discordapp.com/attachments/743422487882104837/1153985664849805392/421-4212617_person-placeholder-image-transparent-hd-png-download.png'}
/>
</a>
</div>
<div>
<div style={{ fontWeight: 700, fontSize: 16, lineHeight: 'initial' }}>
<a>
<span>Benito Mussolini</span>
</a> </a>
</div> </div>
</div> </>
<div className={'inline-block'}> :
<div className={'text-sm text-center'} >80</div> <>
<div style={{ height: 4, width: 25, position: 'relative', backgroundColor: '#d8d8d8' }}> <span className={'text-sm italic'}>No users review to display</span>
<div style={{ height: 4, backgroundColor: '#85ce73', width: '90%' }} /> </>
</div> }
</div>
<div style={{ fontSize: 15, lineHeight: '24px', margin: '10px 65px 1px', wordWrap: 'break-word' }}>
<p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Porro nihil dolor delectus ex minima aliquid quidem veniam officiis temporibus ipsum ea incidunt voluptatum a, repellat illum, cumque consequatur saepe assumenda.</p>
</div>
<div className={'reviewLinks'} style={{ marginLeft: 63 }}>
<div className={'mr-2'} style={{ minWidth: 55, display: 'inline-block', verticalAlign: 'middle' }}>
<a className={'text-sm'} href={'#'}>
<svg className={'inline-block mr-1'} fill={'currentColor'} xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 -960 960 960"><path d="M200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h280v80H200v560h560v-280h80v280q0 33-23.5 56.5T760-120H200Zm188-212-56-56 372-372H560v-80h280v280h-80v-144L388-332Z" /></svg>
<div className={'inline-block'}>Video</div>
</a>
</div>
<div style={{ minWidth: 55, display: 'inline-block', verticalAlign: 'middle' }}>
<a className={'text-sm'} href={'#'}>
<svg className={'inline-block mr-1'} fill={'currentColor'} xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 -960 960 960"><path d="M200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h280v80H200v560h560v-280h80v280q0 33-23.5 56.5T760-120H200Zm188-212-56-56 372-372H560v-80h280v280h-80v-144L388-332Z" /></svg>
<div className={'inline-block'}>Instagram</div>
</a>
</div>
</div>
</div>
<div className={''} style={{ padding: '15px 0', borderTop: '1px solid #38444d' }}>
<div className={'mr-5'} style={{ width: 45, float: 'left' }}>
<a href="#">
<img
loading={'lazy'}
style={{ width: '100%' }}
src={'https://cdn.discordapp.com/attachments/743422487882104837/1153985664849805392/421-4212617_person-placeholder-image-transparent-hd-png-download.png'}
/>
</a>
</div>
<div>
<div style={{ fontWeight: 700, fontSize: 16, lineHeight: 'initial' }}>
<a>
<span>Benito Mussolini</span>
</a>
</div>
</div>
<div className={'inline-block'}>
<div className={'text-sm text-center'} >80</div>
<div style={{ height: 4, width: 25, position: 'relative', backgroundColor: '#d8d8d8' }}>
<div style={{ height: 4, backgroundColor: '#85ce73', width: '90%' }} />
</div>
</div>
<div style={{ fontSize: 15, lineHeight: '24px', margin: '10px 65px 1px', wordWrap: 'break-word' }}>
<p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Porro nihil dolor delectus ex minima aliquid quidem veniam officiis temporibus ipsum ea incidunt voluptatum a, repellat illum, cumque consequatur saepe assumenda.</p>
</div>
<div className={'reviewLinks'} style={{ marginLeft: 63 }}>
<div className={'mr-2'} style={{ minWidth: 55, display: 'inline-block', verticalAlign: 'middle' }}>
<a className={'text-sm'} href={'#'}>
<svg className={'inline-block mr-1'} fill={'currentColor'} xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 -960 960 960"><path d="M200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h280v80H200v560h560v-280h80v280q0 33-23.5 56.5T760-120H200Zm188-212-56-56 372-372H560v-80h280v280h-80v-144L388-332Z" /></svg>
<div className={'inline-block'}>Video</div>
</a>
</div>
<div style={{ minWidth: 55, display: 'inline-block', verticalAlign: 'middle' }}>
<a className={'text-sm'} href={'#'}>
<svg className={'inline-block mr-1'} fill={'currentColor'} xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 -960 960 960"><path d="M200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h280v80H200v560h560v-280h80v280q0 33-23.5 56.5T760-120H200Zm188-212-56-56 372-372H560v-80h280v280h-80v-144L388-332Z" /></svg>
<div className={'inline-block'}>Instagram</div>
</a>
</div>
</div>
</div>
<div className={'review-more-button text-center text-sm mt-5'}>
<a style={{ borderRadius: 15, padding: '8px 32px', border: '1px solid #d8d8d8'}}>
More
</a>
</div>
</div> </div>
<div name={'USERS REVIEW'} style={{ margin: '50px 0', textAlign: 'left' }}> {/* <div name={'USERS REVIEW'} style={{ margin: '50px 0', textAlign: 'left' }}>
<SeparatorWithAnchor pageName={"Recent User's review"} pageLink='#' secondLink='#'/> <SeparatorWithAnchor pageName={"Popular User's review"} pageLink='#' secondLink='#' />
<div className={''} style={{ padding: '15px 0' }}> <div className={''} style={{ padding: '15px 0' }}>
<div className={'mr-5'} style={{ width: 45, float: 'left' }}> <div className={'mr-5'} style={{ width: 45, float: 'left' }}>
<a href="#"> <a href="#">
@ -597,24 +592,24 @@ function LocationDetail() {
</div> </div>
</div> </div>
<div className={'review-more-button text-center text-sm mt-5'}> <div className={'review-more-button text-center text-sm mt-5'}>
<a style={{ borderRadius: 15, padding: '8px 32px', border: '1px solid #d8d8d8'}}> <a style={{ borderRadius: 15, padding: '8px 32px', border: '1px solid #d8d8d8' }}>
More More
</a> </a>
</div> </div>
</div> </div> */}
<div className={'mb-5'}> <div className={'mb-5'}>
CONTRUBITION CONTRUBITION
<DefaultSeparator /> <DefaultSeparator />
anoeantoeh aoenthaoe aoenth aot anoeantoeh aoenthaoe aoenth aot
</div> </div>
</div> </div>
{screen.width >= 1024 && {/* {screen.width >= 1024 &&
<div className={'bg-secondary'} style={{ display: 'table-cell', position: 'relative', verticalAlign: 'top', width: 330, textAlign: 'left', padding: 15, boxSizing: 'border-box', height: 1080 }}> <div className={'bg-secondary'} style={{ display: 'table-cell', position: 'relative', verticalAlign: 'top', width: 330, textAlign: 'left', padding: 15, boxSizing: 'border-box', height: 1080 }}>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Reprehenderit cumque aliquam doloribus in reiciendis? Laborum, ea assumenda, tempora dolore placeat aspernatur, cumque totam sequi debitis dolor nam eligendi suscipit aliquid? Lorem ipsum dolor sit amet consectetur adipisicing elit. Reprehenderit cumque aliquam doloribus in reiciendis? Laborum, ea assumenda, tempora dolore placeat aspernatur, cumque totam sequi debitis dolor nam eligendi suscipit aliquid?
</div> </div>
} } */}
</div> </div>
<div style={{ clear: 'both'}} /> <div style={{ clear: 'both' }} />
</section> </section>
<section> <section>

View File

@ -4,13 +4,16 @@ export interface ILocationDetail {
id: Number, id: Number,
name: String, name: String,
address: String, address: String,
regency_name: String,
province_name: String,
region_name: String,
google_maps_link: String, google_maps_link: String,
thumbnail: NullValueRes<"String", String>, thumbnail: NullValueRes<"String", String>,
submitted_by: Number, submitted_by: Number,
regency_name: String, critic_score: Number,
province_name: String, critic_count: Number,
region_name: String, user_score: Number,
submitted_by_user: String user_count: Number
} }
export function emptyLocationDetail(): ILocationDetail { export function emptyLocationDetail(): ILocationDetail {
@ -24,19 +27,38 @@ export function emptyLocationDetail(): ILocationDetail {
regency_name: '', regency_name: '',
region_name: '', region_name: '',
submitted_by: 0, submitted_by: 0,
submitted_by_user: '' critic_score: 0,
critic_count: 0,
user_score: 0,
user_count: 0,
} }
} }
export interface LocationReviewsResponse {
id: number,
score: number,
comments: string,
user_id: number,
username: string,
user_avatar: NullValueRes<"String", string>,
created_at: string,
updated_at: string
}
export interface LocationDetailResponse { export interface LocationDetailResponse {
detail: ILocationDetail, detail: ILocationDetail,
tags: Array<String> tags: Array<String>
users_review: Array<LocationReviewsResponse>,
critics_review: Array<LocationReviewsResponse>
} }
export function EmptyLocationDetailResponse(): LocationDetailResponse { export function EmptyLocationDetailResponse(): LocationDetailResponse {
return { return {
detail: emptyLocationDetail(), detail: emptyLocationDetail(),
tags: [] tags: [],
critics_review: Array<LocationReviewsResponse>(),
users_review: Array<LocationReviewsResponse>()
} }
} }
@ -47,15 +69,6 @@ export interface LocationImage extends SlideImage {
uploaded_by: String uploaded_by: String
} }
export function emptyLocationImage(): LocationImage {
return {
id: 0,
src: '',
created_at: '',
uploaded_by: ''
}
}
export interface LocationResponse { export interface LocationResponse {
total_image: Number, total_image: Number,
images: Array<LocationImage> images: Array<LocationImage>
@ -64,6 +77,18 @@ export interface LocationResponse {
export function emptyLocationResponse(): LocationResponse { export function emptyLocationResponse(): LocationResponse {
return { return {
total_image: 0, total_image: 0,
images: [emptyLocationImage()] images: Array<LocationImage>()
} }
}
export type CurrentUserLocationReviews = {
id: Number,
comments: string,
is_from_critic: boolean,
is_hided: boolean,
location_id: Number,
score: Number,
submitted_by: Number,
created_at: NullValueRes<"Time", string>,
updated_at: NullValueRes<"Time", string>,
} }

View File

@ -0,0 +1,4 @@
input {
padding: 5px;
font-size: 14px;
}

120
src/pages/Login/index.tsx Normal file
View File

@ -0,0 +1,120 @@
import { ChangeEvent, TargetedEvent, useState } from "preact/compat";
import { createAccountService, loginService } from "../../services";
import { useLocation, useNavigate } from "react-router-dom";
import { useDispatch } from "react-redux";
import { authAdded } from "../../features/auth/authSlice/authSlice";
import './index.css';
function Login() {
const dispatch = useDispatch();
const { state } = useLocation()
const [form, setFrom] = useState({
username: '',
password: ''
})
const [errorMsg, setErrorMsg] = useState([{
field: '',
msg: ''
}])
const navigation = useNavigate()
async function handleSignIn(e: TargetedEvent) {
e.preventDefault();
try {
const res = await loginService({ username: form.username, password: form.password })
if (res.error) {
setErrorMsg(res.error.response.data.errors)
return
}
dispatch(authAdded(res.data))
if (state) {
navigation(state.from)
return
}
navigation("/")
} catch (err) {
console.log(err)
}
}
async function handleSignUp(e: TargetedEvent) {
e.preventDefault();
try {
const res = await createAccountService({
username: form.username,
password: form.password
})
if (res.error) {
setErrorMsg(res.error.response.data.errors)
return;
}
dispatch(authAdded(res.data))
if (state) {
navigation(state.from)
return
}
navigation("/")
} catch (err) {
alert(err)
}
}
function onChangeInput(e: ChangeEvent<HTMLInputElement>) {
const val = e.target as HTMLInputElement;
setFrom({ ...form, [val.placeholder]: val.value })
}
return (
<div className={'p-2'}>
<h1 className={'text-2xl mb-2'}>Sign in</h1>
<form onSubmit={handleSignIn}>
<table style={{ borderSpacing: '0 0.5em', borderCollapse: 'separate' }}>
<tbody>
<tr>
<td>username: </td>
<td><input value={form.username} onChange={onChangeInput} placeholder={'username'} className={'bg-secondary'} /></td>
</tr>
<tr>
<td>password: </td>
<td><input value={form.password} onChange={onChangeInput} type={'password'} placeholder={'password'} className={'bg-secondary'} /></td>
</tr>
</tbody>
</table>
<a className={'block'}>Forgout account ?</a>
<input type={'submit'} value={'sign in'} className={'p-1 text-sm text-primary mt-4'} style={{ backgroundColor: '#a8adb3', borderRadius: 7 }} />
</form>
<h1 className={'text-2xl mt-10'}>Create Account</h1>
<form onSubmit={handleSignUp} >
<table style={{ borderSpacing: '0 0.5em', borderCollapse: 'separate' }}>
<tbody>
<tr>
<td>username: </td>
<td><input value={form.username} onChange={onChangeInput} placeholder={'username'} className={'bg-secondary'} /></td>
</tr>
<tr>
<td>password: </td>
<td><input value={form.password} onChange={onChangeInput} type={'password'} placeholder={'password'} className={'bg-secondary'} /></td>
</tr>
</tbody>
</table>
{errorMsg.map(x => (
<p>{x.msg}</p>
))}
<input type={'submit'} value={'create account'} className={'p-1 text-sm text-primary mt-4'} style={{ backgroundColor: '#a8adb3', borderRadius: 7 }} />
</form>
</div>
)
}
export default Login;

View File

@ -0,0 +1,9 @@
const NotFound = () => (
<div class={'main-content content'}>
<img
src={'https://external-content.duckduckgo.com/iu/?u=https%3A%2F%2Fmedia.tenor.com%2FUWHgJ8QRcZsAAAAi%2Fturtle-huh-meme.gif&f=1&nofb=1&ipt=64af045b4bd24f01c00528f14b53fb17c49ed5b603fc58332e8a5de65a1b89ef&ipo=images'}
/>
</div>
)
export default NotFound;

View File

@ -4,10 +4,16 @@ import Discovery from "./Discovery";
import Story from "./Stories"; import Story from "./Stories";
import NewsEvent from "./NewsEvents"; import NewsEvent from "./NewsEvents";
import LocationDetail from "./LocationDetail"; import LocationDetail from "./LocationDetail";
import Login from './Login';
import NotFound from "./NotFound";
export { export {
Login,
Home, Home,
NotFound,
BestLocation, BestLocation,
LocationDetail, LocationDetail,

18
src/reducers/index.ts Normal file
View File

@ -0,0 +1,18 @@
import { combineReducers } from "@reduxjs/toolkit";
import { LOGOUT } from "../constants/actions";
import { authSlice } from "../features";
const appReducer = combineReducers({
auth: authSlice
});
const rootReducer = (state: any, action: any) => {
if (action.type === LOGOUT) {
// remove token
state = undefined
}
return appReducer(state, action);
}
export default rootReducer;

View File

@ -4,7 +4,8 @@ import {
Home, Home,
LocationDetail, LocationDetail,
NewsEvent, NewsEvent,
Story Story,
Login
} from '../pages'; } from '../pages';
const routes = [ const routes = [

59
src/services/auth.ts Normal file
View File

@ -0,0 +1,59 @@
import { AxiosError } from "axios";
import { LOGIN_URI, SIGNUP_URI } from "../constants/api";
import { client } from "./config";
const initialState: IEmptyResponseState = {
data: null,
error: AxiosError
}
interface IAuthentication {
username: String
password: String
}
async function createAccountService({ username, password }: IAuthentication) {
const newState = { ...initialState };
try {
const response = await client({ method: 'POST', url: SIGNUP_URI, data: { username, password } })
newState.data = response.data
newState.error = null
return newState
} catch (error) {
newState.error = error
return newState
}
}
async function loginService({ username, password }: IAuthentication) {
const newState = { ...initialState };
try {
const response = await client({ method: 'POST', url: LOGIN_URI, data: { username, password }, withCredentials: true })
newState.data = response.data
newState.error = null
return newState
} catch (error) {
newState.error = error
return newState
}
}
async function logoutService() {
const newState = { ...initialState };
try {
const response = await client({ method: 'POST', url: LOGIN_URI})
newState.data = response.data
newState.error = null
return newState
} catch (error) {
newState.error = error
return newState
}
}
export {
loginService,
createAccountService,
logoutService
};

View File

@ -6,7 +6,8 @@ export const client = (props: AxiosRequestConfig): AxiosPromise => axios({
baseURL: `${BASE_URL}`, baseURL: `${BASE_URL}`,
url: props.url, url: props.url,
headers: props.headers, headers: props.headers,
data: props.data data: props.data,
...props
}) })
// export const authClient = (props: AxiosRequestConfig) => axios({ // export const authClient = (props: AxiosRequestConfig) => axios({

View File

@ -5,14 +5,21 @@ import {
getLocationService, getLocationService,
getLocationTagsService, getLocationTagsService,
} from "./locations"; } from "./locations";
import { getImagesByLocationService } from "./images" import { getImagesByLocationService } from "./images"
import { createAccountService, loginService, logoutService } from "./auth";
import { postReviewLocation } from "./review";
export { export {
createAccountService,
loginService,
logoutService,
getListLocationsService, getListLocationsService,
getListRecentLocationsRatingsService, getListRecentLocationsRatingsService,
getListTopLocationsService, getListTopLocationsService,
getLocationService, getLocationService,
getLocationTagsService, getLocationTagsService,
getImagesByLocationService, getImagesByLocationService,
postReviewLocation
} }

View File

@ -80,7 +80,7 @@ async function getLocationService(id: Number) {
return newState; return newState;
} }
} catch (error) { } catch (error) {
console.log(error) throw(error)
} }
} }

34
src/services/review.ts Normal file
View File

@ -0,0 +1,34 @@
import { AxiosError } from "axios"
import { client } from "./config";
import { POST_REVIEW_LOCATION_URI } from "../constants/api";
const initialState: IEmptyResponseState = {
data: null,
error: AxiosError
}
interface postReviewLocationReq {
submitted_by: number,
comments: string,
score: number,
is_from_critic: boolean,
is_hided: boolean,
location_id: number
}
async function postReviewLocation(req: postReviewLocationReq) {
const newState = { ...initialState };
try {
const response = await client({ method: 'POST', url: POST_REVIEW_LOCATION_URI, data: req, withCredentials: true})
newState.data = response.data
newState.error = null
return newState
} catch (error) {
newState.error = error
throw(error)
}
}
export {
postReviewLocation
}

25
src/store/config.ts Normal file
View File

@ -0,0 +1,25 @@
import { applyMiddleware, createStore, compose } from '@reduxjs/toolkit'
import storage from 'redux-persist/lib/storage'
import persistReducer from 'redux-persist/es/persistReducer';
import rootReducer from '../reducers';
import thunk from 'redux-thunk';
import { persistStore } from 'redux-persist';
const composeEnhancers = (window as any)['__REDUX_DEVTOOLS_EXTENSION_COMPOSE__'] as typeof compose || compose;
const persistConfig = {
key: 'root',
blacklist: [],
whitelist: [
"auth"
],
storage
}
const persistedReducer = persistReducer(persistConfig, rootReducer);
export type RootState = ReturnType<typeof store.getState>
export const store = createStore(persistedReducer, composeEnhancers(applyMiddleware(thunk)))
export const persistore = persistStore(store)

6
src/store/type.ts Normal file
View File

@ -0,0 +1,6 @@
import { IUser } from "../features/auth/authSlice/type";
import { RootState } from "./config";
export interface UserRootState extends RootState {
auth: IUser
}

View File

@ -5,3 +5,9 @@ interface GetRequestPagination {
page: number, page: number,
page_size: number, page_size: number,
} }
interface IEmptyResponseState {
data: any,
error: any,
};

View File

@ -0,0 +1,5 @@
import { AxiosError } from "axios";
export function handleAxiosError(error: AxiosError) {
return error.response?.data
}

View File

@ -1,5 +1,7 @@
import useAutosizeTextArea from "./useAutosizeTextArea"; import useAutosizeTextArea from "./useAutosizeTextArea";
import { handleAxiosError } from "./common";
export { export {
useAutosizeTextArea useAutosizeTextArea,
handleAxiosError
} }

View File

@ -7,7 +7,8 @@ export default {
current: 'currentColor', current: 'currentColor',
primary: '#202225', primary: '#202225',
secondary: '#2f3136', secondary: '#2f3136',
tertiary: '#a8adb3' tertiary: '#a8adb3',
error: '#ff5454',
}, },
borderColor: { borderColor: {
primary: '#38444d', primary: '#38444d',

191
yarn.lock
View File

@ -197,6 +197,13 @@
"@babel/plugin-syntax-jsx" "^7.22.5" "@babel/plugin-syntax-jsx" "^7.22.5"
"@babel/types" "^7.22.5" "@babel/types" "^7.22.5"
"@babel/runtime@^7.12.1", "@babel/runtime@^7.9.2":
version "7.22.15"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.15.tgz#38f46494ccf6cf020bd4eed7124b425e83e523b8"
integrity sha512-T0O+aa+4w0u06iNmapipJXMV4HoUir03hpx3/YqXXhu9xim3w+dVphjFWl1OH8NbZHw5Lbm9k45drDkgq2VNNA==
dependencies:
regenerator-runtime "^0.14.0"
"@babel/template@^7.22.5": "@babel/template@^7.22.5":
version "7.22.5" version "7.22.5"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.5.tgz#0c8c4d944509875849bd0344ff0050756eefc6ec" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.5.tgz#0c8c4d944509875849bd0344ff0050756eefc6ec"
@ -434,6 +441,16 @@
"@prefresh/utils" "^1.2.0" "@prefresh/utils" "^1.2.0"
"@rollup/pluginutils" "^4.2.1" "@rollup/pluginutils" "^4.2.1"
"@reduxjs/toolkit@^1.9.5":
version "1.9.5"
resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-1.9.5.tgz#d3987849c24189ca483baa7aa59386c8e52077c4"
integrity sha512-Rt97jHmfTeaxL4swLRNPD/zV4OxTes4la07Xc4hetpUW/vc75t5m1ANyxG6ymnEQ2FsLQsoMlYB2vV1sO3m8tQ==
dependencies:
immer "^9.0.21"
redux "^4.2.1"
redux-thunk "^2.4.2"
reselect "^4.1.8"
"@remix-run/router@1.9.0": "@remix-run/router@1.9.0":
version "1.9.0" version "1.9.0"
resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.9.0.tgz#9033238b41c4cbe1e961eccb3f79e2c588328cf6" resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.9.0.tgz#9033238b41c4cbe1e961eccb3f79e2c588328cf6"
@ -447,6 +464,48 @@
estree-walker "^2.0.1" estree-walker "^2.0.1"
picomatch "^2.2.2" picomatch "^2.2.2"
"@types/hoist-non-react-statics@^3.3.0", "@types/hoist-non-react-statics@^3.3.1":
version "3.3.2"
resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#dc1e9ded53375d37603c479cc12c693b0878aa2a"
integrity sha512-YIQtIg4PKr7ZyqNPZObpxfHsHEmuB8dXCxd6qVcGuQVDK2bpsF7bYNnBJ4Nn7giuACZg+WewExgrtAJ3XnA4Xw==
dependencies:
"@types/react" "*"
hoist-non-react-statics "^3.3.0"
"@types/prop-types@*":
version "15.7.6"
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.6.tgz#bbf819813d6be21011b8f5801058498bec555572"
integrity sha512-RK/kBbYOQQHLYj9Z95eh7S6t7gq4Ojt/NT8HTk8bWVhA5DaF+5SMnxHKkP4gPNN3wAZkKP+VjAf0ebtYzf+fxg==
"@types/react-redux@^7.1.26":
version "7.1.26"
resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.26.tgz#84149f5614e40274bb70fcbe8f7cae6267d548b1"
integrity sha512-UKPo7Cm7rswYU6PH6CmTNCRv5NYF3HrgKuHEYTK8g/3czYLrUux50gQ2pkxc9c7ZpQZi+PNhgmI8oNIRoiVIxg==
dependencies:
"@types/hoist-non-react-statics" "^3.3.0"
"@types/react" "*"
hoist-non-react-statics "^3.3.0"
redux "^4.0.0"
"@types/react@*":
version "18.2.22"
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.22.tgz#abe778a1c95a07fa70df40a52d7300a40b949ccb"
integrity sha512-60fLTOLqzarLED2O3UQImc/lsNRgG0jE/a1mPW9KjMemY0LMITWEsbS4VvZ4p6rorEHd5YKxxmMKSDK505GHpA==
dependencies:
"@types/prop-types" "*"
"@types/scheduler" "*"
csstype "^3.0.2"
"@types/scheduler@*":
version "0.16.3"
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.3.tgz#cef09e3ec9af1d63d2a6cc5b383a737e24e6dcf5"
integrity sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==
"@types/use-sync-external-store@^0.0.3":
version "0.0.3"
resolved "https://registry.yarnpkg.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz#b6725d5f4af24ace33b36fafd295136e75509f43"
integrity sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==
ansi-styles@^3.2.1: ansi-styles@^3.2.1:
version "3.2.1" version "3.2.1"
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
@ -611,6 +670,11 @@ cssesc@^3.0.0:
resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
csstype@^3.0.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b"
integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==
debug@^4.1.0, debug@^4.3.1: debug@^4.1.0, debug@^4.3.1:
version "4.3.4" version "4.3.4"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
@ -638,6 +702,21 @@ electron-to-chromium@^1.4.477:
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.499.tgz#dc36b67f4c8e273524e8d2080c5203a6a76987b6" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.499.tgz#dc36b67f4c8e273524e8d2080c5203a6a76987b6"
integrity sha512-0NmjlYBLKVHva4GABWAaHuPJolnDuL0AhV3h1hES6rcLCWEIbRL6/8TghfsVwkx6TEroQVdliX7+aLysUpKvjw== integrity sha512-0NmjlYBLKVHva4GABWAaHuPJolnDuL0AhV3h1hES6rcLCWEIbRL6/8TghfsVwkx6TEroQVdliX7+aLysUpKvjw==
emojibase-regex@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/emojibase-regex/-/emojibase-regex-6.0.1.tgz#dc0b33d05c02f045ea44795d453698b205d41f0f"
integrity sha512-Mj1UT6IIk4j91yMFE0QetpUYcmsr5ZDkkOIMSGafhIgC086mBMaCh2Keaykx8YEllmV7hmx5zdANDzCYBYAVDw==
emojibase@^15.0.0:
version "15.0.0"
resolved "https://registry.yarnpkg.com/emojibase/-/emojibase-15.0.0.tgz#f41b7773ec9a8a332373c18628ff4471255bd769"
integrity sha512-bvSIs98sHaVnyKPmW+obRjo49MFx0g+rhfSz6mTePAagEZSlDPosq0b6AcSJa5gt48z3VP2ooXclyBs8vIkpGA==
emojibase@^6.1.0:
version "6.1.0"
resolved "https://registry.yarnpkg.com/emojibase/-/emojibase-6.1.0.tgz#c3bc281e998a0e06398416090c23bac8c5ed3ee8"
integrity sha512-1GkKJPXP6tVkYJHOBSJHoGOr/6uaDxZ9xJ6H7m6PfdGXTmQgbALHLWaVRY4Gi/qf5x/gT/NUXLPuSHYLqtLtrQ==
esbuild@^0.18.10: esbuild@^0.18.10:
version "0.18.20" version "0.18.20"
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.18.20.tgz#4709f5a34801b43b799ab7d6d82f7284a9b7a7a6" resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.18.20.tgz#4709f5a34801b43b799ab7d6d82f7284a9b7a7a6"
@ -671,6 +750,11 @@ escalade@^3.1.1:
resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==
escape-html@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==
escape-string-regexp@^1.0.5: escape-string-regexp@^1.0.5:
version "1.0.5" version "1.0.5"
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
@ -788,6 +872,18 @@ has@^1.0.3:
dependencies: dependencies:
function-bind "^1.1.1" function-bind "^1.1.1"
hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2:
version "3.3.2"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
dependencies:
react-is "^16.7.0"
immer@^9.0.21:
version "9.0.21"
resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.21.tgz#1e025ea31a40f24fb064f1fef23e931496330176"
integrity sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==
inflight@^1.0.4: inflight@^1.0.4:
version "1.0.6" version "1.0.6"
resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
@ -801,6 +897,26 @@ inherits@2:
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
interweave-autolink@^5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/interweave-autolink/-/interweave-autolink-5.1.0.tgz#a5a5438c45c5e4d631838473845be1fdd38a664b"
integrity sha512-WOEakAdwqv/W2H85cLdigkpMM7o6qVg4CWM6iO5cHrFCywwUh+ILVmZgX1tHphEpa55sFdzpKNO2EHhAjbR4GA==
interweave-emoji@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/interweave-emoji/-/interweave-emoji-7.0.0.tgz#937455fcc616121761034d0c120edec4af8586c6"
integrity sha512-3yFBreW2h+I/Tjf9LpF/bKKdjGSi5DT1RxrRFibmmDjTB1tCyPe5X3XFNdglwoRPErgFL1qqFpQWQJKUlUwARg==
dependencies:
emojibase "^6.1.0"
emojibase-regex "^6.0.1"
interweave@^13.1.0:
version "13.1.0"
resolved "https://registry.yarnpkg.com/interweave/-/interweave-13.1.0.tgz#4b7a0a87a7eb32001bef64525f68d95296dee03c"
integrity sha512-JIDq0+2NYg0cgL7AB26fBcV0yZdiJvPDBp+aF6k8gq6Cr1kH5Gd2/Xqn7j8z+TGb8jCWZn739jzalCz+nPYwcA==
dependencies:
escape-html "^1.0.3"
is-binary-path@~2.1.0: is-binary-path@~2.1.0:
version "2.1.0" version "2.1.0"
resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
@ -837,7 +953,7 @@ jiti@^1.18.2:
resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.19.3.tgz#ef554f76465b3c2b222dc077834a71f0d4a37569" resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.19.3.tgz#ef554f76465b3c2b222dc077834a71f0d4a37569"
integrity sha512-5eEbBDQT/jF1xg6l36P+mWGGoH9Spuy0PCdSr2dtWRDGC6ph/w9ZCL4lmESW8f8F7MwT3XKescfP0wnZWAKL9w== integrity sha512-5eEbBDQT/jF1xg6l36P+mWGGoH9Spuy0PCdSr2dtWRDGC6ph/w9ZCL4lmESW8f8F7MwT3XKescfP0wnZWAKL9w==
js-tokens@^4.0.0: "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
version "4.0.0" version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
@ -867,6 +983,13 @@ lines-and-columns@^1.1.6:
resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632"
integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==
loose-envify@^1.1.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
dependencies:
js-tokens "^3.0.0 || ^4.0.0"
lru-cache@^5.1.1: lru-cache@^5.1.1:
version "5.1.1" version "5.1.1"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920"
@ -906,6 +1029,11 @@ minimatch@^3.0.4:
dependencies: dependencies:
brace-expansion "^1.1.7" brace-expansion "^1.1.7"
moment@^2.29.4:
version "2.29.4"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108"
integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==
ms@2.1.2: ms@2.1.2:
version "2.1.2" version "2.1.2"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
@ -1055,6 +1183,28 @@ queue-microtask@^1.2.2:
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
react-is@^16.7.0:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
react-is@^18.0.0:
version "18.2.0"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b"
integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==
react-redux@^8.1.2:
version "8.1.2"
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-8.1.2.tgz#9076bbc6b60f746659ad6d51cb05de9c5e1e9188"
integrity sha512-xJKYI189VwfsFc4CJvHqHlDrzyFTY/3vZACbE+rr/zQ34Xx1wQfB4OTOSeOSNrF6BDVe8OOdxIrAnMGXA3ggfw==
dependencies:
"@babel/runtime" "^7.12.1"
"@types/hoist-non-react-statics" "^3.3.1"
"@types/use-sync-external-store" "^0.0.3"
hoist-non-react-statics "^3.3.2"
react-is "^18.0.0"
use-sync-external-store "^1.0.0"
react-router-dom@^6.16.0: react-router-dom@^6.16.0:
version "6.16.0" version "6.16.0"
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.16.0.tgz#86f24658da35eb66727e75ecbb1a029e33ee39d9" resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.16.0.tgz#86f24658da35eb66727e75ecbb1a029e33ee39d9"
@ -1070,6 +1220,13 @@ react-router@6.16.0:
dependencies: dependencies:
"@remix-run/router" "1.9.0" "@remix-run/router" "1.9.0"
react@^18.2.0:
version "18.2.0"
resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"
integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==
dependencies:
loose-envify "^1.1.0"
read-cache@^1.0.0: read-cache@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/read-cache/-/read-cache-1.0.0.tgz#e664ef31161166c9751cdbe8dbcf86b5fb58f774" resolved "https://registry.yarnpkg.com/read-cache/-/read-cache-1.0.0.tgz#e664ef31161166c9751cdbe8dbcf86b5fb58f774"
@ -1084,6 +1241,33 @@ readdirp@~3.6.0:
dependencies: dependencies:
picomatch "^2.2.1" picomatch "^2.2.1"
redux-persist@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/redux-persist/-/redux-persist-6.0.0.tgz#b4d2972f9859597c130d40d4b146fecdab51b3a8"
integrity sha512-71LLMbUq2r02ng2We9S215LtPu3fY0KgaGE0k8WRgl6RkqxtGfl7HUozz1Dftwsb0D/5mZ8dwAaPbtnzfvbEwQ==
redux-thunk@^2.4.2:
version "2.4.2"
resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.4.2.tgz#b9d05d11994b99f7a91ea223e8b04cf0afa5ef3b"
integrity sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==
redux@^4.0.0, redux@^4.2.1:
version "4.2.1"
resolved "https://registry.yarnpkg.com/redux/-/redux-4.2.1.tgz#c08f4306826c49b5e9dc901dee0452ea8fce6197"
integrity sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==
dependencies:
"@babel/runtime" "^7.9.2"
regenerator-runtime@^0.14.0:
version "0.14.0"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz#5e19d68eb12d486f797e15a3c6a918f7cec5eb45"
integrity sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==
reselect@^4.1.8:
version "4.1.8"
resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.1.8.tgz#3f5dc671ea168dccdeb3e141236f69f02eaec524"
integrity sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==
resolve@^1.1.7, resolve@^1.20.0, resolve@^1.22.2: resolve@^1.1.7, resolve@^1.20.0, resolve@^1.22.2:
version "1.22.4" version "1.22.4"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.4.tgz#1dc40df46554cdaf8948a486a10f6ba1e2026c34" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.4.tgz#1dc40df46554cdaf8948a486a10f6ba1e2026c34"
@ -1219,6 +1403,11 @@ update-browserslist-db@^1.0.11:
escalade "^3.1.1" escalade "^3.1.1"
picocolors "^1.0.0" picocolors "^1.0.0"
use-sync-external-store@^1.0.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a"
integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==
util-deprecate@^1.0.2: util-deprecate@^1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"