@@ -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
+
+ }
+
+
+
+
+
+ address: {locationDetail.detail.address} {locationDetail.detail.regency_name}
+
+
+
+
+ average cost: IDR 25.0000
+
+
+
+ Tags:
+
+ {locationDetail.tags.map(x => (
+
+ ))
+ }
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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