Skip to content

Commit

Permalink
added Image upload functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
ShivanshPlays committed Nov 8, 2024
1 parent 657540f commit 5b79606
Show file tree
Hide file tree
Showing 10 changed files with 222 additions and 16 deletions.
23 changes: 22 additions & 1 deletion alimento-nextjs/.env.example
Original file line number Diff line number Diff line change
@@ -1 +1,22 @@
DATABASE_URL="your mongodb url"
# Database URL for MongoDB connection
# Format: "mongodb+srv://<username>:<password>@<cluster-url>/<database-name>"
DATABASE_URL="your_mongodb_connection_string_here"

# SMTP Configuration for Email Services
# Replace with your SMTP provider's details if not using Gmail.
SMTP_HOST="smtp.gmail.com" # SMTP server host
SMTP_PORT=587 # SMTP server port (587 for TLS)
SMTP_SECURE=false # Use false for TLS/STARTTLS, true for SSL
SMTP_USER="[email protected]" # Email address to send from
SMTP_PASS="your_smtp_password_here" # SMTP password or app-specific password for Gmail

# NextAuth Configuration
# NEXTAUTH_URL: Set to your app's URL for production; use http://localhost:3000 for local development.
NEXTAUTH_URL="http://localhost:3000" # Base URL for NextAuth authentication
NEXTAUTH_SECRET="your_nextauth_secret_here" # Secret used to sign NextAuth tokens

# Cloudinary Configuration for Image Uploads
# Replace these with your Cloudinary account details for image uploads.
NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME="your_cloud_name_here" # Cloudinary cloud name
NEXT_PUBLIC_CLOUDINARY_CLOUD_PRESET="your_upload_preset" # Cloudinary upload preset

