Compare commits
15 Commits
f01bf17fcf
...
ae26e4f282
Author | SHA1 | Date | |
---|---|---|---|
ae26e4f282 | |||
e454c00847 | |||
2c7f1cf7a0 | |||
8eb25a2a4a | |||
3efee9fb61 | |||
7be88c798f | |||
925a0f2922 | |||
5cdf8624d6 | |||
819527ca20 | |||
976cdd1fc3 | |||
bd6baa69a5 | |||
8e84411fa4 | |||
ae942b8b7c | |||
2157534002 | |||
1ecefbf60a |
@ -10,7 +10,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.5.0",
|
||||
"preact": "^10.16.0"
|
||||
"preact": "^10.16.0",
|
||||
"react-router-dom": "^6.16.0",
|
||||
"yet-another-react-lightbox": "^3.12.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@preact/preset-vite": "^2.5.0",
|
||||
|
35
src/app.css
35
src/app.css
@ -4,6 +4,12 @@
|
||||
|
||||
} */
|
||||
|
||||
.divider {
|
||||
border-bottom-width: 1px;
|
||||
padding-bottom: 3px;
|
||||
border-color: #38444d;
|
||||
}
|
||||
|
||||
.content {
|
||||
max-width: 1440px;
|
||||
margin: 0 auto;
|
||||
@ -11,13 +17,20 @@
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
padding: 20px 25px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 6em;
|
||||
padding: 1.5em;
|
||||
}
|
||||
|
||||
.logo:hover {
|
||||
filter: drop-shadow(0 0 2em #646cffaa);
|
||||
}
|
||||
|
||||
.logo.preact:hover {
|
||||
filter: drop-shadow(0 0 2em #673ab8aa);
|
||||
}
|
||||
@ -30,8 +43,28 @@
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.users-score {
|
||||
color: #a8adb3;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
|
||||
@media screen and(max-width: 425px) {
|
||||
.users-score {
|
||||
font-size: .75rem;
|
||||
line-height: 1rem;
|
||||
}
|
||||
|
||||
.users-score-bar p {
|
||||
font-size: 0.75rem;
|
||||
line-height: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@layer components {
|
||||
.text-prime {
|
||||
@apply text-secondary
|
||||
}
|
||||
}
|
||||
}
|
24
src/app.tsx
24
src/app.tsx
@ -1,11 +1,29 @@
|
||||
import { Route, Routes } from 'react-router-dom'
|
||||
import { BrowserRouter as Router } from 'react-router-dom'
|
||||
import './app.css'
|
||||
import { Home } from './pages'
|
||||
import { DefaultLayout } from './layouts'
|
||||
import routes from './routes'
|
||||
import "yet-another-react-lightbox/styles.css";
|
||||
|
||||
|
||||
export function App() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Home />
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route element={<DefaultLayout />}>
|
||||
{routes.map(({ path, name, element}) => (
|
||||
<>
|
||||
<Route
|
||||
path={path}
|
||||
id={name}
|
||||
element={element}
|
||||
/>
|
||||
</>
|
||||
))}
|
||||
</Route>
|
||||
</Routes>
|
||||
</Router>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -30,7 +30,9 @@ function Header() {
|
||||
return (
|
||||
<header>
|
||||
<div className="flex flex-row content">
|
||||
<h1 className={`title ${dropdown ? 'title-dropdown' : ""}`}>Hilingin</h1>
|
||||
<a href={"/"}>
|
||||
<h1 className={`title ${dropdown ? 'title-dropdown' : ""}`}>Hilingin</h1>
|
||||
</a>
|
||||
<form onSubmit={onSearchSubmit} className={`search-input ${dropdown ? "search-input-dropdown" : ""}`}>
|
||||
<label>
|
||||
<input
|
||||
@ -48,13 +50,13 @@ function Header() {
|
||||
</div>
|
||||
<div className={`nav-container ${!dropdown ? "nav-container-disabled" : ""}`}>
|
||||
{dropdown &&
|
||||
<a href="#" className={`navLink ${!dropdown ? "navLink-disabled" : ""}`}>Home</a>
|
||||
<a href="/" className={`navLink ${!dropdown ? "navLink-disabled" : ""}`}>Home</a>
|
||||
}
|
||||
<a href="#" className={`navLink ${!dropdown ? "navLink-disabled" : ""}`}>Best places</a>
|
||||
<a href="#" className={`navLink ${!dropdown ? "navLink-disabled" : ""}`}>Discover</a>
|
||||
<a href="/best-places" className={`navLink ${!dropdown ? "navLink-disabled" : ""}`}>Top Places</a>
|
||||
<a href="/discover" className={`navLink ${!dropdown ? "navLink-disabled" : ""}`}>Discover</a>
|
||||
<a href="#" className={`navLink ${!dropdown ? "navLink-disabled" : ""}`}>Trending Places</a>
|
||||
<a href="#" className={`navLink ${!dropdown ? "navLink-disabled" : ""}`}>Stories</a>
|
||||
<a href="#" className={`navLink ${!dropdown ? "navLink-disabled" : ""}`}>News / Events</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>
|
||||
|
@ -99,7 +99,7 @@ label:before {
|
||||
|
||||
.navLink {
|
||||
padding: 0px 30px;
|
||||
font-size: 14px;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
|
||||
@ -114,9 +114,4 @@ label:before {
|
||||
/* display: 'inline-block';
|
||||
max-width: '100%';
|
||||
text-align: 'center'; */
|
||||
}
|
||||
|
||||
.navLink:hover{
|
||||
border-bottom-width: 2px;
|
||||
margin-bottom: -2px;
|
||||
}
|
7
src/components/Separator/Default/index.tsx
Normal file
7
src/components/Separator/Default/index.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
function DefaultSeparator() {
|
||||
return (
|
||||
<div class={"flex flex-row justify-between divider mb-2"} />
|
||||
)
|
||||
}
|
||||
|
||||
export default DefaultSeparator;
|
@ -1,8 +1,4 @@
|
||||
.divider {
|
||||
border-bottom-width: 1px;
|
||||
padding-bottom: 3px;
|
||||
border-color: #38444d;
|
||||
}
|
||||
|
||||
|
||||
a:hover {
|
||||
color: #92a8b7;
|
||||
|
@ -1,9 +1,11 @@
|
||||
import Header from "./Header";
|
||||
import SeparatorWithAnchor from "./Separator/WithAnchor";
|
||||
import DefaultSeparator from "./Separator/Default";
|
||||
import Footer from './Footer/';
|
||||
|
||||
export {
|
||||
Header,
|
||||
SeparatorWithAnchor,
|
||||
DefaultSeparator,
|
||||
Footer
|
||||
}
|
23
src/constants/api.ts
Normal file
23
src/constants/api.ts
Normal file
@ -0,0 +1,23 @@
|
||||
const BASE_URL = "http://localhost:8888"
|
||||
|
||||
const SIGNUP_URI = `${BASE_URL}/user/signup`
|
||||
|
||||
|
||||
const GET_LIST_LOCATIONS_URI = `${BASE_URL}/locations`;
|
||||
const GET_LIST_TOP_LOCATIONS = `${BASE_URL}/locations/top-ratings`
|
||||
const GET_LIST_RECENT_LOCATIONS_RATING_URI = `${BASE_URL}/locations/recent`
|
||||
const GET_LOCATION_URI = `${BASE_URL}/location`;
|
||||
const GET_LOCATION_TAGS_URI = `${BASE_URL}/location/tags`
|
||||
|
||||
const GET_IMAGES_BY_LOCATION_URI = `${BASE_URL}/images/location`
|
||||
|
||||
export {
|
||||
BASE_URL,
|
||||
SIGNUP_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,
|
||||
}
|
@ -1,16 +1,12 @@
|
||||
import { ComponentChildren } from "preact";
|
||||
import { Footer, Header } from "../../components";
|
||||
import { Outlet } from "react-router-dom";
|
||||
|
||||
type ChildrenProps = {
|
||||
children: ComponentChildren
|
||||
}
|
||||
|
||||
function DefaultLayout({ children }: ChildrenProps) {
|
||||
function DefaultLayout() {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<main style={{ overflow: 'hidden' }}>
|
||||
{children}
|
||||
<main>
|
||||
<Outlet />
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
|
217
src/pages/BestLocations/index.tsx
Normal file
217
src/pages/BestLocations/index.tsx
Normal file
@ -0,0 +1,217 @@
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
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,
|
||||
id: Number,
|
||||
name: String,
|
||||
thumbnail: NullValueRes<"String", String>,
|
||||
address: String,
|
||||
google_maps_link: string,
|
||||
regency_name: string,
|
||||
critic_score: Number,
|
||||
critic_count: Number,
|
||||
user_score: Number,
|
||||
user_count: Number,
|
||||
critic_bayes: Number,
|
||||
user_bayes: Number,
|
||||
avg_bayes: Number,
|
||||
}
|
||||
|
||||
const REGIONS = [
|
||||
'All',
|
||||
'Sumatera',
|
||||
'Jawa',
|
||||
'Kalimantan',
|
||||
'Nusa Tenggara',
|
||||
'Maluku',
|
||||
'Papua',
|
||||
'Sulawesi',
|
||||
];
|
||||
|
||||
const REVIEWERS_TYPE =[
|
||||
'All',
|
||||
'Critics',
|
||||
'Users'
|
||||
]
|
||||
|
||||
const MIN_REVIEWS = [
|
||||
2,
|
||||
5,
|
||||
7,
|
||||
11
|
||||
]
|
||||
|
||||
function BestLocation() {
|
||||
const [page, setPage] = useState<number>(1);
|
||||
const [topLocations, setTopLocations] = useState<Array<TopLocation>>([])
|
||||
const [pageState, setPageState] = useState({
|
||||
filterScoreType: 'all',
|
||||
filterScoreTypeidx: 1,
|
||||
filterRegionTypeName: 'All',
|
||||
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 })
|
||||
setTopLocations(res.data)
|
||||
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
}
|
||||
}
|
||||
|
||||
function onChangeReviewType(e: TargetedEvent<HTMLAnchorElement>, reviewer_type: string, i: number) {
|
||||
e.preventDefault()
|
||||
setPageState({...pageState, filterScoreType: reviewer_type, filterScoreTypeidx: i })
|
||||
}
|
||||
|
||||
function onChangeRegionType(e: TargetedEvent<HTMLAnchorElement>, region_name: string, type: number) {
|
||||
e.preventDefault();
|
||||
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])
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={'content main-content mt-3'}>
|
||||
<section name={"Top locations header"}>
|
||||
<h1 className={'text-3xl mb-5 font-bold'}>Top Locations</h1>
|
||||
<div className={'regions-dropdown text-xs pr-3 inline-block'}>
|
||||
<p className={'inline-block'}>Regions</p>
|
||||
<a style={{ cursor: 'pointer' }}>
|
||||
<p className={'ml-2 inline-block'}>{pageState.filterRegionTypeName}</p>
|
||||
<svg style={{ display: 'inline-block' }} fill='white' xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M480-345 240-585l56-56 184 184 184-184 56 56-240 240Z" /></svg>
|
||||
</a>
|
||||
<div className={'dropdown-content text-sm'}>
|
||||
{REGIONS.map((x, index) => (
|
||||
<a onClick={(e) => onChangeRegionType(e, x, index) } className={'block pt-1'}>{x}</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className={'regions-dropdown text-xs pr-3 inline-block'}>
|
||||
<p className={'inline-block'}>Min. Reviews</p>
|
||||
<a style={{ cursor: 'pointer' }}>
|
||||
<p className={'ml-2 inline-block'}>All</p>
|
||||
<svg style={{ display: 'inline-block' }} fill='white' xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M480-345 240-585l56-56 184 184 184-184 56 56-240 240Z" /></svg>
|
||||
</a>
|
||||
<div className={'dropdown-content-min-reviews text-sm'}>
|
||||
{MIN_REVIEWS.map(x => (
|
||||
<a className={'block pt-1'}>{x}</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className={'regions-dropdown text-xs pr-3 inline-block'}>
|
||||
<p className={'inline-block'}>Tags</p>
|
||||
<a style={{ cursor: 'pointer' }}>
|
||||
<p className={'ml-2 inline-block'}>All</p>
|
||||
<svg style={{ display: 'inline-block' }} fill='white' xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M480-345 240-585l56-56 184 184 184-184 56 56-240 240Z" /></svg>
|
||||
</a>
|
||||
<div className={'dropdown-content text-sm'}>
|
||||
{/* {REGIONS.map(x => (
|
||||
<a className={'block pt-1'}>{x}</a>
|
||||
))} */}
|
||||
</div>
|
||||
</div>
|
||||
<DefaultSeparator />
|
||||
</section>
|
||||
|
||||
<section name={'Main content'}>
|
||||
<div className={'flex flex-row pt-10 pb-10'}>
|
||||
<div className={'mr-5'} style={{ flex: 1}}>
|
||||
{topLocations.map(x => (
|
||||
<>
|
||||
{/* UNCOMMENT....IF THERES A PURPOSE FOR RIGHT THREE DOTS */}
|
||||
{/* <div style={{ float: 'right', cursor: 'pointer' }}>
|
||||
<a className={'text-xl'}>
|
||||
...
|
||||
</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>
|
||||
</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)}>
|
||||
<img src={x.thumbnail.String.toString()} loading={'lazy'} style={{ width: '100%', objectFit: 'cover', height: '100%', aspectRatio: '1/1' }} />
|
||||
</a>
|
||||
</div>
|
||||
<div className={'text-md font-bold'}>{x.regency_name}</div>
|
||||
<div className={'text-xs mb-2'}>{x.address}</div>
|
||||
<div>$$$ (IDR 1000-12000)</div>
|
||||
<a href={x.google_maps_link} target={'_'}><div className={'text-sm mt-2 items-center'}><svg style={{ display: 'inline-block', marginBottom: 3}} xmlns="http://www.w3.org/2000/svg" height="12" fill={'white'} viewBox="0 -960 960 960" width="12 "><path d="M480-480q33 0 56.5-23.5T560-560q0-33-23.5-56.5T480-640q-33 0-56.5 23.5T400-560q0 33 23.5 56.5T480-480Zm0 294q122-112 181-203.5T720-552q0-109-69.5-178.5T480-800q-101 0-170.5 69.5T240-552q0 71 59 162.5T480-186Zm0 106Q319-217 239.5-334.5T160-552q0-150 96.5-239T480-880q127 0 223.5 89T800-552q0 100-79.5 217.5T480-80Zm0-480Z"/></svg>Maps Location</div></a>
|
||||
<div className={'mt-4'}>
|
||||
<div className={'text-xs bg-secondary'} style={{ width: 160, display: 'inline-block', borderRadius: 5 }}>
|
||||
<div className={'text-center p-1 bg-tertiary text-primary'} style={{ borderTopRightRadius: 5, borderTopLeftRadius: 5}}>CRITICS SCORE</div>
|
||||
<div className={"flex flex-row items-center p-2"}>
|
||||
<div className={'mr-3 users-score-bar'}>
|
||||
<p className={`text-xl text-center ${x.critic_score !== 0 ? 'font-bold' : ''}`}>{x.critic_score !== 0 ? Number(x.critic_score) / Number(x.critic_count) * 10 : "N/A"}</p>
|
||||
<div className={"mt-1"} style={{ height: 4, width: 40, backgroundColor: "#72767d" }}>
|
||||
<div style={{ height: 4, width: ` ${x.critic_count !== 0 ? Number(x.critic_score) / Number(x.critic_count) * 10 : 0}%`, backgroundColor: 'green' }} />
|
||||
</div>
|
||||
</div>
|
||||
<p className={'text-xs users-score'}>{x.critic_count} reviews</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className={'text-xs bg-secondary ml-3'} style={{ width: 160, display: 'inline-block', borderRadius: 5 }}>
|
||||
<div className={'text-center p-1 bg-tertiary text-primary'} style={{ borderTopLeftRadius: 5, borderTopRightRadius: 5}}>USERS SCORE</div>
|
||||
<div className={"flex flex-row items-center p-2"}>
|
||||
<div className={'mr-3 users-score-bar'}>
|
||||
<p className={`text-xl text-center ${x.user_score !== 0 ? 'font-bold' : ''}`}>{x.user_score !== 0 ? x.user_score : "N/A" }</p>
|
||||
<div className={"mt-1"} style={{ height: 4, width: 40, backgroundColor: "#72767d" }}>
|
||||
<div style={{ height: 4, width: ` ${x.user_score !== 0 ? x.user_score : 0}%`, backgroundColor: 'green' }} />
|
||||
</div>
|
||||
</div>
|
||||
<p className={'text-xs users-score'}>{x.user_count} reviews</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ clear: 'both'}}/>
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
<div className={'p-4 bg-secondary'} style={{ minWidth: 300}}>
|
||||
<div className={'h-30 bg-primary p-4'} style={{ position: 'sticky', alignSelf: 'flex-start', top: 10}}>
|
||||
{REVIEWERS_TYPE.map((x, idx) => (
|
||||
<a
|
||||
onClick={(e) => onChangeReviewType(e, x.toLowerCase(), idx+1)}
|
||||
href={'#'}
|
||||
style={ pageState.filterScoreType == x.toLowerCase() ? { pointerEvents: 'none'} : ''}
|
||||
>
|
||||
<div className={`pt-1 pb-1 ${pageState.filterScoreType == x.toLowerCase() ? 'pl-1 bg-tertiary selected-reviewer-filter' : ''}`}>{x} Score</div>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
export default BestLocation;
|
57
src/pages/BestLocations/style.css
Normal file
57
src/pages/BestLocations/style.css
Normal file
@ -0,0 +1,57 @@
|
||||
.dropdown-content {
|
||||
display: none;
|
||||
position: absolute;
|
||||
background-color: #202225;
|
||||
min-width: 120px;
|
||||
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.dropdown-content-min-reviews {
|
||||
margin-left: 60px;
|
||||
display: none;
|
||||
position: absolute;
|
||||
background-color: #202225;
|
||||
min-width: 70px;
|
||||
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.best-locations-title a:hover {
|
||||
cursor: pointer;
|
||||
color: white;
|
||||
border-bottom: 1px solid white;
|
||||
}
|
||||
|
||||
.regions-dropdown:hover .dropdown-content {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.regions-dropdown:hover .dropdown-content-min-reviews {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.dropdown-content a {
|
||||
cursor: pointer;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.dropdown-content a:hover {
|
||||
background-color: rgb(73, 73, 73);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.dropdown-content-min-reviews a {
|
||||
cursor: pointer;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.dropdown-content-min-reviews a:hover {
|
||||
background-color: rgb(73, 73, 73);
|
||||
color: white;
|
||||
}
|
||||
|
||||
a .selected-reviewer-filter:hover{
|
||||
color: white;
|
||||
cursor: default;
|
||||
}
|
10
src/pages/Discovery/index.tsx
Normal file
10
src/pages/Discovery/index.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
function Discovery() {
|
||||
return(
|
||||
<>
|
||||
<h1>Best PLaces</h1>
|
||||
</>
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
export default Discovery;
|
@ -1,21 +1,23 @@
|
||||
import { DefaultLayout } from '../../layouts/';
|
||||
import { SeparatorWithAnchor } from '../../components';
|
||||
import data from '../../datas/home.json';
|
||||
import news from '../../datas/recent_news_event.json';
|
||||
import popular from '../../datas/popular.json';
|
||||
import critics_users_pick from '../../datas/critics_users_best_pick.json';
|
||||
import popular_user_review from '../../datas/popular_user_reviews.json';
|
||||
import './style.css';
|
||||
import { useEffect, useState } from 'preact/hooks';
|
||||
import { getListRecentLocationsRatingsService } from '../../services';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
type NewPlaces = {
|
||||
id: Number,
|
||||
name: string,
|
||||
thumbnail: string,
|
||||
location: string,
|
||||
critic_rating: Number,
|
||||
critic_voters: Number,
|
||||
user_rating: Number,
|
||||
user_voters: Number
|
||||
thumbnail: NullValueRes<'String', string>,
|
||||
regency_name: String,
|
||||
province_name: String,
|
||||
critic_score: Number,
|
||||
critic_count: Number,
|
||||
user_score: Number,
|
||||
user_count: Number
|
||||
}
|
||||
|
||||
type News = {
|
||||
@ -27,40 +29,77 @@ type News = {
|
||||
}
|
||||
|
||||
function Home() {
|
||||
const [recentLocations, setRecentLocations] = useState<Array<NewPlaces>>([])
|
||||
// const [isLoading, setIsLoading] = useState<boolean>(true)
|
||||
|
||||
const navigate = useNavigate()
|
||||
|
||||
|
||||
async function getRecentLocations() {
|
||||
try {
|
||||
const locations = await getListRecentLocationsRatingsService(12)
|
||||
setRecentLocations(locations.data)
|
||||
// setIsLoading(false)
|
||||
} catch(error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
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(() => {
|
||||
getRecentLocations()
|
||||
},[])
|
||||
return (
|
||||
<DefaultLayout>
|
||||
<>
|
||||
<div className="content main-content mt-3">
|
||||
|
||||
{/* RECENTLY ADDED SECTION */}
|
||||
<section about={"Recently added places"} className={'mt-3'}>
|
||||
<SeparatorWithAnchor pageLink='#' pageName='recently added' secondLink='#' />
|
||||
{data.new_places.map((x: NewPlaces) => (
|
||||
{recentLocations.map((x) => (
|
||||
<div className={"recently-added-section-card"}>
|
||||
<div className={'border-secondary recently-img-container'}>
|
||||
<img alt={x.name} src={x.thumbnail} loading="eager" style={{ width: '100%', height: '100%' }} />
|
||||
</div>
|
||||
<a onClick={() => onNavigateToDetail(x.id, x.critic_count, x.critic_score, x.user_count, x.user_score)}>
|
||||
<div className={'border-secondary recently-img-container'}>
|
||||
<img alt={x.name} src={x.thumbnail.String.toString()} loading="lazy" style={{ width: '100%', height: '100%' }} />
|
||||
</div>
|
||||
</a>
|
||||
<div className={"border-primary pb-2 location-container text-sm mb-2 mt-2"}>
|
||||
<p className={'location-title'}>{x.name}</p>
|
||||
<p className={'text-xs mt-1'}>{x.location}</p>
|
||||
<p className={'text-xs mt-1'}>{x.regency_name}, {x.province_name}</p>
|
||||
</div>
|
||||
<div className={"flex flex-row items-center mb-3"}>
|
||||
<div className={'mr-3 users-score-bar'}>
|
||||
<p className={'text-sm text-center'}>{x.critic_rating}</p>
|
||||
<div style={{ height: 4, width: 30, backgroundColor: "#72767d"}}>
|
||||
<div style={{ height: 4, width: `${x.critic_rating}%`, backgroundColor: 'green' }} />
|
||||
{ x.critic_count !== 0 &&
|
||||
<div className={"flex flex-row items-center mb-3"}>
|
||||
<div className={'mr-3 users-score-bar'}>
|
||||
<p className={'text-sm text-center'}>{x.critic_score}</p>
|
||||
<div style={{ height: 4, width: 30, backgroundColor: "#72767d"}}>
|
||||
<div style={{ height: 4, width: `${x.critic_score}%`, backgroundColor: 'green' }} />
|
||||
</div>
|
||||
</div>
|
||||
<p className={"users-score"}>critic score ({x.critic_count})</p>
|
||||
</div>
|
||||
<p className={"users-score"}>critic score ({x.critic_voters})</p>
|
||||
</div>
|
||||
<div className={"flex flex-row items-center"}>
|
||||
<div className={'mr-3 users-score-bar'}>
|
||||
<p className={'text-sm text-center'}>{x.user_rating}</p>
|
||||
<div style={{ height: 4, width: 30, backgroundColor: "#72767d" }}>
|
||||
<div style={{ height: 4, width: ` ${x.user_rating}%`, backgroundColor: 'green' }} />
|
||||
}
|
||||
{ x.user_score !== 0 &&
|
||||
<div className={"flex flex-row items-center"}>
|
||||
<div className={'mr-3 users-score-bar'}>
|
||||
<p className={'text-sm text-center'}>{x.user_score}</p>
|
||||
<div style={{ height: 4, width: 30, backgroundColor: "#72767d" }}>
|
||||
<div style={{ height: 4, width: ` ${x.user_score}%`, backgroundColor: 'green' }} />
|
||||
</div>
|
||||
</div>
|
||||
<p className={'users-score'}>user score ({x.user_count})</p>
|
||||
</div>
|
||||
<p className={'users-score'}>user score ({x.user_voters})</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
@ -75,7 +114,7 @@ function Home() {
|
||||
{news.data.map((x: News) => (
|
||||
<div class={"text-sm news-card"}>
|
||||
<div className={"image-news-container"}>
|
||||
<img src={x.thumbnail} className={"news-img"}/>
|
||||
<img src={x.thumbnail} loading={'lazy'} className={"news-img"}/>
|
||||
</div>
|
||||
<a className={'news-link'} target="_blank" href={x.link}>{x.link.split("/")[2].replace(/www\./, '')}</a>
|
||||
<p className={'mt-2 mb-2'}>{x.header}</p>
|
||||
@ -100,7 +139,7 @@ function Home() {
|
||||
{popular_user_review.data.map((x) => (
|
||||
<div className={'text-sm reviews-container'}>
|
||||
<div style={{ float: 'left', width: 120, margin: '0 12px 10px 0' }}>
|
||||
<img src={x.thumbnail} style={{ width: '100%' }} />
|
||||
<img src={x.thumbnail} loading={'lazy'} style={{ width: '100%' }} />
|
||||
</div>
|
||||
<p className={"text-sm location-titles"}>{x.place_name}</p>
|
||||
<p className={'text-xs mb-2'}>{x.location}</p>
|
||||
@ -149,7 +188,7 @@ function Home() {
|
||||
{critics_users_pick.critics.map((x) => (
|
||||
<div className={"pt-2 text-sm"}>
|
||||
<div className={'mr-2 critics-users-image'}>
|
||||
<img src={x.thumbnail} style={{ height: '100%', width: '100%', borderRadius: 3}} />
|
||||
<img src={x.thumbnail} loading={'lazy'} style={{ height: '100%', width: '100%', borderRadius: 3}} />
|
||||
</div>
|
||||
<p className={'location-title'}>{x.name}</p>
|
||||
<p className={'text-xs location-province location-title'}>{x.location}</p>
|
||||
@ -187,7 +226,7 @@ function Home() {
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</DefaultLayout>
|
||||
</>
|
||||
|
||||
)
|
||||
}
|
||||
|
@ -1,8 +1,3 @@
|
||||
.main-content {
|
||||
padding: 20px 25px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.recently-added-section-card {
|
||||
padding: 10px 1% 15px;
|
||||
display: inline-block;
|
||||
@ -11,6 +6,10 @@
|
||||
width: 16.6%;
|
||||
}
|
||||
|
||||
.recently-added-section-card a:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.location-container {
|
||||
text-align: left;
|
||||
border-bottom-width: 1px;
|
||||
@ -59,12 +58,6 @@
|
||||
float: left;
|
||||
}
|
||||
|
||||
.users-score {
|
||||
color: #a8adb3;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
|
||||
.critics-users-rating-container {
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
@ -185,15 +178,4 @@
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
|
||||
.users-score {
|
||||
font-size: .75rem;
|
||||
line-height: 1rem;
|
||||
}
|
||||
|
||||
.users-score-bar p {
|
||||
font-size: 0.75rem;;
|
||||
line-height: 1rem;
|
||||
}
|
||||
|
||||
}
|
79
src/pages/LocationDetail/index.css
Normal file
79
src/pages/LocationDetail/index.css
Normal file
@ -0,0 +1,79 @@
|
||||
.header-link{
|
||||
font-size: 0.7em;
|
||||
padding-bottom: 5px;
|
||||
border-bottom: 1px solid #38444d;
|
||||
}
|
||||
|
||||
.header-link a:hover{
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.image-stack {
|
||||
display: grid;
|
||||
position: relative;
|
||||
grid-template-columns: repeat(12, 1fr);
|
||||
}
|
||||
|
||||
.image-stack__item--bottom {
|
||||
grid-column: -3 / 1;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
.image-stack__item--middle {
|
||||
margin-left: 10px;
|
||||
grid-column: -2 / 1;
|
||||
grid-row: 1;
|
||||
padding-top: 2%;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.image-stack__item--top {
|
||||
grid-row: 1;
|
||||
grid-column: -1 / 2;
|
||||
padding-top: 4%;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.location-detail-container {
|
||||
padding: 15px;
|
||||
width: 35%;
|
||||
vertical-align: top;
|
||||
border: 1px solid #38444d
|
||||
}
|
||||
|
||||
.location-detail-container div span {
|
||||
font-size: 12px;
|
||||
color: #a8adb3
|
||||
}
|
||||
|
||||
.tags-box {
|
||||
display: inline-block;
|
||||
background-color: #484848;
|
||||
border-radius: 3;
|
||||
}
|
||||
|
||||
.tags-box a:hover {
|
||||
color: white;
|
||||
border-bottom: 1px solid white;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 380px) {
|
||||
.header-link {
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
overflow-x: scroll;
|
||||
overflow-y: hidden;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
-ms-overflow-style: none;
|
||||
padding: 0 10px;
|
||||
}
|
||||
.header-link::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
229
src/pages/LocationDetail/index.tsx
Normal file
229
src/pages/LocationDetail/index.tsx
Normal file
@ -0,0 +1,229 @@
|
||||
import { useLocation, useParams } from 'react-router-dom';
|
||||
import { getImagesByLocationService, getLocationService } from "../../services";
|
||||
import { useEffect, 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';
|
||||
|
||||
function LocationDetail() {
|
||||
const [locationDetail, setLocationDetail] = useCallbackState<LocationDetailResponse>(EmptyLocationDetailResponse)
|
||||
const [locationImages, setLocationImages] = useState<LocationResponse>(emptyLocationResponse())
|
||||
const [lightboxOpen, setLightboxOpen] = useState<boolean>(false)
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true)
|
||||
|
||||
const { state } = useLocation();
|
||||
const { id } = useParams()
|
||||
|
||||
async function getLocationDetail() {
|
||||
try {
|
||||
const res = await getLocationService(Number(id))
|
||||
setLocationDetail(res.data, (val) => {
|
||||
getImage(val.detail.thumbnail.String.toString())
|
||||
})
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
}
|
||||
}
|
||||
|
||||
async function getImage(thumbnail?: String) {
|
||||
try {
|
||||
const res = await getImagesByLocationService({ page: 1, page_size: 15, location_id: Number(id) })
|
||||
res.data.images.push({ src: thumbnail })
|
||||
setLocationImages(res.data)
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
setIsLoading(false)
|
||||
}
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
getLocationDetail()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={'content main-content mt-3'}>
|
||||
<section name={"HEADER LINK"}>
|
||||
<div className={'header-link text-tertiary'}>
|
||||
<a style={{ display: 'inline-block' }}>OVERVIEW</a>
|
||||
<a className={'ml-4'} style={{ display: 'inline-block' }}>USER REVIEWS</a>
|
||||
<a className={'ml-4'} style={{ display: 'inline-block' }}>CRITIC REVIEWS</a>
|
||||
<a className={'ml-4'} style={{ display: 'inline-block' }}>COMMENTS</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section name={'LOCATION HEADER'}>
|
||||
<div className={'pb-5'} style={{ borderBottom: '1px solid #38444d'}}>
|
||||
<div className={'font-bold mt-5 text-2xl'}>
|
||||
<h1>{locationDetail?.detail.name}</h1>
|
||||
</div>
|
||||
{isLoading ?
|
||||
<div className={'mt-3'} style={{ width: 250, height: 250, backgroundColor: 'gray', float: 'left' }} />
|
||||
:
|
||||
<div className={'inline-block'} style={{ maxWidth: 320 }}>
|
||||
<a
|
||||
onClick={() => setLightboxOpen(true)}
|
||||
className={'mt-3'}
|
||||
style={{ display: 'grid', position: 'relative', gridTemplateColumns: 'repeat(12,1fr)', cursor: 'zoom-in' }}
|
||||
>{Number(locationImages?.total_image) > 0 &&
|
||||
<div class="image-stack__item image-stack__item--top">
|
||||
<img src={locationDetail.detail.thumbnail.String.toString()} alt="" style={{ aspectRatio: '1/1' }} />
|
||||
{locationImages?.images.length > 1 &&
|
||||
<div className={'text-xs p-2 bg-primary'} style={{ position: 'absolute', bottom: 0, right: 0 }}>
|
||||
Total images ({locationImages?.images.length})
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
{locationImages?.images.length > 1 &&
|
||||
<div class="image-stack__item image-stack__item--middle">
|
||||
<img src={locationImages?.images[0].src} alt="" style={{ aspectRatio: '1/1' }} />
|
||||
</div>
|
||||
}
|
||||
<div class="image-stack__item image-stack__item--bottom" style={Number(locationImages?.total_image) > 1 ? {} : { gridColumn: '13/1' }}>
|
||||
<img src={Number(locationImages?.total_image) > 1 ? locationImages?.images[1].src.toString() : locationDetail.detail.thumbnail.String.toString()} alt="" style={{ aspectRatio: '1/1' }} />
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
<div className={'inline-block'} style={{ verticalAlign: 'top', padding: '0 2%', width: '30%', minWidth: 310 }}>
|
||||
<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"}
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{state.critic_count !== 0 &&
|
||||
<div className={'ml-14 text-sm'}>
|
||||
Based on {state.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={"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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{state.user_count !== 0 &&
|
||||
<div className={'ml-4'}>
|
||||
Based on {state.user_count} reviews
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div className={'inline-block mt-3 bg-primary text-sm location-detail-container'}>
|
||||
<div className={'pb-1'} style={{ borderBottom: '1px solid #38444d' }}>
|
||||
<h2 className={'inline-block font-bold text-xs'} style={{ letterSpacing: .9 }}>DETAILS</h2>
|
||||
<div className={''} style={{ float: 'right', fontSize: 10, letterSpacing: .9 }}>
|
||||
<a href="#">SUBMIT CORRECTION</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className={'mt-2 capitalize'}>
|
||||
<span>address: </span> {locationDetail.detail.address} {locationDetail.detail.regency_name}
|
||||
</div>
|
||||
<div className={'mt-1 capitalize'}>
|
||||
<span>province: </span> <a href={'#'}> {locationDetail.detail.province_name}</a>
|
||||
</div>
|
||||
<div className={'mt-1 capitalize'}>
|
||||
<span>region: </span> <a href={'#'}> {locationDetail.detail.region_name}</a>
|
||||
</div>
|
||||
<div className={'mt-1 capitalize'}>
|
||||
<span>average cost: </span> IDR 25.0000
|
||||
</div>
|
||||
<div className={'mt-1 text-md'}>
|
||||
<a href={locationDetail.detail.google_maps_link.toString()} style={{ borderBottom: '1px solid white' }} target={'_'}> Maps Location</a>
|
||||
</div>
|
||||
<div className={'mt-1'}>
|
||||
<span>Tags: </span>
|
||||
</div>
|
||||
{locationDetail.tags.map(x => (
|
||||
<div className={'p-1 text-xs tags-box mr-1'}>
|
||||
<a href={'#'}>{x}</a>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
<div className={'p-1 text-xs tags-box'}>
|
||||
<a href={'#'}>+ add tags</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<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={'reviewContainer p-4'} style={{ backgroundColor: '#2f3136' }}>
|
||||
<div className={'reviewBoxContent'} style={{ width: '75%', margin: '0 auto' }}>
|
||||
|
||||
<div className={'userImage mr-3'} style={{ width: 50, float: 'left' }}>
|
||||
<a href={'#'}>
|
||||
<img loading={'lazy'} src={'https://cdn.discordapp.com/attachments/743422487882104837/1153985664849805392/421-4212617_person-placeholder-image-transparent-hd-png-download.png'} />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'block' }}>
|
||||
<a href={'#'}>user</a>
|
||||
</div>
|
||||
|
||||
<div style={{ margin: '5px 0 10px' }}>
|
||||
<div style={{ float: 'left' }}>
|
||||
<input
|
||||
type={'text'}
|
||||
pattern={"\d*"}
|
||||
style={{ fontSize: 12, backgroundColor: '#40444b', textAlign: 'center', width: 40, height: 20, lineHeight: 18, border: '1px solid #38444d' }}
|
||||
maxLength={3}
|
||||
placeholder={"0-100"}
|
||||
autoComplete={'off'}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ clear: 'both' }} />
|
||||
</div>
|
||||
|
||||
<div className={'mt-3'} style={{ width: '100%' }}>
|
||||
<textarea style={{ border: 'none', overflow: 'auto', outline: 'none', boxShadow: 'none', backgroundColor: '#40444b' }}></textarea>
|
||||
</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={'#'}>
|
||||
POST
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'table-cell', position: 'relative', verticalAlign: 'top', width: 330, textAlign: 'left', padding: 15, boxSizing: 'border-box', backgroundColor: "gray", 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>
|
||||
</section>
|
||||
<Lightbox
|
||||
open={lightboxOpen}
|
||||
close={() => setLightboxOpen(false)}
|
||||
slides={locationImages?.images}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default LocationDetail;
|
69
src/pages/LocationDetail/types.ts
Normal file
69
src/pages/LocationDetail/types.ts
Normal file
@ -0,0 +1,69 @@
|
||||
import { SlideImage } from "yet-another-react-lightbox"
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
export function emptyLocationDetail(): ILocationDetail {
|
||||
return {
|
||||
id: 0,
|
||||
address: '',
|
||||
google_maps_link: '',
|
||||
thumbnail: { String: '', Valid: false },
|
||||
name: '',
|
||||
province_name: '',
|
||||
regency_name: '',
|
||||
region_name: '',
|
||||
submitted_by: 0,
|
||||
submitted_by_user: ''
|
||||
}
|
||||
}
|
||||
|
||||
export interface LocationDetailResponse {
|
||||
detail: ILocationDetail,
|
||||
tags: Array<String>
|
||||
}
|
||||
|
||||
export function EmptyLocationDetailResponse(): LocationDetailResponse {
|
||||
return {
|
||||
detail: emptyLocationDetail(),
|
||||
tags: []
|
||||
}
|
||||
}
|
||||
|
||||
export interface LocationImage extends SlideImage {
|
||||
id: Number,
|
||||
src: string,
|
||||
created_at: String,
|
||||
uploaded_by: String
|
||||
}
|
||||
|
||||
export function emptyLocationImage(): LocationImage {
|
||||
return {
|
||||
id: 0,
|
||||
src: '',
|
||||
created_at: '',
|
||||
uploaded_by: ''
|
||||
}
|
||||
}
|
||||
|
||||
export interface LocationResponse {
|
||||
total_image: Number,
|
||||
images: Array<LocationImage>
|
||||
}
|
||||
|
||||
export function emptyLocationResponse(): LocationResponse {
|
||||
return {
|
||||
total_image: 0,
|
||||
images: [emptyLocationImage()]
|
||||
}
|
||||
}
|
10
src/pages/NewsEvents/index.tsx
Normal file
10
src/pages/NewsEvents/index.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
function NewsEvent() {
|
||||
return(
|
||||
<>
|
||||
<h1>Best PLaces</h1>
|
||||
</>
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
export default NewsEvent;
|
10
src/pages/Stories/index.tsx
Normal file
10
src/pages/Stories/index.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
function Story() {
|
||||
return(
|
||||
<>
|
||||
<h1>Best PLaces</h1>
|
||||
</>
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
export default Story;
|
@ -1,5 +1,17 @@
|
||||
import Home from "./Home";
|
||||
import BestLocation from "./BestLocations";
|
||||
import Discovery from "./Discovery";
|
||||
import Story from "./Stories";
|
||||
import NewsEvent from "./NewsEvents";
|
||||
import LocationDetail from "./LocationDetail";
|
||||
|
||||
export {
|
||||
Home
|
||||
Home,
|
||||
|
||||
BestLocation,
|
||||
LocationDetail,
|
||||
|
||||
Discovery,
|
||||
Story,
|
||||
NewsEvent
|
||||
}
|
43
src/routes/index.tsx
Normal file
43
src/routes/index.tsx
Normal file
@ -0,0 +1,43 @@
|
||||
import {
|
||||
BestLocation,
|
||||
Discovery,
|
||||
Home,
|
||||
LocationDetail,
|
||||
NewsEvent,
|
||||
Story
|
||||
} from '../pages';
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: "/",
|
||||
name: "Home",
|
||||
element: <Home />
|
||||
},
|
||||
{
|
||||
path: "/best-places",
|
||||
name: "Home",
|
||||
element: <BestLocation />
|
||||
},
|
||||
{
|
||||
path: "/discover",
|
||||
name: "Home",
|
||||
element: <Discovery />
|
||||
},
|
||||
{
|
||||
path: "/stories",
|
||||
name: "Home",
|
||||
element: <Story />
|
||||
},
|
||||
{
|
||||
path: "/news-events",
|
||||
name: "Home",
|
||||
element: <NewsEvent />
|
||||
},
|
||||
{
|
||||
path: "/location/:id",
|
||||
name: "LocationDetail",
|
||||
element: <LocationDetail />
|
||||
}
|
||||
]
|
||||
|
||||
export default routes;
|
19
src/services/config.ts
Normal file
19
src/services/config.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import axios, { AxiosPromise, AxiosRequestConfig } from "axios";
|
||||
import {BASE_URL} from '../constants/api'
|
||||
|
||||
export const client = (props: AxiosRequestConfig): AxiosPromise => axios({
|
||||
method: props.method,
|
||||
baseURL: `${BASE_URL}`,
|
||||
url: props.url,
|
||||
headers: props.headers,
|
||||
data: props.data
|
||||
})
|
||||
|
||||
// export const authClient = (props: AxiosRequestConfig) => axios({
|
||||
// method: props.method,
|
||||
// baseURL: `${BASE_URL}`,
|
||||
// url: props.url,
|
||||
// headers: {
|
||||
// 'Authorization':
|
||||
// }
|
||||
// })
|
36
src/services/images.ts
Normal file
36
src/services/images.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import { GET_IMAGES_BY_LOCATION_URI } from "../constants/api"
|
||||
import { client } from "./config"
|
||||
import statusCode from "./status-code"
|
||||
|
||||
const initialState: any = {
|
||||
data: null,
|
||||
error: null
|
||||
}
|
||||
|
||||
interface getImagesReq extends GetRequestPagination {
|
||||
location_id?: Number
|
||||
}
|
||||
|
||||
|
||||
async function getImagesByLocationService({ page, page_size, location_id }: getImagesReq) {
|
||||
const newState = { ...initialState }
|
||||
const url = `${GET_IMAGES_BY_LOCATION_URI}?location_id=${location_id}&page=${page}&page_size=${page_size}`
|
||||
|
||||
try {
|
||||
const response = await client({ method: 'GET', url: url})
|
||||
switch (response.request.status) {
|
||||
case statusCode.OK:
|
||||
newState.data = response.data;
|
||||
return newState
|
||||
default:
|
||||
newState.error = response.data;
|
||||
return newState
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`GET IMAGE BY LOCATION SERVICE ERROR: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
getImagesByLocationService
|
||||
}
|
18
src/services/index.ts
Normal file
18
src/services/index.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import {
|
||||
getListLocationsService,
|
||||
getListRecentLocationsRatingsService,
|
||||
getListTopLocationsService,
|
||||
getLocationService,
|
||||
getLocationTagsService,
|
||||
} from "./locations";
|
||||
|
||||
import { getImagesByLocationService } from "./images"
|
||||
|
||||
export {
|
||||
getListLocationsService,
|
||||
getListRecentLocationsRatingsService,
|
||||
getListTopLocationsService,
|
||||
getLocationService,
|
||||
getLocationTagsService,
|
||||
getImagesByLocationService,
|
||||
}
|
111
src/services/locations.ts
Normal file
111
src/services/locations.ts
Normal file
@ -0,0 +1,111 @@
|
||||
import { GET_LIST_LOCATIONS_URI, GET_LIST_RECENT_LOCATIONS_RATING_URI, GET_LIST_TOP_LOCATIONS, GET_LOCATION_TAGS_URI, GET_LOCATION_URI } from "../constants/api";
|
||||
import { client } from "./config";
|
||||
import statusCode from "./status-code";
|
||||
|
||||
const initialState: any = {
|
||||
data: null,
|
||||
error: null
|
||||
}
|
||||
|
||||
interface getListLocationsArg extends GetRequestPagination {
|
||||
order_by?: number,
|
||||
region_type?: number
|
||||
}
|
||||
|
||||
async function getListLocationsService({ page, page_size }: getListLocationsArg) {
|
||||
const newState = { ...initialState };
|
||||
const url = `${GET_LIST_LOCATIONS_URI}?page=${page}&page_size=${page_size}`
|
||||
try {
|
||||
const response = await client({ method: 'GET', url: url })
|
||||
switch (response.request.status) {
|
||||
case statusCode.OK:
|
||||
newState.data = response.data;
|
||||
return newState;
|
||||
default:
|
||||
newState.error = response.data;
|
||||
return newState
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
async function getListRecentLocationsRatingsService(page_size: Number) {
|
||||
const newState = { ...initialState };
|
||||
const url = `${GET_LIST_RECENT_LOCATIONS_RATING_URI}?page_size=${page_size}`
|
||||
try {
|
||||
const response = await client({ method: 'GET', url: url })
|
||||
switch (response.request.status) {
|
||||
case statusCode.OK:
|
||||
newState.data = response.data;
|
||||
return newState;
|
||||
default:
|
||||
newState.error = response.data;
|
||||
return newState
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
async function getListTopLocationsService({ page, page_size, order_by, region_type }: getListLocationsArg) {
|
||||
const newState = { ...initialState };
|
||||
const url = `${GET_LIST_TOP_LOCATIONS}?page=${page}&page_size=${page_size}&order_by=${order_by}®ion_type=${region_type}`
|
||||
try {
|
||||
const response = await client({ method: 'GET', url: url })
|
||||
switch (response.request.status) {
|
||||
case statusCode.OK:
|
||||
newState.data = response.data;
|
||||
return newState;
|
||||
default:
|
||||
newState.error = response.data;
|
||||
return newState
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
async function getLocationService(id: Number) {
|
||||
const newState = { ...initialState };
|
||||
const url = `${GET_LOCATION_URI}/${id}`
|
||||
try {
|
||||
const response = await client({ method: 'GET', url: url })
|
||||
switch (response.request.status) {
|
||||
case statusCode.OK:
|
||||
newState.data = response.data;
|
||||
return newState;
|
||||
default:
|
||||
newState.error = response.data;
|
||||
return newState;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
async function getLocationTagsService(id: Number) {
|
||||
const newState = { ...initialState };
|
||||
const url = `${GET_LOCATION_TAGS_URI}/${id}`
|
||||
try {
|
||||
const response = await client({ method: 'GET', url: url })
|
||||
switch (response.request.status) {
|
||||
case statusCode.OK:
|
||||
newState.data = response.data;
|
||||
return newState;
|
||||
default:
|
||||
newState.error = response.data;
|
||||
return newState;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
getListLocationsService,
|
||||
getListRecentLocationsRatingsService,
|
||||
getListTopLocationsService,
|
||||
getLocationTagsService,
|
||||
getLocationService
|
||||
}
|
10
src/services/status-code.ts
Normal file
10
src/services/status-code.ts
Normal file
@ -0,0 +1,10 @@
|
||||
export default {
|
||||
OK: 200,
|
||||
CREATED: 201,
|
||||
ACCEPTED: 202,
|
||||
BAD_REQUEST: 400,
|
||||
UNAUTHORIZED: 401,
|
||||
NOT_ALLOWED: 405,
|
||||
NOT_FOUND: 404,
|
||||
SERVER_ERROR: 500,
|
||||
};
|
7
src/types/common.ts
Normal file
7
src/types/common.ts
Normal file
@ -0,0 +1,7 @@
|
||||
type BaseNullValueRes = { Valid: boolean };
|
||||
type NullValueRes<Key extends string, _> = BaseNullValueRes & Record<Key, string | number>
|
||||
|
||||
interface GetRequestPagination {
|
||||
page: number,
|
||||
page_size: number,
|
||||
}
|
30
src/types/state-callback.ts
Normal file
30
src/types/state-callback.ts
Normal file
@ -0,0 +1,30 @@
|
||||
// https://medium.com/geekculture/usecallbackstate-the-hook-that-let-you-run-code-after-a-setstate-operation-finished-25f40db56661
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
type CallBackType<T> = (updatedValue: T) => void;
|
||||
|
||||
type SetStateType<T> = T | ((prev: T) => T);
|
||||
|
||||
type RetType = <T>(
|
||||
initialValue: T | (() => T)
|
||||
) => [T, (newValue: SetStateType<T>, callback?: CallBackType<T>) => void];
|
||||
|
||||
const useCallbackState: RetType = <T>(initialValue: T | (() => T)) => {
|
||||
const [state, _setState] = useState<T>(initialValue);
|
||||
const callbackQueue = useRef<CallBackType<T>[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
callbackQueue.current.forEach((cb) => cb(state));
|
||||
callbackQueue.current = [];
|
||||
}, [state]);
|
||||
|
||||
const setState = (newValue: SetStateType<T>, callback?: CallBackType<T>) => {
|
||||
_setState(newValue);
|
||||
if (callback && typeof callback === "function") {
|
||||
callbackQueue.current.push(callback);
|
||||
}
|
||||
};
|
||||
return [state, setState];
|
||||
};
|
||||
|
||||
export default useCallbackState;
|
@ -17,6 +17,7 @@ export default {
|
||||
lato: ['Lato', 'sans-serif']
|
||||
},
|
||||
fontSize: {
|
||||
xxs: ['0.65rem', { lineHeight: '.85rem' }],
|
||||
xs: ['0.75rem', { lineHeight: '1rem' }],
|
||||
sm: ['0.875rem', { lineHeight: '1.25rem' }],
|
||||
base: ['1rem', { lineHeight: '1.5rem' }],
|
||||
|
25
yarn.lock
25
yarn.lock
@ -434,6 +434,11 @@
|
||||
"@prefresh/utils" "^1.2.0"
|
||||
"@rollup/pluginutils" "^4.2.1"
|
||||
|
||||
"@remix-run/router@1.9.0":
|
||||
version "1.9.0"
|
||||
resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.9.0.tgz#9033238b41c4cbe1e961eccb3f79e2c588328cf6"
|
||||
integrity sha512-bV63itrKBC0zdT27qYm6SDZHlkXwFL1xMBuhkn+X7l0+IIhNaH5wuuvZKp6eKhCD4KFhujhfhCT1YxXW6esUIA==
|
||||
|
||||
"@rollup/pluginutils@^4.1.1", "@rollup/pluginutils@^4.2.1":
|
||||
version "4.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-4.2.1.tgz#e6c6c3aba0744edce3fb2074922d3776c0af2a6d"
|
||||
@ -1050,6 +1055,21 @@ queue-microtask@^1.2.2:
|
||||
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
|
||||
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
|
||||
|
||||
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"
|
||||
integrity sha512-aTfBLv3mk/gaKLxgRDUPbPw+s4Y/O+ma3rEN1u8EgEpLpPe6gNjIsWt9rxushMHHMb7mSwxRGdGlGdvmFsyPIg==
|
||||
dependencies:
|
||||
"@remix-run/router" "1.9.0"
|
||||
react-router "6.16.0"
|
||||
|
||||
react-router@6.16.0:
|
||||
version "6.16.0"
|
||||
resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.16.0.tgz#abbf3d5bdc9c108c9b822a18be10ee004096fb81"
|
||||
integrity sha512-VT4Mmc4jj5YyjpOi5jOf0I+TYzGpvzERy4ckNSvSh2RArv8LLoCxlsZ2D+tc7zgjxcY34oTz2hZaeX5RVprKqA==
|
||||
dependencies:
|
||||
"@remix-run/router" "1.9.0"
|
||||
|
||||
read-cache@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/read-cache/-/read-cache-1.0.0.tgz#e664ef31161166c9751cdbe8dbcf86b5fb58f774"
|
||||
@ -1229,3 +1249,8 @@ yaml@^2.1.1:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.1.tgz#02fe0975d23cd441242aa7204e09fc28ac2ac33b"
|
||||
integrity sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==
|
||||
|
||||
yet-another-react-lightbox@^3.12.2:
|
||||
version "3.12.2"
|
||||
resolved "https://registry.yarnpkg.com/yet-another-react-lightbox/-/yet-another-react-lightbox-3.12.2.tgz#10b6a023fe6469e51d52e881715f00d5f2e5e82b"
|
||||
integrity sha512-sFypE74Rjjl7WbzKSrGeZ7lbcgduOam/BKj8kqzx/ciC4dJyYdE1SG0dur9oDq/gZiY3VGJh+PYGbq6ZwmPFmA==
|
||||
|
Loading…
Reference in New Issue
Block a user