From 6b4d10e8a0aa70e493639f30a01bf136ae197c40 Mon Sep 17 00:00:00 2001 From: goro Date: Sat, 21 Mar 2026 11:14:39 +0200 Subject: [PATCH] Save changes before submodule conversion --- src/app.css | 2 +- src/app.tsx | 96 ++-- .../Screenshot 2026-01-28 at 19.22.45.png | Bin 0 -> 72724 bytes src/components/Card/RatingsCard/index.tsx | 160 +++++++ src/components/CustomInterweave/index.tsx | 59 ++- src/components/Header/index.tsx | 3 +- src/constants/api.ts | 2 +- src/pages/Discovery/index.tsx | 2 +- src/pages/Home/index.tsx | 22 +- src/pages/LocationDetail/index.tsx | 426 ++++++++++-------- src/pages/LocationDetail/types.ts | 24 +- src/pages/Login/index.tsx | 6 + src/services/auth.ts | 83 ++-- src/services/config.ts | 78 +++- src/services/images.ts | 41 +- src/services/locations.ts | 252 ++++++----- src/services/news.ts | 73 +-- src/services/regions.ts | 71 ++- src/services/review.ts | 65 ++- src/services/users.ts | 109 +++-- src/utils/common.ts | 8 +- tests/example.spec.ts | 18 + 22 files changed, 1074 insertions(+), 526 deletions(-) create mode 100644 src/assets/Screenshot 2026-01-28 at 19.22.45.png create mode 100644 src/components/Card/RatingsCard/index.tsx create mode 100644 tests/example.spec.ts diff --git a/src/app.css b/src/app.css index 81f0dd6..ff92a93 100755 --- a/src/app.css +++ b/src/app.css @@ -15,7 +15,7 @@ overflow: auto; outline: none; box-shadow: none; - background-color: #40444b; + background-color: #202225; width: 100%; min-height: 100px; overflow-y: hidden; diff --git a/src/app.tsx b/src/app.tsx index c05cba2..4cfddb3 100755 --- a/src/app.tsx +++ b/src/app.tsx @@ -9,59 +9,71 @@ import { persistore, store } from './store/config' import { PersistGate } from 'redux-persist/integration/react' import { AdminProtectedRoute, UserProtectedRoute } from './routes/ProtectedRoute' import { getRoutes } from './routes'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + refetchOnWindowFocus: false, + retry: 1, + staleTime: 5 * 60 * 1000, // 5 minutes + }, + }, +}) export function App() { const { routes } = getRoutes(); return ( - - - - - } /> - }> - {routes.map(({ path, name, element, protectedRoute }) => { - let Element = element as any - if (protectedRoute === "user") { - return ( - - - - } - /> - ) - } + + + + + + } /> + }> + {routes.map(({ path, name, element, protectedRoute }) => { + let Element = element as any + if (protectedRoute === "user") { + return ( + + + + } + /> + ) + } - if (protectedRoute === "admin") { + if (protectedRoute === "admin") { + return ( + + + + } + /> + ) + } return ( - - - } + element={element} /> ) - } - return ( - - ) - })} - } /> - - - - - + })} + } /> + + + + + + ) } diff --git a/src/assets/Screenshot 2026-01-28 at 19.22.45.png b/src/assets/Screenshot 2026-01-28 at 19.22.45.png new file mode 100644 index 0000000000000000000000000000000000000000..4a920413b2630871acfb9eab2b44fcd22179784e GIT binary patch literal 72724 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 literal 0 HcmV?d00001 diff --git a/src/components/Card/RatingsCard/index.tsx b/src/components/Card/RatingsCard/index.tsx new file mode 100644 index 0000000..722e392 --- /dev/null +++ b/src/components/Card/RatingsCard/index.tsx @@ -0,0 +1,160 @@ +interface RatingData { + score: number; + count: number; +} + +interface DetailRatings { + environment: number; + cleanliness: number; + price: number; + facility: number; +} + +interface RatingsCardProps { + data: T; + getCriticData: (data: T) => RatingData; + getUserData: (data: T) => RatingData; + getCriticDetails?: (data: T) => DetailRatings; + getUserDetails?: (data: T) => DetailRatings; +} + +const RatingsCard = ({ + data, + getCriticData, + getUserData, + getCriticDetails, + getUserDetails +}: RatingsCardProps) => { + const criticData = getCriticData(data); + const userData = getUserData(data); + const criticDetails = getCriticDetails?.(data); + const userDetails = getUserDetails?.(data); + + const formatCount = (count: number): string | number => { + return count >= 1000 + ? `${(count / 1000).toFixed(1).replace(/\.0$/, '')}k` + : count; + }; + + const calculateScore = (score: number, count: number): string | number => { + return count !== 0 ? Math.floor(score / count) : "NR"; + }; + + return ( +
+ {/* Critics Score Section */} +
+
+
CRITICS SCORE
+
+ {calculateScore(criticData.score, criticData.count)} +
+
+
+
+ {criticData.count !== 0 && ( +
+ Based on {formatCount(criticData.count)} reviews +
+ )} +
+ + {/* Critics Detail Cards */} + {criticDetails && ( +
+
+
ENVIRONMENT
+
+
{criticDetails.environment}
+
+
+
+
+
+ +
+
PRICE
+
+
{criticDetails.price}
+
+
+
+
+
+ +
+
FACILITY
+
+
{criticDetails.facility}
+
+
+
+
+
+
+ )} +
+ + {/* Users Score Section */} +
+
+
USERS SCORE
+
+ {calculateScore(userData.score, userData.count)} +
+
+
+
+ {userData.count !== 0 && ( +
+ Based on {formatCount(userData.count)} reviews +
+ )} +
+ + {/* Users Detail Cards */} + {userDetails && ( +
+
+
ENVIRONMENT
+
+
{userDetails.environment}
+
+
+
+
+
+ +
+
PRICE
+
+
{userDetails.price}
+
+
+
+
+
+ +
+
FACILITY
+
+
{userDetails.facility}
+
+
+
+
+
+
+ )} +
+
+ ); +}; + +export default RatingsCard; diff --git a/src/components/CustomInterweave/index.tsx b/src/components/CustomInterweave/index.tsx index c4d9a24..fe7f18e 100755 --- a/src/components/CustomInterweave/index.tsx +++ b/src/components/CustomInterweave/index.tsx @@ -1,8 +1,64 @@ import { stripHexcode } from 'emojibase'; -import { InterweaveProps, FilterInterface, MatcherInterface, Interweave } from 'interweave'; +import { InterweaveProps, FilterInterface, MatcherInterface, Interweave, Matcher, MatchResponse, Node } from 'interweave'; import { IpMatcher, UrlMatcher, EmailMatcher, HashtagMatcher } from 'interweave-autolink'; import { EmojiMatcher, PathConfig } from 'interweave-emoji'; +class SevenTVMatcher extends Matcher { + private emotes: Record = { + 'booba': '01F6N31ETR0004P7N4A9PKS5X9', + 'pepeJAM': '5f1f0ea5cf6d2144653d7501', + 'OMEGALUL': '5f4b3bc28fb088567e5cbb3b', + 'monkaS': '01F78CHJ2G0005TDSTZFBDGMK4', + 'Sadge': '5f1f0f61b5e9d35e9a2f8a0e', + 'PogU': '5f1f0c1235c7c40e6a3f9c1b', + }; + + replaceWith(match: string): Node { + const emoteName = match.replace(/:/g, '').trim(); + const emoteId = this.emotes[emoteName]; + + if (emoteId) { + return ( + {emoteName} + ); + } + + return match; + } + + asTag(): string { + return 'span'; + } + + match(value: string): MatchResponse<{}> | null { + const emoteNames = Object.keys(this.emotes).join('|'); + // Match emote names wrapped in colons: :emoteName: + const pattern = new RegExp(`:(${emoteNames}):`, 'g'); + const result = pattern.exec(value); + + if (!result) { + return null; + } + + return { + index: result.index, + length: result[0].length, + match: result[0], + valid: true, + }; + } +} + const globalFilters: FilterInterface[] = []; const globalMatchers: MatcherInterface[] = [ @@ -15,6 +71,7 @@ const globalMatchers: MatcherInterface[] = [ convertShortcode: true, convertUnicode: true, }), + new SevenTVMatcher('7tv'), ]; function getEmojiPath(hexcode: string, { enlarged }: PathConfig): string { diff --git a/src/components/Header/index.tsx b/src/components/Header/index.tsx index dbafe6e..03ad0f9 100755 --- a/src/components/Header/index.tsx +++ b/src/components/Header/index.tsx @@ -67,7 +67,7 @@ function Header() { navigate(`/location/${val.value}`) } - const onLoadSelectOptions = async (inputValue: string) => { + const onLoadSelectOptions = async (inputValue: string): Promise => { try { const results = await getSearchLocationService({ name: inputValue, @@ -89,6 +89,7 @@ function Header() { return result } catch (err) { alert(err) + return [] } } diff --git a/src/constants/api.ts b/src/constants/api.ts index ac6856c..ce55a50 100755 --- a/src/constants/api.ts +++ b/src/constants/api.ts @@ -1,4 +1,4 @@ -const BASE_URL = "http://localhost:8888"; +const BASE_URL = import.meta.env.VITE_BASE_URL || "http://192.168.1.13:8888"; const SIGNUP_URI = `${BASE_URL}/user/signup`; const LOGIN_URI = `${BASE_URL}/user/login`; diff --git a/src/pages/Discovery/index.tsx b/src/pages/Discovery/index.tsx index 7e0d3af..2094869 100755 --- a/src/pages/Discovery/index.tsx +++ b/src/pages/Discovery/index.tsx @@ -195,7 +195,7 @@ function Discovery() { ))}
- + + {/*
{news.data.map((x: News) => ( @@ -134,12 +134,12 @@ function Home() { )) }
-
+ */} {/* END RECENT NEWS / EVENT SECTION */} {/* LOCATION CRITICS BEST AND USERS BEST SECTION */} -
+ {/*
{popular_user_review.data.map((x) => ( @@ -166,7 +166,7 @@ function Home() { }
-
+
*/} {/* START LOCATION CRITICS BEST AND USERS BEST SECTION */} @@ -178,7 +178,15 @@ function Home() { {popular.data.map((x) => (
- + { + e.currentTarget.src = 'https://pub-6b637ea51b64436dbf0514bc956972d1.r2.dev/public/upload/misty-forest-black-white.webp'; + e.currentTarget.onerror = null; + }} + />

{x.name}

{x.location}

diff --git a/src/pages/LocationDetail/index.tsx b/src/pages/LocationDetail/index.tsx index eafd49b..98c23d8 100755 --- a/src/pages/LocationDetail/index.tsx +++ b/src/pages/LocationDetail/index.tsx @@ -14,11 +14,12 @@ import { AxiosError } from 'axios'; import { handleAxiosError, useAutosizeTextArea } from '../../utils'; import { getCurrentUserLocationReviewService, getImagesByLocationService, getLocationService, postReviewLocation } from "../../services"; import { DefaultSeparator, SeparatorWithAnchor, CustomInterweave, SpinnerLoading } from '../../components'; +import RatingsCard from '../../components/Card/RatingsCard'; import { useSelector } from 'react-redux'; import { UserRootState } from '../../store/type'; import { DEFAULT_AVATAR_IMG } from '../../constants/default'; -import './index.css'; import { IHttpResponse } from '../../types/common'; +import { ImagePlus } from 'lucide-react' import ReactTextareaAutosize from 'react-textarea-autosize'; const SORT_TYPE = [ @@ -47,6 +48,8 @@ function LocationDetail() { score_input: '', }) const [isLoading, setIsLoading] = useState(true) + const [currentIndex, setCurrentIndex] = useState(0); + const currentImage = locationImages?.images[currentIndex]?.src || locationDetail.detail.thumbnail || ""; const navigate = useNavigate(); const user = useSelector((state: UserRootState) => state.auth) @@ -199,151 +202,217 @@ function LocationDetail() { }, [updatePage, id]) return ( -
+
-
-
-
-
+
+
+

{locationDetail?.detail.name}

- {isLoading ? -
+ {/* {isLoading ? +
: -
+
setLightboxOpen(true)} - className={'mt-3'} - style={{ display: 'grid', position: 'relative', gridTemplateColumns: 'repeat(12,1fr)', cursor: 'zoom-in' }} + className="mt-3 grid relative grid-cols-12 cursor-zoom-in" >{Number(locationImages?.total_image) > 0 && -
- +
+ {locationImages?.images.length > 1 && -
+
Total images ({locationImages?.images.length})
}
} {locationImages?.images.length > 1 && -
- +
+
} -
- } -
-
-
CRITICS SCORE
-
- {locationDetail.detail.critic_count !== 0 ? Math.floor(Number(locationDetail.detail.critic_score) / Number(locationDetail.detail.critic_count)) : "NR"} -
-
-
-
+ } */} + + {isLoading ? ( +
+ ) : ( +
+
+ {/* Main image display */} +
+ setLightboxOpen(true)} + /> + + {locationImages?.images.length > 1 && ( + <> + + + + )} + + {/* Total images badge */} + {locationImages?.images.length > 1 && ( +
+ Total images ({locationImages.images.length})
+ )} +
+ + {/* Thumbnail strip */} + {locationImages?.images.length > 1 && ( +
+ {locationImages.images.map((image, index) => ( + setCurrentIndex(index)} + /> + ))}
-
+ )}
- {locationDetail.detail.critic_count !== 0 && -
- Based on {locationDetail.detail.critic_count} reviews -
- }
-
-
USERS SCORE
-
- {locationDetail.detail.user_count !== 0 ? Math.floor(Number(locationDetail.detail.user_score) / Number(locationDetail.detail.user_count)) : "NR"} -
-
-
-
-
+ )} +
+
+

DETAILS

+ +
+
+
Address:
+
{locationDetail.detail.address} {locationDetail.detail.regency_name}
+
+ + +
+
Average Cost
+
Rp 25.000
+
+
+ +
+
+
Tags :
+
+ {locationDetail.tags.map((x, index) => ( +
+ Badge
+ ))} +
- {locationDetail.detail.user_count !== 0 && -
- Based on {locationDetail.detail.user_count} reviews -
- } -
-
-
-
-

DETAILS

- -
-
- address: {locationDetail.detail.address} {locationDetail.detail.regency_name} -
- - -
- average cost: IDR 25.0000 -
- -
- Tags: -
- {locationDetail.tags.map(x => ( -
- {x} -
- )) - } -
+ + ({ + score: Number(data.detail.critic_score), + count: Number(data.detail.critic_count) + })} + getUserData={(data) => ({ + score: Number(data.detail.user_score), + count: Number(data.detail.user_count) + })} + getCriticDetails={() => ({ + environment: 85, + cleanliness: 90, + price: 75, + facility: 80 + })} + getUserDetails={() => ({ + environment: 82, + cleanliness: 88, + price: 70, + facility: 78 + })} + /> +
-
-
-
- {!user.username ? -
SIGN IN TO REVIEW
- : -
-
-
+
+
+
+ {!user.username ? +
SIGN IN TO REVIEW
+ : +
+
+ + -
+ -
+
{currentUserReview ? -
-

{currentUserReview.score}

-
-
+
+

{currentUserReview.score}

+
+
: @@ -351,24 +420,24 @@ function LocationDetail() { -
/ score
+
/ score
{pageState.is_score_rating_panic_msg && -
{pageState.is_score_rating_panic_msg}
+
{pageState.is_score_rating_panic_msg}
} } -
+
-
+
{currentUserReview ? }
- -
-
- Review Guidelines +
+
+ +

add image

+
+
+ + {pageState.on_submit_loading ? + + : + + + POST + + + }
- {pageState.on_submit_loading ? - - : - - - POST - - - }
} -
+
{locationDetail.critics_review.length > 0 ? <> -
-
Sort by:
- setPageState({ ...pageState, show_sort: !pageState.show_sort })}> -

{pageState.critic_filter_name}

- +
+
Sort by:
+
setPageState({ ...pageState, show_sort: !pageState.show_sort })}> +

{pageState.critic_filter_name}

+
- -
+
{locationDetail.critics_review.map(x => ( -
-
-
+
+
+
{x.score}
-
-
+
+
-
+ -
-
+ -
+
-
-
- - -
Video
+
+ - @@ -469,85 +543,85 @@ function LocationDetail() { : - No Critics review to display + No Critics review to display }
-
+
0 ? '#' : ''} /> {locationDetail.users_review.length > 0 ? <> {locationDetail.users_review.map(x => ( -
-
+
+
- -
-
{x.score}
-
-
+
+
{x.score}
+
+
-
+
-
-
- - -
Video
+
))} -
- + : <> - No users review to display + No users review to display }
-
+
CONTRUBITION anoeantoeh aoenthaoe aoenth aot
-
+
-
+
Added on: 28 May 1988
diff --git a/src/pages/LocationDetail/types.ts b/src/pages/LocationDetail/types.ts index b65e628..f3b4bf0 100755 --- a/src/pages/LocationDetail/types.ts +++ b/src/pages/LocationDetail/types.ts @@ -2,7 +2,7 @@ import { NullValueRes } from "../../types/common" import { SlideImage } from "yet-another-react-lightbox" export interface ILocationDetail { - id: Number, + id: number, name: String, address: String, regency_name: String, @@ -10,11 +10,11 @@ export interface ILocationDetail { region_name: String, google_maps_link: String, thumbnail: string | null, - submitted_by: Number, - critic_score: Number, - critic_count: Number, - user_score: Number, - user_count: Number + submitted_by: number, + critic_score: number, + critic_count: number, + user_score: number, + user_count: number } export function emptyLocationDetail(): ILocationDetail { @@ -64,14 +64,14 @@ export function EmptyLocationDetailResponse(): LocationDetailResponse { } export interface LocationImage extends SlideImage { - id: Number, + id: number, src: string, created_at: String, uploaded_by: String } export interface LocationResponse { - total_image: Number, + total_image: number, images: Array } @@ -83,13 +83,13 @@ export function emptyLocationResponse(): LocationResponse { } export type CurrentUserLocationReviews = { - id: Number, + id: number, comments: string, is_from_critic: boolean, is_hided: boolean, - location_id: Number, - score: Number, - submitted_by: Number, + location_id: number, + score: number, + submitted_by: number, created_at: NullValueRes<"Time", string>, updated_at: NullValueRes<"Time", string>, } \ No newline at end of file diff --git a/src/pages/Login/index.tsx b/src/pages/Login/index.tsx index bdeb53c..eba2f1a 100755 --- a/src/pages/Login/index.tsx +++ b/src/pages/Login/index.tsx @@ -58,6 +58,11 @@ function Login() { }) if (res.error) { + if(res.error.response.status == 409) { + setErrorMsg([{ field: 'username', msg: 'Username Already exist' }]) + return; + } + console.log(res.error) setErrorMsg(res.error.response.data.errors) return; } @@ -96,6 +101,7 @@ function Login() { + {console.log(errorMsg)} {errorMsg.map(x => (

{x.msg}

))} diff --git a/src/services/auth.ts b/src/services/auth.ts index f36840b..7703340 100755 --- a/src/services/auth.ts +++ b/src/services/auth.ts @@ -1,55 +1,76 @@ -import { AxiosError } from "axios"; -import { LOGIN_URI, SIGNUP_URI } from "../constants/api"; +import { useMutation, UseMutationOptions } from "@tanstack/react-query"; +import { LOGIN_URI, SIGNUP_URI, LOGOUT_URI } from "../constants/api"; import { client } from "./config"; import { IHttpResponse } from "../types/common"; -const initialState: IHttpResponse = { - data: null, - error: AxiosError -} - interface IAuthentication { - username: String - password: String + username: string + password: string } +// API Functions +const createAccount = async ({ username, password }: IAuthentication) => { + const response = await client({ method: 'POST', url: SIGNUP_URI, data: { username, password }, withCredentials: true }) + return response.data +} + +const login = async ({ username, password }: IAuthentication) => { + const response = await client({ method: 'POST', url: LOGIN_URI, data: { username, password }, withCredentials: true }) + return response.data +} + +const logout = async () => { + const response = await client({ method: 'POST', url: LOGOUT_URI, withCredentials: true }) + return response.data +} + +// React Query Hooks +export const useCreateAccount = (options?: UseMutationOptions) => { + return useMutation({ + mutationFn: createAccount, + ...options + }) +} + +export const useLogin = (options?: UseMutationOptions) => { + return useMutation({ + mutationFn: login, + ...options + }) +} + +export const useLogout = (options?: UseMutationOptions) => { + return useMutation({ + mutationFn: logout, + ...options + }) +} + +// Legacy service functions for backward compatibility async function createAccountService({ username, password }: IAuthentication) { - const newState = { ...initialState }; try { - const response = await client({ method: 'POST', url: SIGNUP_URI, data: { username, password }, withCredentials: true }) - newState.data = response.data - newState.error = null - return newState + const data = await createAccount({ username, password }) + return { data, error: null } } catch (error) { - newState.error = error - return newState + return { data: null, error } } } 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 + const data = await login({ username, password }) + return { data, error: null } } catch (error) { - newState.error = error - return newState + return { data: null, error } } - } 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 + const data = await logout() + return { data, error: null } } catch (error) { - newState.error = error - return newState + return { data: null, error } } } diff --git a/src/services/config.ts b/src/services/config.ts index 62e757c..9fc9085 100755 --- a/src/services/config.ts +++ b/src/services/config.ts @@ -1,20 +1,62 @@ -import axios, { AxiosPromise, AxiosRequestConfig } from "axios"; -import {BASE_URL} from '../constants/api' +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, - ...props -}) +interface FetchConfig extends RequestInit { + url: string; + data?: any; + withCredentials?: boolean; +} -// export const authClient = (props: AxiosRequestConfig) => axios({ -// method: props.method, -// baseURL: `${BASE_URL}`, -// url: props.url, -// headers: { -// 'Authorization': -// } -// }) \ No newline at end of file +export async function client(config: FetchConfig): Promise<{ data: T; status: number; request: { status: number } }> { + const { url, data, withCredentials, headers, ...rest } = config; + + const fullUrl = url.startsWith('http') ? url : `${BASE_URL}${url}`; + + const fetchOptions: RequestInit = { + ...rest, + headers: { + 'Content-Type': 'application/json', + ...headers, + }, + credentials: withCredentials ? 'include' : 'same-origin', + }; + + // Handle body data + if (data) { + if (data instanceof FormData) { + // Remove Content-Type header for FormData to let browser set it with boundary + const headersObj = fetchOptions.headers as Record; + delete headersObj['Content-Type']; + fetchOptions.body = data; + } else { + fetchOptions.body = JSON.stringify(data); + } + } + + const response = await fetch(fullUrl, fetchOptions); + + let responseData: T; + const contentType = response.headers.get('content-type'); + + if (contentType && contentType.includes('application/json')) { + responseData = await response.json(); + } else { + responseData = await response.text() as any; + } + + if (!response.ok) { + const error: any = new Error('HTTP Error'); + error.response = { + data: responseData, + status: response.status, + statusText: response.statusText, + }; + error.status = response.status; + throw error; + } + + return { + data: responseData, + status: response.status, + request: { status: response.status } + }; +} \ No newline at end of file diff --git a/src/services/images.ts b/src/services/images.ts index c441250..97fa7e0 100755 --- a/src/services/images.ts +++ b/src/services/images.ts @@ -1,34 +1,37 @@ +import { useQuery, UseQueryOptions } from "@tanstack/react-query"; import { GetRequestPagination } from "../types/common" 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 + location_id?: number } - -async function getImagesByLocationService({ page, page_size, location_id }: getImagesReq) { - const newState = { ...initialState } +// API Functions +const fetchImagesByLocation = async ({ page, page_size, location_id }: getImagesReq) => { const url = `${GET_IMAGES_BY_LOCATION_URI}?location_id=${location_id}&page=${page}&page_size=${page_size}` + const response = await client({ method: 'GET', url }) + return response.data +} +// React Query Hooks +export const useImagesByLocation = (params: getImagesReq, options?: Omit, 'queryKey' | 'queryFn'>) => { + return useQuery({ + queryKey: ['images', 'location', params], + queryFn: () => fetchImagesByLocation(params), + enabled: !!params.location_id, + ...options + }) +} + +// Legacy service functions for backward compatibility +async function getImagesByLocationService({ page, page_size, location_id }: getImagesReq) { 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 - } + const data = await fetchImagesByLocation({ page, page_size, location_id }) + return { data, error: null } } catch (error) { console.log(`GET IMAGE BY LOCATION SERVICE ERROR: ${error}`) + return { data: null, error } } } diff --git a/src/services/locations.ts b/src/services/locations.ts index 463ee67..b898996 100755 --- a/src/services/locations.ts +++ b/src/services/locations.ts @@ -1,21 +1,15 @@ +import { useQuery, useMutation, UseQueryOptions, UseMutationOptions } from "@tanstack/react-query"; import { GetRequestPagination, IHttpResponse } from "../types/common"; -import { - GET_LIST_LOCATIONS_URI, - GET_LIST_RECENT_LOCATIONS_RATING_URI, - GET_LIST_TOP_LOCATIONS, - GET_LOCATION_TAGS_URI, +import { + GET_LIST_LOCATIONS_URI, + GET_LIST_RECENT_LOCATIONS_RATING_URI, + GET_LIST_TOP_LOCATIONS, + GET_LOCATION_TAGS_URI, GET_LOCATION_URI, GET_SEARCH_LOCATIONS_URI, POST_CREATE_LOCATION } from "../constants/api"; import { client } from "./config"; -import statusCode from "./status-code"; -import { AxiosError } from "axios"; - -const initialState: any = { - data: null, - error: null -} interface GetListLocationsArg extends GetRequestPagination { order_by?: number, @@ -27,131 +21,181 @@ interface GetSearchLocations extends GetRequestPagination { filter?: string } -async function getListLocationsService({ page, page_size }: GetListLocationsArg) { - const newState = { ...initialState }; +// API Functions +const fetchListLocations = async ({ page, page_size }: GetListLocationsArg) => { const url = `${GET_LIST_LOCATIONS_URI}?page=${page}&page_size=${page_size}` + const response = await client({ method: 'GET', url }) + return response.data +} + +const fetchRecentLocationsRatings = async (page_size: number, page: number) => { + const url = `${GET_LIST_RECENT_LOCATIONS_RATING_URI}?page_size=${page_size}&page=${page}` + const response = await client({ method: 'GET', url }) + return response.data +} + +const fetchTopLocations = async ({ page, page_size, order_by, region_type }: GetListLocationsArg) => { + const url = `${GET_LIST_TOP_LOCATIONS}?page=${page}&page_size=${page_size}&order_by=${order_by}®ion_type=${region_type}` + const response = await client({ method: 'GET', url }) + return response.data +} + +const fetchLocation = async (id: number) => { + const url = `${GET_LOCATION_URI}/${id}` + const response = await client({ method: 'GET', url }) + return response.data +} + +const fetchLocationTags = async (id: number) => { + const url = `${GET_LOCATION_TAGS_URI}/${id}` + const response = await client({ method: 'GET', url }) + return response.data +} + +const createLocation = async (data: FormData) => { + const response = await client({ method: 'POST', url: POST_CREATE_LOCATION, data, withCredentials: true }) + return response.data +} + +const searchLocations = async (arg: GetSearchLocations) => { + const filter = arg.filter ? arg.filter : '' + const pageSize = arg.page_size ? arg.page_size : 12 + const page = arg.page ? arg.page : 1 + const response = await client({ + method: 'GET', + url: `${GET_SEARCH_LOCATIONS_URI}?name=${arg.name}&filter${filter}&limit=${pageSize}&offset=${page}` + }) + return response.data +} + +// React Query Hooks +export const useListLocations = (params: GetListLocationsArg, options?: Omit, 'queryKey' | 'queryFn'>) => { + return useQuery({ + queryKey: ['locations', params], + queryFn: () => fetchListLocations(params), + ...options + }) +} + +export const useRecentLocationsRatings = (page_size: number, page: number, options?: Omit, 'queryKey' | 'queryFn'>) => { + return useQuery({ + queryKey: ['locations', 'recent-ratings', page_size, page], + queryFn: () => fetchRecentLocationsRatings(page_size, page), + ...options + }) +} + +export const useTopLocations = (params: GetListLocationsArg, options?: Omit, 'queryKey' | 'queryFn'>) => { + return useQuery({ + queryKey: ['locations', 'top', params], + queryFn: () => fetchTopLocations(params), + ...options + }) +} + +export const useLocation = (id: number, options?: Omit, 'queryKey' | 'queryFn'>) => { + return useQuery({ + queryKey: ['location', id], + queryFn: () => fetchLocation(id), + enabled: !!id, + ...options + }) +} + +export const useLocationTags = (id: number, options?: Omit, 'queryKey' | 'queryFn'>) => { + return useQuery({ + queryKey: ['location', 'tags', id], + queryFn: () => fetchLocationTags(id), + enabled: !!id, + ...options + }) +} + +export const useSearchLocations = (params: GetSearchLocations, options?: Omit, 'queryKey' | 'queryFn'>) => { + return useQuery({ + queryKey: ['locations', 'search', params], + queryFn: () => searchLocations(params), + enabled: !!params.name, + ...options + }) +} + +export const useCreateLocation = (options?: UseMutationOptions) => { + return useMutation({ + mutationFn: createLocation, + ...options + }) +} + +// Legacy service functions for backward compatibility +async function getListLocationsService({ page, page_size }: GetListLocationsArg) { 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 - } + const data = await fetchListLocations({ page, page_size }) + return { data, error: null } } catch (error) { - console.log(error) + return { data: null, error } } } async function getListRecentLocationsRatingsService(page_size: number, page: number) { - const newState = { ...initialState }; - const url = `${GET_LIST_RECENT_LOCATIONS_RATING_URI}?page_size=${page_size}&page=${page}` 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 - } + const data = await fetchRecentLocationsRatings(page_size, page) + return { data, error: null } } catch (error) { - console.log(error) + return { data: null, 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}` +async function getListTopLocationsService(params: GetListLocationsArg) { 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 - } + const data = await fetchTopLocations(params) + return { data, error: null } } catch (error) { - console.log(error) + return { data: null, error } } } -async function getLocationService(id: Number) { - const newState = { ...initialState }; - const url = `${GET_LOCATION_URI}/${id}` +async function getLocationService(id: number) { 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; - } + const data = await fetchLocation(id) + return { data, error: null } } catch (error) { - throw(error) + throw error } } -async function getLocationTagsService(id: Number) { - const newState = { ...initialState }; - const url = `${GET_LOCATION_TAGS_URI}/${id}` +async function getLocationTagsService(id: number) { 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; - } + const data = await fetchLocationTags(id) + return { data, error: null } } catch (error) { - console.log(error) + return { data: null, error } } } async function createLocationService(data: FormData): Promise { - const newState: IHttpResponse = { data: null, error: null}; - try { - const response = await client({ method: 'POST', url: POST_CREATE_LOCATION, data: data, withCredentials: true}) - newState.data = response.data; - newState.status = response.status - return newState; - } catch (error) { - let err = error as AxiosError; - newState.error = err; - newState.status = err.status; - return newState; + try { + const responseData = await createLocation(data) + return { data: responseData, error: null } + } catch (error: any) { + return { + data: null, + error, + status: error.status + } } } async function getSearchLocationService(arg: GetSearchLocations): Promise { - const newState: IHttpResponse = { data: null, error: null}; - try { - const filter = arg.filter ? arg.filter : '' - const pageSize= arg.page_size ? arg.page_size : 12 - const page = arg.page ? arg.page : 1 - const response = await client({ - method: 'GET', - url: `${GET_SEARCH_LOCATIONS_URI}?name=${arg.name}&filter${filter}&limit=${pageSize}&offset=${page}` - }) - - newState.data = response.data; - newState.status = response.status; - return newState; - } catch(error) { - const err = error as AxiosError; - newState.error = err; - newState.status = err.status; - return newState; + const data = await searchLocations(arg) + return { data, error: null } + } catch(error: any) { + return { + data: null, + error, + status: error.status + } } } diff --git a/src/services/news.ts b/src/services/news.ts index 0f0b6e2..ea3d857 100755 --- a/src/services/news.ts +++ b/src/services/news.ts @@ -1,4 +1,4 @@ -import { AxiosError } from "axios"; +import { useQuery, useMutation, UseQueryOptions, UseMutationOptions } from "@tanstack/react-query"; import { GetRequestPagination, IHttpResponse } from "..//types/common"; import { client } from "./config"; import { GET_NEWS_EVENTS_URI, POST_NEWS_EVENTS_URI } from "../../src/constants/api"; @@ -7,21 +7,6 @@ interface GetNewsSevice extends GetRequestPagination { is_with_approval: number } -async function getNewsServices({ page, page_size, is_with_approval}: GetNewsSevice): Promise { - const newState: IHttpResponse = { data: null, error: null}; - try { - const response = await client({ method: 'GET', url: `${GET_NEWS_EVENTS_URI}?page=${page}&page_size=${page_size}&is_with_approval=${is_with_approval}`}); - newState.data = response.data; - newState.status = response.status; - return newState; - } catch (error) { - let err = error as AxiosError; - newState.error = err; - newState.status = err.status - throw(newState) - } -} - interface PostNewsServiceBody { title: string, url: string, @@ -29,18 +14,52 @@ interface PostNewsServiceBody { submitted_by: number } -async function postNewsService(req: PostNewsServiceBody): Promise { - const newState: IHttpResponse = { data: null, error: null} +// API Functions +const fetchNews = async ({ page, page_size, is_with_approval }: GetNewsSevice) => { + const response = await client({ + method: 'GET', + url: `${GET_NEWS_EVENTS_URI}?page=${page}&page_size=${page_size}&is_with_approval=${is_with_approval}` + }) + return response.data +} + +const postNews = async (req: PostNewsServiceBody) => { + const response = await client({ method: 'POST', url: POST_NEWS_EVENTS_URI, data: req, withCredentials: true }) + return response.data +} + +// React Query Hooks +export const useNews = (params: GetNewsSevice, options?: Omit, 'queryKey' | 'queryFn'>) => { + return useQuery({ + queryKey: ['news', params], + queryFn: () => fetchNews(params), + ...options + }) +} + +export const usePostNews = (options?: UseMutationOptions) => { + return useMutation({ + mutationFn: postNews, + ...options + }) +} + +// Legacy service functions for backward compatibility +async function getNewsServices({ page, page_size, is_with_approval}: GetNewsSevice): Promise { try { - const response = await client({ method: 'POST', url: POST_NEWS_EVENTS_URI, data: req, withCredentials: true}) - newState.data = response.data - newState.status = response.status - return newState - } catch (error) { - let err = error as AxiosError; - newState.error = err; - newState.status = err.status - throw(newState) + const data = await fetchNews({ page, page_size, is_with_approval }) + return { data, error: null } + } catch (error: any) { + throw { data: null, error, status: error.status } + } +} + +async function postNewsService(req: PostNewsServiceBody): Promise { + try { + const data = await postNews(req) + return { data, error: null } + } catch (error: any) { + throw { data: null, error, status: error.status } } } diff --git a/src/services/regions.ts b/src/services/regions.ts index 394bdb6..331b21c 100755 --- a/src/services/regions.ts +++ b/src/services/regions.ts @@ -1,41 +1,74 @@ +import { useQuery, UseQueryOptions } from "@tanstack/react-query"; import { client } from "./config"; import { GET_PROVINCES, GET_REGENCIES, GET_REGIONS } from "../constants/api"; import { IHttpResponse } from "src/types/common"; +// API Functions +const fetchRegions = async () => { + const response = await client({ method: 'GET', url: GET_REGIONS }) + return response.data +} + +const fetchProvinces = async () => { + const response = await client({ method: 'GET', url: GET_PROVINCES }) + return response.data +} + +const fetchRegencies = async () => { + const response = await client({ method: 'GET', url: GET_REGENCIES }) + return response.data +} + +// React Query Hooks +export const useRegions = (options?: Omit, 'queryKey' | 'queryFn'>) => { + return useQuery({ + queryKey: ['regions'], + queryFn: fetchRegions, + ...options + }) +} + +export const useProvinces = (options?: Omit, 'queryKey' | 'queryFn'>) => { + return useQuery({ + queryKey: ['provinces'], + queryFn: fetchProvinces, + ...options + }) +} + +export const useRegencies = (options?: Omit, 'queryKey' | 'queryFn'>) => { + return useQuery({ + queryKey: ['regencies'], + queryFn: fetchRegencies, + ...options + }) +} + +// Legacy service functions for backward compatibility async function getRegionsService(): Promise { - const newState: IHttpResponse = {data: null, error: null} try { - const response = await client({ method: 'GET', url: GET_REGIONS}) - newState.data = response.data; - return newState + const data = await fetchRegions() + return { data, error: null } } catch(err) { - newState.error = err - throw (newState) + throw { data: null, error: err } } } async function getProvincesService(): Promise { - const newState: IHttpResponse = { data: null, error: null} try { - const response = await client({ method: 'GET', url: GET_PROVINCES}) - newState.data = response.data; - return newState + const data = await fetchProvinces() + return { data, error: null } } catch(err) { - newState.error = err - throw (newState) + throw { data: null, error: err } } } async function getRegenciesService(): Promise { - const newState: IHttpResponse = { data: null, error: null}; try { - const response = await client({ method: 'GET', url: GET_REGENCIES}) - newState.data = response.data; - newState.status = response.status - return newState + const response = await client({ method: 'GET', url: GET_REGENCIES }) + return { data: response.data, error: null, status: response.status } } catch(err) { - newState.error = err - throw (newState) + throw { data: null, error: err } } } diff --git a/src/services/review.ts b/src/services/review.ts index f4747dd..62a83c4 100755 --- a/src/services/review.ts +++ b/src/services/review.ts @@ -1,14 +1,8 @@ -import { AxiosError } from "axios" +import { useQuery, useMutation, UseQueryOptions, UseMutationOptions } from "@tanstack/react-query"; import { client } from "./config"; import { GET_CURRENT_USER_REVIEW_LOCATION_URI, POST_REVIEW_LOCATION_URI } from "../constants/api"; import { IHttpResponse } from "src/types/common"; -const initialState: IHttpResponse = { - data: null, - error: AxiosError, - status: 0, -} - interface postReviewLocationReq { submitted_by: number, comments: string, @@ -18,31 +12,54 @@ interface postReviewLocationReq { location_id: number } +// API Functions +const postReview = async (req: postReviewLocationReq) => { + const response = await client({ method: 'POST', url: POST_REVIEW_LOCATION_URI, data: req, withCredentials: true }) + return response.data +} + +const fetchCurrentUserLocationReview = async (location_id: number) => { + const response = await client({ + method: 'GET', + url: `${GET_CURRENT_USER_REVIEW_LOCATION_URI}/${location_id}`, + withCredentials: true + }) + return response.data +} + +// React Query Hooks +export const usePostReview = (options?: UseMutationOptions) => { + return useMutation({ + mutationFn: postReview, + ...options + }) +} + +export const useCurrentUserLocationReview = (location_id: number, options?: Omit, 'queryKey' | 'queryFn'>) => { + return useQuery({ + queryKey: ['user', 'review', location_id], + queryFn: () => fetchCurrentUserLocationReview(location_id), + enabled: !!location_id, + ...options + }) +} + +// Legacy service functions for backward compatibility 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 + const data = await postReview(req) + return { data, error: null } } catch (error) { - newState.error = error - throw(error) + throw error } } async function getCurrentUserLocationReviewService(location_id: number): Promise { - const newState = { ...initialState }; try { - const response = await client({ method: 'GET', url: `${GET_CURRENT_USER_REVIEW_LOCATION_URI}/${location_id}`, withCredentials: true}) - newState.data = response.data - newState.error = null - return newState - } catch (err) { - let error = err as AxiosError; - newState.error = error - newState.status = error.response?.status; - throw(newState) + const data = await fetchCurrentUserLocationReview(location_id) + return { data, error: null } + } catch (error: any) { + throw { data: null, error, status: error.status } } } diff --git a/src/services/users.ts b/src/services/users.ts index 5f75ee3..135dac5 100755 --- a/src/services/users.ts +++ b/src/services/users.ts @@ -1,67 +1,94 @@ -import { AxiosError } from "axios"; +import { useQuery, useMutation, UseQueryOptions, UseMutationOptions } from "@tanstack/react-query"; import { DELETE_USER_AVATAR, GET_CURRENT_USER_STATS, PATCH_USER_AVATAR, PATCH_USER_INFO } from "../constants/api"; import { IHttpResponse } from "../types/common"; import { client } from "./config"; import { UserInfo } from "../../src/domains/User"; +// API Functions +const fetchUserStats = async () => { + const res = await client({ method: 'GET', url: GET_CURRENT_USER_STATS, withCredentials: true }) + return res.data +} +const patchUserAvatar = async (form: FormData) => { + const res = await client({ method: "PATCH", url: PATCH_USER_AVATAR, data: form, withCredentials: true }) + return res.data +} + +const patchUserInfo = async (data: UserInfo) => { + const res = await client({ method: 'PATCH', url: PATCH_USER_INFO, data, withCredentials: true }) + return res.data +} + +const deleteUserAvatar = async () => { + const res = await client({ method: 'DELETE', url: DELETE_USER_AVATAR, withCredentials: true }) + return res.data +} + +// React Query Hooks +export const useUserStats = (options?: Omit, 'queryKey' | 'queryFn'>) => { + return useQuery({ + queryKey: ['user', 'stats'], + queryFn: fetchUserStats, + ...options + }) +} + +export const usePatchUserAvatar = (options?: UseMutationOptions) => { + return useMutation({ + mutationFn: patchUserAvatar, + ...options + }) +} + +export const usePatchUserInfo = (options?: UseMutationOptions) => { + return useMutation({ + mutationFn: patchUserInfo, + ...options + }) +} + +export const useDeleteUserAvatar = (options?: UseMutationOptions) => { + return useMutation({ + mutationFn: deleteUserAvatar, + ...options + }) +} + +// Legacy service functions for backward compatibility async function getUserStatsService(): Promise { - const newState: IHttpResponse = { data: null, error: null }; try { - const res = await client({ method: 'GET', url: GET_CURRENT_USER_STATS, withCredentials: true}) - newState.data = res.data - newState.status = res.status - return newState - } catch(error) { - let err = error as AxiosError - newState.error = err - newState.status = err.status - throw(newState) + const data = await fetchUserStats() + return { data, error: null } + } catch(error: any) { + throw { data: null, error, status: error.status } } } async function patchUserAvatarService(form: FormData): Promise { - const newState: IHttpResponse = { data: null, error: null}; try { - const res = await client({ method: "PATCH", url: PATCH_USER_AVATAR, data: form, withCredentials: true}) - newState.data = res.data; - newState.status = res.status; - return newState; - } catch(error) { - let err = error as AxiosError; - newState.error = err - newState.status = err.status - throw(newState); + const data = await patchUserAvatar(form) + return { data, error: null } + } catch(error: any) { + throw { data: null, error, status: error.status } } } async function patchUserInfoService(data: UserInfo): Promise { - const newState: IHttpResponse = { data: null, error: null}; try { - const res = await client({ method: 'PATCH', url: PATCH_USER_INFO, data: data, withCredentials: true}) - newState.data = res.data; - newState.status = res.status; - return newState; - } catch(error) { - let err = error as AxiosError; - newState.error = err; - newState.status = err.status; - throw(newState); + const responseData = await patchUserInfo(data) + return { data: responseData, error: null } + } catch(error: any) { + throw { data: null, error, status: error.status } } } async function deleteUserAvatarService(): Promise { - const newState: IHttpResponse = { data: null, error: null}; try { - const res = await client({ method: 'DELETE', url: DELETE_USER_AVATAR, withCredentials: true}) - newState.data = res.data; - newState.status = res.status - return newState - } catch (error) { - let err = error as AxiosError; - newState.error = err; - newState.status = err.status; - throw(newState); + const data = await deleteUserAvatar() + return { data, error: null } + } catch (error: any) { + throw { data: null, error, status: error.status } } } diff --git a/src/utils/common.ts b/src/utils/common.ts index d7553b8..954df98 100755 --- a/src/utils/common.ts +++ b/src/utils/common.ts @@ -1,4 +1,6 @@ import { AxiosError } from "axios"; +import { clsx, type ClassValue } from "clsx" +import { twMerge } from "tailwind-merge" export function handleAxiosError(error: AxiosError) { return error.response?.data @@ -11,4 +13,8 @@ export function enumKeys(obj: O): export function isUrl(val: string): boolean { var urlPattern = /^https:\/\/[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/; return urlPattern.test(val); -} \ No newline at end of file +} + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} diff --git a/tests/example.spec.ts b/tests/example.spec.ts new file mode 100644 index 0000000..54a906a --- /dev/null +++ b/tests/example.spec.ts @@ -0,0 +1,18 @@ +import { test, expect } from '@playwright/test'; + +test('has title', async ({ page }) => { + await page.goto('https://playwright.dev/'); + + // Expect a title "to contain" a substring. + await expect(page).toHaveTitle(/Playwright/); +}); + +test('get started link', async ({ page }) => { + await page.goto('https://playwright.dev/'); + + // Click the get started link. + await page.getByRole('link', { name: 'Get started' }).click(); + + // Expects page to have a heading with the name of Installation. + await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible(); +});