38501-vm/frontend/src/pages/search.tsx
2026-02-17 23:52:44 +00:00

243 lines
11 KiB
TypeScript

import React, { ReactElement, useEffect, useState } from 'react';
import Head from 'next/head';
import { useRouter } from 'next/router';
import {
mdiMagnify,
mdiMapMarker,
mdiStar,
mdiShieldCheck,
mdiClockOutline,
mdiCurrencyUsd,
mdiFilterVariant
} from '@mdi/js';
import axios from 'axios';
import LayoutGuest from '../layouts/Guest';
import BaseIcon from '../components/BaseIcon';
import LoadingSpinner from '../components/LoadingSpinner';
import Link from 'next/link';
const SearchView = () => {
const router = useRouter();
const { query: searchQueryParam, location: locationParam } = router.query;
const [loading, setLoading] = useState(false);
const [searchResults, setSearchResults] = useState([]);
const [searchQuery, setSearchQuery] = useState(searchQueryParam || '');
const [location, setLocation] = useState(locationParam || '');
useEffect(() => {
if (searchQueryParam) {
setSearchQuery(searchQueryParam as string);
fetchData(searchQueryParam as string);
}
}, [searchQueryParam]);
const fetchData = async (query: string) => {
setLoading(true);
try {
const response = await axios.post('/search', { searchQuery: query });
setSearchResults(response.data);
} catch (error) {
console.error('Search error:', error);
} finally {
setLoading(false);
}
};
const handleSearch = (e: React.FormEvent) => {
e.preventDefault();
router.push({
pathname: '/search',
query: { query: searchQuery, location },
});
};
const businesses = searchResults.filter((item: any) => item.tableName === 'businesses');
const getBusinessImage = (biz: any) => {
if (biz.business_photos_business && biz.business_photos_business.length > 0) {
const photo = biz.business_photos_business[0].photos && biz.business_photos_business[0].photos[0];
if (photo && photo.publicUrl) {
return `/api/file/download?privateUrl=${photo.publicUrl}`;
}
}
return null;
};
return (
<div className="min-h-screen bg-slate-50 pb-20">
<Head>
<title>Find Services | Crafted Network</title>
</Head>
{/* Search Header */}
<div className="bg-slate-900 pt-32 pb-12 shadow-inner">
<div className="container mx-auto px-6">
<form onSubmit={handleSearch} className="flex flex-col md:flex-row gap-4 p-2 bg-white/10 backdrop-blur-md rounded-2xl border border-white/10 shadow-xl max-w-4xl mx-auto">
<div className="flex-grow relative">
<BaseIcon path={mdiMagnify} size={24} className="absolute left-4 top-1/2 transform -translate-y-1/2 text-slate-400" />
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Service (e.g. Plumbing)"
className="w-full bg-transparent border-none focus:ring-0 py-3 pl-12 pr-4 text-white placeholder-slate-500"
/>
</div>
<div className="md:w-px h-8 bg-white/10 my-auto"></div>
<div className="flex-grow relative">
<BaseIcon path={mdiMapMarker} size={24} className="absolute left-4 top-1/2 transform -translate-y-1/2 text-slate-400" />
<input
type="text"
value={location}
onChange={(e) => setLocation(e.target.value)}
placeholder="Location"
className="w-full bg-transparent border-none focus:ring-0 py-3 pl-12 pr-4 text-white placeholder-slate-500"
/>
</div>
<button type="submit" className="bg-emerald-500 hover:bg-emerald-600 text-white font-bold py-3 px-8 rounded-xl transition-all">
Update Search
</button>
</form>
</div>
</div>
<div className="container mx-auto px-6 mt-12">
<div className="flex flex-col lg:flex-row gap-8">
{/* Filters Sidebar */}
<aside className="w-full lg:w-64 space-y-8">
<div className="bg-white p-6 rounded-3xl border border-slate-200 shadow-sm">
<div className="flex items-center justify-between mb-6">
<h3 className="font-bold text-lg">Filters</h3>
<BaseIcon path={mdiFilterVariant} size={20} className="text-slate-400" />
</div>
<div className="space-y-6">
<div>
<label className="block text-sm font-semibold text-slate-700 mb-3">Availability</label>
<div className="space-y-2">
{['Available Today', 'This Week', 'Next Week'].map(label => (
<label key={label} className="flex items-center text-sm text-slate-600 cursor-pointer hover:text-emerald-600">
<input type="checkbox" className="rounded text-emerald-500 mr-3 border-slate-300 focus:ring-emerald-500" />
{label}
</label>
))}
</div>
</div>
<div>
<label className="block text-sm font-semibold text-slate-700 mb-3">Reliability Score</label>
<input type="range" min="0" max="100" className="w-full h-2 bg-slate-100 rounded-lg appearance-none cursor-pointer accent-emerald-500" />
<div className="flex justify-between text-xs text-slate-400 mt-2">
<span>Any</span>
<span>80+</span>
</div>
</div>
</div>
</div>
</aside>
{/* Results Area */}
<main className="flex-grow">
<div className="flex items-baseline justify-between mb-8">
<h2 className="text-2xl font-bold">
{loading ? 'Searching...' : `${businesses.length} Results for "${searchQueryParam || 'Businesses'}"`}
</h2>
<div className="text-sm text-slate-500 font-medium">
Sort by: <span className="text-slate-900 cursor-pointer hover:text-emerald-500">Reliability Score</span>
</div>
</div>
{loading ? (
<div className="flex justify-center py-20">
<LoadingSpinner />
</div>
) : (
<div className="grid gap-6">
{businesses.map((biz: any) => (
<Link key={biz.id} href={`/public/businesses-details?id=${biz.id}`}>
<div className="group bg-white rounded-3xl border border-slate-200 hover:border-emerald-500 hover:shadow-xl transition-all overflow-hidden flex flex-col md:flex-row">
{/* Image */}
<div className="md:w-64 h-48 md:h-auto bg-slate-100 relative">
{getBusinessImage(biz) ? (
<img
src={getBusinessImage(biz)}
alt={biz.name}
className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-500"
/>
) : (
<div className="absolute inset-0 flex items-center justify-center text-slate-300">
<BaseIcon path={mdiShieldCheck} size={64} />
</div>
)}
{biz.reliability_score >= 80 && (
<div className="absolute top-4 left-4 bg-emerald-500 text-white text-[10px] font-black uppercase tracking-widest px-2 py-1 rounded-md shadow-lg">
Top Rated
</div>
)}
</div>
<div className="p-8 flex-grow">
<div className="flex justify-between items-start mb-4">
<div>
<h3 className="text-2xl font-bold group-hover:text-emerald-600 transition-colors mb-1">{biz.name}</h3>
<div className="flex items-center text-slate-500 text-sm">
<BaseIcon path={mdiMapMarker} size={16} className="mr-1" />
{biz.city}, {biz.state} {biz.address}
</div>
</div>
<div className="text-right">
<div className="flex items-center justify-end text-emerald-500 font-bold text-xl mb-1">
<BaseIcon path={mdiStar} size={24} className="mr-1 text-amber-400" />
{biz.rating || ((biz.reliability_score || 0) / 20).toFixed(1)}
</div>
<div className="text-[10px] text-slate-400 font-bold uppercase tracking-tighter">Reliability Score</div>
</div>
</div>
<p className="text-slate-600 line-clamp-2 mb-6 leading-relaxed">
{biz.description || 'Verified service professional providing high-quality solutions for your needs.'}
</p>
<div className="flex flex-wrap gap-4 pt-6 border-t border-slate-100">
<div className="flex items-center text-xs font-bold text-slate-500 uppercase tracking-wider">
<BaseIcon path={mdiClockOutline} size={16} className="mr-2 text-emerald-500" />
~{biz.response_time_median_minutes || 30}m Response
</div>
<div className="flex items-center text-xs font-bold text-slate-500 uppercase tracking-wider">
<BaseIcon path={mdiCurrencyUsd} size={16} className="mr-2 text-emerald-500" />
Fair Pricing
</div>
<div className="flex items-center text-xs font-bold text-slate-500 uppercase tracking-wider">
<BaseIcon path={mdiShieldCheck} size={16} className="mr-2 text-emerald-500" />
Verified
</div>
</div>
</div>
</div>
</Link>
))}
{businesses.length === 0 && !loading && (
<div className="bg-white rounded-3xl p-20 text-center border-2 border-dashed border-slate-200">
<div className="w-20 h-20 bg-slate-50 rounded-full flex items-center justify-center mx-auto mb-6 text-slate-300">
<BaseIcon path={mdiMagnify} size={40} />
</div>
<h3 className="text-xl font-bold mb-2">No businesses found</h3>
<p className="text-slate-500">Try adjusting your search terms or location.</p>
</div>
)}
</div>
)}
</main>
</div>
</div>
</div>
);
};
SearchView.getLayout = function getLayout(page: ReactElement) {
return <LayoutGuest>{page}</LayoutGuest>;
};
export default SearchView;