diff --git a/package-lock.json b/package-lock.json index 9eb68fed..2ff7da86 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,6 +44,7 @@ "cmdk": "^1.0.0", "date-fns": "^3.6.0", "embla-carousel-react": "^8.3.0", + "framer-motion": "^12.23.24", "i18next": "^25.0.1", "i18next-browser-languagedetector": "^8.0.5", "input-otp": "^1.2.4", @@ -5594,6 +5595,33 @@ "url": "https://github.com/sponsors/rawify" } }, + "node_modules/framer-motion": { + "version": "12.23.24", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.24.tgz", + "integrity": "sha512-HMi5HRoRCTou+3fb3h9oTLyJGBxHfW+HnNE25tAXOvVx/IvwMHK0cx7IR4a2ZU6sh3IX1Z+4ts32PcYBOqka8w==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.23.23", + "motion-utils": "^12.23.6", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -7337,6 +7365,21 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/motion-dom": { + "version": "12.23.23", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.23.tgz", + "integrity": "sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.23.6" + } + }, + "node_modules/motion-utils": { + "version": "12.23.6", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz", + "integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", diff --git a/package.json b/package.json index bc150965..0f8acfc2 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "cmdk": "^1.0.0", "date-fns": "^3.6.0", "embla-carousel-react": "^8.3.0", + "framer-motion": "^12.23.24", "i18next": "^25.0.1", "i18next-browser-languagedetector": "^8.0.5", "input-otp": "^1.2.4", diff --git a/src/App.tsx b/src/App.tsx index 050ed94d..154ab5c1 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -24,6 +24,10 @@ import Developers from "./pages/categories/Developers"; import Architects from "./pages/categories/Architects"; import Contractors from "./pages/categories/Contractors"; import CategoriesLayout from "./pages/categories/CategoriesLayout"; +import ArchitectDetail from "./pages/categories/ArchitectDetail"; +import ProtectedRoute from "./components/ProtectedRoute"; +import ArchitectDashboard from "./pages/ArchitectDashboard.tsx"; + // Import i18n configuration import './i18n'; @@ -41,6 +45,7 @@ const AppRoutes = () => { } /> } /> } /> + } /> }> } /> } /> @@ -48,6 +53,7 @@ const AppRoutes = () => { } /> } /> } /> + } /> }> } /> diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index 26b67922..70fd14cf 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect } from 'react'; -import { Menu, X, User, Search } from 'lucide-react'; +import { Menu, X, User, Search, LayoutDashboard } from 'lucide-react'; // ⚡ added LayoutDashboard icon import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Link, NavLink } from 'react-router-dom'; @@ -8,30 +8,39 @@ import UserProfileMenu from './UserProfileMenu'; import LanguageSelector from './LanguageSelector'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; +import { supabase } from "@/integrations/supabase/client"; // ✅ Supabase client import const Navbar: React.FC = () => { const [isOpen, setIsOpen] = useState(false); const [isScrolled, setIsScrolled] = useState(false); - const [isServicesOpen, setIsServicesOpen] = useState(false); // Desktop dropdown - const [isMobileServicesOpen, setIsMobileServicesOpen] = useState(false); // Mobile dropdown + const [isServicesOpen, setIsServicesOpen] = useState(false); + const [isMobileServicesOpen, setIsMobileServicesOpen] = useState(false); + const [userRole, setUserRole] = useState(null); // ✅ Store role const { user } = useAuth(); const { t } = useTranslation(); const navigate = useNavigate(); - + // ✅ Fetch user role useEffect(() => { - const handleScroll = () => { - if (window.scrollY > 10) { - setIsScrolled(true); - } else { - setIsScrolled(false); + const fetchUserRole = async () => { + if (user?.id) { + const { data, error } = await supabase + .from('profiles') + .select('user_role') + .eq('id', user.id) + .single(); + if (!error && data) { + setUserRole(data.user_role); + } } }; + fetchUserRole(); + }, [user]); + useEffect(() => { + const handleScroll = () => setIsScrolled(window.scrollY > 10); window.addEventListener('scroll', handleScroll); - return () => { - window.removeEventListener('scroll', handleScroll); - }; + return () => window.removeEventListener('scroll', handleScroll); }, []); const handleSearchClick = () => { @@ -90,27 +99,54 @@ const Navbar: React.FC = () => {
setIsServicesOpen(true)} - onMouseLeave={() => setIsServicesOpen(false)} + overflow-hidden transition-all duration-300 ease-in-out + ${isServicesOpen ? 'max-h-[500px] opacity-100' : 'max-h-0 opacity-0'}`} > - Worker - {/* Home Owners */} - Architects/Designers - Contractors - Developers - Material Suppliers + + Worker + + + Architects/Designers + + + Contractors + + + Developers + + + Material Suppliers +
- {t('common.blog')} - {t('common.about')} - {t('common.contact')} + + {t('common.blog')} + + + {t('common.about')} + + + {t('common.contact')} + + {/* Desktop Right Side */}
+ + {/* ✅ Only for architects */} + {user && userRole === 'architect' && ( + + )} + {user ? ( ) : ( @@ -142,67 +178,87 @@ const Navbar: React.FC = () => {
{/* Mobile Menu */} - {isOpen && ( -
-
-
- - { - if (e.key === 'Enter') { - handleSearchClick(); - setIsOpen(false); - } - }} - /> -
+{isOpen && ( +
+
+
+ + { + if (e.key === 'Enter') { + handleSearchClick(); + setIsOpen(false); + } + }} + /> +
- setIsOpen(false)}>Home + setIsOpen(false)}>Home - {/* Services main link (redirect only) */} - setIsOpen(false)}>Services + {/* Services main link (redirect only) */} + setIsOpen(false)}>Services - {/* New "All Services" mobile button */} + {/* Solutions dropdown */} {isMobileServicesOpen && (
- setIsOpen(false)}>Worker - {/* setIsOpen(false)}>Home Owners */} - setIsOpen(false)}>Architects/Designers - setIsOpen(false)}>Contractors - setIsOpen(false)}>Developers - setIsOpen(false)}>Material Suppliers + setIsOpen(false)}> + Worker + + setIsOpen(false)}> + Architects/Designers + + setIsOpen(false)}> + Contractors + + setIsOpen(false)}> + Developers + + setIsOpen(false)}> + Material Suppliers +
)} - setIsOpen(false)}>Blog - setIsOpen(false)}>About - setIsOpen(false)}>Contact + setIsOpen(false)}> + Blog + + setIsOpen(false)}> + About + + setIsOpen(false)}> + Contact +
- - {/* Profile Button added above sign out */} - {user && ( - )} @@ -210,19 +266,10 @@ const Navbar: React.FC = () => { ) : ( <> - - @@ -236,4 +283,4 @@ const Navbar: React.FC = () => { ); }; -export default Navbar; +export default Navbar; \ No newline at end of file diff --git a/src/components/ProtectedRoute.tsx b/src/components/ProtectedRoute.tsx new file mode 100644 index 00000000..45e5be85 --- /dev/null +++ b/src/components/ProtectedRoute.tsx @@ -0,0 +1,30 @@ +import React, { ReactNode } from "react"; +import { Navigate } from "react-router-dom"; +import { useAuth } from "@/contexts/AuthContext"; + +interface ProtectedRouteProps { + children: ReactNode; + requiredRole?: string; +} + +const ProtectedRoute: React.FC = ({ children, requiredRole }) => { + const { user, userRole, loading } = useAuth(); + + if (loading) { + return

Checking authentication...

; + } + + // If user is not logged in + if (!user) { + return ; + } + + // If user role doesn't match required role + if (requiredRole && userRole !== requiredRole) { + return ; + } + + return <>{children}; +}; + +export default ProtectedRoute; diff --git a/src/contexts/AuthContext.tsx b/src/contexts/AuthContext.tsx index 4857e8fd..71a62622 100644 --- a/src/contexts/AuthContext.tsx +++ b/src/contexts/AuthContext.tsx @@ -100,7 +100,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children try { const { error } = await supabase.auth.signInWithPassword({ email, password }); if (error) throw error; - navigate('/'); + // Don't navigate here - let the Login component handle it via } catch (error: any) { toast({ title: "Error signing in", @@ -129,7 +129,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children description: "Please check your email to verify your account.", }); - navigate('/auth/login'); + // Don't navigate here - let the Register component handle it via } catch (error: any) { toast({ title: "Error signing up", diff --git a/src/integrations/supabase/types.ts b/src/integrations/supabase/types.ts index 4d3810a7..d4527231 100644 Binary files a/src/integrations/supabase/types.ts and b/src/integrations/supabase/types.ts differ diff --git a/src/pages/ArchitectDashboard.tsx b/src/pages/ArchitectDashboard.tsx new file mode 100644 index 00000000..039ad2f6 --- /dev/null +++ b/src/pages/ArchitectDashboard.tsx @@ -0,0 +1,388 @@ +import React, { useEffect, useState } from "react"; +import { supabase } from "@/integrations/supabase/client"; +import { useAuth } from "@/contexts/AuthContext"; +import { useNavigate } from "react-router-dom"; +import { + ArrowLeft, + LogOut, + Edit2, + Save, + X, + CheckCircle, +} from "lucide-react"; +import { motion } from "framer-motion"; + +interface Project { + id: string | number; + arcID: string | number; + project_Name: string; + client_name: string; + price: number; + status: string; +} + +export default function ArchitectDashboard() { + const { user, userRole, loading: authLoading, signOut } = useAuth(); + const navigate = useNavigate(); + + const [projects, setProjects] = useState([]); + const [loading, setLoading] = useState(true); + const [architectName, setArchitectName] = useState(""); + const [editingId, setEditingId] = useState(null); + const [editedProject, setEditedProject] = useState>({}); + + useEffect(() => { + if (authLoading) return; + if (!user) { + navigate("/auth/login"); + return; + } + if (userRole !== "architect") { + navigate("/"); + return; + } + + const fetchProjects = async () => { + setLoading(true); + const { data: architectData } = await supabase + .from("architects") + .select("id, name") + .eq("user_id", user.id) + .single(); + + if (!architectData) { + setProjects([]); + setLoading(false); + return; + } + + setArchitectName(architectData.name || "Architect"); + + const { data: projectsData } = await supabase + .from("ArchitectRequest") + .select("*") + .eq("arcID", architectData.id) + .order("id", { ascending: false }); + + setProjects(projectsData || []); + setLoading(false); + }; + + fetchProjects(); + }, [user, userRole, authLoading, navigate]); + + const handleLogout = async () => { + await signOut(); + navigate("/auth/login"); + }; + + const handleEdit = (project: Project) => { + setEditingId(project.id); + setEditedProject({ ...project }); + }; + + const handleCancel = () => { + setEditingId(null); + setEditedProject({}); + }; + + const handleSave = async (id: string | number) => { + const { error } = await supabase + .from("ArchitectRequest") + .update({ + project_Name: editedProject.project_Name, + price: editedProject.price, + client_name: editedProject.client_name, + status: editedProject.status, + }) + .eq("id", Number(id)); + + if (!error) { + setProjects((prev) => + prev.map((p) => (p.id === id ? { ...p, ...editedProject } : p)) + ); + setEditingId(null); + setEditedProject({}); + } + }; + + const handleComplete = async (id: string | number) => { + await supabase + .from("ArchitectRequest") + .update({ status: "completed" }) + .eq("id", Number(id)); + + setProjects((prev) => + prev.map((p) => (p.id === id ? { ...p, status: "completed" } : p)) + ); + }; + + const activeProjects = projects.filter((p) => p.status !== "completed"); + const completedProjects = projects.filter((p) => p.status === "completed"); + + if (authLoading || loading) + return ( +
+ Loading dashboard... +
+ ); + + return ( +
+ {/* Navbar */} + + +

+ {architectName}'s Dashboard +

+ +
+ + {/* Dashboard Content */} +
+ {/* Stats Cards */} + + {[ + { + title: "Total Projects", + count: projects.length, + gradient: "from-blue-500 to-blue-700", + icon: "📁", + }, + { + title: "Active Projects", + count: activeProjects.length, + gradient: "from-green-500 to-emerald-600", + icon: "⚙️", + }, + { + title: "Completed Projects", + count: completedProjects.length, + gradient: "from-purple-500 to-indigo-600", + icon: "✅", + }, + ].map((card, i) => ( + +
+

+ {card.title} +

+ {card.icon} +
+

{card.count}

+
+ ))} +
+ + {/* Active Projects */} + +

+ 🔧 Active Projects +

+ {activeProjects.length > 0 ? ( + + + + + + + + + + + + {activeProjects.map((p) => ( + + {/* Project Name */} + + + {/* Client */} + + + {/* Price */} + + + {/* Status */} + + + {/* Action Buttons */} + + + ))} + +
NameClientPriceStatusAction
+ {editingId === p.id ? ( + + setEditedProject({ + ...editedProject, + project_Name: e.target.value, + }) + } + className="border p-2 rounded w-full" + /> + ) : ( + p.project_Name + )} + + {editingId === p.id ? ( + + setEditedProject({ + ...editedProject, + client_name: e.target.value, + }) + } + className="border p-2 rounded w-full" + /> + ) : ( + p.client_name + )} + + {editingId === p.id ? ( + + setEditedProject({ + ...editedProject, + price: Number(e.target.value), + }) + } + className="border p-2 rounded w-full" + /> + ) : ( + `₹${p.price}` + )} + + + {p.status} + + + {editingId === p.id ? ( + <> + + + + ) : ( + <> + + + + )} +
+ ) : ( +

+ No active projects found. +

+ )} +
+ + {/* Completed Projects */} + +

+ 🏁 Completed Projects +

+ {completedProjects.length > 0 ? ( + + + + + + + + + + {completedProjects.map((p) => ( + + + + + + ))} + +
NameClientPrice
{p.project_Name}{p.client_name}₹{p.price}
+ ) : ( +

+ No completed projects yet. +

+ )} +
+
+
+ ); +} diff --git a/src/pages/auth/Login.tsx b/src/pages/auth/Login.tsx index b6230b4a..769e96aa 100644 --- a/src/pages/auth/Login.tsx +++ b/src/pages/auth/Login.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { useForm } from 'react-hook-form'; -import { Link, Navigate } from 'react-router-dom'; +import { Link, Navigate, useSearchParams } from 'react-router-dom'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'; @@ -18,6 +18,7 @@ type LoginFormValues = z.infer; const Login = () => { const { user, signIn } = useAuth(); + const [searchParams] = useSearchParams(); const form = useForm({ resolver: zodResolver(loginSchema), defaultValues: { @@ -38,6 +39,14 @@ const Login = () => { }; if (user) { + const redirect = searchParams.get('redirect'); + const showForm = searchParams.get('showForm'); + + if (redirect) { + const redirectUrl = showForm ? `${redirect}?showForm=${showForm}` : redirect; + return ; + } + return ; } @@ -102,7 +111,10 @@ const Login = () => {

Don't have an account?{' '} - + Sign up

diff --git a/src/pages/auth/Register.tsx b/src/pages/auth/Register.tsx index 33be5142..d56d5984 100644 --- a/src/pages/auth/Register.tsx +++ b/src/pages/auth/Register.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { useForm } from 'react-hook-form'; -import { Link, Navigate } from 'react-router-dom'; +import { Link, Navigate, useSearchParams } from 'react-router-dom'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'; @@ -19,6 +19,7 @@ type RegisterFormValues = z.infer; const Register = () => { const { user, signUp } = useAuth(); + const [searchParams] = useSearchParams(); const form = useForm({ resolver: zodResolver(registerSchema), defaultValues: { @@ -40,6 +41,15 @@ const Register = () => { }; if (user) { + // Check for redirect parameter + const redirect = searchParams.get('redirect'); + const showForm = searchParams.get('showForm'); + + if (redirect) { + const redirectUrl = showForm ? `${redirect}?showForm=${showForm}` : redirect; + return ; + } + return ; } @@ -122,7 +132,10 @@ const Register = () => {

Already have an account?{' '} - + Sign in

diff --git a/src/pages/categories/ArchitectDetail.tsx b/src/pages/categories/ArchitectDetail.tsx new file mode 100644 index 00000000..a3b0c02d --- /dev/null +++ b/src/pages/categories/ArchitectDetail.tsx @@ -0,0 +1,313 @@ +import React, { useEffect, useState } from "react"; +import { useParams, useNavigate } from "react-router-dom"; +import { supabase } from "@/integrations/supabase/client"; +import Navbar from "@/components/Navbar"; +import Footer from "@/components/Footer"; +import { Loader2, Upload, X } from "lucide-react"; +import { motion } from "framer-motion"; + +export default function ArchitectDetail() { + const { id } = useParams<{ id: string }>(); + const navigate = useNavigate(); + const [architect, setArchitect] = useState(null); + const [photos, setPhotos] = useState([]); + const [loading, setLoading] = useState(true); + const [uploading, setUploading] = useState(false); + const [photoError, setPhotoError] = useState(null); + const [currentUser, setCurrentUser] = useState(null); + const [canUpload, setCanUpload] = useState(false); + + // Request state + const [requesting, setRequesting] = useState(false); + const [showForm, setShowForm] = useState(false); + const [clientName, setClientName] = useState(""); + const [projectName, setProjectName] = useState(""); + const [price, setPrice] = useState(""); + + // ✅ Get current logged-in user + useEffect(() => { + const getUser = async () => { + const { + data: { user }, + } = await supabase.auth.getUser(); + setCurrentUser(user); + }; + getUser(); + }, []); + + // ✅ Fetch architect data + useEffect(() => { + const fetchArchitect = async () => { + if (!id) return; + setLoading(true); + const { data, error } = await supabase + .from("architects") + .select("*") + .eq("id", Number(id)) + .single(); + if (!error && data) setArchitect(data); + setLoading(false); + }; + fetchArchitect(); + }, [id]); + + // ✅ Determine if user can upload + useEffect(() => { + if (currentUser && architect) { + setCanUpload(currentUser.id === architect.user_id); + } + }, [currentUser, architect]); + + // ✅ Fetch photos + const fetchPhotos = async () => { + if (!id) return; + const folderName = `architect_${id}`; + const { data, error } = await supabase.storage + .from("Architects") + .list(folderName, { limit: 100 }); + if (error) { + setPhotoError("Failed to load photos. Please refresh the page."); + return; + } + const urls = + data?.map((file) => { + const { data: publicData } = supabase.storage + .from("Architects") + .getPublicUrl(`${folderName}/${file.name}`); + return publicData?.publicUrl || ""; + }) || []; + setPhotos(urls.filter(Boolean)); + }; + + useEffect(() => { + fetchPhotos(); + }, [id]); + + // ✅ Handle photo upload + const handleUpload = async (e: React.ChangeEvent) => { + if (!canUpload) { + alert("You can only upload photos to your own profile."); + return; + } + try { + setPhotoError(null); + const file = e.target.files?.[0]; + if (!file || !id) return; + setUploading(true); + const folderName = `architect_${id}`; + const fileName = `${Date.now()}_${file.name}`; + const filePath = `${folderName}/${fileName}`; + const { error: uploadError } = await supabase.storage + .from("Architects") + .upload(filePath, file); + if (uploadError) { + setPhotoError("Failed to upload photo. Please try again."); + return; + } + await fetchPhotos(); + } catch { + setPhotoError("Failed to upload photo. Please try again."); + } finally { + setUploading(false); + } + }; + + // ✅ Handle service request submission + const handleSubmitRequest = async () => { + if (!clientName.trim() || !projectName.trim() || !price.trim()) { + alert("Please fill in all details."); + return; + } + + setRequesting(true); + + try { + const { error } = await supabase.from("ArchitectRequest").insert([ + { + arcID: Number(id), + project_Name: projectName, + client_name: clientName, + price: Number(price), + status: "pending", + created_at: new Date().toISOString(), + }, + ]); + + if (error) throw error; + setShowForm(false); + alert("Service request sent successfully!"); + setClientName(""); + setProjectName(""); + setPrice(""); + } catch (err) { + console.error("Error creating request:", err); + alert("Failed to send request. Please try again."); + } finally { + setRequesting(false); + } + }; + + // ✅ Loading screen + if (loading) { + return ( +
+ +
+ ); + } + + // ✅ Architect not found + if (!architect) { + return ( +
+

Architect not found 😢

+ +
+ ); + } + + // ✅ Main return + return ( +
+ + + {/* ======= Banner Section ======= */} +
+ Banner +
+
+ + {/* ======= Profile Section ======= */} +
+
+ {architect.name} +
+

+ {architect.name} +

+ {/* Removed Rating Stars */} +

{architect.specialization}

+

+ {architect.description} +

+ + {canUpload && ( + + )} + + {/* ===== Request Button ===== */} + {!canUpload && ( + + )} + + {photoError && ( +

{photoError}

+ )} +
+
+
+ + {/* ======= Request Form Modal ======= */} + {showForm && ( +
+
+ +

+ Request Service +

+
+ setClientName(e.target.value)} + className="border rounded-lg px-4 py-2 w-full" + /> + setProjectName(e.target.value)} + className="border rounded-lg px-4 py-2 w-full" + /> + setPrice(e.target.value)} + className="border rounded-lg px-4 py-2 w-full" + /> + +
+
+
+ )} + + {/* ======= Gallery Section ======= */} +
+

+ Design Gallery +

+ {photos.length === 0 ? ( +

+ No photos uploaded yet. Be the first to upload! +

+ ) : ( +
+ {photos.map((url, i) => ( + + ))} +
+ )} +
+ +
+
+ ); +} diff --git a/src/pages/categories/Architects.tsx b/src/pages/categories/Architects.tsx index 720959f3..c578fe22 100644 --- a/src/pages/categories/Architects.tsx +++ b/src/pages/categories/Architects.tsx @@ -1,90 +1,223 @@ -import React, { useState } from 'react'; -import { useNavigate, Link } from 'react-router-dom'; -import Navbar from '@/components/Navbar'; -import Footer from '@/components/Footer'; - -const accentColor = 'text-purple-600'; - -const architects = [ - { - id: 1, - name: 'Aviral Design Studio Jammu', - location: 'Jammu, India', - pincode: '180001', - category: 'Residential', - shortDesc: 'Innovative designs and sustainable solutions.', - fullDesc: - 'Aviral Design Studio is a leading architectural firm based in Jammu, renowned for its innovative and sustainable designs. The firm focuses on residential, commercial, and institutional projects with a modern, eco-friendly approach.', - image: '/architects/Aviral_Design_Studio/image01.png', - }, - { - id: 2, - name: 'Aashiana Architects', - location: 'Jammu, India', - pincode: '180002', - category: 'Interior', - shortDesc: 'Blending traditional aesthetics with modern functionality.', - fullDesc: - 'Aashiana Architects specializes in modular kitchens and bathrooms, offering a perfect blend of traditional aesthetics with contemporary design. Known for detail-oriented residential projects and elegant interior concepts.', - image: 'https://images.pexels.com/photos/7587861/pexels-photo-7587861.jpeg', - }, - { - id: 3, - name: 'UrbanNest Architects', - location: 'Pune, India', - pincode: '411001', - category: 'Residential', - shortDesc: 'Eco-friendly homes with smart space optimization.', - fullDesc: - 'UrbanNest focuses on sustainable architecture, with a strong emphasis on energy efficiency, natural light, and environmental harmony.', - image: 'https://images.pexels.com/photos/7031580/pexels-photo-7031580.jpeg', - }, - { - id: 4, - name: 'Studio Zenith', - location: 'Jaipur, India', - pincode: '302001', - category: 'Commercial', - shortDesc: 'Creative designs for luxury and modern living.', - fullDesc: - 'Studio Zenith is known for creating bespoke architectural experiences that blend cultural heritage with contemporary aesthetics.', - image: 'https://images.pexels.com/photos/27065116/pexels-photo-27065116.jpeg', - }, - { - id: 5, - name: 'DesignWorks Studio', - location: 'Mumbai, India', - pincode: '400001', - category: 'Commercial', - shortDesc: 'Award-winning firm for commercial and urban design.', - fullDesc: - 'DesignWorks Studio specializes in large-scale commercial projects and has received multiple awards for innovation in sustainable city planning.', - image: 'https://images.pexels.com/photos/269077/pexels-photo-269077.jpeg', - }, - { - id: 6, - name: 'Form & Function Architects', - location: 'Delhi, India', - pincode: '110001', - category: 'Residential', - shortDesc: 'Minimalist spaces that balance art and utility.', - fullDesc: - 'Form & Function is a Delhi-based architecture firm known for minimalist and timeless commercial and residential designs.', - image: 'https://images.pexels.com/photos/1115804/pexels-photo-1115804.jpeg', - }, -]; - -const categories = ['Featured', 'Residential', 'Commercial', 'Interior']; - -const Architecture: React.FC = () => { +import React, { useEffect, useState, useRef } from "react"; +import { supabase } from "@/integrations/supabase/client"; +import { useAuth } from "@/contexts/AuthContext"; +import { useNavigate, useSearchParams } from "react-router-dom"; +import Navbar from "@/components/Navbar"; +import Footer from "@/components/Footer"; +import { Loader2, CheckCircle2, Smile } from "lucide-react"; + +const ArchitectsPage = () => { + const { user } = useAuth(); const navigate = useNavigate(); - const [search, setSearch] = useState(''); + const [searchParams, setSearchParams] = useSearchParams(); + const formRef = useRef(null); + const [architects, setArchitects] = useState([]); + const [loading, setLoading] = useState(true); + const [search, setSearch] = useState(""); + const [showForm, setShowForm] = useState(false); + const [uploading, setUploading] = useState(false); + const [formData, setFormData] = useState({ + name: "", + specialization: "", + description: "", + image_url: "", + }); + const [message, setMessage] = useState(""); + const [isAlreadyRegistered, setIsAlreadyRegistered] = useState(false); + + // ✅ Fetch architects + const fetchArchitects = async () => { + setLoading(true); + const { data, error } = await supabase.from("architects").select("*"); + if (error) console.error(error); + setArchitects(data || []); + setLoading(false); + }; + + // ✅ Check if user already registered as architect + const checkIfRegistered = async () => { + if (!user) return; + const { data, error } = await supabase + .from("architects") + .select("id") + .eq("user_id", user.id) + .maybeSingle(); + + if (!error && data) { + setIsAlreadyRegistered(true); + } else { + setIsAlreadyRegistered(false); + } + }; + + useEffect(() => { + fetchArchitects(); + }, []); + + useEffect(() => { + if (user) checkIfRegistered(); + }, [user]); + + useEffect(() => { + const shouldShowForm = searchParams.get("showForm"); + + if (shouldShowForm === "true" && user && !isAlreadyRegistered) { + setShowForm(true); + + const newSearchParams = new URLSearchParams(searchParams); + newSearchParams.delete("showForm"); + setSearchParams(newSearchParams, { replace: true }); + + setTimeout(() => { + formRef.current?.scrollIntoView({ + behavior: "smooth", + block: "start" + }); + }, 300); + } + }, [user, isAlreadyRegistered]); + + // ✅ Handle input + const handleChange = ( + e: React.ChangeEvent + ) => { + setFormData({ ...formData, [e.target.name]: e.target.value }); + }; + + // ✅ Handle file upload + const handleFileUpload = async (e: React.ChangeEvent) => { + try { + const file = e.target.files?.[0]; + if (!file) return; + + if (!user) { + setMessage("Please log in to upload an image."); + return; + } + + setUploading(true); + const fileExt = file.name.split(".").pop(); + const fileName = `${user.id}_${Date.now()}.${fileExt}`; + + const { error: uploadError } = await supabase.storage + .from("Architects") + .upload(fileName, file, { + cacheControl: "3600", + upsert: false, + }); + + if (uploadError) throw uploadError; + + const { data: publicUrlData } = supabase.storage + .from("Architects") + .getPublicUrl(fileName); + + if (publicUrlData?.publicUrl) { + setFormData((prev) => ({ + ...prev, + image_url: publicUrlData.publicUrl, + })); + setMessage("Image uploaded successfully!"); + } + } catch (error: any) { + console.error(error); + setMessage(`Upload failed: ${error.message}`); + } finally { + setUploading(false); + } + }; + + // ✅ Handle form submit with duplicate prevention + update role + const handleRegister = async (e: React.FormEvent) => { + e.preventDefault(); + + if (!user) { + setMessage("Please log in first."); + return; + } + + const { name, specialization, description, image_url } = formData; + if (!name || !specialization || !description) { + setMessage("Please fill all required fields."); + return; + } + + setLoading(true); + setMessage(""); + + try { + const { data: existingArchitect, error: checkError } = await supabase + .from("architects") + .select("id") + .eq("user_id", user.id) + .maybeSingle(); + + if (checkError) throw checkError; + + if (existingArchitect) { + setMessage("You are already registered as an architect."); + setLoading(false); + setIsAlreadyRegistered(true); + return; + } + + const { error } = await supabase.from("architects").insert([ + { + name, + specialization, + description, + image_url, + user_id: user.id, + }, + ]); + + if (error) throw error; + + const { error: roleError } = await supabase + .from("profiles") + .update({ user_role: "architect" }) + .eq("id", user.id); + + if (roleError) throw roleError; + + setMessage("Successfully registered as architect!"); + setFormData({ + name: "", + specialization: "", + description: "", + image_url: "", + }); + setShowForm(false); + setIsAlreadyRegistered(true); + fetchArchitects(); + } catch (err: any) { + console.error(err); + setMessage(`Failed to register: ${err.message}`); + } finally { + setLoading(false); + } + }; + + const handleRegisterClick = () => { + if (!user) { + navigate("/auth/login?redirect=/categories/architects&showForm=true"); + } else if (isAlreadyRegistered) { + setMessage("You are already registered as an architect."); + } else { + setShowForm(true); + setTimeout(() => { + formRef.current?.scrollIntoView({ + behavior: "smooth", + block: "start" + }); + }, 100); + } + }; - // Filter architects based on search input (name or pincode) const filteredArchitects = architects.filter( (arch) => - arch.name.toLowerCase().includes(search.toLowerCase()) || - arch.pincode.includes(search) + arch.name?.toLowerCase().includes(search.toLowerCase()) || + arch.specialization?.toLowerCase().includes(search.toLowerCase()) ); return ( @@ -92,98 +225,239 @@ const Architecture: React.FC = () => { {/* Hero Section */} -
-
-

- Find - Top Architects - for Your Project +
+
+

+ Hire Top Architects
for + Your Dream Project

+

+ Design your ideal space with the best architects — experts in + innovative planning, interior design, and modern construction + solutions. +

+
+ setSearch(e.target.value)} + /> + +
+ +
- {/* Hero image hidden on mobile */} -
- Architectural planning + {/* Right Side Video */} +
+
+ +
+ + {/* Floating Verified Experts */} +
+
+
+ +
+
+

Verified Experts

+

Background Checked

+
+
+
+ + {/* Floating Happy Clients */} +
+
+
+ +
+
+

Happy Clients

+
+
+
- {/* Search Bar */} -
- setSearch(e.target.value)} - /> + {/* Registration Form */} + {showForm && user && !isAlreadyRegistered && ( +
+ + +

+ Register as an Architect +

+ +
+ + +