From 167030a820044ba12e3419ca02178199781e473a Mon Sep 17 00:00:00 2001 From: Shivansh Date: Sat, 9 Nov 2024 06:49:12 +0530 Subject: [PATCH 1/3] Add mapWithPinDrop input component and it's lazy loader component and modify schema and serveractions to accomodate location functionality --- scruter-nextjs/actions/seller/listing.tsx | 14 ++- .../Maps/PinDropInput/LazyMapWithPin.tsx | 26 +++++ .../Maps/PinDropInput/mapWithPin.tsx | 25 ++++ .../Maps/PinDropInput/mapWithPinComp.tsx | 110 ++++++++++++++++++ scruter-nextjs/package-lock.json | 51 ++++++++ scruter-nextjs/package.json | 3 + scruter-nextjs/prisma/schema.prisma | 92 ++++++++------- 7 files changed, 272 insertions(+), 49 deletions(-) create mode 100644 scruter-nextjs/components/Maps/PinDropInput/LazyMapWithPin.tsx create mode 100644 scruter-nextjs/components/Maps/PinDropInput/mapWithPin.tsx create mode 100644 scruter-nextjs/components/Maps/PinDropInput/mapWithPinComp.tsx diff --git a/scruter-nextjs/actions/seller/listing.tsx b/scruter-nextjs/actions/seller/listing.tsx index 918303dd..016a6397 100644 --- a/scruter-nextjs/actions/seller/listing.tsx +++ b/scruter-nextjs/actions/seller/listing.tsx @@ -11,7 +11,7 @@ export async function PostListing({ listingData, }: { sellerId: string; - listingData: Pick & { + listingData: Pick & { images: string[]; }; }): Promise<{ success: boolean; error?: string; data?: Listing }> { @@ -23,7 +23,11 @@ export async function PostListing({ !listingData.description || !listingData.price || !listingData.images || // Check for images array - listingData.images.length === 0 // Ensure images array is not empty + listingData.images.length === 0 ||// Ensure images array is not empty + !listingData.listingLat || + !listingData.listingLng + + ) { return { success: false, error: 'All entries are required!' }; } @@ -54,7 +58,7 @@ export async function UpdateListing({ }: { sellerId: string; listingId: string; - listingData: Pick & { + listingData: Pick & { images: string[]; }; }): Promise<{ success: boolean; error?: string; data?: Listing }> { @@ -64,7 +68,9 @@ export async function UpdateListing({ !listingData.description || !listingData.price || !listingData.images || // Check for images array - listingData.images.length === 0 // Ensure images array is not empty + listingData.images.length === 0 ||// Ensure images array is not empty + !listingData.listingLat || + !listingData.listingLng ) { return { success: false, error: 'All entries are required!' }; } diff --git a/scruter-nextjs/components/Maps/PinDropInput/LazyMapWithPin.tsx b/scruter-nextjs/components/Maps/PinDropInput/LazyMapWithPin.tsx new file mode 100644 index 00000000..a39397d1 --- /dev/null +++ b/scruter-nextjs/components/Maps/PinDropInput/LazyMapWithPin.tsx @@ -0,0 +1,26 @@ +"use client"; + +// import { MapWithPinProps } from "@/components/Maps/mapComponent"; +import dynamic from "next/dynamic"; + +export interface LazyMapProps{ + latitude:number, + longitude:number, + setLatitude:(latitude:number)=>void, + setLongitude:(longitude:number)=>void + +} + +const LazyMapWithPin = dynamic(() => import("@/components/Maps/PinDropInput/mapWithPin"), { + ssr: false, + loading: () =>

Loading...

