Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added public/hero-bg.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
215 changes: 109 additions & 106 deletions src/components/home/Hero.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@ import { FC } from 'react';
import { useTranslation } from 'react-i18next';
import MeilisearchInstantSearch from '../search/MeilisearchInstantSearch';
import { Link } from 'react-router-dom';
import {
UserCheck,
Briefcase,
GraduationCap,
HeartPulse,
TrendingUp,
ArrowRight,
} from 'lucide-react';
import serviceCategories from '../../data/service_categories.json';

interface Subcategory {
Expand Down Expand Up @@ -68,28 +76,50 @@ const Hero: FC = () => {
];

return (
<div className='bg-linear-to-r from-primary-600 to-primary-700 text-white py-12 md:py-24'>
<div className='container mx-auto px-4'>
<div className='grid grid-cols-1 lg:grid-cols-2 gap-8 items-center'>
{/* Left section with title and search */}
<div className='animate-fade-in'>
<h1 className='text-3xl md:text-4xl lg:text-5xl font-bold mb-4 leading-tight'>
{t('hero.title')}
<div className='relative min-h-[600px] lg:min-h-[700px] flex items-center bg-primary-900 text-white overflow-hidden'>
{/* Background Layer */}
<div
className='absolute inset-0 z-0 opacity-40 scale-110 animate-slow-zoom'
style={{
backgroundImage: 'url(/hero-bg.webp)',
backgroundSize: 'cover',
backgroundPosition: 'center',
}}
/>
<div className='absolute inset-0 bg-linear-to-r from-primary-950 via-primary-900/80 to-transparent z-10' />

{/* Content Container */}
<div className='container mx-auto px-4 relative z-20 py-12 md:py-24'>
<div className='grid grid-cols-1 lg:grid-cols-12 gap-12 items-center'>
{/* Left section: Text and Search */}
<div className='lg:col-span-7 animate-fade-in'>
<h1 className='text-4xl md:text-5xl lg:text-6xl font-bold mb-6 leading-tight tracking-tight'>
{t('hero.title')
.split(' ')
.map((word, i) => (
<span key={i} className={i === 0 ? 'text-blue-400' : ''}>
{word}{' '}
</span>
))}
</h1>
<p className='text-lg text-blue-200 mb-8 max-w-lg'>

<p className='text-lg md:text-xl text-blue-100/80 mb-10 max-w-xl leading-relaxed'>
{t('hero.subtitle')}
</p>
{/* Meilisearch component will be full width and include its own styling */}
{/* The background of Hero is dark, MeilisearchInstantSearch has a light theme by default */}
{/* Consider adjusting MeilisearchInstantSearch styles or Hero background for better blending */}
<div className='mb-8'>

<div className='mb-6 max-w-2xl'>
<MeilisearchInstantSearch />
</div>
<div className='mt-4 flex flex-wrap gap-2'>

<div className='flex flex-wrap items-center gap-3'>
<span className='text-sm font-medium text-blue-200/60 flex items-center'>
<TrendingUp className='h-4 w-4 mr-2' />
Popular:
</span>
{popularServices.map(service => (
<Link
key={service.label}
className='bg-white/10 text-white border-white/20 hover:bg-white/20 py-2 px-4 rounded-xl text-sm'
className='bg-white/5 backdrop-blur-sm text-white border border-white/10 hover:bg-white/20 hover:border-white/30 py-1.5 px-4 rounded-full text-sm transition-all duration-300'
to={service.href}
>
{service.label}
Expand All @@ -98,109 +128,53 @@ const Hero: FC = () => {
</div>
</div>

{/* Right section with quick access services */}
<div className='bg-white/10 backdrop-blur-xs rounded-xl p-6 shadow-lg animate-slide-in'>
<h2 className='text-2xl font-semibold mb-4'>
{t('services.title')}
</h2>
<div className='grid grid-cols-2 gap-4'>
<Link
{/* Right section: Quick Access Widgets */}
<div className='lg:col-span-5'>
<div className='grid grid-cols-2 gap-4 animate-slide-up'>
<QuickAccessCard
to={`/services?category=${findCategorySlug(
'Certificates and IDs'
)}`}
className='bg-white/10 hover:bg-white/20 rounded-lg p-4 transition-all duration-200 flex flex-col items-center text-center'
>
<div className='bg-primary-500 p-3 rounded-full mb-3'>
<svg
className='h-6 w-6 text-white'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
>
<path d='M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2'></path>
<circle cx='12' cy='7' r='4'></circle>
</svg>
</div>
<span className='font-medium'>Citizenship & ID</span>
</Link>
<Link
icon={<UserCheck className='h-6 w-6' />}
title='Citizenship & ID'
description='IDs and Certificates'
/>
<QuickAccessCard
to={`/services?category=${findCategorySlug(
'Business and Trade'
)}`}
className='bg-white/10 hover:bg-white/20 rounded-lg p-4 transition-all duration-200 flex flex-col items-center text-center'
>
<div className='bg-primary-500 p-3 rounded-full mb-3'>
<svg
className='h-6 w-6 text-white'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
>
<rect
x='2'
y='7'
width='20'
height='14'
rx='2'
ry='2'
></rect>
<path d='M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16'></path>
</svg>
</div>
<span className='font-medium'>Business</span>
</Link>
<Link
icon={<Briefcase className='h-6 w-6' />}
title='Business'
description='Registration & Permits'
/>
<QuickAccessCard
to={`/services?category=${findCategorySlug('Education')}`}
className='bg-white/10 hover:bg-white/20 rounded-lg p-4 transition-all duration-200 flex flex-col items-center text-center'
>
<div className='bg-primary-500 p-3 rounded-full mb-3'>
<svg
className='h-6 w-6 text-white'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
>
<path d='M22 10v6M2 10l10-5 10 5-10 5z'></path>
<path d='M6 12v5c0 2 2 3 6 3s6-1 6-3v-5'></path>
</svg>
</div>
<span className='font-medium'>Education</span>
</Link>
<Link
icon={<GraduationCap className='h-6 w-6' />}
title='Education'
description='Schools & Scholarships'
/>
<QuickAccessCard
to={`/services?category=${findCategorySlug('Health')}`}
className='bg-white/10 hover:bg-white/20 rounded-lg p-4 transition-all duration-500 flex flex-col items-center text-center'
>
<div className='bg-primary-500 p-3 rounded-full mb-3'>
<svg
className='h-6 w-6 text-white'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
>
<path d='M22 12h-4l-3 9L9 3l-3 9H2'></path>
</svg>
</div>
<span className='font-medium'>Health</span>
</Link>
</div>
<div className='mt-4 flex'>
icon={<HeartPulse className='h-6 w-6' />}
title='Health'
description='Medical & Insurance'
/>

<Link
className='bg-white/10 text-white hover:bg-white/20 transition-all duration-500 w-full rounded-lg p-4 text-center'
to='/services'
className='col-span-2 group mt-2 bg-blue-600 hover:bg-blue-500 text-white rounded-2xl p-5 flex items-center justify-between transition-all duration-300 shadow-lg shadow-blue-900/20'
>
View All Services
<div>
<span className='block font-bold text-lg'>
View All Services
</span>
<span className='text-sm text-blue-100'>
Browse the complete directory
</span>
</div>
<div className='bg-white/20 p-2 rounded-full group-hover:translate-x-2 transition-transform'>
<ArrowRight className='h-6 w-6' />
</div>
</Link>
</div>
</div>
Expand All @@ -210,4 +184,33 @@ const Hero: FC = () => {
);
};

interface QuickAccessCardProps {
to: string;
icon: React.ReactNode;
title: string;
description: string;
}

const QuickAccessCard: FC<QuickAccessCardProps> = ({
to,
icon,
title,
description,
}) => (
<Link
to={to}
className='group bg-white/5 backdrop-blur-lg border border-white/10 hover:border-white/30 rounded-2xl p-5 transition-all duration-500 hover:bg-white/10 hover:-translate-y-1'
>
<div className='bg-blue-500/20 p-3 rounded-xl mb-4 w-fit group-hover:bg-blue-500/30 transition-colors text-blue-400'>
{icon}
</div>
<span className='block font-bold text-sm md:text-lg mb-1 group-hover:text-blue-400 transition-colors'>
{title}
</span>
<p className='text-xs text-blue-100/60 leading-tight tracking-tight md:tracking-wide'>
{description}
</p>
</Link>
);

export default Hero;
57 changes: 19 additions & 38 deletions src/components/search/MeilisearchInstantSearch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ import {
Configure,
Stats,
} from 'react-instantsearch';
import type { Hit, SearchClient } from 'instantsearch.js';
import { instantMeiliSearch } from '@meilisearch/instant-meilisearch';
import 'instantsearch.css/themes/satellite.css'; // Or your preferred theme
import './MeilisearchInstantSearch.css'; // For custom styles

interface SearchHit {
objectID: string;
interface SearchHit extends Hit {
name?: string;
office_name?: string;
office?: string;
Expand Down Expand Up @@ -66,32 +66,7 @@ const { searchClient } = instantMeiliSearch(
);

interface HitProps {
hit: {
// [key: string]: any // Allow any string keys for hit attributes
// objectID: string
name?: string;
office_name?: string;
office?: string;
service?: string;
website?: string;
category?:
| string
| {
name: string;
slug: string;
};
address?: string;
subcategory?:
| string
| {
name: string;
slug: string;
};
description?: string;
slug?: string;
url?: string;
// Add other fields you expect in your search results
};
hit: SearchHit;
}

const Hit: FC<HitProps> = ({ hit }) => {
Expand All @@ -118,20 +93,24 @@ const Hit: FC<HitProps> = ({ hit }) => {
? 'office_name'
: 'office'
}
hit={hit as SearchHit}
hit={hit}
/> */}
</h2>
{hit.description && (
<p className='text-sm text-gray-800 mt-1'>
<Snippet attribute='description' hit={hit as SearchHit} />
<Snippet attribute='description' hit={hit} />
</p>
)}
<div className='text-xs text-gray-800'>
{hit.category && (
<span>
<Highlight
attribute={hit.category?.name ? 'category.name' : 'category'}
hit={hit as SearchHit}
attribute={
typeof hit.category === 'object'
? 'category.name'
: 'category'
}
hit={hit}
/>
{' > '}
</span>
Expand All @@ -140,15 +119,17 @@ const Hit: FC<HitProps> = ({ hit }) => {
<span>
<Highlight
attribute={
hit.subcategory?.name ? 'subcategory.name' : 'subcategory'
typeof hit.subcategory === 'object'
? 'subcategory.name'
: 'subcategory'
}
hit={hit as SearchHit}
hit={hit}
/>{' '}
</span>
)}
{hit.address && (
<span>
<Highlight attribute='address' hit={hit as SearchHit} />
<Highlight attribute='address' hit={hit} />
{' > '}
</span>
)}
Expand Down Expand Up @@ -198,7 +179,7 @@ const MeilisearchInstantSearch: FC = () => {

return (
<InstantSearch
searchClient={searchClient}
searchClient={searchClient as unknown as SearchClient}
indexName='bettergov'
initialUiState={{
bettergov: {
Expand All @@ -209,7 +190,7 @@ const MeilisearchInstantSearch: FC = () => {
>
<Configure hitsPerPage={10} />
<div className='ais-InstantSearch rounded-lg'>
<div className='mb-2 w-full' ref={searchContainerRef}>
<div className='relative mb-2 w-full' ref={searchContainerRef}>
<SearchBox
placeholder='Search for services, directory items...'
className='w-full'
Expand All @@ -227,7 +208,7 @@ const MeilisearchInstantSearch: FC = () => {
/>

{hasInteracted && (
<div className='bg-white rounded-lg shadow-sm overflow-y-scroll h-96 absolute z-30 w-[calc(100%-2rem)] max-w-[calc(100%-4rem)] lg:w-1/2'>
<div className='bg-white rounded-lg shadow-xl overflow-y-auto max-h-96 absolute z-30 w-full top-full left-0 mt-1 border border-gray-100'>
<Stats
classNames={{
root: 'text-sm text-gray-800 p-2 text-right text-xs',
Expand Down
Loading
Loading