Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add dishes page with responsiveness and implement all 4 filters with beautiful carousal card #261

Merged
merged 1 commit into from
Nov 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 27 additions & 6 deletions alimento-nextjs/actions/dish/dishGETALL.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,44 @@
'use server';

import { DishWithImages } from '@/app/vendor/[vendorId]/page';
import prismadb from '@/lib/prismadb';
import { Dish, Category, Tag } from '@prisma/client';

export async function getAllDishes({
tags,
categories,
sort, // Accept the sort parameter (which can be "", "asc", or "desc")
query, // New parameter to handle search query
}: {
tags?: Tag[];
categories?: Category[];
}): Promise<{ success: boolean; error?: string; data?: Dish[] }> {
sort?: "" | "asc" | "desc"; // Updated to include empty string ("")
query?: string; // Added query parameter
}): Promise<{ success: boolean; error?: string; data?: DishWithImages[] }> {
try {
// Build the `where` clause conditionally based on the presence of tags and categories
const whereConditions: any = {
AND: [
// Only include the tags filter if the tags array is non-empty
...(tags && tags.length > 0 ? [{ tags: { hasSome: tags } }] : []),

// Only include the categories filter if the categories array is non-empty
...(categories && categories.length > 0 ? [{ category: { in: categories } }] : []),

// Apply the query filter if it's provided
...(query ? [{ OR: [
{ name: { contains: query, mode: 'insensitive' } },
{ description: { contains: query, mode: 'insensitive' } }
] }] : []),
],
};

const dishes = await prismadb.dish.findMany({
where: {
AND: [
tags ? { tags: { hasSome: tags } } : {},
categories ? { category: { in: categories } } : {},
],
where: whereConditions,
include: {
images: true, // Including images for the dishes
},
orderBy: sort === "" ? undefined : { price: sort }, // If sort is "", don't apply sorting
});

return { success: true, data: dishes };
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
'use client';

import React, { useEffect, useState } from 'react';
import { Image as ImageInterface } from '@prisma/client';
import {
Carousel,
CarouselContent,
CarouselItem,
} from '@/components/ui/carousel';
import Image from 'next/image';
import Autoplay from 'embla-carousel-autoplay';
import { Button } from '@/components/ui/button';
import { useSession } from 'next-auth/react';

interface DishCardProps {
id: string;
name: string;
price: number;
description: string;
images: ImageInterface[];
}

const DishCardFE: React.FC<DishCardProps> = ({
id,
name,
price,
description,
images,
}) => {
const session = useSession();
const customerId = session.data?.user.id;

// State to track if the Dish is bookmarked
const [isAlreadyBookmarked, setIsAlreadyBookmarked] = useState<boolean>(false);


return (
<div className="bg-white overflow-hidden shadow-lg rounded-lg">
<Carousel
className="h-48"
opts={{
align: 'start',
loop: true,
}}
plugins={[
Autoplay({
delay: 2000,
}),
]}
>
<CarouselContent className="flex gap-4">
{images.map((image, index) => (
<CarouselItem key={index}>
<Image
src={image.url} // Use the image URL from the database
alt={name}
width={1000}
height={1000} // Fill the parent container
// objectFit="cover" // Cover the area while maintaining aspect ratio
className="rounded-t-lg h-48" // Optional: Add styling for rounded corners
/>
</CarouselItem>
))}
</CarouselContent>
</Carousel>
<div className="p-4">
<h3 className="text-xl font-semibold mb-2">{name}</h3>
<p className="text-gray-600 mb-2">{description}</p>
<p className="text-lg font-bold mb-2">Price: ₹{price}</p>
<div className="flex justify-between">
<Button variant="outline" className="text-blue-600">
Add
</Button>
{customerId && session.data?.user.role === 'customer' && !isAlreadyBookmarked && (
<Button
variant="outline"
className="text-red-600"
>
Bookmark
</Button>
)}
{customerId && session.data?.user.role === 'customer' && isAlreadyBookmarked && (
<Button variant="outline" className="text-green-600">
Bookmarked
</Button>
)}
</div>
</div>
</div>
);
};

export default DishCardFE;
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import {
Select,
SelectTrigger,
SelectValue,
SelectContent,
SelectItem,
} from "@/components/ui/select";
import { Category } from "@prisma/client";
import React from "react";

interface CategorySelectorDishPageProps {
dishCategories: Category[];
setDishCategories: (Categorys: Category[]) => void;
disabled: boolean;
}

const CategorySelectorDishPage: React.FC<CategorySelectorDishPageProps> = ({
dishCategories,
setDishCategories,
disabled,
}) => {
const Categories = Object.values(Category); // Get Categorys from Prisma enum

// Handle selecting or deselecting a Category
const handleCategorySelect = (Category: Category) => {
if (disabled) return; // Prevent action if disabled
if (dishCategories.includes(Category)) {
// Remove Category if already selected
setDishCategories(dishCategories.filter((t) => t !== Category));
} else {
// Add Category if not already selected
setDishCategories([...dishCategories, Category]);
}
};

return (
<div className={disabled ? "opacity-50 pointer-events-none" : ""}>
<div className="mb-2">
<Select onValueChange={(value) => handleCategorySelect(value as Category)}>
<SelectTrigger className="w-64" disabled={disabled}>
<SelectValue placeholder="Select Categories" />
</SelectTrigger>
<SelectContent>
{Categories.map((Category) => (
<SelectItem key={Category} value={Category}>
<div className="flex items-center">
<input
type="checkbox"
checked={dishCategories.includes(Category)}
onChange={() => handleCategorySelect(Category)}
disabled={disabled}
className="mr-2"
/>
{Category}
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div>

{/* Display Selected Categorys */}
<div className="flex flex-wrap gap-2 mt-2">
{dishCategories.map((Category) => (
<span
key={Category}
className="bg-blue-200 text-blue-800 px-3 py-1 rounded-full flex items-center space-x-2"
>
<span>{Category}</span>
<button
type="button"
onClick={() => handleCategorySelect(Category)}
disabled={disabled}
className="text-sm text-blue-600"
>
×
</button>
</span>
))}
</div>
</div>
);
};

export default CategorySelectorDishPage;

150 changes: 150 additions & 0 deletions alimento-nextjs/app/(PublicRoutes)/dishes/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
"use client";

import React, { useEffect, useState } from "react";
import toast, { Toaster } from "react-hot-toast";
import DishCardFE from "./components/dishCardFE";
import { DishWithImages } from "@/app/vendor/[vendorId]/page";
import { getAllDishes } from "@/actions/dish/dishGETALL";
import { Category, Tag } from "@prisma/client";
import { Search } from "lucide-react";
import TagSelectorCreateDish from "@/components/vendor/tagSelectorCreateDishForm";
import CategorySelectorDishPage from "./components/selectMultipleCategories";

const FoodPage: React.FC = () => {
const [foodItems, setFoodItems] = useState<DishWithImages[]>([]);
const [loading, setLoading] = useState(true);
const [tags, setTags] = useState<Tag[]>([]); // State to manage selected tags
const [categories, setCategories] = useState<Category[]>([]);
const [sort, setSort] = useState<"" | "asc" | "desc">("");
const [query, setQuery] = useState("");

useEffect(() => {
const fetchData = async () => {
setLoading(true);
try {
// Example API call to fetch food data
const foodResponse = await getAllDishes({
query,
categories,
tags,
sort,
});
console.log(foodResponse.data, foodResponse.success);
if (!foodResponse || !foodResponse.data) {
toast.error("No data fetched from BE");
return;
}
setFoodItems(foodResponse.data);
} catch (error) {
console.error("Error fetching food data:", error);
} finally {
setLoading(false);
}
};

fetchData();
}, [query, sort, tags, categories]); // Added tags as a dependency to refetch when tags change

console.log(foodItems);

return (
<div className="bg-gray-50 text-gray-800">
<Toaster />
{/* Hero Section with Banner Image */}
<section
className="relative h-[60vh] bg-cover bg-center text-white"
style={{ backgroundImage: "url(/food.jpg)" }}
>
<div className="absolute inset-0 bg-black opacity-40"></div>
<div className={`relative bg-[url(/pngFood.png)] bg-contain z-10 flex flex-col items-center justify-center h-full text-center`}>
<h1 className="text-4xl font-bold mb-4">Find Your Favorite Food</h1>
<p className="text-lg mb-6">
Discover delicious dishes and explore a variety of food options.
</p>
</div>
</section>

{/* Custom Search Bar */}
<div
className="grid my-6 px-4"
>
<div className="max-w-7xl mx-auto grid grid-cols-1 md:grid-cols-2 gap-8">
{/* Search Input with Font Awesome Search Icon */}
<div className="w-full md:w-auto mb-2 flex-grow relative">
<input
type="text"
className="form-control w-full p-3 pl-12 border border-gray-300 rounded-md"
id="search-input"
name="query"
placeholder="Search"
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400" />
</div>

{/* Sort by Price Dropdown */}
<div className="mb-2">
<select
className="form-select p-3 border border-gray-300 rounded-md"
id="sort-by-price"
name="sort"
value={sort}
onChange={(e) => {
const value = e.target.value as "" | "asc" | "desc";
setSort(value); // Update the sort state
}}
>
<option value="asc">Low to High</option>
<option value="desc">High to Low</option>
</select>
</div>

<div className="mb-6">
<TagSelectorCreateDish
dishTags={tags}
setDishTags={setTags} // Update selected tags
disabled={loading} // Disable if data is being loaded
/>
</div>
<div className="mb-6">
<CategorySelectorDishPage
dishCategories={categories}
setDishCategories={setCategories} // Update selected categories
disabled={loading} // Disable if data is being loaded
/>
</div>
</div>
</div>

{/* Food Items Section */}
<section className="py-16 bg-white">
<h2 className="text-2xl font-bold text-center mb-12">Available Food</h2>

<div
id="food-items"
className="max-w-7xl mx-auto grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8"
>
{loading ? (
<div className="loading flex justify-center items-center h-48">
<div className="spinner border-4 border-t-4 border-blue-600 rounded-full w-10 h-10 animate-spin"></div>
</div>
) : (
foodItems.map((food) => (
<DishCardFE
id={food.id}
key={food.id}
name={food.name}
price={food.price}
description={food.description}
images={food.images}
/>
))
)}
</div>
</section>
</div>
);
};

export default FoodPage;