, +}); + +export default function LazyMap({latitude,longitude,setLatitude,setLongitude}:LazyMapProps) { + return ( +
+ +
+ ); +} + diff --git a/scruter-nextjs/components/Maps/PinDropInput/mapWithPin.tsx b/scruter-nextjs/components/Maps/PinDropInput/mapWithPin.tsx new file mode 100644 index 00000000..7c1972de --- /dev/null +++ b/scruter-nextjs/components/Maps/PinDropInput/mapWithPin.tsx @@ -0,0 +1,25 @@ +"use client"; + +import { useCallback, useEffect } from 'react'; +import MapWithPin from './mapWithPinComp'; +import { LazyMapProps } from './LazyMapWithPin'; + +const StorePage = ({latitude,longitude,setLatitude,setLongitude}:LazyMapProps) => { + + const handleLocationSelect = useCallback((lat: number, lng: number) => { + setLatitude(lat); + setLongitude(lng); + },[setLatitude,setLongitude]); + useEffect(()=>{ + handleLocationSelect(latitude,longitude) + },[latitude,longitude,handleLocationSelect]) + + return ( +
+ +
+ ); +}; + +export default StorePage; + diff --git a/scruter-nextjs/components/Maps/PinDropInput/mapWithPinComp.tsx b/scruter-nextjs/components/Maps/PinDropInput/mapWithPinComp.tsx new file mode 100644 index 00000000..acf2e465 --- /dev/null +++ b/scruter-nextjs/components/Maps/PinDropInput/mapWithPinComp.tsx @@ -0,0 +1,110 @@ +import { useEffect, useState } from "react"; +import { MapContainer, TileLayer, Marker, useMap, Popup } from "react-leaflet"; +import L, { LatLngExpression } from "leaflet"; + +export type MapWithPinProps = { + // centerr: LatLng; + latitude:number, + longitude:number, + onLocationSelect: (lat: number, lng: number) => void; +}; + + + +const MapWithPin: React.FC = ({ + // centerr, + latitude, + longitude, + onLocationSelect, +}) => { + const defaultCenter: LatLngExpression = [latitude,longitude ]; + const [position, setPosition] = useState(defaultCenter); + + // Custom marker icon fix + useEffect(() => { + delete (L.Icon.Default.prototype as L.IconDefault)._getIconUrl; + L.Icon.Default.mergeOptions({ + iconRetinaUrl: + "https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon-2x.png", + iconUrl: + "https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon.png", + shadowUrl: + "https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-shadow.png", + }); + }, []); + + // Detect user location using Geolocation API + // useEffect(() => { + // if (navigator.geolocation) { + // navigator.geolocation.getCurrentPosition( + // (position) => { + // const { latitude, longitude } = position.coords; + // setPosition([latitude, longitude]); + // onLocationSelect(latitude, longitude); + // }, + // (error) => { + // console.error("Geolocation error:", error); + // // setPosition(centerr); + // setPosition(defaultCenter) // Fallback to default location + // } + // ); + // } + // }, [onLocationSelect]); + + return ( + + + {position && ( + + )} + + ); +}; + +type DraggableMarkerProps = { + position: LatLngExpression; + onDragEnd: (pos: LatLngExpression) => void; + onLocationSelect: (lat: number, lng: number) => void; +}; + +const DraggableMarker: React.FC = ({ + position, + // onDragEnd, + onLocationSelect, +}) => { + const [draggablePosition, setDraggablePosition] = + useState(position); + const map = useMap(); + + const handleDragEnd = (event: L.DragEndEvent) => { + const marker = event.target as L.Marker; + const { lat, lng } = marker.getLatLng(); + setDraggablePosition([lat, lng]); + onLocationSelect(lat, lng); + map.setView([lat, lng]); // Re-center the map + }; + + return ( + + Your Store Location + + ); +}; + +export default MapWithPin; diff --git a/scruter-nextjs/package-lock.json b/scruter-nextjs/package-lock.json index c890f6c2..1cd97149 100644 --- a/scruter-nextjs/package-lock.json +++ b/scruter-nextjs/package-lock.json @@ -28,6 +28,7 @@ "embla-carousel-react": "^8.3.1", "framer-motion": "^11.11.11", "input-otp": "^1.2.5", + "leaflet": "^1.9.4", "lucide-react": "^0.453.0", "next": "15.0.1", "next-auth": "^4.24.10", @@ -40,12 +41,14 @@ "react-hook-form": "^7.53.1", "react-hot-toast": "^2.4.1", "react-icons": "^5.3.0", + "react-leaflet": "^4.2.1", "tailwind-merge": "^2.5.4", "tailwindcss-animate": "^1.0.7", "zod": "^3.23.8" }, "devDependencies": { "@eslint/js": "^9.14.0", + "@types/leaflet": "^1.9.14", "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", @@ -1413,6 +1416,17 @@ "integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==", "license": "MIT" }, + "node_modules/@react-leaflet/core": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@react-leaflet/core/-/core-2.1.0.tgz", + "integrity": "sha512-Qk7Pfu8BSarKGqILj4x7bCSZ1pjuAPZ+qmRwH5S7mDS91VSbVVsJSrW4qA+GPrro8t69gFYVMWb1Zc4yFmPiVg==", + "license": "Hippocratic-2.1", + "peerDependencies": { + "leaflet": "^1.9.0", + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -1474,6 +1488,13 @@ "optional": true, "peer": true }, + "node_modules/@types/geojson": { + "version": "7946.0.14", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.14.tgz", + "integrity": "sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", @@ -1481,6 +1502,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/leaflet": { + "version": "1.9.14", + "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.14.tgz", + "integrity": "sha512-sx2q6MDJaajwhKeVgPSvqXd8rhNJSTA3tMidQGduZn9S6WBYxDkCpSpV5xXEmSg7Cgdk/5vJGhVF1kMYLzauBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, "node_modules/@types/node": { "version": "20.17.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.1.tgz", @@ -4546,6 +4577,12 @@ "node": ">=0.10" } }, + "node_modules/leaflet": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", + "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", + "license": "BSD-2-Clause" + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -5621,6 +5658,20 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, + "node_modules/react-leaflet": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-4.2.1.tgz", + "integrity": "sha512-p9chkvhcKrWn/H/1FFeVSqLdReGwn2qmiobOQGO3BifX+/vV/39qhY8dGqbdcPh1e6jxh/QHriLXr7a4eLFK4Q==", + "license": "Hippocratic-2.1", + "dependencies": { + "@react-leaflet/core": "^2.1.0" + }, + "peerDependencies": { + "leaflet": "^1.9.0", + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, "node_modules/react-remove-scroll": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.0.tgz", diff --git a/scruter-nextjs/package.json b/scruter-nextjs/package.json index ab432d21..ff630f1a 100644 --- a/scruter-nextjs/package.json +++ b/scruter-nextjs/package.json @@ -32,6 +32,7 @@ "embla-carousel-react": "^8.3.1", "framer-motion": "^11.11.11", "input-otp": "^1.2.5", + "leaflet": "^1.9.4", "lucide-react": "^0.453.0", "next": "15.0.1", "next-auth": "^4.24.10", @@ -44,12 +45,14 @@ "react-hook-form": "^7.53.1", "react-hot-toast": "^2.4.1", "react-icons": "^5.3.0", + "react-leaflet": "^4.2.1", "tailwind-merge": "^2.5.4", "tailwindcss-animate": "^1.0.7", "zod": "^3.23.8" }, "devDependencies": { "@eslint/js": "^9.14.0", + "@types/leaflet": "^1.9.14", "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", diff --git a/scruter-nextjs/prisma/schema.prisma b/scruter-nextjs/prisma/schema.prisma index 6471decb..ede3e5b4 100644 --- a/scruter-nextjs/prisma/schema.prisma +++ b/scruter-nextjs/prisma/schema.prisma @@ -14,27 +14,27 @@ datasource db { } model User { - id String @id @default(uuid()) @map("_id") - name String - email String @unique - otp String? - role String @default("user") - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + id String @id @default(uuid()) @map("_id") + name String + email String @unique + otp String? + role String @default("user") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt // Many-to-many relationship with Listing via Bookmark - bookmarks Bookmark[] @relation("UserBookmarks") + bookmarks Bookmark[] @relation("UserBookmarks") } model Seller { - id String @id @default(uuid()) @map("_id") - name String - email String @unique - otp String? - role String @default("Seller") - Listings Listing[] @relation("SellerToListing") - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + id String @id @default(uuid()) @map("_id") + name String + email String @unique + otp String? + role String @default("Seller") + Listings Listing[] @relation("SellerToListing") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt } enum Category { @@ -44,59 +44,61 @@ enum Category { } model Listing { - id String @id @default(uuid()) @map("_id") + id String @id @default(uuid()) @map("_id") SellerId String - Seller Seller @relation("SellerToListing", fields: [SellerId], references: [id]) + Seller Seller @relation("SellerToListing", fields: [SellerId], references: [id]) name String price Int description String category Category images Image[] - isArchived Boolean @default(false) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + isArchived Boolean @default(false) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + listingLat Float? @default(28.61) + listingLng Float? @default(77.23) // Many-to-many relationship with User via Bookmark - bookmarks Bookmark[] @relation("UserBookmarks") + bookmarks Bookmark[] @relation("UserBookmarks") } model Image { - id String @id @default(uuid()) @map("_id") - listingId String - listing Listing @relation(fields: [listingId], references: [id], onDelete: Cascade) - url String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + id String @id @default(uuid()) @map("_id") + listingId String + listing Listing @relation(fields: [listingId], references: [id], onDelete: Cascade) + url String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt } model Bookmark { - id String @id @default(uuid()) @map("_id") - userId String - listingId String - user User @relation("UserBookmarks", fields: [userId], references: [id]) - listing Listing @relation("UserBookmarks", fields: [listingId], references: [id]) + id String @id @default(uuid()) @map("_id") + userId String + listingId String + user User @relation("UserBookmarks", fields: [userId], references: [id]) + listing Listing @relation("UserBookmarks", fields: [listingId], references: [id]) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt @@unique([userId, listingId]) // Ensures a user can only bookmark a listing once } - model Question { - id String @id @default(cuid()) @map("_id") + id String @id @default(cuid()) @map("_id") content String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - answers Answer[] @relation("QuestionAnswers") // Removed onDelete: Cascade from here - answered Boolean @default(false) + answers Answer[] @relation("QuestionAnswers") // Removed onDelete: Cascade from here + answered Boolean @default(false) } model Answer { - id String @id @default(cuid()) @map("_id") - content String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - questionId String - question Question @relation("QuestionAnswers", fields: [questionId], references: [id], onDelete: Cascade) // Moved onDelete: Cascade to this side + id String @id @default(cuid()) @map("_id") + content String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + questionId String + question Question @relation("QuestionAnswers", fields: [questionId], references: [id], onDelete: Cascade) // Moved onDelete: Cascade to this side } From 2aae3b50ef1c3254eb027cc810d6a61178681052 Mon Sep 17 00:00:00 2001 From: Shivansh Date: Sat, 9 Nov 2024 07:08:56 +0530 Subject: [PATCH 2/3] added location to first setup multistep --- .../app/seller/[sellerId]/components/main.tsx | 10 ++++--- .../[sellerId]/components/mapLocationForm.tsx | 27 +++++++++++++++++++ .../components/sidebarConstants.tsx | 5 ++++ .../context/GlobalListingProvider.tsx | 25 +++++++++++++++++ 4 files changed, 63 insertions(+), 4 deletions(-) create mode 100644 scruter-nextjs/app/seller/[sellerId]/components/mapLocationForm.tsx diff --git a/scruter-nextjs/app/seller/[sellerId]/components/main.tsx b/scruter-nextjs/app/seller/[sellerId]/components/main.tsx index d01f000f..9c4b693b 100644 --- a/scruter-nextjs/app/seller/[sellerId]/components/main.tsx +++ b/scruter-nextjs/app/seller/[sellerId]/components/main.tsx @@ -4,6 +4,7 @@ import ListingDetails from './listingDetails'; import SelectCategory from './selectCategory'; import { Toaster } from 'react-hot-toast'; import SelectImage from './selectImage'; +import MapLocationForm from './mapLocationForm'; const Main = ({ sellerId }: { sellerId: string }) => { const { @@ -83,6 +84,7 @@ const Main = ({ sellerId }: { sellerId: string }) => { {currentStep === 1 && } {currentStep === 2 && } {currentStep === 3 && } + {currentStep === 3 && } {!formCompleted && (