Skip to content

Commit

Permalink
opening hours
Browse files Browse the repository at this point in the history
  • Loading branch information
Felix-Asante committed Feb 22, 2024
1 parent ecb31bb commit 704a168
Show file tree
Hide file tree
Showing 7 changed files with 310 additions and 1 deletion.
23 changes: 22 additions & 1 deletion src/actions/place.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@

import { apiConfig } from "@/lib/apiConfig";
import { apiHandler } from "@/lib/apiHandler";
import { CountResponse, ResponseMeta, SeverActionResponse } from "@/types";
import {
CountResponse,
OpeningHours,
ResponseMeta,
SeverActionResponse,
} from "@/types";
import { User } from "@/types/auth";
import { Place, PlaceService } from "@/types/place";
import { Ratings } from "@/types/ratings";
Expand Down Expand Up @@ -174,3 +179,19 @@ export async function getPlaceRatings(
return { error: getErrorMessage(error) };
}
}

export async function addPlaceOpeningHour(placeId: string, body: OpeningHours) {
try {
const endpoint = apiConfig.places.opening_hrs(placeId);
await apiHandler({
endpoint,
method: "POST",
body,
});
} catch (error) {
console.log(error);
return { error: getErrorMessage(error) };
} finally {
revalidateTag(Tags.place);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
"use client";
import { addPlaceOpeningHour } from "@/actions/place";
import TextField from "@/components/shared/input/TextField";
import HStack from "@/components/shared/layout/HStack";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import { DAYS_OF_WEEK } from "@/config/constants";
import { useServerAction } from "@/hooks/useServerAction";
import { OpeningHour as OpeningHourType, OpeningHours } from "@/types";
import { Place } from "@/types/place";
import { getErrorMessage } from "@/utils/helpers";
import { Button, Radio, RadioGroup } from "@nextui-org/react";
import { TrashIcon } from "lucide-react";
import React from "react";
import { Controller, useFieldArray, useForm } from "react-hook-form";
import { toast } from "sonner";

const enum OPENING_HOURS {
CLOSED = "closed",
OPEN_ALL_DAY = "openAllDay",
CUSTOM = "custom",
}

interface OpenHrs {
status: "closed" | "openAllDay" | "custom";
ranges: { from: string; to: string }[];
}

const transformHoursToFormValue = (value?: OpeningHourType) => {
if (!value || value?.openAllDay) return { status: "openAllDay" };

if (value?.closed) {
return { status: "closed" };
} else {
return {
status: "custom",
ranges: value.ranges ?? [],
};
}
};

function formatOpenHours(data: { [key: string]: OpenHrs }): OpeningHours {
const formattedHours = Object.keys(data).reduce((acc, day) => {
if (data[day].status === "closed") {
return { ...acc, [day]: { closed: true } };
} else if (data[day].status === "openAllDay") {
return { ...acc, [day]: { openAllDay: true } };
}

if (data[day].status === "custom" && data[day]?.ranges?.length === 0) {
return { ...acc, [day]: { openAllDay: true } };
}

return {
...acc,
[day]: {
ranges: data[day].ranges?.map((range) => {
if (range?.from && range?.to)
return { from: range?.from, to: range?.to };
}),
},
};
}, {});

return formattedHours as OpeningHours;
}

function DisplayStatus({ status }: { status: string }) {
if (status === "closed") {
return <div>Closed</div>;
} else if (status === "openAllDay") {
return <div>Opened all day</div>;
} else if (status === "custom") {
return <div>Custom</div>;
} else {
return null;
}
}

interface Props {
place: Place;
}
export default function PlaceOpeningHour({ place }: Props) {
const openingHour = place?.openingHours;

const form = useForm({
defaultValues: {
monday: transformHoursToFormValue(openingHour?.monday),

tuesday: transformHoursToFormValue(openingHour?.tuesday),

wednesday: transformHoursToFormValue(openingHour?.wednesday),
thursday: transformHoursToFormValue(openingHour?.thursday),

friday: transformHoursToFormValue(openingHour?.friday),

saturday: transformHoursToFormValue(openingHour?.saturday),

sunday: transformHoursToFormValue(openingHour?.sunday),
},
});

const [runCreateOpeningHour, { loading }] = useServerAction<
any,
typeof addPlaceOpeningHour
>(addPlaceOpeningHour);

const onSubmit = async (data: any) => {
try {
const payload = formatOpenHours(data);
console.log(payload);
const response = await runCreateOpeningHour(place.id, payload);
if (response?.error) {
toast.error(response?.error);
return;
}
toast.success("Opening hours updated successfully");
} catch (error) {
toast.error(getErrorMessage(error));
}
};
return (
<div>
<HStack className='justify-end mb-4'>
<Button
radius='none'
disableRipple
color='primary'
className='mt-6'
onClick={form.handleSubmit(onSubmit)}
isLoading={loading}
>
Save opening hours
</Button>
</HStack>
<div className='mt-5 p-3 border rounded-md'>
<Accordion type='multiple'>
{DAYS_OF_WEEK.map((day, index) => (
<OpeningHour
day={day?.toLowerCase()}
index={index}
key={day}
control={form.control}
watch={form.watch}
/>
))}
</Accordion>
</div>
</div>
);
}

interface OpeningHourProps {
day: string;
index: number;
control: any;
watch: (key: string) => string;
}
function OpeningHour(props: OpeningHourProps) {
const { index, day, control, watch } = props;
const { fields, append, remove } = useFieldArray({
name: `${day}.ranges`,
control,
});
const isCustomHour = watch(`${day}.status`) === OPENING_HOURS.CUSTOM;
return (
<AccordionItem
value={index?.toString()}
className='rounded-md shadow bg-white mb-4'
>
<AccordionTrigger className='border-b p-5 px-8'>
<HStack className='items-center justify-between w-full pr-3'>
<h4 className='font-semibold text-lg capitalize'>{day}</h4>
<DisplayStatus status={watch(`${day}.status`)} />
</HStack>
</AccordionTrigger>
<AccordionContent className='px-5 py-3'>
<Controller
name={`${day}.status`}
control={control}
render={({ field }) => (
<RadioGroup
{...field}
orientation='horizontal'
classNames={{
wrapper: "flex-row justify-between",
}}
// defaultValue='closed'
>
<Radio value='closed'>Closed</Radio>
<Radio value='openAllDay'>Opened all day</Radio>
<Radio value='custom'>Custom</Radio>
</RadioGroup>
)}
/>
{isCustomHour &&
fields.map((field, index) => (
<HStack key={field.id} className='items-center my-5'>
<div className='w-1/2'>
<TextField
name={`${day}.ranges.${index}.from`}
type='time'
control={control}
label='From'
placeholder='00:00'
labelPlacement='outside'
variant='bordered'
radius='sm'
/>
</div>
<div className='w-1/2'>
<TextField
name={`${day}.ranges.${index}.to`}
type='time'
control={control}
label='To'
placeholder='00:00'
labelPlacement='outside'
variant='bordered'
radius='sm'
/>
</div>
<Button
radius='sm'
isIconOnly
disableRipple
color='primary'
className='mt-6'
onClick={() => remove(index)}
>
<TrashIcon />
</Button>
</HStack>
))}
{isCustomHour && (
<HStack className='my-4 justify-center'>
<Button onClick={append} variant='light' color='primary'>
+ Add more
</Button>
</HStack>
)}
</AccordionContent>
</AccordionItem>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import ProductCategoriesSection from "./ProductCategoriesSection";
import PlaceServices from "./PlaceServices";
import PlaceBookings from "./PlaceBookings";
import PlaceReviews from "./PlaceReviews";
import PlaceOpeningHour from "./PlaceOpeningHour";

interface Props {
place: Place;
Expand Down Expand Up @@ -35,6 +36,9 @@ export default function PlaceContent({ place }: Props) {
<Tab className='p-4' key='Reviews' title='Reviews'>
<PlaceReviews place={place} />
</Tab>
<Tab className='p-4' key='openingHours' title='openingHours'>
<PlaceOpeningHour place={place} />
</Tab>
</Tabs>
</div>
</div>
Expand Down
10 changes: 10 additions & 0 deletions src/config/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,13 @@ export enum UserRoles {
PLACE_ADMIN = "Place Admin",
COURIER = "Rider",
}

export const DAYS_OF_WEEK = [
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
"Sunday",
];
1 change: 1 addition & 0 deletions src/lib/apiConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export const apiConfig = {
popular: () => `places/popular/locations`,
ratings: (placeId: string, query: Query) =>
`places/${placeId}/ratings${toQuery(query)}`,
opening_hrs: (placeId: string) => `places/${placeId}/openingHours`,
},
categories: {
create: () => `categories`,
Expand Down
22 changes: 22 additions & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,25 @@ export type Status =
| "rejected"
| "delivered"
| "cancelled";

export type OpeningHoursRange = {
from: string;
to: string;
};

export type OpeningHour = {
closed?: boolean;
openAllDay?: boolean;
ranges?: OpeningHoursRange[];
};

export type OpeningHours = {
exceptions: string[];
monday: OpeningHour;
tuesday: OpeningHour;
wednesday: OpeningHour;
thursday: OpeningHour;
friday: OpeningHour;
saturday: OpeningHour;
sunday: OpeningHour;
};
2 changes: 2 additions & 0 deletions src/types/place.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { OpeningHours } from ".";
import { Category, ProductCategory } from "./category";
import { CreatePlaceDto } from "./dtos/places";
import { Special } from "./specials";
Expand All @@ -14,6 +15,7 @@ export interface Place extends placeBaseType {
updatedAt: string;
category: Category;
visits: number;
openingHours: OpeningHours | null;
}

export interface PlaceService extends ProductCategory {
Expand Down

0 comments on commit 704a168

Please sign in to comment.