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"
},
"dependencies": {
"@reduxjs/toolkit": "^1.9.5",
"@types/react-redux": "^7.1.26",
"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",
"react": "^18.2.0",
"react-redux": "^8.1.2",
"react-router-dom": "^6.16.0",
"redux-persist": "^6.0.0",
"redux-thunk": "^2.4.2",
"yet-another-react-lightbox": "^3.12.2"
},
"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,13 +4,20 @@ import './app.css'
import { DefaultLayout } from './layouts'
import routes from './routes'
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() {
return (
<>
<Provider store={store}>
<PersistGate persistor={persistore}>
<Router>
<Routes>
<Route path='/login' element={<Login />} />
<Route element={<DefaultLayout />}>
{routes.map(({ path, name, element }) => (
<>
@ -22,8 +29,11 @@ export function App() {
</>
))}
</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,18 +1,37 @@
import React from "preact/compat";
import React, { TargetedEvent } from "preact/compat";
import { useState } from "preact/hooks";
import { useSelector, useDispatch } from "react-redux";
import { UserRootState } from "../../store/type";
import { logout } from '../../actions';
import './style.css';
import { logoutService } from "../../services";
function Header() {
const [searchVal, setSearchVal] = useState('');
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 val = e.target as HTMLInputElement;
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 => {
e.preventDefault();
}
@ -33,6 +52,25 @@ function Header() {
<a href={"/"}>
<h1 className={`title ${dropdown ? 'title-dropdown' : ""}`}>Hilingin</h1>
</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" : ""}`}>
<label>
<input
@ -44,7 +82,7 @@ function Header() {
/>
</label>
</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>
</button>
</div>
@ -54,11 +92,22 @@ function Header() {
}
<a href="/best-places" className={`navLink ${!dropdown ? "navLink-disabled" : ""}`}>Top Places</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="/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" : ""}`}>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>
</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 {
display: none;
}
.search-input {
margin-left: auto;
}
label {
position: relative;
@ -102,6 +35,33 @@ label:before {
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 {
background-color: #2f3136;
@ -115,3 +75,90 @@ label:before {
max-width: '100%';
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 DefaultSeparator from "./Separator/Default";
import Footer from './Footer/';
import CustomInterweave from "./CustomInterweave";
import SpinnerLoading from "./Loading/Spinner";
export {
Header,
SeparatorWithAnchor,
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 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_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 POST_REVIEW_LOCATION_URI = `${BASE_URL}/review/location`
export {
BASE_URL,
SIGNUP_URI,
LOGIN_URI,
LOGOUT_URI,
GET_LIST_RECENT_LOCATIONS_RATING_URI,
GET_LIST_TOP_LOCATIONS,
GET_LIST_LOCATIONS_URI,
GET_LOCATION_URI,
GET_LOCATION_TAGS_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%;
}
input:focus {
outline: none;
}
a:hover {
cursor: pointer;
}
/* a {
font-weight: 500;
color: #646cff;

View File

@ -3,8 +3,6 @@ import { getListTopLocationsService } from "../../services";
import { DefaultSeparator } from "../../components";
import { TargetedEvent } from "preact/compat";
import './style.css';
import { useNavigate } from "react-router-dom";
interface TopLocation {
row_number: Number,
@ -57,8 +55,6 @@ function BestLocation() {
filterRegionType: 0,
})
const navigate = useNavigate()
async function getTopLocations() {
try {
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})
}
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(() => {
getTopLocations()
}, [pageState])
@ -152,10 +136,10 @@ function BestLocation() {
</a>
</div> */}
<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 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' }} />
</a>
</div>

View File

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

View File

@ -1,35 +1,23 @@
import { useLocation, useParams } from 'react-router-dom';
import { useNavigate, useParams } from 'react-router-dom';
import { ChangeEvent, TargetedEvent } from 'preact/compat';
import { useEffect, useRef, useState } from 'preact/hooks';
import './index.css';
import Lightbox from 'yet-another-react-lightbox';
import useCallbackState from '../../types/state-callback';
import { EmptyLocationDetailResponse, LocationDetailResponse, LocationResponse, emptyLocationResponse } from './types';
import { useAutosizeTextArea } from '../../utils';
import { getImagesByLocationService, getLocationService } from "../../services";
import { DefaultSeparator, SeparatorWithAnchor } from '../../components';
function LocationDetail() {
const [locationDetail, setLocationDetail] = useCallbackState<LocationDetailResponse>(EmptyLocationDetailResponse)
const [locationImages, setLocationImages] = useState<LocationResponse>(emptyLocationResponse())
const [lightboxOpen, setLightboxOpen] = useState<boolean>(false)
const [pageState, setPageState] = useState({
critic_filter_name: 'highest rated',
critic_filter_type: 0,
show_sort: false
})
const [reviewValue, setReviewValue] = useState({
review_textArea: '',
score_input: '',
})
const [isLoading, setIsLoading] = useState<boolean>(true)
const textAreaRef = useRef<HTMLTextAreaElement>(null);
useAutosizeTextArea(textAreaRef.current, reviewValue.review_textArea);
const { state } = useLocation();
const { id } = useParams()
import {
EmptyLocationDetailResponse,
LocationDetailResponse,
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',
@ -38,14 +26,46 @@ function LocationDetail() {
'oldest'
]
function LocationDetail() {
const [locationDetail, setLocationDetail] = useCallbackState<LocationDetailResponse>(EmptyLocationDetailResponse)
const [locationImages, setLocationImages] = useState<LocationResponse>(emptyLocationResponse())
const [currentUserReview, setCurrentUserReview] = useState<CurrentUserLocationReviews>()
const [lightboxOpen, setLightboxOpen] = useState<boolean>(false)
const [pageState, setPageState] = useState({
critic_filter_name: 'highest rated',
critic_filter_type: 0,
show_sort: false,
enable_post: true,
on_submit_loading: false,
is_score_rating_panic_msg: '',
})
const [reviewValue, setReviewValue] = useState({
review_textArea: '',
score_input: '',
})
const [isLoading, setIsLoading] = useState<boolean>(true)
const navigate = useNavigate();
const user = useSelector((state: UserRootState) => state.auth)
const textAreaRef = useRef<HTMLTextAreaElement>(null);
useAutosizeTextArea(textAreaRef.current, reviewValue.review_textArea);
const { id } = useParams()
async function getLocationDetail(): Promise<void> {
try {
const res = await getLocationService(Number(id))
setLocationDetail(res.data, (val) => {
getImage(val.detail.thumbnail.String.toString())
})
} catch (err) {
console.log(err)
} catch (error) {
let err = error as AxiosError;
if (err.response?.status == 404) {
navigate("/")
}
alert(error)
}
}
@ -76,11 +96,75 @@ function LocationDetail() {
...reviewValue,
score_input: val.value
})
setPageState({
...pageState,
is_score_rating_panic_msg: ''
})
}
function onChangeCriticsSort(e: TargetedEvent<HTMLAnchorElement>, sort_name: string, sort_type: number): void {
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(() => {
@ -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={'font-bold ml-1 text-xs'}>CRITICS SCORE</div>
<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={'mr-3 users-score-bar'}>
<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>
{state.critic_count !== 0 &&
{locationDetail.detail.critic_count !== 0 &&
<div className={'ml-14 text-sm'}>
Based on {state.critic_count} reviews
Based on {locationDetail.detail.critic_count} reviews
</div>
}
</div>
<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={'text-4xl text-center mt-2'} style={{ width: 95, float: 'left' }}>
{state.user_count !== 0 ? state.user_score : "NR"}
<div className={'text-4xl text-center mt-2 mr-4'} style={{ width: 95, float: 'left' }}>
{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={'mr-3 users-score-bar'}>
<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>
{state.user_count !== 0 &&
<div className={'ml-4'}>
Based on {state.user_count} reviews
{locationDetail.detail.user_count !== 0 &&
<div className={'ml-14 text-sm'}>
Based on {locationDetail.detail.user_count} reviews
</div>
}
</div>
@ -212,23 +296,33 @@ function LocationDetail() {
<section name={'REVIEWS SECTION'}>
<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={'bg-secondary text-center p-3'} style={{ width: '100%'}}><a href={'#'}>SIGN IN</a> TO REVIEW</div> */}
{!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={'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'} />
<img loading={'lazy'} src={user.avatar_picture != '' ? user.avatar_picture.toString() : DEFAULT_AVATAR_IMG} style={{ aspectRatio: '1/1' }} />
</a>
</div>
<div style={{ display: 'block' }}>
<a href={'#'}>user</a>
<a href={'#'}>{user.username}</a>
</div>
<div className={'ratingInput'} style={{ margin: '5px 0 10px' }}>
<div style={{ float: 'left' }}>
<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*"}
@ -240,11 +334,21 @@ function LocationDetail() {
autoComplete={'off'}
/>
<div className={'inline-block text-xs ml-2 text-tertiary'}>/ score</div>
</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}
@ -252,21 +356,27 @@ function LocationDetail() {
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={'#'}>
{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 name={'CRTICITS REVIEW'} style={{ margin: '50px 0', textAlign: 'left' }}>
<SeparatorWithAnchor pageName={"critic's review"} pageLink='#' />
<div className={'criticSortFilter'}>
@ -283,13 +393,14 @@ function LocationDetail() {
</div>
<div style={{ clear: 'both' }} />
{locationDetail.critics_review.map(x => (
<div className={''} style={{ padding: '15px 0' }}>
<div style={{ float: 'left' }}>
<div style={{ fontSize: 20, marginRight: 20, textAlign: 'center', width: 55, marginBottom: 3 }}>
90
{x.score}
</div>
<div style={{ height: 4, width: 55, position: 'relative', backgroundColor: '#d8d8d8' }}>
<div style={{ height: 4, backgroundColor: '#85ce73', width: '90%' }} />
<div style={{ height: 4, backgroundColor: '#85ce73', width: `${x.score}%` }} />
</div>
</div>
<div className={'mr-3'} style={{ display: 'inline-block', width: 40 }}>
@ -297,19 +408,21 @@ function LocationDetail() {
<img
loading={'lazy'}
style={{ width: '100%' }}
src={'https://cdn.discordapp.com/attachments/743422487882104837/1153985664849805392/421-4212617_person-placeholder-image-transparent-hd-png-download.png'}
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 style={{ display: 'inline-block', verticalAlign: 'top' }}>
<div style={{ fontWeight: 700, fontSize: 16, lineHeight: 'initial' }}>
<a>
<span>Benito Mussolini</span>
<span>{x.username}</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 style={{ fontSize: 15, lineHeight: '24px', margin: '5px 75px 1px' }}>
<CustomInterweave
content={x.comments}
/>
</div>
<div className={'reviewLinks'} style={{ marginLeft: 72 }}>
<div className={'mr-2'} style={{ minWidth: 55, display: 'inline-block', verticalAlign: 'middle' }}>
@ -326,193 +439,75 @@ function LocationDetail() {
</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 name={'USERS REVIEW'} style={{ margin: '50px 0', textAlign: 'left' }}>
<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={'review-more-button text-center text-sm mt-5'}>
<a style={{ borderRadius: 15, padding: '8px 32px', border: '1px solid #d8d8d8' }}>
More
</a>
</div>
</>
:
<>
<span className={'text-sm italic'}>No users review to display</span>
</>
}
</div>
{/* <div name={'USERS REVIEW'} style={{ margin: '50px 0', textAlign: 'left' }}>
<SeparatorWithAnchor pageName={"Popular User's review"} pageLink='#' secondLink='#' />
<div className={''} style={{ padding: '15px 0' }}>
<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={''} 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 name={'USERS REVIEW'} style={{ margin: '50px 0', textAlign: 'left' }}>
<SeparatorWithAnchor pageName={"Recent User's review"} pageLink='#' secondLink='#'/>
<div className={''} style={{ padding: '15px 0' }}>
<div className={'mr-5'} style={{ width: 45, float: 'left' }}>
<a href="#">
@ -601,18 +596,18 @@ function LocationDetail() {
More
</a>
</div>
</div>
</div> */}
<div className={'mb-5'}>
CONTRUBITION
<DefaultSeparator />
anoeantoeh aoenthaoe aoenth aot
</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 }}>
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 style={{ clear: 'both' }} />
</section>

View File

@ -4,13 +4,16 @@ export interface ILocationDetail {
id: Number,
name: String,
address: String,
google_maps_link: String,
thumbnail: NullValueRes<"String", String>,
submitted_by: Number,
regency_name: String,
province_name: String,
region_name: String,
submitted_by_user: String
google_maps_link: String,
thumbnail: NullValueRes<"String", String>,
submitted_by: Number,
critic_score: Number,
critic_count: Number,
user_score: Number,
user_count: Number
}
export function emptyLocationDetail(): ILocationDetail {
@ -24,19 +27,38 @@ export function emptyLocationDetail(): ILocationDetail {
regency_name: '',
region_name: '',
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 {
detail: ILocationDetail,
tags: Array<String>
users_review: Array<LocationReviewsResponse>,
critics_review: Array<LocationReviewsResponse>
}
export function EmptyLocationDetailResponse(): LocationDetailResponse {
return {
detail: emptyLocationDetail(),
tags: []
tags: [],
critics_review: Array<LocationReviewsResponse>(),
users_review: Array<LocationReviewsResponse>()
}
}
@ -47,15 +69,6 @@ export interface LocationImage extends SlideImage {
uploaded_by: String
}
export function emptyLocationImage(): LocationImage {
return {
id: 0,
src: '',
created_at: '',
uploaded_by: ''
}
}
export interface LocationResponse {
total_image: Number,
images: Array<LocationImage>
@ -64,6 +77,18 @@ export interface LocationResponse {
export function emptyLocationResponse(): LocationResponse {
return {
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 NewsEvent from "./NewsEvents";
import LocationDetail from "./LocationDetail";
import Login from './Login';
import NotFound from "./NotFound";
export {
Login,
Home,
NotFound,
BestLocation,
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,
LocationDetail,
NewsEvent,
Story
Story,
Login
} from '../pages';
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}`,
url: props.url,
headers: props.headers,
data: props.data
data: props.data,
...props
})
// export const authClient = (props: AxiosRequestConfig) => axios({

View File

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

View File

@ -80,7 +80,7 @@ async function getLocationService(id: Number) {
return newState;
}
} 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_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 { handleAxiosError } from "./common";
export {
useAutosizeTextArea
useAutosizeTextArea,
handleAxiosError
}

View File

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

191
yarn.lock
View File

@ -197,6 +197,13 @@
"@babel/plugin-syntax-jsx" "^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":
version "7.22.5"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.5.tgz#0c8c4d944509875849bd0344ff0050756eefc6ec"
@ -434,6 +441,16 @@
"@prefresh/utils" "^1.2.0"
"@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":
version "1.9.0"
resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.9.0.tgz#9033238b41c4cbe1e961eccb3f79e2c588328cf6"
@ -447,6 +464,48 @@
estree-walker "^2.0.1"
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:
version "3.2.1"
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"
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:
version "4.3.4"
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"
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:
version "0.18.20"
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"
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:
version "1.0.5"
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:
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:
version "1.0.6"
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"
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:
version "2.1.0"
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"
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"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
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"
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:
version "5.1.1"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920"
@ -906,6 +1029,11 @@ minimatch@^3.0.4:
dependencies:
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:
version "2.1.2"
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"
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:
version "6.16.0"
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:
"@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:
version "1.0.0"
resolved "https://registry.yarnpkg.com/read-cache/-/read-cache-1.0.0.tgz#e664ef31161166c9751cdbe8dbcf86b5fb58f774"
@ -1084,6 +1241,33 @@ readdirp@~3.6.0:
dependencies:
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:
version "1.22.4"
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"
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:
version "1.0.2"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"