From 3e7adbca5c7364b4505918a6832ba2ba7fec7a79 Mon Sep 17 00:00:00 2001 From: shramana263 Date: Fri, 18 Apr 2025 22:30:16 +0530 Subject: [PATCH 001/110] feat: Add @heroicons/react dependency and refactor ThemeToggle import paths --- package-lock.json | 13 ++- package.json | 1 + src/ThemeToggle.tsx | 31 ------- .../authPage/structures/Sidebar.tsx | 3 +- src/layouts/GuestLayout.tsx | 2 +- src/layouts/Layout.tsx | 2 +- src/pages/Home.tsx | 89 ++++++++++++++++++- 7 files changed, 102 insertions(+), 39 deletions(-) delete mode 100644 src/ThemeToggle.tsx diff --git a/package-lock.json b/package-lock.json index 839e6c4..48008ee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@aws-sdk/lib-storage": "^3.758.0", "@aws-sdk/s3-request-presigner": "^3.758.0", "@fortawesome/fontawesome-svg-core": "^6.7.2", + "@heroicons/react": "^2.2.0", "@radix-ui/react-checkbox": "^1.1.4", "@radix-ui/react-slider": "^1.2.3", "@radix-ui/react-slot": "^1.1.2", @@ -48,8 +49,8 @@ "devDependencies": { "@eslint/js": "^9.21.0", "@types/node": "^22.13.10", - "@types/react": "^19.1.2", - "@types/react-dom": "^19.1.2", + "@types/react": "^19.0.10", + "@types/react-dom": "^19.0.4", "@vitejs/plugin-react": "^4.3.4", "eslint": "^9.21.0", "eslint-plugin-react-hooks": "^5.1.0", @@ -3768,6 +3769,14 @@ "node": ">=6" } }, + "node_modules/@heroicons/react": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.2.0.tgz", + "integrity": "sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ==", + "peerDependencies": { + "react": ">= 16 || ^19.0.0-rc" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", diff --git a/package.json b/package.json index 40c1c8a..4344806 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "@aws-sdk/lib-storage": "^3.758.0", "@aws-sdk/s3-request-presigner": "^3.758.0", "@fortawesome/fontawesome-svg-core": "^6.7.2", + "@heroicons/react": "^2.2.0", "@radix-ui/react-checkbox": "^1.1.4", "@radix-ui/react-slider": "^1.2.3", "@radix-ui/react-slot": "^1.1.2", diff --git a/src/ThemeToggle.tsx b/src/ThemeToggle.tsx deleted file mode 100644 index da0c6b1..0000000 --- a/src/ThemeToggle.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import React from 'react'; -import { useThemeContext } from './contexts/ThemeContext'; // Update the import path -import { useStateContext } from './contexts/StateContext'; - -const ThemeToggle: React.FC = () => { - const { theme, toggleTheme } = useThemeContext(); // Use the correct properties - const { user } = useStateContext(); - - return ( - <> - { - user ? - - : - - } - - ); -} - -export default ThemeToggle; \ No newline at end of file diff --git a/src/components/authPage/structures/Sidebar.tsx b/src/components/authPage/structures/Sidebar.tsx index 5e6f5ff..a39aaf7 100644 --- a/src/components/authPage/structures/Sidebar.tsx +++ b/src/components/authPage/structures/Sidebar.tsx @@ -1,4 +1,4 @@ -import ThemeToggle from "@/ThemeToggle"; + import { getPreSignedUrl } from "@/utils/aws/aws"; import { useEffect, useState } from "react"; import { Home, User, FileText, Share, Inbox, Archive } from "lucide-react"; @@ -6,6 +6,7 @@ import { useNavigate } from "react-router-dom"; import { auth, db } from "@/firebase"; import { doc, getDoc } from "firebase/firestore"; import {AiOutlineHeart} from "react-icons/ai"; +import ThemeToggle from "@/components/common/ThemeToggle"; interface SidebarProps { diff --git a/src/layouts/GuestLayout.tsx b/src/layouts/GuestLayout.tsx index f4bc68e..8eed272 100644 --- a/src/layouts/GuestLayout.tsx +++ b/src/layouts/GuestLayout.tsx @@ -1,4 +1,4 @@ -import ThemeToggle from '@/ThemeToggle'; +import ThemeToggle from '@/components/common/ThemeToggle'; import React, { useEffect, ReactNode } from 'react' diff --git a/src/layouts/Layout.tsx b/src/layouts/Layout.tsx index a8e07f1..a695614 100644 --- a/src/layouts/Layout.tsx +++ b/src/layouts/Layout.tsx @@ -1,4 +1,4 @@ -import ThemeToggle from '@/ThemeToggle'; +import ThemeToggle from '@/components/common/ThemeToggle'; import React, { ReactNode } from 'react'; interface LayoutProps { diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index 28f5c36..73740a5 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, JSX } from "react"; import { useNavigate } from "react-router-dom"; import { collection, query, orderBy, getDocs, Timestamp, doc, getDoc } from "firebase/firestore"; import { db, auth } from "../firebase"; @@ -13,6 +13,8 @@ import { motion } from "framer-motion"; import Sidebar from "../components/authPage/structures/Sidebar"; import Bottombar from "@/components/authPage/structures/Bottombar"; import { AiOutlineLoading3Quarters } from "react-icons/ai"; +import { Plus, BookOpen, Gift, Calendar, Bell } from 'lucide-react'; + type FilterType = "all" | "need" | "offer"; @@ -614,12 +616,13 @@ const Home: React.FC = () => { {/* Floating Action Button */} - + */} + @@ -634,3 +637,83 @@ const Home: React.FC = () => { }; export default Home; + + +export const FloatingActionMenu: React.FC = () => { + const [isOpen, setIsOpen] = useState(false); + const navigate= useNavigate(); + + const toggleMenu = () => { + setIsOpen(!isOpen); + }; + + // Define our menu options + const menuOptions = [ + { icon: , label: "Resource", id: "resource" }, + { icon: , label: "Promotion", id: "promotion" }, + { icon: , label: "Event", id: "event" }, + { icon: , label: "Local Updates", id: "update" } + ]; + + // Handler for button click to navigate with query param + const handleNavigation = (type: string) => { + navigate(`/post?type=${type}`); + setIsOpen(false); // optionally close menu on navigation + }; + + // // Calculate the bottom-right position for animation origin + // const originPosition = "bottom-20 right-5"; + + return ( +
+ {/* Backdrop overlay when modal is open */} + {isOpen && ( +
+ )} + + {/* Buttons - always rendering but only visible when open */} +
+
+ {menuOptions.map((option, index) => ( + + ))} +
+
+ + + {/* Plus button */} + +
+ ); +}; From cede7759caf4a5e7b8e886dd669398be9e487c5d Mon Sep 17 00:00:00 2001 From: The-Parthib Date: Fri, 18 Apr 2025 22:53:43 +0530 Subject: [PATCH 002/110] theme toggler --- src/components/common/ThemeToggle.tsx | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/components/common/ThemeToggle.tsx b/src/components/common/ThemeToggle.tsx index 82ce952..1a1add2 100644 --- a/src/components/common/ThemeToggle.tsx +++ b/src/components/common/ThemeToggle.tsx @@ -10,12 +10,19 @@ const ThemeToggle: React.FC = () => { <> { user ? - +
+ {theme === 'dark' ? 'Dark Mode' : 'Light Mode'} + +
: - : - - } - - ); -} - -export default ThemeToggle; \ No newline at end of file diff --git a/src/components/Forms/DefaultValues.ts b/src/components/Forms/DefaultValues.ts new file mode 100644 index 0000000..e2e2779 --- /dev/null +++ b/src/components/Forms/DefaultValues.ts @@ -0,0 +1,77 @@ +import { PostType } from './SchemaDef'; + +// Default values for resource form +export const defaultResourceValues = { + title: "", + category: "Medical", + customCategory: "", + description: "", + urgency: 1, + duration: "1 week", + locationType: "default" as const, + latitude: "", + longitude: "", + address: "", + visibilityRadius: 3, + resourceType: "need" as const, + isAnonymous: false, + photos: [] +}; + +// Default values for event form +export const defaultEventValues = { + title: "", + description: "", + eventType: "Cultural", + organizerDetails: "", + locationType: "default" as const, + latitude: "", + longitude: "", + address: "", + eventDate: "", + eventTime: "", + registrationRequired: false, + registrationLink: "", + visibilityRadius: 5, + duration: "1 day", + photos: [] +}; + +// Default values for promotion form +export const defaultPromotionValues = { + title: "", + description: "", + contactDetails: "", + locationType: "default" as const, + latitude: "", + longitude: "", + address: "", + visibilityRadius: 5, + duration: "1 week", + photos: [] +}; + +// Default values for update form +export const defaultUpdateValues = { + title: "", + description: "", + locationType: "default" as const, + latitude: "", + longitude: "", + address: "", + date: "", + visibilityRadius: 5, + duration: "1 week", + photos: [] +}; + +// Get default values based on post type +export const getDefaultValues = (postType: PostType) => { + switch (postType) { + case 'resource': return defaultResourceValues; + case 'event': return defaultEventValues; + case 'promotion': return defaultPromotionValues; + case 'update': return defaultUpdateValues; + default: return defaultResourceValues; + } +}; \ No newline at end of file diff --git a/src/components/Forms/FileUpload.tsx b/src/components/Forms/FileUpload.tsx new file mode 100644 index 0000000..9799561 --- /dev/null +++ b/src/components/Forms/FileUpload.tsx @@ -0,0 +1,65 @@ +import React from 'react'; +import { useDropzone, DropzoneOptions } from 'react-dropzone'; +import { Button } from "@/components/ui/button"; + +interface FileUploadProps { + files: File[]; + setFiles: React.Dispatch>; + acceptTypes?: { [key: string]: string[] }; + maxFiles?: number; +} + +const FileUpload: React.FC = ({ + files, + setFiles, + acceptTypes = { 'image/*': [] }, + maxFiles +}) => { + const { getRootProps, getInputProps } = useDropzone({ + accept: acceptTypes, + maxFiles, + onDrop: acceptedFiles => { + setFiles(prev => [...prev, ...acceptedFiles]); + } + }); + + return ( +
+
+ +

