Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6c664aa7d2 | |||
| c3a3bfb151 | |||
| 40e81f9118 | |||
| 90d87acc12 | |||
| 65193fb7fc | |||
| 7c094c6b21 | |||
| 98c2faed1f | |||
| 815349bc6b | |||
| 6ffddf00b4 |
@@ -1,127 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { LucideIcon } from 'lucide-react';
|
||||
|
||||
interface FloatingContactButtonsProps {
|
||||
phoneNumber: string;
|
||||
mapsUrl: string;
|
||||
phoneIcon?: LucideIcon;
|
||||
mapsIcon?: LucideIcon;
|
||||
onPhoneClick?: () => void;
|
||||
onMapsClick?: () => void;
|
||||
className?: string;
|
||||
containerClassName?: string;
|
||||
buttonClassName?: string;
|
||||
phoneButtonClassName?: string;
|
||||
mapsButtonClassName?: string;
|
||||
tooltipClassName?: string;
|
||||
}
|
||||
|
||||
export default function FloatingContactButtons({
|
||||
phoneNumber,
|
||||
mapsUrl,
|
||||
phoneIcon: PhoneIcon,
|
||||
mapsIcon: MapsIcon,
|
||||
onPhoneClick,
|
||||
onMapsClick,
|
||||
className = '',
|
||||
containerClassName = '',
|
||||
buttonClassName = '',
|
||||
phoneButtonClassName = '',
|
||||
mapsButtonClassName = '',
|
||||
tooltipClassName = ''
|
||||
}: FloatingContactButtonsProps) {
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const [showPhoneTooltip, setShowPhoneTooltip] = useState(false);
|
||||
const [showMapsTooltip, setShowMapsTooltip] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Show floating buttons after a short delay for better UX
|
||||
const timer = setTimeout(() => setIsVisible(true), 500);
|
||||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
|
||||
const handlePhoneClick = () => {
|
||||
onPhoneClick?.();
|
||||
window.location.href = `tel:${phoneNumber}`;
|
||||
};
|
||||
|
||||
const handleMapsClick = () => {
|
||||
onMapsClick?.();
|
||||
window.open(mapsUrl, '_blank');
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`fixed bottom-6 right-6 z-40 flex flex-col gap-3 md:bottom-8 md:right-8 transition-opacity duration-500 ${
|
||||
isVisible ? 'opacity-100' : 'opacity-0 pointer-events-none'
|
||||
} ${className}`}
|
||||
aria-label="Floating contact buttons"
|
||||
>
|
||||
{/* Maps Button */}
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={handleMapsClick}
|
||||
className={`
|
||||
w-14 h-14 md:w-16 md:h-16 rounded-full shadow-lg hover:shadow-xl
|
||||
bg-primary-cta hover:bg-primary-cta/90 text-white
|
||||
flex items-center justify-center transition-all duration-300
|
||||
hover:scale-110 active:scale-95
|
||||
${mapsButtonClassName}
|
||||
${buttonClassName}
|
||||
`}
|
||||
aria-label="Open location in Google Maps"
|
||||
title="Get Directions"
|
||||
onMouseEnter={() => setShowMapsTooltip(true)}
|
||||
onMouseLeave={() => setShowMapsTooltip(false)}
|
||||
>
|
||||
{MapsIcon ? (
|
||||
<MapsIcon className="w-6 h-6 md:w-7 md:h-7" strokeWidth={1.5} />
|
||||
) : (
|
||||
<svg className="w-6 h-6 md:w-7 md:h-7" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8zm3.5-9c.83 0 1.5-.67 1.5-1.5S16.33 8 15.5 8 14 8.67 14 9.5s.67 1.5 1.5 1.5z" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
{showMapsTooltip && (
|
||||
<div className={`absolute right-16 top-1/2 -translate-y-1/2 bg-foreground text-background px-3 py-2 rounded-lg text-xs font-medium whitespace-nowrap pointer-events-none ${tooltipClassName}`}>
|
||||
Get Directions
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Phone Button */}
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={handlePhoneClick}
|
||||
className={`
|
||||
w-14 h-14 md:w-16 md:h-16 rounded-full shadow-lg hover:shadow-xl
|
||||
bg-secondary-cta hover:bg-secondary-cta/90 text-white
|
||||
flex items-center justify-center transition-all duration-300
|
||||
hover:scale-110 active:scale-95
|
||||
${phoneButtonClassName}
|
||||
${buttonClassName}
|
||||
`}
|
||||
aria-label="Call restaurant"
|
||||
title="Call Us"
|
||||
onMouseEnter={() => setShowPhoneTooltip(true)}
|
||||
onMouseLeave={() => setShowPhoneTooltip(false)}
|
||||
>
|
||||
{PhoneIcon ? (
|
||||
<PhoneIcon className="w-6 h-6 md:w-7 md:h-7" strokeWidth={1.5} />
|
||||
) : (
|
||||
<svg className="w-6 h-6 md:w-7 md:h-7" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M17.92 7.02C17.45 6.18 16.7 5.51 15.77 5.36c-1.36-.2-2.77.12-3.29 1.32l-.5 1.13c-.41.98-.3 2.04.35 2.77l1.96 2.16c-.38.86-.87 1.66-1.46 2.38l-2.38-2.38c-.7.13-1.74.7-2.45 1.36l-4.88 4.88C.5 17.66 0 19.52 0 21.5V24h2.5c1.98 0 3.84-.5 5.5-1.38l4.88-4.88c.67-.71 1.23-1.75 1.36-2.45l-2.38-2.38c.72-.59 1.52-1.08 2.38-1.46l2.16 1.96c.73.65 1.79.76 2.77.35l1.13-.5c1.2-.52 1.52-1.93 1.32-3.29-.15-.93-.82-1.68-1.66-2.15zM6.55 18.08c-1.19.84-2.58 1.32-4.05 1.32H2V21.5c0 1.47.48 2.86 1.32 4.05l4.88-4.88c-.26-.31-.48-.65-.65-1.01l-1.55-1.58z" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
{showPhoneTooltip && (
|
||||
<div className={`absolute right-16 top-1/2 -translate-y-1/2 bg-foreground text-background px-3 py-2 rounded-lg text-xs font-medium whitespace-nowrap pointer-events-none ${tooltipClassName}`}>
|
||||
Call Us
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import React from 'react';
|
||||
import { Star } from 'lucide-react';
|
||||
|
||||
interface GoogleRatingWidgetProps {
|
||||
rating: number; // 0-5
|
||||
reviewCount: number;
|
||||
businessName: string;
|
||||
className?: string;
|
||||
containerClassName?: string;
|
||||
ratingContainerClassName?: string;
|
||||
starsClassName?: string;
|
||||
textClassName?: string;
|
||||
reviewCountClassName?: string;
|
||||
}
|
||||
|
||||
export default function GoogleRatingWidget({
|
||||
rating,
|
||||
reviewCount,
|
||||
businessName,
|
||||
className = '',
|
||||
containerClassName = '',
|
||||
ratingContainerClassName = '',
|
||||
starsClassName = '',
|
||||
textClassName = '',
|
||||
reviewCountClassName = ''
|
||||
}: GoogleRatingWidgetProps) {
|
||||
const fullStars = Math.floor(rating);
|
||||
const hasHalfStar = rating % 1 >= 0.5;
|
||||
|
||||
const renderStars = () => {
|
||||
const stars = [];
|
||||
for (let i = 0; i < 5; i++) {
|
||||
if (i < fullStars) {
|
||||
stars.push(
|
||||
<Star
|
||||
key={`star-${i}`}
|
||||
className={`w-4 h-4 md:w-5 md:h-5 fill-yellow-400 text-yellow-400 ${starsClassName}`}
|
||||
/>
|
||||
);
|
||||
} else if (i === fullStars && hasHalfStar) {
|
||||
stars.push(
|
||||
<div key={`star-${i}`} className="relative">
|
||||
<Star className={`w-4 h-4 md:w-5 md:h-5 text-gray-300 ${starsClassName}`} />
|
||||
<div className="absolute top-0 left-0 overflow-hidden" style={{ width: '50%' }}>
|
||||
<Star className={`w-4 h-4 md:w-5 md:h-5 fill-yellow-400 text-yellow-400 ${starsClassName}`} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
stars.push(
|
||||
<Star
|
||||
key={`star-${i}`}
|
||||
className={`w-4 h-4 md:w-5 md:h-5 text-gray-300 ${starsClassName}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
return stars;
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`w-full flex justify-center py-8 md:py-12 px-4 ${className}`}
|
||||
aria-label={`Google rating: ${rating} out of 5 stars with ${reviewCount} reviews`}
|
||||
>
|
||||
<div
|
||||
className={`flex flex-col items-center gap-3 md:gap-4 p-4 md:p-6 rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900/50 ${containerClassName}`}
|
||||
>
|
||||
<div className={`flex items-center gap-3 md:gap-4 ${ratingContainerClassName}`}>
|
||||
{/* Google Icon */}
|
||||
<svg
|
||||
className="w-8 h-8 md:w-10 md:h-10"
|
||||
viewBox="0 0 48 48"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M44.5 20H24v8.5h11.8C34.7 33 30.1 36 24 36c-6.6 0-12-5.4-12-12s5.4-12 12-12c3 0 5.8 1.1 7.9 2.9l6.4-6.4C34.6 4.1 29.6 2 24 2 11.9 2 2 11.9 2 24s9.9 22 22 22c13 0 21-9 21-21 0-1.3-.2-2.7-.5-4z"
|
||||
fill="#4285F4"
|
||||
/>
|
||||
</svg>
|
||||
<div className={`flex flex-col ${textClassName}`}>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xl md:text-2xl font-bold">{rating.toFixed(1)}</span>
|
||||
<div className="flex gap-1">{renderStars()}</div>
|
||||
</div>
|
||||
<p className={`text-xs md:text-sm text-gray-600 dark:text-gray-400 ${reviewCountClassName}`}>
|
||||
{reviewCount} reviews on Google
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<a
|
||||
href={`https://www.google.com/search?q=${encodeURIComponent(businessName)} reviews`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-xs md:text-sm text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 underline mt-2 transition-colors"
|
||||
>
|
||||
View on Google
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user