38501-vm/frontend/src/pages/public/businesses-details.tsx
2026-02-17 21:09:07 +00:00

372 lines
18 KiB
TypeScript

import React, { ReactElement, useEffect, useState } from 'react';
import Head from 'next/head';
import { useRouter } from 'next/router';
import {
mdiStar,
mdiShieldCheck,
mdiClockOutline,
mdiMapMarker,
mdiPhone,
mdiWeb,
mdiEmail,
mdiCurrencyUsd,
mdiCheckDecagram,
mdiMessageDraw,
mdiAccount
} from '@mdi/js';
import axios from 'axios';
import LayoutGuest from '../../layouts/Guest';
import BaseIcon from '../../components/BaseIcon';
import LoadingSpinner from '../../components/LoadingSpinner';
import dataFormatter from '../../helpers/dataFormatter';
import { useAppSelector } from '../../stores/hooks';
const BusinessDetailsPublic = () => {
const router = useRouter();
const { id } = router.query;
const [loading, setLoading] = useState(true);
const [business, setBusiness] = useState<any>(null);
const { currentUser } = useAppSelector((state) => state.auth);
useEffect(() => {
if (id) {
fetchBusiness();
}
}, [id]);
const fetchBusiness = async () => {
setLoading(true);
try {
const response = await axios.get(`/businesses/${id}`);
setBusiness(response.data);
} catch (error) {
console.error('Error fetching business:', error);
} finally {
setLoading(false);
}
};
const claimListing = async () => {
if (!currentUser) {
router.push('/login');
return;
}
try {
await axios.post(`/businesses/${id}/claim`);
fetchBusiness(); // Refresh data
} catch (error) {
console.error('Error claiming business:', error);
alert('Failed to claim business. Please try again.');
}
};
const getBusinessImage = () => {
if (business && business.business_photos_business && business.business_photos_business.length > 0) {
const photo = business.business_photos_business[0].photos && business.business_photos_business[0].photos[0];
if (photo && photo.publicUrl) {
return `/api/file/download?privateUrl=${photo.publicUrl}`;
}
}
return null;
};
if (loading) return <div className="min-h-screen flex items-center justify-center bg-slate-50"><LoadingSpinner /></div>;
if (!business) return <div className="min-h-screen flex items-center justify-center bg-slate-50">Business not found.</div>;
const displayRating = business.rating ? Number(business.rating).toFixed(1) : 'New';
return (
<div className="min-h-screen bg-slate-50 pb-20 pt-20">
<Head>
<title>{business.name} | Crafted Network</title>
</Head>
{/* Hero Header */}
<section className="bg-white border-b border-slate-200 pt-16 pb-12">
<div className="container mx-auto px-6">
<div className="flex flex-col lg:flex-row gap-12 items-start">
{/* Business Photo */}
<div className="w-32 h-32 lg:w-48 lg:h-48 bg-slate-100 rounded-[2.5rem] overflow-hidden flex items-center justify-center shadow-inner relative flex-shrink-0">
{getBusinessImage() ? (
<img
src={getBusinessImage()}
alt={business.name}
className="w-full h-full object-cover"
/>
) : (
<BaseIcon path={mdiShieldCheck} size={64} className="text-slate-300" />
)}
{(business.reliability_score >= 80 || business.is_claimed) && (
<div className="absolute -top-2 -right-2 bg-emerald-500 text-white p-2 rounded-full shadow-lg">
<BaseIcon path={mdiCheckDecagram} size={24} />
</div>
)}
</div>
<div className="flex-grow w-full">
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6 mb-6">
<div>
<h1 className="text-4xl lg:text-5xl font-bold mb-3">{business.name}</h1>
<div className="flex flex-wrap items-center gap-4 text-slate-500 font-medium">
<span className="flex items-center">
<BaseIcon path={mdiMapMarker} size={18} className="mr-1 text-emerald-500" />
{business.city}, {business.state}
</span>
<span className="flex items-center">
<BaseIcon path={mdiStar} size={18} className="mr-1 text-amber-400" />
{displayRating} Rating
</span>
{business.is_claimed ? (
<span className="bg-emerald-50 text-emerald-600 px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider">
Verified Pro
</span>
) : (
<span className="bg-slate-100 text-slate-500 px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider flex items-center">
Unclaimed Listing
</span>
)}
</div>
</div>
<div className="flex gap-4">
<button
onClick={() => router.push(`/public/request-service?businessId=${business.id}`)}
className="bg-emerald-500 hover:bg-emerald-600 text-white font-bold py-4 px-8 rounded-2xl transition-all shadow-xl shadow-emerald-500/20"
>
Request Service
</button>
</div>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-8 py-6 border-t border-slate-100">
<div>
<div className="text-[10px] font-black uppercase tracking-widest text-slate-400 mb-1">Avg Rating</div>
<div className="text-2xl font-bold text-slate-900">{displayRating} / 5.0</div>
</div>
<div>
<div className="text-[10px] font-black uppercase tracking-widest text-slate-400 mb-1">Response Time</div>
<div className="text-2xl font-bold text-slate-900">~{business.response_time_median_minutes || 30}m</div>
</div>
<div>
<div className="text-[10px] font-black uppercase tracking-widest text-slate-400 mb-1">Status</div>
<div className="flex items-center text-emerald-500 font-bold">
<div className="w-2 h-2 rounded-full bg-emerald-500 mr-2 animate-pulse"></div>
Available
</div>
</div>
<div>
<div className="text-[10px] font-black uppercase tracking-widest text-slate-400 mb-1">Total Reviews</div>
<div className="text-2xl font-bold text-slate-900">{business.reviews_business?.length || 0}</div>
</div>
</div>
</div>
</div>
</div>
</section>
<div className="container mx-auto px-6 py-12">
<div className="grid lg:grid-cols-3 gap-12">
{/* Main Content */}
<div className="lg:col-span-2 space-y-12">
{!business.is_claimed && (
<div className="bg-amber-50 border border-amber-200 p-8 rounded-[2rem] flex flex-col md:flex-row items-center justify-between gap-6">
<div>
<h4 className="text-xl font-bold text-amber-900 mb-2">Is this your business?</h4>
<p className="text-amber-700">Claim your listing to respond to reviews, update your profile, and get more leads.</p>
</div>
<button
onClick={claimListing}
className="bg-amber-500 hover:bg-amber-600 text-white font-bold py-3 px-8 rounded-xl transition-all flex-shrink-0"
>
Claim Listing
</button>
</div>
)}
{/* Photos Gallery */}
{business.business_photos_business?.length > 0 && (
<section className="bg-white p-10 rounded-[3rem] border border-slate-200 shadow-sm">
<h3 className="text-2xl font-bold mb-6">Photos</h3>
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
{business.business_photos_business.map((bp: any) => (
bp.photos?.map((p: any) => (
<div key={p.id} className="aspect-square rounded-2xl overflow-hidden bg-slate-100">
<img
src={`/api/file/download?privateUrl=${p.publicUrl}`}
alt="Business"
className="w-full h-full object-cover hover:scale-110 transition-transform duration-500"
/>
</div>
))
))}
</div>
</section>
)}
{/* About */}
<section className="bg-white p-10 rounded-[3rem] border border-slate-200 shadow-sm">
<h3 className="text-2xl font-bold mb-6">About the Business</h3>
<div className="text-slate-600 leading-relaxed text-lg"
dangerouslySetInnerHTML={{ __html: business.description || 'No description provided.' }} />
</section>
{/* Pricing */}
<section className="bg-white p-10 rounded-[3rem] border border-slate-200 shadow-sm">
<h3 className="text-2xl font-bold mb-6">Service Pricing Range</h3>
<div className="grid gap-4">
{business.service_prices_business?.map((price: any) => (
<div key={price.id} className="flex items-center justify-between p-6 rounded-2xl bg-slate-50 hover:bg-emerald-50 transition-colors group">
<div>
<h4 className="font-bold text-slate-800 text-lg group-hover:text-emerald-700">{price.service_name}</h4>
<p className="text-slate-500 text-sm">{price.notes || 'Standard professional service.'}</p>
</div>
<div className="text-right">
<div className="text-emerald-600 font-bold text-xl">${price.typical_price}</div>
<div className="text-xs text-slate-400 font-medium">Typical Price</div>
</div>
</div>
))}
{!business.service_prices_business?.length && <p className="text-slate-500">No pricing information available.</p>}
</div>
</section>
{/* Reviews */}
<section>
<div className="flex items-center justify-between mb-8">
<h3 className="text-2xl font-bold">Customer Reviews</h3>
<button
onClick={() => router.push(`/reviews/reviews-new?businessId=${business.id}`)}
className="flex items-center gap-2 bg-white border border-slate-200 px-6 py-3 rounded-2xl text-emerald-600 font-bold hover:bg-slate-50 transition-all shadow-sm"
>
<BaseIcon path={mdiMessageDraw} size={20} />
Write a Review
</button>
</div>
<div className="grid gap-6">
{business.reviews_business?.map((review: any) => (
<div key={review.id} className="bg-white p-8 rounded-3xl border border-slate-200 shadow-sm hover:shadow-md transition-all">
<div className="flex justify-between items-start mb-4">
<div className="flex items-center gap-1">
{[...Array(5)].map((_, i) => (
<BaseIcon key={i} path={mdiStar} size={18} className={i < review.rating ? 'text-amber-400' : 'text-slate-200'} />
))}
</div>
<span className="text-xs text-slate-400 font-medium">{dataFormatter.dateFormatter(review.created_at_ts)}</span>
</div>
<p className="text-slate-700 leading-relaxed mb-4 italic text-lg">&quot;{review.text}&quot;</p>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="w-8 h-8 rounded-full bg-slate-100 flex items-center justify-center text-slate-400">
<BaseIcon path={mdiAccount} size={18} />
</div>
<span className="text-sm font-bold text-slate-600">
{review.user?.firstName || 'Anonymous'}
</span>
</div>
{review.is_verified_job && (
<div className="inline-flex items-center text-[10px] font-bold text-emerald-600 uppercase tracking-widest bg-emerald-50 px-2 py-1 rounded">
<BaseIcon path={mdiShieldCheck} size={14} className="mr-1" />
Verified Job
</div>
)}
</div>
</div>
))}
{!business.reviews_business?.length && (
<div className="text-center py-20 bg-white rounded-[3rem] border border-dashed border-slate-300 text-slate-400">
<BaseIcon path={mdiMessageDraw} size={48} className="mx-auto mb-4 opacity-20" />
<p className="text-xl font-medium">No reviews yet.</p>
<p className="mb-6">Be the first to share your experience!</p>
<button
onClick={() => router.push(`/reviews/reviews-new?businessId=${business.id}`)}
className="bg-emerald-500 hover:bg-emerald-600 text-white font-bold py-3 px-8 rounded-xl"
>
Write Review
</button>
</div>
)}
</div>
</section>
</div>
{/* Sidebar */}
<div className="space-y-8">
{/* Contact Info */}
<div className="bg-slate-900 text-white p-10 rounded-[3rem] shadow-xl relative overflow-hidden group">
<div className="absolute top-0 right-0 w-32 h-32 bg-emerald-500/10 rounded-full -mr-16 -mt-16 group-hover:scale-110 transition-transform"></div>
<h3 className="text-xl font-bold mb-8">Contact & Location</h3>
<div className="space-y-6 relative z-10">
<div className="flex items-start">
<BaseIcon path={mdiPhone} size={24} className="mr-4 text-emerald-400" />
<div>
<div className="text-xs font-black uppercase tracking-widest text-slate-500 mb-1">Call Now</div>
<div className="font-bold">{business.phone || 'Contact for details'}</div>
</div>
</div>
<div className="flex items-start">
<BaseIcon path={mdiEmail} size={24} className="mr-4 text-emerald-400" />
<div>
<div className="text-xs font-black uppercase tracking-widest text-slate-500 mb-1">Email</div>
<div className="font-bold truncate max-w-[180px]">{business.email}</div>
</div>
</div>
<div className="flex items-start">
<BaseIcon path={mdiWeb} size={24} className="mr-4 text-emerald-400" />
<div>
<div className="text-xs font-black uppercase tracking-widest text-slate-500 mb-1">Website</div>
<div className="font-bold truncate max-w-[180px]">{business.website || 'N/A'}</div>
</div>
</div>
<div className="flex items-start">
<BaseIcon path={mdiMapMarker} size={24} className="mr-4 text-emerald-400" />
<div>
<div className="text-xs font-black uppercase tracking-widest text-slate-500 mb-1">Address</div>
<div className="font-bold">{business.address}, {business.city}, {business.state} {business.zip}</div>
</div>
</div>
</div>
</div>
{/* Badges */}
<div className="bg-white p-10 rounded-[3rem] border border-slate-200 shadow-sm">
<h3 className="text-xl font-bold mb-8">Trust Signals</h3>
<div className="space-y-6">
{business.business_badges_business?.filter((b:any) => b.status === 'APPROVED').map((badge: any) => (
<div key={badge.id} className="flex items-center p-4 rounded-2xl bg-slate-50">
<div className="w-10 h-10 bg-emerald-100 rounded-xl flex items-center justify-center mr-4 text-emerald-600">
<BaseIcon path={mdiShieldCheck} size={24} />
</div>
<div>
<div className="font-bold text-slate-800 text-sm leading-tight">{badge.badge_type.replace(/_/g, ' ')}</div>
<div className="text-[10px] text-slate-400 font-bold uppercase tracking-widest">Verified Badge</div>
</div>
</div>
))}
{business.is_claimed && (
<div className="flex items-center p-4 rounded-2xl bg-emerald-50">
<div className="w-10 h-10 bg-emerald-200 rounded-xl flex items-center justify-center mr-4 text-emerald-700">
<BaseIcon path={mdiCheckDecagram} size={24} />
</div>
<div>
<div className="font-bold text-emerald-900 text-sm leading-tight">Claimed Listing</div>
<div className="text-[10px] text-emerald-600 font-bold uppercase tracking-widest">Verified Owner</div>
</div>
</div>
)}
{!business.business_badges_business?.length && !business.is_claimed && <p className="text-slate-400 text-sm italic">Pending verification...</p>}
</div>
</div>
</div>
</div>
</div>
</div>
);
};
BusinessDetailsPublic.getLayout = function getLayout(page: ReactElement) {
return <LayoutGuest>{page}</LayoutGuest>;
};
export default BusinessDetailsPublic;