5 changes: 5 additions & 0 deletions alimento-nextjs/actions/dish/dishCREATE.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ export async function createDish({
category,
tags,
vendorId,
images,
}: {
name: string;
description?: string;
price: number;
category: Category;
tags: Tag[];
vendorId: string;
images: string[]
}): Promise<{ success: boolean; error?: string; data?: Dish }> {
try {
const newDish = await prismadb.dish.create({
Expand All @@ -27,6 +29,9 @@ export async function createDish({
category,
tags,
vendorId,
images: {
create: images.map(url => ({ url })), // Assuming you're passing URLs
},
},
});
return { success: true, data: newDish };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,10 @@ export const data = [
step: 'step 3',
title: 'select tags',
},
{
id: 4,
step: 'step 4',
title: 'add images',
},
];

25 changes: 25 additions & 0 deletions alimento-nextjs/components/multistepForm/pages/formPage4.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import ImageUpload from '@/components/ui/imageUpload';
import { useGlobalDish } from '@/context/dishFormContext';

const FormPage4 = () => {
const { imageUrl, handleImageChange, handleImageRemove } = useGlobalDish();

return (
<div className="h-3/4 flex mb-5 flex-col items-start gap-8 justify-start">
<h1 className="text-primary-marineBlue font-bold text-[1.6rem] md:text-4xl leading-9">
Add Images
</h1>
<h3 className="text-gray-400 mt-2 ">
Please add some images for your listing
<br />
</h3>
<ImageUpload
value={imageUrl.map(imageUrl => imageUrl)}
onChange={handleImageChange}
onRemove={handleImageRemove}
/>
</div>
);
};

export default FormPage4;
13 changes: 8 additions & 5 deletions alimento-nextjs/components/multistepForm/pages/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { Toaster } from 'react-hot-toast';
import FormPage1 from './formPage1';
import FormPage2 from './formPage2';
import TagSelector from './formPage3';
import FormPage3 from './formPage3';
import FormPage4 from './formPage4';

const Main = ({ VendorId }: { VendorId: string }) => {
const {
Expand Down Expand Up @@ -77,7 +79,8 @@ const Main = ({ VendorId }: { VendorId: string }) => {
<Toaster />
{currentStep === 1 && <FormPage1/>}
{currentStep === 2 && <FormPage2/>}
{currentStep === 3 && <TagSelector/>}
{currentStep === 3 && <FormPage3/>}
{currentStep === 4 && <FormPage4/>}
{!formCompleted && (
<footer className="md:block hidden w-full left-0 right-0 bottom-0">
<div className="flex">
Expand All @@ -94,7 +97,7 @@ const Main = ({ VendorId }: { VendorId: string }) => {
<div className="text-right text-sm mt-2">
<button
onClick={
currentStep === 3
currentStep === 4
? e => {
e.preventDefault();
submitListingForm(VendorId)
Expand All @@ -103,7 +106,7 @@ const Main = ({ VendorId }: { VendorId: string }) => {
}
className="bg-black text-gray-200 rounded-full p-3"
>
{currentStep === 3 ? 'Confirm' : 'Next Step'}
{currentStep === 4 ? 'Confirm' : 'Next Step'}
</button>
</div>
</div>
Expand All @@ -128,7 +131,7 @@ const Main = ({ VendorId }: { VendorId: string }) => {
<div className="text-right">
<button
onClick={
currentStep === 3
currentStep === 4
? e => {
e.preventDefault();
submitListingForm(VendorId)
Expand All @@ -137,7 +140,7 @@ const Main = ({ VendorId }: { VendorId: string }) => {
}
className="bg-black text-gray-200 rounded-full p-2"
>
{currentStep === 3 ? 'Confirm' : 'Next Step'}
{currentStep === 4 ? 'Confirm' : 'Next Step'}
</button>
</div>
</div>
Expand Down
89 changes: 89 additions & 0 deletions alimento-nextjs/components/ui/imageUpload.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
'use client';

import { useEffect, useState } from 'react';
import { ImagePlus, Trash } from 'lucide-react';
import Image from 'next/image';
import {
CldUploadWidget,
CloudinaryUploadWidgetResults,
} from 'next-cloudinary';

import { Button } from '@/components/ui/button';

interface ImageUploadProps {
disabled?: boolean;
onChange: (value: string) => void;
onRemove: (value: string) => void;
value: string[];
}

const ImageUpload: React.FC<ImageUploadProps> = ({
disabled,
onChange,
onRemove,
value,
}) => {
const [isMounted, setIsMounted] = useState(false);

useEffect(() => {
setIsMounted(true);
}, []);

const onUpload = (result: CloudinaryUploadWidgetResults) => {
if (typeof result.info !== 'string' && result.info?.secure_url) {
onChange(result.info.secure_url);
console.log(value);
}
};

if (!isMounted) return null;

return (
<div>
<div className="mb-4 flex items-center gap-4">
{/* iterate to display existing images */}
{value.map(url => (
<div
key={url}
className="relative w-[200px] h-[200px] rounded-md overflow-hidden"
>
<div className="z-10 absolute top-2 right-2">
<Button
type="button"
variant={'destructive'}
onClick={() => onRemove(url)}
size={'icon'}
>
<Trash className="h-4 w-4" />
</Button>
</div>
<Image fill className="object-cover" alt="Image" src={url} />
</div>
))}
</div>

{/* upload image button */}
<CldUploadWidget onSuccess={onUpload} uploadPreset={process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_PRESET}>
{({ open }) => {
const onClick = () => {
open();
};

return (
<Button
type="button"
disabled={disabled}
variant={'secondary'}
onClick={onClick}
>
<ImagePlus className="w-4 h-4 mr-2" />
Upload an Image
</Button>
);
}}
</CldUploadWidget>
</div>
);
};

export default ImageUpload;
40 changes: 39 additions & 1 deletion alimento-nextjs/context/dishFormContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,15 @@ interface GlobalContextType {
setValidDishCategory: (valid: boolean) => void;
validDishTags: boolean;
setValidDishTags: (valid: boolean) => void;
validImageUrls: boolean; // New state for image URL validation
setValidImageUrls: (valid: boolean) => void;


imageUrl: string[];
setImageUrl: (url: string[]) => void;

handleImageChange: (url: string) => void;
handleImageRemove: (url: string) => void;

checkedBox: boolean;
setCheckedBox: (checkedBox: boolean) => void;
Expand Down Expand Up @@ -71,11 +80,22 @@ export const GlobalDishProvider = ({ children }: { children: ReactNode }) => {
);
const [dishTags, setDishTags] = useState<Tag[]>([]);

const [imageUrl, setImageUrl] = useState<string[]>([]);

const handleImageChange = useCallback((url: string) => {
setImageUrl(prevUrls => [...prevUrls, url]); // Use the previous state
}, []);

const handleImageRemove = useCallback((url: string) => {
setImageUrl(prevUrls => prevUrls.filter(image => image !== url)); // Use the previous state
}, []);

const [validDishName, setValidDishName] = useState(false);
const [validDishPrice, setValidDishPrice] = useState(false);
const [validDishDescription, setValidDishDescription] = useState(false);
const [validDishCategory, setValidDishCategory] = useState(false);
const [validDishTags, setValidDishTags] = useState(false);
const [validImageUrls, setValidImageUrls] = useState(false);

const [checkedBox, setCheckedBox] = useState(false);
const [formCompleted, setFormCompleted] = useState(false);
Expand Down Expand Up @@ -124,6 +144,14 @@ export const GlobalDishProvider = ({ children }: { children: ReactNode }) => {
setValidDishTags(true);
}


if (imageUrl.length === 0) {
setValidImageUrls(false); // No image URLs provided
allValid = false;
} else {
setValidImageUrls(true); // At least one image URL is present
}

// If all fields are valid, proceed with submission
if (allValid) {
setFormCompleted(true);
Expand All @@ -140,7 +168,8 @@ export const GlobalDishProvider = ({ children }: { children: ReactNode }) => {
description: dishDescription,
category: dishCategory,
tags: dishTags,
vendorId:vendorId
vendorId:vendorId,
images:imageUrl
});

if (!response.data) {
Expand Down Expand Up @@ -177,6 +206,11 @@ export const GlobalDishProvider = ({ children }: { children: ReactNode }) => {
setDishCategory,
dishTags,
setDishTags,
imageUrl,
setImageUrl,
handleImageChange,
handleImageRemove,


validDishName,
setValidDishName,
Expand All @@ -189,6 +223,10 @@ export const GlobalDishProvider = ({ children }: { children: ReactNode }) => {
validDishTags,
setValidDishTags,

validImageUrls,
setValidImageUrls,


checkedBox,
setCheckedBox,
formCompleted,
Expand Down
13 changes: 11 additions & 2 deletions alimento-nextjs/next.config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
import type { NextConfig } from "next";
import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
/* config options here */
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'res.cloudinary.com',
port: '',
pathname: '/**',
},
]
},
};

export default nextConfig;
1 change: 1 addition & 0 deletions alimento-nextjs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"lucide-react": "^0.454.0",
"next": "15.0.2",
"next-auth": "^4.24.10",
"next-cloudinary": "^6.16.0",
"nodemailer": "^6.9.16",
"react": "^18.3.1",
"react-dom": "^18.3.1",
Expand Down
24 changes: 17 additions & 7 deletions alimento-nextjs/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -55,17 +55,27 @@ enum Tag {
}

model Dish {
id String @id @default(uuid()) @map("_id")
id String @id @default(uuid()) @map("_id")
name String
description String?
price Float
category Category
tags Tag[]
category Category
tags Tag[]
vendorId String
vendor Vendor @relation(fields: [vendorId], references: [id])
images Image[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
vendorId String
vendor Vendor @relation(fields: [vendorId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}

model Image {
id String @id @default(uuid()) @map("_id")
dishId String
dish Dish @relation(fields: [dishId], references: [id], onDelete: Cascade)
url String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}

0 comments on commit 5b79606

Please sign in to comment.