diff --git a/src/api/generated/ApiClient.ts b/src/api/generated/ApiClient.ts index 764ed79..08a0970 100644 --- a/src/api/generated/ApiClient.ts +++ b/src/api/generated/ApiClient.ts @@ -10,6 +10,7 @@ import { CalendarService } from './services/CalendarService'; import { CategoryService } from './services/CategoryService'; import { CommentService } from './services/CommentService'; import { CompetitionsService } from './services/CompetitionsService'; +import { RecommendationService } from './services/RecommendationService'; import { CourseService } from './services/CourseService'; import { CurhatService } from './services/CurhatService'; import { InfoService } from './services/InfoService'; @@ -18,11 +19,11 @@ import { MediaService } from './services/MediaService'; import { OpenGraphService } from './services/OpenGraphService'; import { PushService } from './services/PushService'; import { ReactionService } from './services/ReactionService'; -import { RecommendationService } from './services/RecommendationService'; import { TestimoniService } from './services/TestimoniService'; import { UnsubscribeService } from './services/UnsubscribeService'; import { UserFinderService } from './services/UserFinderService'; import { UserProfileService } from './services/UserProfileService'; + type HttpRequestConstructor = new (config: OpenAPIConfig) => BaseHttpRequest; export class ApiClient { public readonly auth: AuthService; @@ -30,6 +31,7 @@ export class ApiClient { public readonly category: CategoryService; public readonly comment: CommentService; public readonly competitions: CompetitionsService; + public readonly recommendation: RecommendationService; public readonly course: CourseService; public readonly curhat: CurhatService; public readonly info: InfoService; @@ -38,7 +40,6 @@ export class ApiClient { public readonly openGraph: OpenGraphService; public readonly push: PushService; public readonly reaction: ReactionService; - public readonly recommendation: RecommendationService; public readonly testimoni: TestimoniService; public readonly unsubscribe: UnsubscribeService; public readonly userFinder: UserFinderService; @@ -59,6 +60,7 @@ export class ApiClient { this.auth = new AuthService(this.request); this.calendar = new CalendarService(this.request); this.category = new CategoryService(this.request); + this.recommendation = new RecommendationService(this.request); this.comment = new CommentService(this.request); this.competitions = new CompetitionsService(this.request); this.course = new CourseService(this.request); @@ -69,7 +71,6 @@ export class ApiClient { this.openGraph = new OpenGraphService(this.request); this.push = new PushService(this.request); this.reaction = new ReactionService(this.request); - this.recommendation = new RecommendationService(this.request); this.testimoni = new TestimoniService(this.request); this.unsubscribe = new UnsubscribeService(this.request); this.userFinder = new UserFinderService(this.request); diff --git a/src/api/generated/index.ts b/src/api/generated/index.ts index f0fdf44..e375b57 100644 --- a/src/api/generated/index.ts +++ b/src/api/generated/index.ts @@ -22,6 +22,8 @@ export type { Comment } from './models/Comment'; export type { CommentWithReactions } from './models/CommentWithReactions'; export type { Competition } from './models/Competition'; export type { CompetitionCategories } from './models/CompetitionCategories'; +export type { Voucher } from './models/Voucher'; +export type { CoWorking } from './models/CoWorking'; export type { Course } from './models/Course'; export type { Error } from './models/Error'; export type { Info } from './models/Info'; diff --git a/src/api/generated/models/CoWorking.ts b/src/api/generated/models/CoWorking.ts new file mode 100644 index 0000000..7d51de7 --- /dev/null +++ b/src/api/generated/models/CoWorking.ts @@ -0,0 +1,13 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type CoWorking = { + title: string; + imageUrl: string; + location: string; + address: string; + mapUrl: string; + description: string | null; +}; + diff --git a/src/api/generated/models/LocationCategories.ts b/src/api/generated/models/LocationCategories.ts new file mode 100644 index 0000000..8bad5d9 --- /dev/null +++ b/src/api/generated/models/LocationCategories.ts @@ -0,0 +1,5 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type LocationCategories = Array<'Ganesha' | 'Jatinangor'>; diff --git a/src/api/generated/models/Voucher.ts b/src/api/generated/models/Voucher.ts new file mode 100644 index 0000000..866e594 --- /dev/null +++ b/src/api/generated/models/Voucher.ts @@ -0,0 +1,13 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type Voucher = { + title: string; + imageURL: string; + link: string| null; + startPeriod: string| null; + endPeriod: string| null; + description: string | null; +}; + diff --git a/src/api/generated/services/RecommendationService.ts b/src/api/generated/services/RecommendationService.ts index 7c25b25..17c1a79 100644 --- a/src/api/generated/services/RecommendationService.ts +++ b/src/api/generated/services/RecommendationService.ts @@ -2,221 +2,238 @@ /* istanbul ignore file */ /* tslint:disable */ /* eslint-disable */ +import type { Voucher } from '../models/Voucher'; +import type { CoWorking } from '../models/CoWorking'; import type { CancelablePromise } from '../core/CancelablePromise'; import type { BaseHttpRequest } from '../core/BaseHttpRequest'; export class RecommendationService { constructor(public readonly httpRequest: BaseHttpRequest) {} /** - * Create a new voucher recommendation - * @returns any Voucher recommendation created + * @returns any Created coworking * @throws ApiError */ - public postRecommendationVoucher({ + public createCoWorkingSpace({ requestBody, }: { - requestBody: { + requestBody?: { title: string; imageURL: string; - link: string | null; - startPeriod: string | null; - endPeriod: string | null; - description: string | null; + location: string; + address: string; + mapsURL: string; + description?: string | null; }, }): CancelablePromise<{ + coWorkingSpaceId: string; title: string; - imageURL: string; - link: string | null; - startPeriod: string | null; - endPeriod: string | null; + location: Array<'Ganesha' | 'Jatinangor'>; + address: string; + mapsURL: string; description: string | null; - id: string; + imageURL: Array<{ + coWorkinSpaceId: string; + mediaId: string; + order: number; + media: { + id: string; + creatorId: string; + name: string; + type: string; + url: string; + createdAt: string; + }; + }>; }> { return this.httpRequest.request({ method: 'POST', - url: '/api/recommendation/voucher', + url: '/api/recommendation/co-working-space', body: requestBody, mediaType: 'application/json', errors: { - 400: `Bad request: validation error`, - 500: `Something went wrong!`, + 400: `Bad request`, }, }); } /** - * Create a new co-working space recommendation - * @returns any Co-working space recommendation created + * @returns any Fetched list of coworkingspace * @throws ApiError */ - public postRecommendationCoWorkingSpace({ - requestBody, + public getCoWorkingSpaceList({ + filter, + offset, }: { - requestBody?: { - title: string; - imageURL: string; - location: 'Ganesha' | 'Jatinangor'; - address: string; - mapsURL: string; - description: string | null; - }, + /** + * is active or not + */ + filter?: string, + + offset?: number | null, }): CancelablePromise<{ - title: string; - imageURL: string; - location: 'Ganesha' | 'Jatinangor'; - address: string; - mapsURL: string; - description: string | null; - id: string; + vouchers: Array; }> { return this.httpRequest.request({ - method: 'POST', + method: 'GET', url: '/api/recommendation/co-working-space', - body: requestBody, - mediaType: 'application/json', + query: { + 'filter': filter, + 'offset': offset, + }, errors: { 400: `Bad request: validation error`, - 500: `Something went wrong!`, }, }); } /** - * Create a new review for a voucher recommendation - * @returns any Voucher review created + * @returns any Updated coworkingspace * @throws ApiError */ - public postVoucherReview({ - voucherId, + public updateCoWorkingSpace({ + id, requestBody, }: { - voucherId: string, + id: string, requestBody?: { - /** - * Rating for the voucher (1-5) - */ - rating: number; - /** - * Detailed review about the voucher - */ - review: string; + title?: string; + imageURL?: Array; + location?: string; + address?: string; + mapsURL?: string; + description?: string | null; }, }): CancelablePromise<{ - userId: string; - voucherId: string; - rating: number; - review: string; + coWorkingSpaceId: string; + title: string; + location: Array<'Ganesha' | 'Jatinangor'>; + address: string; + mapsURL: string; + description: string | null; + imageURL?: Array<{ + coWorkingSpaceId: string; + mediaId: string; + order: number; + media: { + id: string; + creatorId: string; + name: string; + type: string; + url: string; + createdAt: string; + }; + }>; }> { return this.httpRequest.request({ - method: 'POST', - url: '/api/recommendation/voucher/{voucherId}/review', + method: 'PUT', + url: '/api/recommendation/co-working-space/{id}', path: { - 'voucherId': voucherId, + 'id': id, }, body: requestBody, mediaType: 'application/json', errors: { - 400: `Bad request: validation error`, - 500: `Something went wrong!`, + 400: `Bad request`, + 404: `Competition not found`, }, }); } /** - * @returns any Co-working space review created + * @returns coworkingspace Successfully deleted comment * @throws ApiError */ - public createCoWorkingSpaceReview({ + public deleteCoWorkingSpace({ coWorkingSpaceId, - requestBody, }: { + /** + * Id of fetched/deleted coworkingspace + */ coWorkingSpaceId: string, - requestBody?: { - /** - * Rating for the voucher (1-5) - */ - rating: number; - /** - * Detailed review about the voucher - */ - review: string; - }, - }): CancelablePromise<{ - coWorkingSpaceId: string; - userId: string; - rating: number; - review: string; - }> { + }): CancelablePromise { return this.httpRequest.request({ - method: 'POST', - url: '/api/recommendation/co-working-space/{coWorkingSpaceId}/review', + method: 'DELETE', + url: '/api/recommendation/co-working-space/{coWorkingSpaceId}', path: { 'coWorkingSpaceId': coWorkingSpaceId, }, - body: requestBody, - mediaType: 'application/json', errors: { - 400: `Bad request: validation error`, - 500: `Something went wrong!`, + 400: `Bad request`, + 404: `Error`, + 500: `Internal server error`, }, }); } - /** - * Delete a review for a voucher recommendation - * @returns any Review deleted successfully + + /** + * @returns any Created voucher * @throws ApiError */ - public deleteVoucherReview({ - voucherId, - userId, + public createVoucher({ + requestBody, }: { - voucherId: string, - userId: string, + requestBody?: { + title: string; + imageURL: string; + link: string; + startPeriod?: string | null; + endPeriod?: string | null; + description?: string | null; + }, }): CancelablePromise<{ - /** - * Indicates whether the review was deleted successfully - */ - success: boolean; + voucherId: string; + title: string; + link: string; + startPeriod: string | null; + endPeriod: string | null; + description: string | null + imageURL: Array<{ + voucherId: string; + mediaId: string; + order: number; + media: { + id: string; + creatorId: string; + name: string; + type: string; + url: string; + createdAt: string; + }; + }>; }> { return this.httpRequest.request({ - method: 'DELETE', - url: '/api/recommendation/voucher/{voucherId}/review/{userId}', - path: { - 'voucherId': voucherId, - 'userId': userId, - }, + method: 'POST', + url: '/api/recommendation/voucher', + body: requestBody, + mediaType: 'application/json', errors: { - 400: `Bad request: validation error`, - 404: `Error`, - 500: `Something went wrong!`, + 400: `Bad request`, }, }); } /** - * Delete a review for a co-working space recommendation - * @returns any Review deleted successfully + * @returns any Fetched list of vouchers * @throws ApiError */ - public deleteCoWorkingSpaceReview({ - coWorkingSpaceId, - userId, + public getVoucherList({ + filter, + offset, }: { - coWorkingSpaceId: string, - userId: string, - }): CancelablePromise<{ /** - * Indicates whether the review was deleted successfully + * is active or not */ - success: boolean; + filter?: string, + + offset?: number | null, + }): CancelablePromise<{ + vouchers: Array; }> { return this.httpRequest.request({ - method: 'DELETE', - url: '/api/recommendation/co-working-space/{coWorkingSpaceId}/review/{userId}', - path: { - 'coWorkingSpaceId': coWorkingSpaceId, - 'userId': userId, + method: 'GET', + url: '/api/recommendation/voucher', + query: { + 'filter': filter, + 'offset': offset, }, errors: { 400: `Bad request: validation error`, - 404: `Error`, - 500: `Something went wrong!`, }, }); } -} +} \ No newline at end of file diff --git a/src/assets/icons/rekomendasi/docs.svg b/src/assets/icons/rekomendasi/docs.svg new file mode 100644 index 0000000..1813f9b --- /dev/null +++ b/src/assets/icons/rekomendasi/docs.svg @@ -0,0 +1,4 @@ + + + diff --git a/src/assets/icons/rekomendasi/home.svg b/src/assets/icons/rekomendasi/home.svg new file mode 100644 index 0000000..700242f --- /dev/null +++ b/src/assets/icons/rekomendasi/home.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/rekomendasi/marker.svg b/src/assets/icons/rekomendasi/marker.svg new file mode 100644 index 0000000..7ed986c --- /dev/null +++ b/src/assets/icons/rekomendasi/marker.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/components/custom-form/fields/FormTextField.tsx b/src/components/custom-form/fields/FormTextField.tsx index af6d034..fee1909 100644 --- a/src/components/custom-form/fields/FormTextField.tsx +++ b/src/components/custom-form/fields/FormTextField.tsx @@ -42,7 +42,7 @@ export default function FormTextField( diff --git a/src/components/custom-form/fields/MultiSelect.tsx b/src/components/custom-form/fields/MultiSelect.tsx index d650526..45c23f2 100644 --- a/src/components/custom-form/fields/MultiSelect.tsx +++ b/src/components/custom-form/fields/MultiSelect.tsx @@ -121,7 +121,7 @@ export default function MultiSelect( ...form.getValues(name), opt, // eslint-disable-next-line - ] as any); + ] as any); }} className="gap-2" key={idx} diff --git a/src/components/custom-form/fields/SingleSelect.tsx b/src/components/custom-form/fields/SingleSelect.tsx index 95db304..c8745db 100644 --- a/src/components/custom-form/fields/SingleSelect.tsx +++ b/src/components/custom-form/fields/SingleSelect.tsx @@ -79,7 +79,7 @@ export default function SingleSelect( value={opt.id} key={opt.id} // eslint-disable-next-line - onSelect={() => form.setValue(name, opt.id as any)} + onSelect={() => form.setValue(name, opt.id as any)} keywords={[opt.title]} className="cursor-pointer" > diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index d9218cd..62f09eb 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -28,6 +28,7 @@ import { Route as AppLeftNavbarSettingsSettingsItemImport } from './routes/_app/ import { Route as AppLeftNavbarHomeTestimoniImport } from './routes/_app/_left-navbar/home/testimoni' import { Route as AppLeftNavbarTimelineInfoIdIndexImport } from './routes/_app/_left-navbar/timeline/$infoId/index' import { Route as AppLeftNavbarHomeTestimoniIndexImport } from './routes/_app/_left-navbar/home/testimoni/index' +import { Route as AppLeftNavbarHomeRekomendasiIndexImport } from './routes/_app/_left-navbar/home/rekomendasi/index' import { Route as AppLeftNavbarHomeNimFinderIndexImport } from './routes/_app/_left-navbar/home/nim-finder/index' import { Route as AppLeftNavbarHomeDingdongIndexImport } from './routes/_app/_left-navbar/home/dingdong/index' import { Route as AppLeftNavbarHomeCurhatIndexImport } from './routes/_app/_left-navbar/home/curhat/index' @@ -137,6 +138,12 @@ const AppLeftNavbarHomeTestimoniIndexRoute = getParentRoute: () => AppLeftNavbarHomeTestimoniRoute, } as any) +const AppLeftNavbarHomeRekomendasiIndexRoute = + AppLeftNavbarHomeRekomendasiIndexImport.update({ + path: '/home/rekomendasi/', + getParentRoute: () => AppLeftNavbarRoute, + } as any) + const AppLeftNavbarHomeNimFinderIndexRoute = AppLeftNavbarHomeNimFinderIndexImport.update({ path: '/home/nim-finder/', @@ -339,6 +346,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AppLeftNavbarHomeNimFinderIndexImport parentRoute: typeof AppLeftNavbarImport } + '/_app/_left-navbar/home/rekomendasi/': { + id: '/_app/_left-navbar/home/rekomendasi/' + path: '/home/rekomendasi' + fullPath: '/home/rekomendasi' + preLoaderRoute: typeof AppLeftNavbarHomeRekomendasiIndexImport + parentRoute: typeof AppLeftNavbarImport + } '/_app/_left-navbar/home/testimoni/': { id: '/_app/_left-navbar/home/testimoni/' path: '/' @@ -441,6 +455,7 @@ export const routeTree = rootRoute.addChildren({ AppLeftNavbarHomeCurhatIndexRoute, AppLeftNavbarHomeDingdongIndexRoute, AppLeftNavbarHomeNimFinderIndexRoute, + AppLeftNavbarHomeRekomendasiIndexRoute, AppLeftNavbarTimelineInfoIdIndexRoute, AppLeftNavbarHomeRekomendasiRekomendasiIdIndexRoute, AppLeftNavbarHomeTestimoniTypeSemesterCourseIdIndexRoute, @@ -488,6 +503,7 @@ export const routeTree = rootRoute.addChildren({ "/_app/_left-navbar/home/curhat/", "/_app/_left-navbar/home/dingdong/", "/_app/_left-navbar/home/nim-finder/", + "/_app/_left-navbar/home/rekomendasi/", "/_app/_left-navbar/timeline/$infoId/", "/_app/_left-navbar/home/rekomendasi/$rekomendasiId/", "/_app/_left-navbar/home/testimoni/$type/$semester/$courseId/" @@ -569,6 +585,10 @@ export const routeTree = rootRoute.addChildren({ "filePath": "_app/_left-navbar/home/nim-finder/index.tsx", "parent": "/_app/_left-navbar" }, + "/_app/_left-navbar/home/rekomendasi/": { + "filePath": "_app/_left-navbar/home/rekomendasi/index.tsx", + "parent": "/_app/_left-navbar" + }, "/_app/_left-navbar/home/testimoni/": { "filePath": "_app/_left-navbar/home/testimoni/index.tsx", "parent": "/_app/_left-navbar/home/testimoni" diff --git a/src/routes/_app/_left-navbar/home/rekomendasi/-components/AddCoWorkingSpaceDialog.tsx b/src/routes/_app/_left-navbar/home/rekomendasi/-components/AddCoWorkingSpaceDialog.tsx new file mode 100644 index 0000000..79246f5 --- /dev/null +++ b/src/routes/_app/_left-navbar/home/rekomendasi/-components/AddCoWorkingSpaceDialog.tsx @@ -0,0 +1,108 @@ +import { useEffect } from 'react'; +import HouseIcon from '~/assets/icons/rekomendasi/home.svg'; +import ClockIcon from '~/assets/icons/clock.svg'; +import DocsIcon from '~/assets/icons/rekomendasi/docs.svg'; +import LinkIcon from '~/assets/icons/competition/link.svg'; +import MarkerIcon from '~/assets/icons/rekomendasi/marker.svg'; +import Attachment from '~/components/custom-form/fields/Attachment'; +import DesktopForm from '~/components/custom-form/DesktopForm'; +import DesktopTitleField from '~/components/custom-form/fields/DesktopTitleField'; +import FormTextField from '~/components/custom-form/fields/FormTextField'; +import SingleSelect from '~/components/custom-form/fields/SingleSelect'; +import useAddCoWorkingSpace from '../-useAddCoWorkingSpace'; + +type ComponentProps = { + isOpen: boolean; + setOpen: React.Dispatch>; + constraintRef: React.MutableRefObject; +}; + +export default function AddCoWorkingSpaceDialog( + props: Readonly, +) { + const { isOpen, setOpen, constraintRef } = props; + + const options = [ + { id: 'Ganesha', title: 'Ganesha' }, + { id: 'Jatinangor', title: 'Jatinangor' }, + ]; + + const { form, onSubmit, image, setImage } = useAddCoWorkingSpace({ + onSubmitSuccess: () => setOpen(false), + }); + + useEffect(() => { + if (!isOpen) { + form.reset(); + setImage(null); + } + }, [isOpen]); + + if (!isOpen) { + return null; + } + + return ( + + + + + + + + + + + + + + ); +} diff --git a/src/routes/_app/_left-navbar/home/rekomendasi/-components/AddCoWorkingSpaceDrawer.tsx b/src/routes/_app/_left-navbar/home/rekomendasi/-components/AddCoWorkingSpaceDrawer.tsx new file mode 100644 index 0000000..508e20d --- /dev/null +++ b/src/routes/_app/_left-navbar/home/rekomendasi/-components/AddCoWorkingSpaceDrawer.tsx @@ -0,0 +1,92 @@ +import { useEffect } from 'react'; +import BookIcon from '~/assets/icons/calendar/book.svg'; +import LinkIcon from '~/assets/icons/competition/link.svg'; +import MarkerIcon from '~/assets/icons/rekomendasi/marker.svg'; +import HouseIcon from '~/assets/icons/rekomendasi/home.svg'; +import Attachment from '~/components/custom-form/fields/Attachment'; +import FormTextField from '~/components/custom-form/fields/FormTextField'; +import MobileForm from '~/components/custom-form/MobileForm'; +import SingleSelect from '~/components/custom-form/fields/SingleSelect'; +import useAddCoWorkingSpace from '../-useAddCoWorkingSpace'; + +type ComponentProps = { + isOpen: boolean; + setOpen: React.Dispatch>; +}; + +export function AddCoWorkingSpaceDrawer(props: Readonly) { + const options = [ + { id: 'Ganesha', title: 'Ganesha' }, + { id: 'Jatinangor', title: 'Jatinangor' }, + ]; + + const { isOpen, setOpen } = props; + + const { form, onSubmit, image, setImage } = useAddCoWorkingSpace({ + onSubmitSuccess: () => setOpen(false), + }); + + useEffect(() => { + if (!isOpen) { + form.reset(); + setImage(null); + } + }, [isOpen]); + + return ( + + + + + + + + + + + + + + ); +} diff --git a/src/routes/_app/_left-navbar/home/rekomendasi/-components/AddVoucherDialog.tsx b/src/routes/_app/_left-navbar/home/rekomendasi/-components/AddVoucherDialog.tsx new file mode 100644 index 0000000..d5577f4 --- /dev/null +++ b/src/routes/_app/_left-navbar/home/rekomendasi/-components/AddVoucherDialog.tsx @@ -0,0 +1,95 @@ +import { useEffect } from 'react'; +import ClockIcon from '~/assets/icons/clock.svg'; +import BookIcon from '~/assets/icons/calendar/book.svg'; +import LinkIcon from '~/assets/icons/competition/link.svg'; +import Attachment from '~/components/custom-form/fields/Attachment'; +import DatePicker from '~/components/custom-form/fields/DatePicker'; +import DesktopForm from '~/components/custom-form/DesktopForm'; +import DesktopTitleField from '~/components/custom-form/fields/DesktopTitleField'; +import FormTextField from '~/components/custom-form/fields/FormTextField'; +import useAddVoucher from '../-useAddVoucher'; + +type ComponentProps = { + isOpen: boolean; + setOpen: React.Dispatch>; + constraintRef: React.MutableRefObject; +}; + +export default function AddVoucherDialog(props: Readonly) { + const { isOpen, setOpen, constraintRef } = props; + + const { form, onSubmit, image, setImage } = useAddVoucher({ + onSubmitSuccess: () => setOpen(false), + }); + + useEffect(() => { + if (!isOpen) { + form.reset(); + setImage(null); + } + }, [isOpen]); + + if (!isOpen) { + return null; + } + + return ( + + + + + + + + + + + + + ); +} diff --git a/src/routes/_app/_left-navbar/home/rekomendasi/-components/AddVoucherDrawer.tsx b/src/routes/_app/_left-navbar/home/rekomendasi/-components/AddVoucherDrawer.tsx new file mode 100644 index 0000000..bf476c8 --- /dev/null +++ b/src/routes/_app/_left-navbar/home/rekomendasi/-components/AddVoucherDrawer.tsx @@ -0,0 +1,82 @@ +import { useEffect } from 'react'; +import ClockIcon from '~/assets/icons/competition/clock.svg'; +import LinkIcon from '~/assets/icons/competition/link.svg'; +import BookIcon from '~/assets/icons/calendar/book.svg'; +import Attachment from '~/components/custom-form/fields/Attachment'; +import DatePicker from '~/components/custom-form/fields/DatePicker'; +import FormTextField from '~/components/custom-form/fields/FormTextField'; +import MobileForm from '~/components/custom-form/MobileForm'; +import useAddVoucher from '../-useAddVoucher'; + +type ComponentProps = { + isOpen: boolean; + setOpen: React.Dispatch>; +}; + +export function AddVoucherDrawer(props: Readonly) { + const { isOpen, setOpen } = props; + + const { form, onSubmit, image, setImage } = useAddVoucher({ + onSubmitSuccess: () => setOpen(false), + }); + + useEffect(() => { + if (!isOpen) { + form.reset(); + setImage(null); + } + }, [isOpen]); + + return ( + + + + + + + + + + + + + ); +} diff --git a/src/routes/_app/_left-navbar/home/rekomendasi/-constants.ts b/src/routes/_app/_left-navbar/home/rekomendasi/-constants.ts new file mode 100644 index 0000000..f758a2b --- /dev/null +++ b/src/routes/_app/_left-navbar/home/rekomendasi/-constants.ts @@ -0,0 +1,29 @@ +import { z } from 'zod'; +import { isGreater } from '~/lib/date'; + +export const VoucherSchema = z + .object({ + title: z.string().min(1), + imageURL: z.string(), + link: z.string().min(1), + periodeAwal: z.string().datetime(), + periodeAkhir: z.string().datetime(), + description: z.string(), + }) + .refine((data) => isGreater(data.periodeAkhir, data.periodeAwal), { + message: 'Voucher end date must be greater than the start date.', + path: ['voucherDeadline'], + }); + +export type VoucherSchemaType = z.infer; + +export const CoWorkingSpaceSchema = z.object({ + title: z.string().min(1), + imageURL: z.string(), + location: z.string().min(1), + address: z.string().min(1), + mapsURL: z.string().min(1), + description: z.string().min(1), +}); + +export type CoWorkingSpaceSchemaType = z.infer; diff --git a/src/routes/_app/_left-navbar/home/rekomendasi/-useAddCoWorkingSpace.ts b/src/routes/_app/_left-navbar/home/rekomendasi/-useAddCoWorkingSpace.ts new file mode 100644 index 0000000..25e124c --- /dev/null +++ b/src/routes/_app/_left-navbar/home/rekomendasi/-useAddCoWorkingSpace.ts @@ -0,0 +1,101 @@ +import { useState } from 'react'; +import { useForm } from 'react-hook-form'; +import { CoWorkingSpaceSchema, CoWorkingSpaceSchemaType } from './-constants'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { useMutation } from '@tanstack/react-query'; +import { api, queryClient } from '~/api/client'; +import toast from 'react-hot-toast'; +import { PresignedURL } from '~/api/generated'; +import { FileUpload } from '../../add-announcement'; + +const TOAST_ID = 'add-coworkingspace-toast'; + +type ComponentProps = { + onSubmitSuccess?: () => void; +}; + +export default function useAddCoWorkingSpace(props: Readonly) { + const { onSubmitSuccess } = props; + + const [image, setImage] = useState(null); + const [pendingUpload, setPendingUpload] = useState(null); + + const form = useForm({ + resolver: zodResolver(CoWorkingSpaceSchema), + defaultValues: { + title: '', + imageURL: '', + location: '', + address: '', + mapsURL: '', + description: '', + }, + }); + + const postCoWorkingSpace = useMutation({ + mutationFn: api.recommendation.createCoWorkingSpace.bind( + api.recommendation, + ), + onSuccess: () => { + toast.success('CoWorkingSpace Posted!', { id: TOAST_ID }); + queryClient.invalidateQueries({ queryKey: ['coworkingspace'] }); + setPendingUpload(''); + onSubmitSuccess?.(); + }, + onError: () => + toast.error('Failed to post CoWorkingSpace', { id: TOAST_ID }), + }); + + const postMediaUpload = useMutation({ + mutationFn: api.media.createPresignedUrl.bind(api.media), + }); + + const onSubmit = async (values: CoWorkingSpaceSchemaType) => { + toast.loading('Please wait...', { id: TOAST_ID }); + + try { + if (!pendingUpload && image) { + const presignedUrl: PresignedURL = await postMediaUpload.mutateAsync({ + requestBody: { + fileName: image.file.name.split('.')[0], + fileType: image.file.name.split('.').at(-1) ?? '', + }, + }); + await fetch(presignedUrl.presignedUrl, { + method: 'PUT', + body: image.file, + }).then(() => + setPendingUpload(() => { + postCoWorkingSpace.mutate({ + requestBody: { + title: values.title, + address: values.address, + mapsURL: values.mapsURL, + description: values.description, + location: values.location, + imageURL: presignedUrl.mediaUrl, + }, + }); + return presignedUrl.mediaUrl; + }), + ); + } else { + postCoWorkingSpace.mutate({ + requestBody: { + title: values.title, + address: values.address, + mapsURL: values.mapsURL, + description: values.description, + location: values.location, + imageURL: pendingUpload ? pendingUpload : '', + }, + }); + } + } catch (err) { + console.log(err); + toast.error('Failed to post CoWorkingSpace', { id: TOAST_ID }); + } + }; + + return { form, onSubmit, image, setImage }; +} diff --git a/src/routes/_app/_left-navbar/home/rekomendasi/-useAddVoucher.ts b/src/routes/_app/_left-navbar/home/rekomendasi/-useAddVoucher.ts new file mode 100644 index 0000000..893dcf3 --- /dev/null +++ b/src/routes/_app/_left-navbar/home/rekomendasi/-useAddVoucher.ts @@ -0,0 +1,98 @@ +import { useState } from 'react'; +import { useForm } from 'react-hook-form'; +import { VoucherSchema, VoucherSchemaType } from './-constants'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { useMutation } from '@tanstack/react-query'; +import { api, queryClient } from '~/api/client'; +import toast from 'react-hot-toast'; +import { PresignedURL } from '~/api/generated'; +import { FileUpload } from '../../add-announcement'; + +const TOAST_ID = 'add-voucher-toast'; + +type ComponentProps = { + onSubmitSuccess?: () => void; +}; + +export default function useVoucher(props: Readonly) { + const { onSubmitSuccess } = props; + + const [image, setImage] = useState(null); + const [pendingUpload, setPendingUpload] = useState(null); + + const form = useForm({ + resolver: zodResolver(VoucherSchema), + defaultValues: { + title: '', + imageURL: '', + link: '', + periodeAkhir: '', + periodeAwal: '', + description: '', + }, + }); + + const postVoucher = useMutation({ + mutationFn: api.recommendation.createVoucher.bind(api.recommendation), + onSuccess: () => { + toast.success('Voucher Posted!', { id: TOAST_ID }); + queryClient.invalidateQueries({ queryKey: ['voucher'] }); + setPendingUpload(''); + onSubmitSuccess?.(); + }, + onError: () => toast.error('Failed to post voucher', { id: TOAST_ID }), + }); + + const postMediaUpload = useMutation({ + mutationFn: api.media.createPresignedUrl.bind(api.media), + }); + + const onSubmit = async (values: VoucherSchemaType) => { + toast.loading('Please wait...', { id: TOAST_ID }); + + try { + if (!pendingUpload && image) { + const presignedUrl: PresignedURL = await postMediaUpload.mutateAsync({ + requestBody: { + fileName: image.file.name.split('.')[0], + fileType: image.file.name.split('.').at(-1) ?? '', + }, + }); + await fetch(presignedUrl.presignedUrl, { + method: 'PUT', + body: image.file, + }).then(() => + setPendingUpload(() => { + postVoucher.mutate({ + requestBody: { + title: values.title, + link: values.link, + endPeriod: values.periodeAkhir, + startPeriod: values.periodeAwal, + description: values.description, + imageURL: presignedUrl.mediaUrl, + }, + }); + return presignedUrl.mediaUrl; + }), + ); + } else { + postVoucher.mutate({ + requestBody: { + title: values.title, + link: values.link, + startPeriod: values.periodeAwal, + endPeriod: values.periodeAkhir, + description: values.description, + imageURL: pendingUpload ? pendingUpload : '', + }, + }); + } + } catch (err) { + console.log(err); + toast.error('Failed to post Voucher', { id: TOAST_ID }); + } + }; + + return { form, onSubmit, image, setImage }; +} diff --git a/src/routes/_app/_left-navbar/home/rekomendasi/-useLocationCategories.ts b/src/routes/_app/_left-navbar/home/rekomendasi/-useLocationCategories.ts new file mode 100644 index 0000000..cb72cb8 --- /dev/null +++ b/src/routes/_app/_left-navbar/home/rekomendasi/-useLocationCategories.ts @@ -0,0 +1,19 @@ +import { useQuery } from '@tanstack/react-query'; + +export function useLocationCategories() { + const query = useQuery({ + queryKey: ['location-categories'], + queryFn: () => + Promise.resolve( + [ + { id: 'Ganesha', name: 'Ganesha Campus' }, + { id: 'Jatinangor', name: 'Jatinangor Campus' }, + ].map((c) => ({ + id: c.id, + title: `${c.name}`, + })), + ), + }); + + return query.data ?? []; +} diff --git a/tsconfig.json b/tsconfig.json index 4453ae1..9072caa 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,12 +2,7 @@ "compilerOptions": { "target": "ES2020", "useDefineForClassFields": true, - "lib": [ - "ES2020", - "DOM", - "DOM.Iterable", - "WebWorker" - ], + "lib": ["ES2020", "DOM", "DOM.Iterable", "WebWorker"], "module": "ESNext", "skipLibCheck": true, /* Bundler mode */ @@ -23,18 +18,14 @@ "noUnusedParameters": false, "noFallthroughCasesInSwitch": true, "paths": { - "~/*": [ - "src/*" - ], + "~/*": ["src/*"] }, "baseUrl": "." }, - "include": [ - "src" - ], + "include": ["src"], "references": [ { "path": "./tsconfig.node.json" } ] -} \ No newline at end of file +}