Compare commits
9 Commits
0295d18574
...
00c8831dc9
Author | SHA1 | Date | |
---|---|---|---|
00c8831dc9 | |||
cd680de68b | |||
19f7f6f07b | |||
5e13d85ff8 | |||
ba1ceb1af3 | |||
ade7e71d74 | |||
c869ce47c5 | |||
3e810c80e9 | |||
d86738dccf |
11
package.json
11
package.json
@ -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": {
|
||||
|
5
src/actions/LogoutAction.ts
Normal file
5
src/actions/LogoutAction.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { LOGOUT } from "../constants/actions";
|
||||
|
||||
export const logout = () => ({
|
||||
type: LOGOUT
|
||||
});
|
1
src/actions/index.ts
Normal file
1
src/actions/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./LogoutAction";
|
12
src/app.tsx
12
src/app.tsx
@ -4,15 +4,22 @@ 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}) => (
|
||||
{routes.map(({ path, name, element }) => (
|
||||
<>
|
||||
<Route
|
||||
path={path}
|
||||
@ -22,8 +29,11 @@ export function App() {
|
||||
</>
|
||||
))}
|
||||
</Route>
|
||||
<Route path="*" element={<NotFound />} />
|
||||
</Routes>
|
||||
</Router>
|
||||
</PersistGate>
|
||||
</Provider>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
64
src/components/CustomInterweave/index.tsx
Normal file
64
src/components/CustomInterweave/index.tsx
Normal 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}
|
||||
// />
|
||||
);
|
||||
}
|
@ -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();
|
||||
}
|
||||
@ -20,7 +39,7 @@ function Header() {
|
||||
const onDropdown = (): void => {
|
||||
document.body.style.overflow = "hidden"
|
||||
|
||||
if(dropdown) {
|
||||
if (dropdown) {
|
||||
document.body.style.overflowX = "hidden";
|
||||
document.body.style.overflowY = "scroll";
|
||||
}
|
||||
@ -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>
|
||||
)
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
6
src/components/Loading/Spinner/index.tsx
Normal file
6
src/components/Loading/Spinner/index.tsx
Normal file
@ -0,0 +1,6 @@
|
||||
import './style.css'
|
||||
export default function SpinnerLoading() {
|
||||
return (
|
||||
<div className={'spinner'} style={{ display: 'inline-block'}}></div>
|
||||
)
|
||||
}
|
16
src/components/Loading/Spinner/style.css
Normal file
16
src/components/Loading/Spinner/style.css
Normal 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;
|
||||
}
|
@ -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
1
src/constants/actions.ts
Normal file
@ -0,0 +1 @@
|
||||
export const LOGOUT = 'LOGOUT';
|
@ -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
2
src/constants/default.ts
Normal 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';
|
||||
|
20
src/features/auth/authSlice/authSlice.ts
Normal file
20
src/features/auth/authSlice/authSlice.ts
Normal 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;
|
14
src/features/auth/authSlice/type.ts
Normal file
14
src/features/auth/authSlice/type.ts
Normal 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
5
src/features/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import authSlice from "./auth/authSlice/authSlice";
|
||||
|
||||
export {
|
||||
authSlice
|
||||
}
|
@ -18,6 +18,13 @@
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
input:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
/* a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
|
@ -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>
|
||||
|
@ -67,6 +67,11 @@ img {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.text-area-button {
|
||||
background-color: gray;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.criticSortFilter {
|
||||
float: right;
|
||||
}
|
||||
|
@ -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 { 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';
|
||||
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',
|
||||
'lowest rated',
|
||||
'newest',
|
||||
'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
|
||||
show_sort: false,
|
||||
enable_post: true,
|
||||
on_submit_loading: false,
|
||||
is_score_rating_panic_msg: '',
|
||||
})
|
||||
const [reviewValue, setReviewValue] = useState({
|
||||
review_textArea: '',
|
||||
@ -24,28 +45,27 @@ function LocationDetail() {
|
||||
})
|
||||
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 { state } = useLocation();
|
||||
const { id } = useParams()
|
||||
|
||||
const SORT_TYPE = [
|
||||
'highest rated',
|
||||
'lowest rated',
|
||||
'newest',
|
||||
'oldest'
|
||||
]
|
||||
|
||||
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,166 +439,41 @@ 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={"Popular User's review"} pageLink='#' secondLink='#'/>
|
||||
|
||||
<div className={''} style={{ padding: '15px 0' }}>
|
||||
<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={'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>
|
||||
<div style={{ fontWeight: 700, fontSize: 16, lineHeight: 'initial' }}>
|
||||
<a>
|
||||
<span>Benito Mussolini</span>
|
||||
<span>{x.username}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className={'inline-block'}>
|
||||
<div className={'text-sm text-center'} >80</div>
|
||||
<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: '90%' }} />
|
||||
<div style={{ height: 4, backgroundColor: '#85ce73', width: `${x.score}%` }} />
|
||||
</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>
|
||||
<CustomInterweave
|
||||
content={x.comments}
|
||||
/>
|
||||
</div>
|
||||
<div className={'reviewLinks'} style={{ marginLeft: 63 }}>
|
||||
<div className={'mr-2'} style={{ minWidth: 55, display: 'inline-block', verticalAlign: 'middle' }}>
|
||||
@ -502,17 +490,24 @@ function LocationDetail() {
|
||||
</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'}}>
|
||||
<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={"Recent User's review"} pageLink='#' secondLink='#'/>
|
||||
{/* <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="#">
|
||||
@ -597,24 +592,24 @@ function LocationDetail() {
|
||||
</div>
|
||||
</div>
|
||||
<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
|
||||
</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'}} />
|
||||
<div style={{ clear: 'both' }} />
|
||||
</section>
|
||||
|
||||
<section>
|
||||
|
@ -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>,
|
||||
}
|
4
src/pages/Login/index.css
Normal file
4
src/pages/Login/index.css
Normal file
@ -0,0 +1,4 @@
|
||||
input {
|
||||
padding: 5px;
|
||||
font-size: 14px;
|
||||
}
|
120
src/pages/Login/index.tsx
Normal file
120
src/pages/Login/index.tsx
Normal 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;
|
9
src/pages/NotFound/index.tsx
Normal file
9
src/pages/NotFound/index.tsx
Normal 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;
|
@ -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
18
src/reducers/index.ts
Normal 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;
|
@ -4,7 +4,8 @@ import {
|
||||
Home,
|
||||
LocationDetail,
|
||||
NewsEvent,
|
||||
Story
|
||||
Story,
|
||||
Login
|
||||
} from '../pages';
|
||||
|
||||
const routes = [
|
||||
|
59
src/services/auth.ts
Normal file
59
src/services/auth.ts
Normal 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
|
||||
};
|
@ -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({
|
||||
|
@ -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
|
||||
}
|
@ -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
34
src/services/review.ts
Normal 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
25
src/store/config.ts
Normal 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
6
src/store/type.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { IUser } from "../features/auth/authSlice/type";
|
||||
import { RootState } from "./config";
|
||||
|
||||
export interface UserRootState extends RootState {
|
||||
auth: IUser
|
||||
}
|
@ -5,3 +5,9 @@ interface GetRequestPagination {
|
||||
page: number,
|
||||
page_size: number,
|
||||
}
|
||||
|
||||
|
||||
interface IEmptyResponseState {
|
||||
data: any,
|
||||
error: any,
|
||||
};
|
@ -0,0 +1,5 @@
|
||||
import { AxiosError } from "axios";
|
||||
|
||||
export function handleAxiosError(error: AxiosError) {
|
||||
return error.response?.data
|
||||
}
|
@ -1,5 +1,7 @@
|
||||
import useAutosizeTextArea from "./useAutosizeTextArea";
|
||||
import { handleAxiosError } from "./common";
|
||||
|
||||
export {
|
||||
useAutosizeTextArea
|
||||
useAutosizeTextArea,
|
||||
handleAxiosError
|
||||
}
|
@ -7,7 +7,8 @@ export default {
|
||||
current: 'currentColor',
|
||||
primary: '#202225',
|
||||
secondary: '#2f3136',
|
||||
tertiary: '#a8adb3'
|
||||
tertiary: '#a8adb3',
|
||||
error: '#ff5454',
|
||||
},
|
||||
borderColor: {
|
||||
primary: '#38444d',
|
||||
|
191
yarn.lock
191
yarn.lock
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user