diff --git a/admin/components/VideoPlayer/index.tsx b/admin/components/VideoPlayer/index.tsx new file mode 100644 index 0000000..117346d --- /dev/null +++ b/admin/components/VideoPlayer/index.tsx @@ -0,0 +1,45 @@ +import { useState } from "react"; +import Image from "next/image"; +interface VideoPlayerProps { + src: string; + poster: string; + classes: string; +} + +const VideoPlayer = ({ src, poster, classes }: VideoPlayerProps) => { + const [loaded, setLoaded] = useState(false); + + const handleVideoLoaded = () => { + setLoaded(true); + }; + + return ( +
+ {!loaded && ( + Video Thumbnail + )} + +
+ ); +}; + +export default VideoPlayer; diff --git a/admin/pages/dishes/add/index.tsx b/admin/pages/dishes/add/index.tsx index e1bfe8e..44f34aa 100644 --- a/admin/pages/dishes/add/index.tsx +++ b/admin/pages/dishes/add/index.tsx @@ -2,7 +2,7 @@ import Image from "next/image"; import supabase from "@/utils/supabase"; -import { FormEvent, useEffect, useState } from "react"; +import { useEffect, useState } from "react"; import { z } from "zod"; import { useRouter } from "next/navigation"; import { TagsInput } from "react-tag-input-component"; @@ -37,6 +37,17 @@ const AddDishPage = () => { previewName: string; }>; + const [thumbnailData, setThumbnail] = useState(null); + let thumbnail: string | null = null; + const [previewThumbnailData, setPreviewThumbnailData] = useState( + {} as { + previewType: string; + previewUrl: string; + previewName: string; + isDragging: boolean; + } + ); + const [videoData, setVideo] = useState(null); let video: string | null = null; const [previewVideoData, setPreviewVideoData] = useState( @@ -63,11 +74,19 @@ const AddDishPage = () => { images: isImagesChecked ? z.array(z.string().min(10)).min(1, { message: "Image is required" }) : z.null(), + video_thumbnail: isVideoChecked + ? z + .string({ + required_error: "Video thumbnail is required", + invalid_type_error: "Invalid video thumbnail", + }) + .min(10, { message: "Video thumbnail is required" }) + : z.null(), video: isVideoChecked ? z .string({ required_error: "Video is required", - invalid_type_error: "Video is required", + invalid_type_error: "Invalid video", }) .min(10, { message: "Video is required" }) : z.null(), @@ -94,6 +113,20 @@ const AddDishPage = () => { } } else if (videoData) { const currentDate = new Date(); + const { data: imgData, error: imgErr } = await supabase.storage + .from("dishes") + .upload( + currentDate.getTime() + "-" + thumbnailData.name, + thumbnailData + ); + + if (imgErr) throw imgErr; + + thumbnail = + process.env.NEXT_PUBLIC_SUPABASE_STORAGE_URL + + "/dishes/" + + imgData.path; + const { data: videoStore, error: videoErr } = await supabase.storage .from("dishes") .upload(currentDate.getTime() + "-" + videoData.name, videoData); @@ -113,6 +146,7 @@ const AddDishPage = () => { tags: tags, price: price, images: isVideoChecked ? null : images, + video_thumbnail: thumbnail, video: video, }); @@ -135,6 +169,7 @@ const AddDishPage = () => { price: price, description: description, images: isVideoChecked ? null : images, + video_thumbnail: thumbnail, video: video, tags: tags.map((tag) => tag.toLowerCase()), }); @@ -189,8 +224,12 @@ const AddDishPage = () => { setImagesData(files); }; - const handleFileUploading = (file: any) => { - setVideo(file); + const handleThumbnailUploading = (file: any) => { + setThumbnail(file); + }; + + const handleVideoUploading = (video: any) => { + setVideo(video); }; return ( @@ -539,14 +578,67 @@ const AddDishPage = () => { ) : ( -
+
+ + {!previewThumbnailData || !previewThumbnailData.previewUrl ? ( +
+
+ + + + + + + +

Click to upload

+

video thumbnail

+

PNG, JPG or JPEG

+
+
+ ) : ( +
+ thumbnail-image +
+ )} +
{!previewVideoData || !previewVideoData.previewUrl ? ( -
+
{ /> -

- Click to upload -

+

Click to upload

video for dish

Video MP4

@@ -589,7 +679,7 @@ const AddDishPage = () => { autoPlay loop muted - className="h-55 w-40 object-cover" + className="h-55 w-36 rounded-xl object-cover" key={previewVideoData.previewUrl} > { )}
{errors.find((error) => error.for === "images")?.message || - errors.find((error) => error.for === "video")?.message} + errors.find((error) => error.for === "video")?.message || + errors.find((error) => error.for === "video_thumbnail")?.message}
) : ( -
+
+ + {!previewThumbnailData || !previewThumbnailData.previewUrl ? ( +
+
+ + + + + + + +

Click to upload

+

video thumbnail

+

PNG, JPG or JPEG

+
+
+ ) : ( +
+ thumbnail-image +
+ )} +
{!previewVideoData || !previewVideoData.previewUrl ? ( -
+
{ /> -

- Click to upload -

+

Click to upload

video for dish

Video MP4

@@ -660,7 +775,7 @@ const EditDishPage = () => { autoPlay loop muted - className="h-55 w-40 object-cover" + className="h-55 w-36 rounded-xl object-cover" key={previewVideoData.previewUrl} > { const [restaurantId, setRestaurantId] = useState(0); @@ -38,7 +39,9 @@ const DishesPage = () => { const { data: dishes, error } = await supabase .from("dishes") - .select(`id, name, images, video, price, menus(*), categories(name)`) + .select( + `id, name, images, video, video_thumbnail, price, menus(*), categories(name)` + ) .range((page - 1) * pageSize, pageSize * page - 1) .in("menu_id", arrayOfIds); @@ -207,14 +210,11 @@ const DishesPage = () => {
{dish.video ? ( - + ) : ( { - const { name, description, image, video, price, menus } = item; + const { name, description, image, video, video_thumbnail, price, menus } = + item; return ( <>
{ data-wow-delay=".1s" >
- {video ? ( - + {video && video_thumbnail ? ( + ) : ( { + return ( +
+
+
+

+

+

+
+
+
+ ); +}; + +export default MenuDish; diff --git a/website/components/VideoPlayer/index.tsx b/website/components/VideoPlayer/index.tsx new file mode 100644 index 0000000..117346d --- /dev/null +++ b/website/components/VideoPlayer/index.tsx @@ -0,0 +1,45 @@ +import { useState } from "react"; +import Image from "next/image"; +interface VideoPlayerProps { + src: string; + poster: string; + classes: string; +} + +const VideoPlayer = ({ src, poster, classes }: VideoPlayerProps) => { + const [loaded, setLoaded] = useState(false); + + const handleVideoLoaded = () => { + setLoaded(true); + }; + + return ( +
+ {!loaded && ( + Video Thumbnail + )} + +
+ ); +}; + +export default VideoPlayer; diff --git a/website/pages/category/restaurant.tsx b/website/pages/category/restaurant.tsx index 194cdd7..80f5827 100644 --- a/website/pages/category/restaurant.tsx +++ b/website/pages/category/restaurant.tsx @@ -31,26 +31,13 @@ const Restaurant = ({ key={"may-like-" + item.id} className="animated-fade-y group relative h-full cursor-pointer" > - {item.video ? ( - - ) : ( - item-image - )} + item-image

{item.name}

@@ -60,7 +47,7 @@ const Restaurant = ({
{item.reviews > 0 ? ( -

+

{item.reviews}{" "} Reviews

diff --git a/website/pages/restaurants/[id]/index.tsx b/website/pages/restaurants/[id]/index.tsx index dd0b997..e8a5b06 100644 --- a/website/pages/restaurants/[id]/index.tsx +++ b/website/pages/restaurants/[id]/index.tsx @@ -14,6 +14,8 @@ import { InView } from "react-intersection-observer"; import { useRouter } from "next/router"; import RootLayout from "@/pages/layout"; import NotFound from "@/components/PageNotFound"; +import VideoPlayer from "@/components/VideoPlayer"; +import MenuDish from "@/components/SkeletonPlaceholders/MenuDish"; const RestaurantMenu = () => { const router = useRouter(); @@ -21,6 +23,7 @@ const RestaurantMenu = () => { const suffix = id?.toString().substring(id?.lastIndexOf("-") + 1); const [isRestaurantLoading, setIsRestaurantLoading] = useState(true); + const [isDishesLoading, setIsDishesLoading] = useState(true); const [restaurantData, setRestaurantData] = useState(null); const [menuData, setMenuData] = useState([]); @@ -59,7 +62,9 @@ const RestaurantMenu = () => { menusData.map(async (menu) => { const { data: dishData, error: dishError } = await supabase .from("dishes") - .select("id, name, description, price, images, video") + .select( + "id, name, description, price, images, video, video_thumbnail" + ) .eq("menu_id", menu.id); if (dishError) { @@ -76,6 +81,8 @@ const RestaurantMenu = () => { setMenuData(dishes); } catch (error) { console.error("Error fetching dishes data:", error); + } finally { + setIsDishesLoading(false); } } }; @@ -178,70 +185,75 @@ const RestaurantMenu = () => {

{item.name}

-
- {item.dishes.map((data: any) => ( -
- {data.video ? ( -
+ ) : ( +
+ {item.dishes.map((data: any) => ( +
+ {data.video ? ( + - - ) : ( - - {data.images.map((data: any) => ( -
- - menu-dish-image - + ) : ( + + {data.images.map((data: any) => ( +
+ + menu-dish-image + +
+ ))} +
+ )} +
+
+
+

+ {data.name} +

+

+ ₹{data.price} +

- ))} - - )} -
-
-
-

- {data.name} +

+ {data.description}

-

₹{data.price}

-

- {data.description} -

-
- ))} -
+ ))} +
+ )}
)} @@ -330,68 +342,71 @@ const RestaurantMenu = () => { {menuData.map((item: any) => item.dishes.length > 0 ? (
-
- {item.dishes.map((data: any) => ( -
- {data.video ? ( - - ) : ( - - {data.images.map((data: any) => ( -
- -
-
- menu-dish-image + {isDishesLoading ? ( +
+ +
+ ) : ( +
+ {item.dishes.map((data: any) => ( +
+ {data.video ? ( + + ) : ( + + {data.images.map((data: any) => ( +
+ +
+
+ menu-dish-image +
-
- -
- ))} - - )} -
-
-

{data.name}

-

- ₹{data.price} -

+ +
+ ))} + + )} +
+
+

{data.name}

+

+ ₹{data.price} +

+
+

{data.description}

-

{data.description}

-
- ))} -
+ ))} +
+ )}
) : ( "" diff --git a/website/types/card-item.ts b/website/types/card-item.ts index a4fa2f4..a97ca98 100644 --- a/website/types/card-item.ts +++ b/website/types/card-item.ts @@ -5,6 +5,7 @@ export type ItemProps = { tags?: string[]; image: string; video?: string; + video_thumbnail?: string; price: number; rating: number; menus: {