From e454c00847250c8a93c550a027e876d83cf3e185 Mon Sep 17 00:00:00 2001 From: NCanggoro Date: Wed, 20 Sep 2023 21:26:25 +0700 Subject: [PATCH] add location detail --- src/app.tsx | 3 + src/constants/api.ts | 7 +- src/pages/BestLocations/index.tsx | 37 +++-- src/pages/Home/index.tsx | 47 ++++-- src/pages/Home/style.css | 4 + src/pages/LocationDetail/index.css | 79 ++++++++++ src/pages/LocationDetail/index.tsx | 226 ++++++++++++++++++++++++++++- src/pages/LocationDetail/types.ts | 69 +++++++++ src/services/images.ts | 36 +++++ src/services/index.ts | 9 +- src/services/locations.ts | 72 ++++++--- src/types/common.ts | 7 +- src/types/state-callback.ts | 30 ++++ 13 files changed, 576 insertions(+), 50 deletions(-) create mode 100644 src/pages/LocationDetail/index.css create mode 100644 src/pages/LocationDetail/types.ts create mode 100644 src/services/images.ts create mode 100644 src/types/state-callback.ts diff --git a/src/app.tsx b/src/app.tsx index 3fa7dea..c3a199d 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -3,6 +3,9 @@ import { BrowserRouter as Router } from 'react-router-dom' import './app.css' import { DefaultLayout } from './layouts' import routes from './routes' +import "yet-another-react-lightbox/styles.css"; + + export function App() { return ( <> diff --git a/src/constants/api.ts b/src/constants/api.ts index c60eec5..6adaff8 100644 --- a/src/constants/api.ts +++ b/src/constants/api.ts @@ -7,12 +7,17 @@ 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, - SIGNUP_URI + GET_LOCATION_TAGS_URI, + GET_IMAGES_BY_LOCATION_URI, } \ No newline at end of file diff --git a/src/pages/BestLocations/index.tsx b/src/pages/BestLocations/index.tsx index 34a0894..4383ea7 100644 --- a/src/pages/BestLocations/index.tsx +++ b/src/pages/BestLocations/index.tsx @@ -3,6 +3,7 @@ import { getListTopLocationsService } from "../../services"; import { DefaultSeparator } from "../../components"; import { TargetedEvent } from "preact/compat"; import './style.css'; +import { useNavigate } from "react-router-dom"; interface TopLocation { @@ -13,9 +14,9 @@ interface TopLocation { address: String, google_maps_link: string, regency_name: string, - critic_score: NullValueRes<"Int32", Number>, + critic_score: Number, critic_count: Number, - user_score: NullValueRes<"Int32", Number>, + user_score: Number, user_count: Number, critic_bayes: Number, user_bayes: Number, @@ -56,6 +57,8 @@ 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 }) @@ -76,6 +79,18 @@ 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]) @@ -137,11 +152,11 @@ function BestLocation() { */}
- {x.row_number}.{x.name} + onNavigateToDetail(x.id, x.critic_count, x.critic_score, x.user_count, x.user_score)}>{x.row_number}.{x.name}
- - + onNavigateToDetail(x.id, x.critic_count, x.critic_score, x.user_count, x.user_score)}> +
{x.regency_name}
@@ -153,9 +168,9 @@ function BestLocation() {
CRITICS SCORE
-

{x.critic_score.Valid ? Number(x.critic_score.Int32) / Number(x.critic_count) * 10 : "N/A"}

+

{x.critic_score !== 0 ? Number(x.critic_score) / Number(x.critic_count) * 10 : "N/A"}

-
+

{x.critic_count} reviews

@@ -165,9 +180,9 @@ function BestLocation() {
USERS SCORE
-

{x.user_score.Valid ? x.user_score.Int32 : "N/A" }

+

{x.user_score !== 0 ? x.user_score : "N/A" }

-
+

{x.user_count} reviews

@@ -178,8 +193,8 @@ function BestLocation() { ))}
-
-
+
+
{REVIEWERS_TYPE.map((x, idx) => ( onChangeReviewType(e, x.toLowerCase(), idx+1)} diff --git a/src/pages/Home/index.tsx b/src/pages/Home/index.tsx index 5369b04..f55fe3b 100644 --- a/src/pages/Home/index.tsx +++ b/src/pages/Home/index.tsx @@ -6,12 +6,14 @@ 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: NullValueRes<'String', string>, - regency_name: NullValueRes<'String', string>, + regency_name: String, + province_name: String, critic_score: Number, critic_count: Number, user_score: Number, @@ -30,6 +32,9 @@ function Home() { const [recentLocations, setRecentLocations] = useState>([]) // const [isLoading, setIsLoading] = useState(true) + const navigate = useNavigate() + + async function getRecentLocations() { try { const locations = await getListRecentLocationsRatingsService(12) @@ -40,6 +45,18 @@ function Home() { } } + 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() },[]) @@ -52,30 +69,32 @@ function Home() { {recentLocations.map((x) => (
-
- {x.name} -
+
onNavigateToDetail(x.id, x.critic_count, x.critic_score, x.user_count, x.user_score)}> +
+ {x.name} +
+

{x.name}

-

{x.regency_name.String}

+

{x.regency_name}, {x.province_name}

- { x.critic_score !== -1 && + { x.critic_count !== 0 &&
-

{x.critic_score === -1 ? "NR" : x.critic_score}

+

{x.critic_score}

-
+

critic score ({x.critic_count})

} - { x.user_score !== -1 && + { x.user_score !== 0 &&
-

{x.user_score === -1 ? "NR" : x.user_score}

+

{x.user_score}

-
+

user score ({x.user_count})

@@ -95,7 +114,7 @@ function Home() { {news.data.map((x: News) => (
- +
{x.link.split("/")[2].replace(/www\./, '')}

{x.header}

@@ -120,7 +139,7 @@ function Home() { {popular_user_review.data.map((x) => (
- +

{x.place_name}

{x.location}

@@ -169,7 +188,7 @@ function Home() { {critics_users_pick.critics.map((x) => (
- +

{x.name}

{x.location}

diff --git a/src/pages/Home/style.css b/src/pages/Home/style.css index 22bf954..8742070 100644 --- a/src/pages/Home/style.css +++ b/src/pages/Home/style.css @@ -6,6 +6,10 @@ width: 16.6%; } +.recently-added-section-card a:hover { + cursor: pointer; +} + .location-container { text-align: left; border-bottom-width: 1px; diff --git a/src/pages/LocationDetail/index.css b/src/pages/LocationDetail/index.css new file mode 100644 index 0000000..31bbf61 --- /dev/null +++ b/src/pages/LocationDetail/index.css @@ -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; + } +} \ No newline at end of file diff --git a/src/pages/LocationDetail/index.tsx b/src/pages/LocationDetail/index.tsx index 1e39b84..6d2cc0b 100644 --- a/src/pages/LocationDetail/index.tsx +++ b/src/pages/LocationDetail/index.tsx @@ -1,9 +1,227 @@ +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() { - return( + const [locationDetail, setLocationDetail] = useCallbackState(EmptyLocationDetailResponse) + const [locationImages, setLocationImages] = useState(emptyLocationResponse()) + const [lightboxOpen, setLightboxOpen] = useState(false) + const [isLoading, setIsLoading] = useState(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 ( <> -
- LOCATION DETAIL -
+
+
+ +
+ +
+
+
+

{locationDetail?.detail.name}

+
+ {isLoading ? +
+ : + + } +
+
+
CRITICS SCORE
+
+ {state.critic_count !== 0 ? state.critic_score : "NR"} +
+
+
+
+
+
+
+
+ {state.critic_count !== 0 && +
+ Based on {state.critic_count} reviews +
+ } +
+
+
USERS SCORE
+
+ {state.user_count !== 0 ? state.user_score : "NR"} +
+
+
+
+
+
+
+
+ {state.user_count !== 0 && +
+ Based on {state.user_count} reviews +
+ } +
+
+
+
+

DETAILS

+ +
+
+ address: {locationDetail.detail.address} {locationDetail.detail.regency_name} +
+ + +
+ average cost: IDR 25.0000 +
+ +
+ Tags: +
+ {locationDetail.tags.map(x => ( +
+ {x} +
+ )) + } + +
+
+
+ +
+
+
+
+
+ +
+ + + +
+ +
+ user +
+ +
+
+ +
+
+
+ +
+ +
+ + +
+
+
+
+ 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? +
+
+
+ setLightboxOpen(false)} + slides={locationImages?.images} + /> +
) } diff --git a/src/pages/LocationDetail/types.ts b/src/pages/LocationDetail/types.ts new file mode 100644 index 0000000..3143f8b --- /dev/null +++ b/src/pages/LocationDetail/types.ts @@ -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 +} + +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 +} + +export function emptyLocationResponse(): LocationResponse { + return { + total_image: 0, + images: [emptyLocationImage()] + } +} \ No newline at end of file diff --git a/src/services/images.ts b/src/services/images.ts new file mode 100644 index 0000000..b9cbfa3 --- /dev/null +++ b/src/services/images.ts @@ -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 +} \ No newline at end of file diff --git a/src/services/index.ts b/src/services/index.ts index 3bc86df..78f3560 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -1,11 +1,18 @@ import { getListLocationsService, getListRecentLocationsRatingsService, - getListTopLocationsService + getListTopLocationsService, + getLocationService, + getLocationTagsService, } from "./locations"; +import { getImagesByLocationService } from "./images" + export { getListLocationsService, getListRecentLocationsRatingsService, getListTopLocationsService, + getLocationService, + getLocationTagsService, + getImagesByLocationService, } \ No newline at end of file diff --git a/src/services/locations.ts b/src/services/locations.ts index 6f17fc8..1177ee7 100644 --- a/src/services/locations.ts +++ b/src/services/locations.ts @@ -1,4 +1,4 @@ -import { GET_LIST_LOCATIONS_URI, GET_LIST_RECENT_LOCATIONS_RATING_URI, GET_LIST_TOP_LOCATIONS } from "../constants/api"; +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"; @@ -7,18 +7,16 @@ const initialState: any = { error: null } -type getListLocationsArg = { - page: number, - page_size: number, +interface getListLocationsArg extends GetRequestPagination { order_by?: number, region_type?: number } -async function getListLocationsService ({ page, page_size}: getListLocationsArg) { - const newState = {...initialState}; +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}) + try { + const response = await client({ method: 'GET', url: url }) switch (response.request.status) { case statusCode.OK: newState.data = response.data; @@ -30,13 +28,13 @@ async function getListLocationsService ({ page, page_size}: getListLocationsArg) } catch (error) { console.log(error) } -} +} -async function getListRecentLocationsRatingsService (page_size: Number) { - const newState = {...initialState}; +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}) + try { + const response = await client({ method: 'GET', url: url }) switch (response.request.status) { case statusCode.OK: newState.data = response.data; @@ -48,13 +46,13 @@ async function getListRecentLocationsRatingsService (page_size: Number) { } catch (error) { console.log(error) } -} +} -async function getListTopLocationsService({ page, page_size, order_by, region_type}: getListLocationsArg) { - const newState = {...initialState}; +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}) + try { + const response = await client({ method: 'GET', url: url }) switch (response.request.status) { case statusCode.OK: newState.data = response.data; @@ -68,8 +66,46 @@ async function getListTopLocationsService({ page, page_size, order_by, region_ty } } +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 } \ No newline at end of file diff --git a/src/types/common.ts b/src/types/common.ts index 5cd7b73..a32d500 100644 --- a/src/types/common.ts +++ b/src/types/common.ts @@ -1,2 +1,7 @@ type BaseNullValueRes = { Valid: boolean }; -type NullValueRes = BaseNullValueRes & Record \ No newline at end of file +type NullValueRes = BaseNullValueRes & Record + +interface GetRequestPagination { + page: number, + page_size: number, +} diff --git a/src/types/state-callback.ts b/src/types/state-callback.ts new file mode 100644 index 0000000..a227819 --- /dev/null +++ b/src/types/state-callback.ts @@ -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 = (updatedValue: T) => void; + +type SetStateType = T | ((prev: T) => T); + +type RetType = ( + initialValue: T | (() => T) +) => [T, (newValue: SetStateType, callback?: CallBackType) => void]; + +const useCallbackState: RetType = (initialValue: T | (() => T)) => { + const [state, _setState] = useState(initialValue); + const callbackQueue = useRef[]>([]); + + useEffect(() => { + callbackQueue.current.forEach((cb) => cb(state)); + callbackQueue.current = []; + }, [state]); + + const setState = (newValue: SetStateType, callback?: CallBackType) => { + _setState(newValue); + if (callback && typeof callback === "function") { + callbackQueue.current.push(callback); + } + }; + return [state, setState]; +}; + +export default useCallbackState; \ No newline at end of file