Drag & drop files here, or click to select files

+
+ + {files.length > 0 && ( +
+

Selected Files:

+
+ {files.map((file, index) => ( +
+ {`Preview + +
+ ))} +
+
+ )} +
+ ); +}; + +export default FileUpload; \ No newline at end of file diff --git a/src/components/Forms/LocationPicker.tsx b/src/components/Forms/LocationPicker.tsx new file mode 100644 index 0000000..39d123b --- /dev/null +++ b/src/components/Forms/LocationPicker.tsx @@ -0,0 +1,136 @@ +import React from 'react'; +import { UseFormReturn } from 'react-hook-form'; +import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'; +import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; +import { Label } from '@/components/ui/label'; +import { Input } from '@/components/ui/input'; +import { Button } from '@/components/ui/button'; +import { Slider } from '@/components/ui/slider'; +import { FaMapMarkerAlt } from 'react-icons/fa'; +import { toast } from 'react-toastify'; + +interface LocationPickerProps { + form: UseFormReturn; +} + +const LocationPicker: React.FC = ({ form }) => { + const getCurrentLocation = () => { + if (navigator.geolocation) { + navigator.geolocation.getCurrentPosition( + (position) => { + const { latitude, longitude } = position.coords; + form.setValue('locationType', 'custom'); + form.setValue('latitude', latitude.toString()); + form.setValue('longitude', longitude.toString()); + toast.success("Location detected successfully!"); + }, + (error) => { + console.error("Error getting location:", error); + toast.error("Unable to retrieve your location. Please enable location access."); + }, + { enableHighAccuracy: true, timeout: 10000, maximumAge: 0 } + ); + } else { + toast.error("Geolocation is not supported by your browser."); + } + }; + + return ( + <> + ( + + Location Type + + +
+ + +
+
+ + +
+
+
+ +
+ )} + /> + + {form.watch('locationType') === 'custom' && ( + <> +
+ ( + + Latitude + + + + + + )} + /> + ( + + Longitude + + + + + + )} + /> +
+ + + + )} + + ( + + Visibility Radius: {field.value} km + + field.onChange(value)} + className="py-4" + /> + + + Set how far your post can be seen from the location + + + + )} + /> + + ); +}; + +export default LocationPicker; \ No newline at end of file diff --git a/src/components/Forms/NewPostForm.tsx b/src/components/Forms/NewPostForm.tsx new file mode 100644 index 0000000..07efb4e --- /dev/null +++ b/src/components/Forms/NewPostForm.tsx @@ -0,0 +1,268 @@ +import React, { useState, useEffect } from 'react'; +import { useLocation, useNavigate } from 'react-router-dom'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { collection, addDoc, Timestamp } from 'firebase/firestore'; +import { db, auth } from "../../firebase"; +import { toast } from 'react-toastify'; +import { uploadFileToS3 } from '@/utils/aws/aws'; + +// Import schemas and default values +import { PostType, getSchemaForType } from './SchemaDef'; +import { getDefaultValues } from './DefaultValues'; + +// Import components +import { Button } from "@/components/ui/button"; +import { Form } from "@/components/ui/form"; +import { Card, CardContent } from "@/components/ui/card"; +import FormProgress from './components/FormProgress'; +import ResourceBasicInfo from './steps/ResourceBasicInfo'; +import ResourceDetails from './steps/ResourceDetails'; +import ResourceLocation from './steps/ResourceLocation'; +import ResourceUpload from './steps/ResourceUpload'; +import EventForm from './steps/EventForm'; +import PromotionForm from './steps/PromotionForm'; +import UpdateForm from './steps/UpdateForm'; + +const NewPostForm = () => { + const location = useLocation(); + const navigate = useNavigate(); + const [currentStep, setCurrentStep] = useState(1); + const [totalSteps, setTotalSteps] = useState(4); + const [postType, setPostType] = useState('resource'); + const [loading, setLoading] = useState(false); + const [uploadedFiles, setUploadedFiles] = useState([]); + const [uploadedUrls, setUploadedUrls] = useState([]); + + // Extract query parameters and set post type + useEffect(() => { + const queryParams = new URLSearchParams(location.search); + const typeParam = queryParams.get('type') as PostType; + if (typeParam && ['resource', 'event', 'promotion', 'update'].includes(typeParam)) { + setPostType(typeParam); + + // Set total steps based on post type + switch (typeParam) { + case 'resource': setTotalSteps(4); break; + case 'event': setTotalSteps(5); break; + case 'promotion': setTotalSteps(4); break; + case 'update': setTotalSteps(3); break; + } + } + }, [location]); + + // Initialize form with the right schema and default values + // Use a more generic typing approach to avoid type errors + const form = useForm({ + // @ts-ignore - The schema types are compatible at runtime, even if TypeScript doesn't see it + resolver: zodResolver(getSchemaForType(postType)), + defaultValues: getDefaultValues(postType), + }); + + // Update form when post type changes + useEffect(() => { + form.reset(getDefaultValues(postType)); + }, [postType, form]); + + // Update form values from query parameters + useEffect(() => { + const queryParams = new URLSearchParams(location.search); + const formValues: Record = {}; + + if (postType === 'resource') { + if (queryParams.get('resourceType')) formValues.resourceType = queryParams.get('resourceType'); + if (queryParams.get('urgency')) formValues.urgency = Number(queryParams.get('urgency')); + if (queryParams.get('duration')) formValues.duration = queryParams.get('duration'); + if (queryParams.get('description')) formValues.description = queryParams.get('description'); + if (queryParams.get('visibilityRadius')) formValues.visibilityRadius = Number(queryParams.get('visibilityRadius')); + + if (queryParams.get('lat') && queryParams.get('lon')) { + formValues.locationType = 'custom'; + formValues.latitude = queryParams.get('lat'); + formValues.longitude = queryParams.get('lon'); + } + } + + // Apply values from query parameters + if (Object.keys(formValues).length > 0) { + form.reset({ ...getDefaultValues(postType), ...formValues }); + } + }, [postType, location, form]); + + // Function to upload files to S3 and get URLs + const uploadFiles = async () => { + if (uploadedFiles.length === 0) return []; + + const urls = []; + setLoading(true); + + try { + for (const file of uploadedFiles) { + // Add the filename as the second argument + const url = await uploadFileToS3(file, file.name); + urls.push(url); + } + + setUploadedUrls(urls); + return urls; + } catch (error) { + console.error("Error uploading files:", error); + toast.error("Error uploading files. Please try again."); + return []; + } finally { + setLoading(false); + } + }; + + // Submit handler + const onSubmit = async (data: any) => { + setLoading(true); + + try { + const user = auth.currentUser; + if (!user) { + toast.error("You must be logged in to create a post"); + return; + } + + // Upload files and get URLs + const urls = await uploadFiles(); + + // Prepare data for Firestore + const postData = { + ...data, + photos: urls, + createdAt: Timestamp.now(), + userId: user.uid, + responders: [], + }; + + // Add to appropriate collection + const docRef = await addDoc(collection(db, `${postType}s`), postData); + + toast.success("Post created successfully!"); + navigate(`/post/${docRef.id}`); + } catch (error) { + console.error("Error creating post:", error); + toast.error("Error creating post. Please try again."); + } finally { + setLoading(false); + } + }; + + // Navigation between steps + const nextStep = () => { + // Validate current step fields + const fieldsToValidate = getFieldsForStep(currentStep); + + // @ts-ignore - TS doesn't understand that the string array is valid for form.trigger + form.trigger(fieldsToValidate).then(isValid => { + if (isValid) { + setCurrentStep(prev => Math.min(prev + 1, totalSteps)); + } + }); + }; + + const prevStep = () => { + setCurrentStep(prev => Math.max(prev - 1, 1)); + }; + + // Fields to validate per step (for resource form) + const getFieldsForStep = (step: number): string[] => { + if (postType === 'resource') { + switch (step) { + case 1: return ['title', 'category', 'customCategory']; + case 2: return ['description', 'resourceType', 'urgency', 'duration']; + case 3: return ['locationType', 'visibilityRadius']; + default: return []; + } + } + return []; + }; + + // Render resource form steps + const renderResourceForm = () => { + switch (currentStep) { + case 1: + return ; + case 2: + return ; + case 3: + return ; + case 4: + return ( + + ); + default: + return
Unknown step
; + } + }; + + // Render form content based on post type + const renderFormContent = () => { + switch (postType) { + case 'resource': return renderResourceForm(); + case 'event': return ; + case 'promotion': return ; + case 'update': return ; + default: return renderResourceForm(); + } + }; + + return ( +
+

+ {postType === 'resource' && 'Create Resource Post'} + {postType === 'event' && 'Create Event Post'} + {postType === 'promotion' && 'Create Promotion Post'} + {postType === 'update' && 'Create Update Post'} +

+ + + + + +
+ + {renderFormContent()} + +
+ {currentStep > 1 && ( + + )} + + {currentStep < totalSteps ? ( + + ) : ( + + )} +
+
+ +
+
+
+ ); +}; + +export default NewPostForm; \ No newline at end of file diff --git a/src/components/Forms/ResourceForm.tsx b/src/components/Forms/ResourceForm.tsx index 4f66b08..2a3dcbe 100644 --- a/src/components/Forms/ResourceForm.tsx +++ b/src/components/Forms/ResourceForm.tsx @@ -8,7 +8,6 @@ import { FaMedkit, FaTools, FaBook, FaHome, FaUtensils, FaMapMarkerAlt } from "r import { BsThreeDots } from "react-icons/bs"; import { ImageDisplay } from "../AWS/UploadFile"; import { useNavigate } from "react-router-dom"; -import { sendEmergencyNotifications } from "@/services/notificationService"; interface ResourceFormProps { userId: string; @@ -198,13 +197,7 @@ const ResourceForm: React.FC = ({ userId }) => { // Only send emergency notifications for "need" posts with emergency urgency if (postType === "need" && urgency === 3 && coordinates) { // Send emergency notifications to nearby users - await sendEmergencyNotifications( - postId, - title, - description, - coordinates, - visibilityRadius - ); + } console.log("Resource posted with ID: ", docRef.id); diff --git a/src/components/Forms/SchemaDef.ts b/src/components/Forms/SchemaDef.ts new file mode 100644 index 0000000..7006377 --- /dev/null +++ b/src/components/Forms/SchemaDef.ts @@ -0,0 +1,80 @@ +import * as z from 'zod'; + +// Define post types +export type PostType = 'resource' | 'event' | 'promotion' | 'update'; + +// Resource form schema +export const resourceSchema = z.object({ + title: z.string().min(3, { message: "Title must be at least 3 characters" }), + category: z.string().min(1, { message: "Please select a category" }), + customCategory: z.string().optional(), + description: z.string().min(10, { message: "Description must be at least 10 characters" }), + urgency: z.number().min(1).max(3), + duration: z.string().min(1, { message: "Please specify duration" }), + locationType: z.enum(['default', 'custom']), + latitude: z.string().optional(), + longitude: z.string().optional(), + address: z.string().optional(), + visibilityRadius: z.number().min(1).max(100), + resourceType: z.enum(['need', 'offer']), + isAnonymous: z.boolean().default(false), + photos: z.array(z.any()).optional(), +}); + +// Event form schema +export const eventSchema = z.object({ + title: z.string().min(3, { message: "Title must be at least 3 characters" }), + description: z.string().min(10, { message: "Description must be at least 10 characters" }), + eventType: z.string().min(1, { message: "Please select an event type" }), + organizerDetails: z.string(), + locationType: z.enum(['default', 'custom']), + latitude: z.string().optional(), + longitude: z.string().optional(), + address: z.string().optional(), + eventDate: z.string().min(1, { message: "Please specify event date" }), + eventTime: z.string().min(1, { message: "Please specify event time" }), + registrationRequired: z.boolean().default(false), + registrationLink: z.string().optional(), + visibilityRadius: z.number().min(1).max(100), + duration: z.string().min(1, { message: "Please specify duration" }), + photos: z.array(z.any()).optional(), +}); + +// Promotion form schema +export const promotionSchema = z.object({ + title: z.string().min(3, { message: "Title must be at least 3 characters" }), + description: z.string().min(10, { message: "Description must be at least 10 characters" }), + contactDetails: z.string(), + locationType: z.enum(['default', 'custom']), + latitude: z.string().optional(), + longitude: z.string().optional(), + address: z.string().optional(), + visibilityRadius: z.number().min(1).max(100), + duration: z.string().min(1, { message: "Please specify duration" }), + photos: z.array(z.any()).optional(), +}); + +// Update form schema +export const updateSchema = z.object({ + title: z.string().min(3, { message: "Title must be at least 3 characters" }), + description: z.string().min(10, { message: "Description must be at least 10 characters" }), + locationType: z.enum(['default', 'custom']), + latitude: z.string().optional(), + longitude: z.string().optional(), + address: z.string().optional(), + date: z.string().min(1, { message: "Please specify date" }), + visibilityRadius: z.number().min(1).max(100), + duration: z.string().min(1, { message: "Please specify duration" }), + photos: z.array(z.any()).optional(), +}); + +// Get schema based on post type +export const getSchemaForType = (type: PostType) => { + switch (type) { + case 'resource': return resourceSchema; + case 'event': return eventSchema; + case 'promotion': return promotionSchema; + case 'update': return updateSchema; + default: return resourceSchema; + } +}; \ No newline at end of file diff --git a/src/components/Forms/components/FormProgress.tsx b/src/components/Forms/components/FormProgress.tsx new file mode 100644 index 0000000..23ca03c --- /dev/null +++ b/src/components/Forms/components/FormProgress.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { Progress } from "@/components/ui/progress"; + +interface FormProgressProps { + currentStep: number; + totalSteps: number; +} + +const FormProgress: React.FC = ({ currentStep, totalSteps }) => { + return ( +
+ +
+ {Array.from({ length: totalSteps }).map((_, i) => ( +
+ Step {i + 1} +
+ ))} +
+
+ ); +}; + +export default FormProgress; \ No newline at end of file diff --git a/src/components/Forms/steps/EventForm.tsx b/src/components/Forms/steps/EventForm.tsx new file mode 100644 index 0000000..46b9154 --- /dev/null +++ b/src/components/Forms/steps/EventForm.tsx @@ -0,0 +1,636 @@ +import React, { useEffect } from 'react'; +import { UseFormReturn } from 'react-hook-form'; +import { + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, + FormDescription, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { Textarea } from "@/components/ui/textarea"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Checkbox } from "@/components/ui/checkbox"; +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; +import { Label } from "@/components/ui/label"; +import { Button } from "@/components/ui/button"; +import { Slider } from "@/components/ui/slider"; +import FileUpload from "../../Forms/FileUpload"; +import DatePicker from 'react-datepicker'; +import "react-datepicker/dist/react-datepicker.css"; +import { FaMapMarkerAlt } from 'react-icons/fa'; +import { toast } from 'react-toastify'; +import MapContainer, { useOlaMaps } from '@/components/MapContainer'; +import { auth } from '@/firebase'; + +interface EventFormProps { + form: UseFormReturn; + currentStep: number; + uploadedFiles?: File[]; + setUploadedFiles?: React.Dispatch>; +} + +const EventForm: React.FC = ({ + form, + currentStep, + uploadedFiles = [], + setUploadedFiles = () => {} +}) => { + + const { ref: mapRef, data: mapData } = useOlaMaps(); + + + useEffect(() => { + if (mapData?.selectedLocations && mapData.selectedLocations.length > 0) { + const location = mapData.selectedLocations[0]; + form.setValue('locationType', 'custom'); + form.setValue('latitude', location.latitude.toString()); + form.setValue('longitude', location.longitude.toString()); + + + if (location.address) { + form.setValue('address', location.address); + } + } + }, [mapData?.selectedLocations, form]); + + + useEffect(() => { + if (mapData?.currentLocation && form.watch('locationType') === 'custom') { + const { latitude, longitude } = mapData.currentLocation; + form.setValue('latitude', latitude.toString()); + form.setValue('longitude', longitude.toString()); + + if (mapData.currentAddress) { + form.setValue('address', mapData.currentAddress); + } + } + }, [mapData?.currentLocation, mapData?.currentAddress, form.watch('locationType')]); + + + const getCurrentLocation = () => { + if (navigator.geolocation) { + navigator.geolocation.getCurrentPosition( + (position) => { + const { latitude, longitude } = position.coords; + form.setValue('locationType', 'custom'); + form.setValue('latitude', latitude.toString()); + form.setValue('longitude', longitude.toString()); + toast.success("Location detected successfully!"); + }, + (error) => { + console.error("Error getting location:", error); + toast.error("Unable to retrieve your location. Please enable location access."); + }, + { enableHighAccuracy: true, timeout: 10000, maximumAge: 0 } + ); + } else { + toast.error("Geolocation is not supported by your browser."); + } + }; + + + const getMapCenter = () => { + if (form.watch('latitude') && form.watch('longitude')) { + return [ + parseFloat(form.watch('longitude')), + parseFloat(form.watch('latitude')) + ] as [number, number]; + } + return undefined; + }; + + switch (currentStep) { + case 1: + return ( +
+

Event Details

+ + ( + + Event Title + + + + + + )} + /> + + ( + + Event Description + +