fixd larg lines in HomePage , add cookies rather than localStorage,make canvas background Login page
This commit is contained in:
113
app/components/Background.js
Normal file
113
app/components/Background.js
Normal file
@ -0,0 +1,113 @@
|
||||
"use client";
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
export default function InteractiveBackground() {
|
||||
const canvasRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
const ctx = canvas.getContext("2d");
|
||||
|
||||
let animationFrameId;
|
||||
let width = (canvas.width = window.innerWidth);
|
||||
let height = (canvas.height = window.innerHeight);
|
||||
|
||||
// تتبع حركة الماوس
|
||||
const mouse = { x: null, y: null, radius: 180 };
|
||||
|
||||
const handleMouseMove = (e) => {
|
||||
mouse.x = e.clientX;
|
||||
mouse.y = e.clientY;
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
mouse.x = null;
|
||||
mouse.y = null;
|
||||
};
|
||||
|
||||
const handleResize = () => {
|
||||
width = canvas.width = window.innerWidth;
|
||||
height = canvas.height = window.innerHeight;
|
||||
};
|
||||
|
||||
window.addEventListener("mousemove", handleMouseMove);
|
||||
window.addEventListener("mouseleave", handleMouseLeave);
|
||||
window.addEventListener("resize", handleResize);
|
||||
|
||||
const particles = Array.from({ length: 70 }, () => ({
|
||||
x: Math.random() * width,
|
||||
y: Math.random() * height,
|
||||
vx: (Math.random() - 0.5) * 0.8,
|
||||
vy: (Math.random() - 0.5) * 0.8,
|
||||
radius: Math.random() * 2 + 1,
|
||||
}));
|
||||
|
||||
const draw = () => {
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
|
||||
particles.forEach((p, i) => {
|
||||
p.x += p.vx;
|
||||
p.y += p.vy;
|
||||
|
||||
if (p.x < 0 || p.x > width) p.vx *= -1;
|
||||
if (p.y < 0 || p.y > height) p.vy *= -1;
|
||||
|
||||
|
||||
if (mouse.x !== null && mouse.y !== null) {
|
||||
const dx = mouse.x - p.x;
|
||||
const dy = mouse.y - p.y;
|
||||
const dist = Math.hypot(dx, dy);
|
||||
|
||||
if (dist < mouse.radius) {
|
||||
const angle = Math.atan2(dy, dx);
|
||||
const force = (mouse.radius - dist) / mouse.radius;
|
||||
p.x -= Math.cos(angle) * force * 2;
|
||||
p.y -= Math.sin(angle) * force * 2;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(p.x, p.y, p.radius, 0, Math.PI * 2);
|
||||
ctx.fillStyle = "rgba(99, 102, 241, 0.7)";
|
||||
ctx.fill();
|
||||
|
||||
|
||||
for (let j = i + 1; j < particles.length; j++) {
|
||||
const p2 = particles[j];
|
||||
const dist = Math.hypot(p.x - p2.x, p.y - p2.y);
|
||||
if (dist < 140) {
|
||||
const opacity = 1 - dist / 140;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(p.x, p.y);
|
||||
ctx.lineTo(p2.x, p2.y);
|
||||
ctx.strokeStyle = `rgba(99, 102, 241, ${opacity * 0.25})`;
|
||||
ctx.lineWidth = 1;
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
animationFrameId = requestAnimationFrame(draw);
|
||||
};
|
||||
|
||||
draw();
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("mousemove", handleMouseMove);
|
||||
window.removeEventListener("mouseleave", handleMouseLeave);
|
||||
window.removeEventListener("resize", handleResize);
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 pointer-events-none z-0 bg-slate-950 overflow-hidden">
|
||||
<div className="absolute top-1/4 -left-20 w-96 h-96 bg-indigo-600/20 rounded-full blur-3xl animate-pulse" />
|
||||
<div className="absolute bottom-1/4 -right-20 w-96 h-96 bg-blue-600/20 rounded-full blur-3xl animate-pulse delay-1000" />
|
||||
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[500px] h-[500px] bg-purple-600/10 rounded-full blur-3xl" />
|
||||
<canvas ref={canvasRef} className="absolute inset-0 w-full h-full" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,14 +1,14 @@
|
||||
'use client';
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import L from 'leaflet';
|
||||
import 'leaflet/dist/leaflet.css';
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import L from "leaflet";
|
||||
import "leaflet/dist/leaflet.css";
|
||||
|
||||
delete L.Icon.Default.prototype._getIconUrl;
|
||||
L.Icon.Default.mergeOptions({
|
||||
iconRetinaUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon-2x.png',
|
||||
iconUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon.png',
|
||||
shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-shadow.png',
|
||||
iconRetinaUrl: "https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon-2x.png",
|
||||
iconUrl: "https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon.png",
|
||||
shadowUrl: "https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-shadow.png",
|
||||
});
|
||||
|
||||
export default function PropertyMapWithMarkers({ properties = [], onPropertyClick }) {
|
||||
@ -22,7 +22,7 @@ export default function PropertyMapWithMarkers({ properties = [], onPropertyClic
|
||||
|
||||
const map = L.map(mapRef.current).setView([33.5138, 38.9968], 7);
|
||||
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
|
||||
maxZoom: 19,
|
||||
}).addTo(map);
|
||||
@ -41,32 +41,32 @@ export default function PropertyMapWithMarkers({ properties = [], onPropertyClic
|
||||
useEffect(() => {
|
||||
if (!mapInstanceRef.current || !mapLoaded) return;
|
||||
|
||||
markersRef.current.forEach(marker => marker.remove());
|
||||
markersRef.current.forEach((marker) => marker.remove());
|
||||
markersRef.current = [];
|
||||
|
||||
properties.forEach(property => {
|
||||
properties.forEach((property) => {
|
||||
if (property.lat && property.lng) {
|
||||
const marker = L.marker([property.lat, property.lng]).addTo(mapInstanceRef.current);
|
||||
|
||||
const popupContent = `
|
||||
<div dir="rtl" style="text-align: right; padding: 12px; max-width: 250px;">
|
||||
<h3 style="font-weight: bold; font-size: 16px; margin-bottom: 8px; color: #111;">${property.title || 'عقار'}</h3>
|
||||
<p style="font-size: 14px; color: #666; margin-bottom: 8px;">${property.address || property.location?.address || ''}</p>
|
||||
<h3 style="font-weight: bold; font-size: 16px; margin-bottom: 8px; color: #111;">${property.title || "عقار"}</h3>
|
||||
<p style="font-size: 14px; color: #666; margin-bottom: 8px;">${property.address || property.location?.address || ""}</p>
|
||||
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px;">
|
||||
<span style="font-weight: bold; font-size: 18px; color: #d97706;">${formatPrice(property)}</span>
|
||||
</div>
|
||||
${property.images && property.images.length > 0 ? `<img src="${property.images[0]}" alt="${property.title}" style="width: 100%; height: 96px; object-fit: cover; border-radius: 8px; margin-bottom: 8px;" onerror="this.src='/property-placeholder.jpg'" />` : ''}
|
||||
${property.images && property.images.length > 0 ? `<img src="${property.images[0] ?? "https://tse4.mm.bing.net/th/id/OIP.-VT-J9Brp8KDSlb0Z_990wAAAA?r=0&rs=1&pid=ImgDetMain&o=7&rm=3"}" alt="${property.title}" style="width: 100%; height: 96px; object-fit: cover; border-radius: 8px; margin-bottom: 8px;" onerror="this.src='/property-placeholder.jpg'" />` : ""}
|
||||
<div style="font-size: 12px; color: #888;">
|
||||
${property.type ? `<p>النوع: ${property.type}</p>` : ''}
|
||||
${property.bedrooms > 0 ? `<p>غرف نوم: ${property.bedrooms}</p>` : ''}
|
||||
${property.bathrooms > 0 ? `<p>حمامات: ${property.bathrooms}</p>` : ''}
|
||||
${property.type ? `<p>النوع: ${property.type}</p>` : ""}
|
||||
${property.bedrooms > 0 ? `<p>غرف نوم: ${property.bedrooms}</p>` : ""}
|
||||
${property.bathrooms > 0 ? `<p>حمامات: ${property.bathrooms}</p>` : ""}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
marker.bindPopup(popupContent);
|
||||
|
||||
marker.on('click', () => {
|
||||
marker.on("click", () => {
|
||||
if (onPropertyClick) {
|
||||
onPropertyClick(property);
|
||||
}
|
||||
@ -83,9 +83,9 @@ export default function PropertyMapWithMarkers({ properties = [], onPropertyClic
|
||||
}, [properties, mapLoaded]);
|
||||
|
||||
const formatPrice = (property) => {
|
||||
if (property.priceUnit === 'monthly') {
|
||||
if (property.priceUnit === "monthly") {
|
||||
return `${property.price?.toLocaleString() || 0} ل.س/شهر`;
|
||||
} else if (property.priceUnit === 'daily') {
|
||||
} else if (property.priceUnit === "daily") {
|
||||
return `${property.price?.toLocaleString() || 0} ل.س/يوم`;
|
||||
} else {
|
||||
return `${property.price?.toLocaleString() || 0} ل.س`;
|
||||
@ -97,4 +97,4 @@ export default function PropertyMapWithMarkers({ properties = [], onPropertyClic
|
||||
<div ref={mapRef} className="w-full h-full z-0" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
28
app/components/home/ArrowForScroll.js
Normal file
28
app/components/home/ArrowForScroll.js
Normal file
@ -0,0 +1,28 @@
|
||||
import { motion } from "framer-motion";
|
||||
export default function ArrowForScroll() {
|
||||
return (
|
||||
<>
|
||||
<motion.div
|
||||
className="absolute bottom-8 left-1/2 transform -translate-x-1/2 cursor-pointer"
|
||||
animate={{
|
||||
y: [0, 10, 0],
|
||||
}}
|
||||
transition={{
|
||||
duration: 1.5,
|
||||
repeat: Infinity,
|
||||
ease: "easeInOut",
|
||||
}}
|
||||
onClick={() =>
|
||||
window.scrollTo({
|
||||
top: window.innerHeight,
|
||||
behavior: "smooth",
|
||||
})
|
||||
}
|
||||
>
|
||||
<svg className="w-6 h-6 text-amber-50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 14l-7 7m0 0l-7-7m7 7V3" />
|
||||
</svg>
|
||||
</motion.div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
21
app/components/home/FeatureSection.js
Normal file
21
app/components/home/FeatureSection.js
Normal file
@ -0,0 +1,21 @@
|
||||
import { Lock, ShieldCheck, Zap } from "lucide-react";
|
||||
import FeaturesOurCompany from "./FeaturesOurCompany";
|
||||
import { motion } from "framer-motion";
|
||||
import { useTranslation } from "react-i18next";
|
||||
export default function FeatureSection() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<>
|
||||
<motion.div initial={{ opacity: 0, y: 20 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: false }} transition={{ duration: 0.6 }} className="text-center mt-5">
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-gray-900 mb-4 tracking-tight">{t("whyChooseUsTitle")}</h2>
|
||||
<p className="text-gray-600 max-w-2xl mx-auto text-lg">{t("whyChooseUsSubtitle")}</p>
|
||||
</motion.div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mt-5 p-5">
|
||||
<FeaturesOurCompany icon={<ShieldCheck className="w-6 h-6 text-amber-600" />} title={t("feature1Title")} discripction={t("feature1Description")} />
|
||||
<FeaturesOurCompany icon={<Lock className="w-6 h-6 text-blue-600" />} title={t("feature2Title")} discripction={t("feature2Description")} />
|
||||
<FeaturesOurCompany icon={<Zap className="w-6 h-6 text-green-600" />} title={t("feature3Title")} discripction={t("feature3Description")} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
21
app/components/home/FeaturesOurCompany.js
Normal file
21
app/components/home/FeaturesOurCompany.js
Normal file
@ -0,0 +1,21 @@
|
||||
import { motion } from "framer-motion";
|
||||
export default function FeaturesOurCompany({ icon, title, discripction }) {
|
||||
return (
|
||||
<>
|
||||
<motion.div
|
||||
className="group bg-white p-6 rounded-xl shadow-sm hover:shadow-md transition-all duration-300 border border-gray-100"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: false }}
|
||||
transition={{ duration: 0.5, delay: 0.1 }}
|
||||
whileHover={{ y: -4 }}
|
||||
>
|
||||
<div className="flex items-center gap-4 mb-4">
|
||||
<div className="w-12 h-12 bg-amber-100 rounded-xl flex items-center justify-center group-hover:bg-amber-200 transition-colors duration-300">{icon}</div>
|
||||
<h3 className="text-lg font-bold text-gray-900">{title}</h3>
|
||||
</div>
|
||||
<p className="text-gray-600 text-sm leading-relaxed">{discripction}</p>
|
||||
</motion.div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
32
app/components/home/FilterSelect.js
Normal file
32
app/components/home/FilterSelect.js
Normal file
@ -0,0 +1,32 @@
|
||||
export default function FilterSelect({ onChange, icon, value, option, label }) {
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-amber-50 mb-2">
|
||||
<div className="flex items-center gap-1">
|
||||
{icon??""}
|
||||
{label}
|
||||
</div>
|
||||
</label>
|
||||
<select
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
className="w-full px-4 py-3 bg-white/90 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-amber-500 text-sm appearance-none cursor-pointer"
|
||||
style={{
|
||||
backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%23666'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E")`,
|
||||
backgroundRepeat: "no-repeat",
|
||||
backgroundPosition: "left 1rem center",
|
||||
backgroundSize: "1rem",
|
||||
paddingLeft: "2.5rem",
|
||||
}}
|
||||
>
|
||||
{option.map((opt) => (
|
||||
<option key={opt.id} value={opt.id}>
|
||||
{opt.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -1,321 +1,117 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { motion } from 'framer-motion';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Search, MapPin, Home, DollarSign, ShieldCheck } from 'lucide-react';
|
||||
|
||||
export default function HeroSearch({ onSearch, isAuthenticated }) {
|
||||
"use client";
|
||||
import { useState } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Search, MapPin, Home, DollarSign } from "lucide-react";
|
||||
import { useFilterOptions } from "@/app/hooks/UseFilterOfHeroSearch";
|
||||
import ShowLoginDialog from "./showLoginDialog";
|
||||
import FilterSelect from "./FilterSelect";
|
||||
export default function HeroSearch({ onSearch }) {
|
||||
const { t } = useTranslation();
|
||||
const [activeTab, setActiveTab] = useState('buy');
|
||||
const { cities, identityTypes, ownerSources, priceRanges, propertyTypes, rentPeriods } = useFilterOptions();
|
||||
const [activeTab, setActiveTab] = useState("buy");
|
||||
const [filters, setFilters] = useState({
|
||||
city: 'all',
|
||||
propertyType: 'all',
|
||||
priceRange: 'all',
|
||||
identityType: 'syrian',
|
||||
ownerSource: 'all',
|
||||
rentPeriod: 'all',
|
||||
availableToday: false
|
||||
city: "all",
|
||||
propertyType: "all",
|
||||
priceRange: "all",
|
||||
identityType: "syrian",
|
||||
ownerSource: "all",
|
||||
rentPeriod: "all",
|
||||
availableToday: false,
|
||||
});
|
||||
const [showLoginDialog, setShowLoginDialog] = useState(false);
|
||||
|
||||
const cities = [
|
||||
{ id: 'all', label: t('allCities') },
|
||||
{ id: 'دمشق', label: t('damascus') },
|
||||
{ id: 'حلب', label: t('aleppo') },
|
||||
{ id: 'حمص', label: t('homs') },
|
||||
{ id: 'اللاذقية', label: t('latakia') },
|
||||
{ id: 'درعا', label: t('daraa') }
|
||||
];
|
||||
|
||||
const propertyTypes = [
|
||||
{ id: 'all', label: t('all') },
|
||||
{ id: 'apartment', label: t('residentialApartments') },
|
||||
{ id: 'studio', label: t('studio') },
|
||||
{ id: 'commercial', label: t('commercialProperty') },
|
||||
{ id: 'villa', label: t('villaFarm') }
|
||||
];
|
||||
|
||||
const priceRanges = [
|
||||
{ id: 'all', label: t('allPrices') },
|
||||
{ id: '0-500', label: t('priceRange1') },
|
||||
{ id: '500-1000', label: t('priceRange2') },
|
||||
{ id: '1000-2000', label: t('priceRange3') },
|
||||
{ id: '2000-3000', label: t('priceRange4') },
|
||||
{ id: '3000+', label: t('priceRange5') }
|
||||
];
|
||||
|
||||
const identityTypes = [
|
||||
{ id: 'syrian', label: t('syrianId') },
|
||||
{ id: 'passport', label: t('passport') }
|
||||
];
|
||||
|
||||
const ownerSources = [
|
||||
{ id: 'all', label: t('all') },
|
||||
{ id: 'owner', label: t('fromOwner') },
|
||||
{ id: 'agency', label: t('fromAgency') }
|
||||
];
|
||||
|
||||
const rentPeriods = [
|
||||
{ id: 'all', label: t('all') },
|
||||
{ id: 'daily', label: t('dailyRent') },
|
||||
{ id: 'monthly', label: t('monthlyRent') }
|
||||
];
|
||||
|
||||
const handleTabClick = (tab) => {
|
||||
setActiveTab(tab);
|
||||
if ((tab === 'rent' || tab === 'sell') && !isAuthenticated) {
|
||||
setShowLoginDialog(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearch = () => {
|
||||
if ((activeTab === 'rent' || activeTab === 'sell') && !isAuthenticated) {
|
||||
setShowLoginDialog(true);
|
||||
return;
|
||||
}
|
||||
|
||||
onSearch({
|
||||
...filters,
|
||||
mode: activeTab,
|
||||
city: filters.city || 'all',
|
||||
propertyType: filters.propertyType || 'all',
|
||||
priceRange: filters.priceRange || 'all',
|
||||
ownerSource: filters.ownerSource || 'all',
|
||||
rentPeriod: filters.rentPeriod || 'all'
|
||||
city: filters.city || "all",
|
||||
propertyType: filters.propertyType || "all",
|
||||
priceRange: filters.priceRange || "all",
|
||||
ownerSource: filters.ownerSource || "all",
|
||||
rentPeriod: filters.rentPeriod || "all",
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<motion.div
|
||||
className="bg-white/10 backdrop-blur-lg rounded-2xl p-6 sm:p-8 border border-white/20 shadow-2xl"
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, delay: 0.5 }}
|
||||
>
|
||||
<div className="flex flex-wrap gap-2 mb-8">
|
||||
{['rent', 'buy', 'sell'].map((tab) => (
|
||||
<motion.button
|
||||
key={tab}
|
||||
onClick={() => handleTabClick(tab)}
|
||||
className={`px-4 py-2 rounded-lg font-medium text-sm transition-all ${
|
||||
activeTab === tab
|
||||
? 'bg-amber-500 text-white'
|
||||
: 'bg-white/20 text-white hover:bg-white/30'
|
||||
}`}
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
{t(`${tab}Tab`)}
|
||||
</motion.button>
|
||||
))}
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-white mb-2">
|
||||
<div className="flex items-center gap-1">
|
||||
<MapPin className="w-4 h-4" />
|
||||
{t("cityStreetLabel")}
|
||||
</div>
|
||||
</label>
|
||||
<select
|
||||
value={filters.city}
|
||||
onChange={(e) => setFilters({...filters, city: e.target.value})}
|
||||
className="w-full px-4 py-3 bg-white/90 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-amber-500 text-sm appearance-none cursor-pointer"
|
||||
style={{
|
||||
backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%23666'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E")`,
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundPosition: 'left 1rem center',
|
||||
backgroundSize: '1rem',
|
||||
paddingLeft: '2.5rem'
|
||||
}}
|
||||
>
|
||||
{cities.map(city => (
|
||||
<option key={city.id} value={city.id}>{city.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
{activeTab === 'rent' && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-white mb-2">
|
||||
<div className="flex items-center gap-1">
|
||||
<Home className="w-4 h-4" />
|
||||
{t("rentTypeLabel")}
|
||||
</div>
|
||||
</label>
|
||||
<select
|
||||
value={filters.propertyType}
|
||||
onChange={(e) => setFilters({...filters, propertyType: e.target.value})}
|
||||
className="w-full px-4 py-3 bg-white/90 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-amber-500 text-sm appearance-none cursor-pointer"
|
||||
style={{
|
||||
backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%23666'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E")`,
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundPosition: 'left 1rem center',
|
||||
backgroundSize: '1rem',
|
||||
paddingLeft: '2.5rem'
|
||||
}}
|
||||
>
|
||||
{propertyTypes.map(type => (
|
||||
<option key={type.id} value={type.id}>{type.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-white mb-2">
|
||||
<div className="flex items-center gap-1">
|
||||
<DollarSign className="w-4 h-4" />
|
||||
{t("priceLabel")}
|
||||
</div>
|
||||
</label>
|
||||
<select
|
||||
value={filters.priceRange}
|
||||
onChange={(e) => setFilters({...filters, priceRange: e.target.value})}
|
||||
className="w-full px-4 py-3 bg-white/90 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-amber-500 text-sm appearance-none cursor-pointer"
|
||||
style={{
|
||||
backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%23666'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E")`,
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundPosition: 'left 1rem center',
|
||||
backgroundSize: '1rem',
|
||||
paddingLeft: '2.5rem'
|
||||
}}
|
||||
>
|
||||
{priceRanges.map(range => (
|
||||
<option key={range.id} value={range.id}>{range.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-white mb-2">
|
||||
<div className="flex items-center gap-1">
|
||||
{t("identityTypeLabel")}
|
||||
</div>
|
||||
</label>
|
||||
<select
|
||||
value={filters.identityType}
|
||||
onChange={(e) => setFilters({...filters, identityType: e.target.value})}
|
||||
className="w-full px-4 py-3 bg-white/90 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-amber-500 text-sm appearance-none cursor-pointer"
|
||||
style={{
|
||||
backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%23666'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E")`,
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundPosition: 'left 1rem center',
|
||||
backgroundSize: '1rem',
|
||||
paddingLeft: '2.5rem'
|
||||
}}
|
||||
>
|
||||
{identityTypes.map(type => (
|
||||
<option key={type.id} value={type.id}>{type.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mt-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-white mb-2">{t("ownerSourceLabel")}</label>
|
||||
<select
|
||||
value={filters.ownerSource}
|
||||
onChange={(e) => setFilters({ ...filters, ownerSource: e.target.value })}
|
||||
className="w-full px-4 py-3 bg-white/90 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-amber-500 text-sm appearance-none cursor-pointer"
|
||||
style={{
|
||||
backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%23666'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E")`,
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundPosition: 'left 1rem center',
|
||||
backgroundSize: '1rem',
|
||||
paddingLeft: '2.5rem'
|
||||
}}
|
||||
>
|
||||
{ownerSources.map((source) => (
|
||||
<option key={source.id} value={source.id}>
|
||||
{source.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<motion.div
|
||||
className="bg-white/10 backdrop-blur-lg rounded-2xl p-6 sm:p-8 border border-white/20 shadow-2xl"
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, delay: 0.5 }}
|
||||
>
|
||||
<div className="flex flex-wrap gap-2 mb-8">
|
||||
{["rent", "buy", "sell"].map((tab) => {
|
||||
const isActive = activeTab === tab;
|
||||
|
||||
{activeTab === 'rent' && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-white mb-2">{t("rentTypeLabel")}</label>
|
||||
<select
|
||||
value={filters.rentPeriod}
|
||||
onChange={(e) => setFilters({ ...filters, rentPeriod: e.target.value })}
|
||||
className="w-full px-4 py-3 bg-white/90 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-amber-500 text-sm appearance-none cursor-pointer"
|
||||
style={{
|
||||
backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%23666'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E")`,
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundPosition: 'left 1rem center',
|
||||
backgroundSize: '1rem',
|
||||
paddingLeft: '2.5rem'
|
||||
}}
|
||||
>
|
||||
{rentPeriods.map((period) => (
|
||||
<option key={period.id} value={period.id}>
|
||||
{period.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={`${activeTab === 'rent' ? 'md:col-span-2' : 'md:col-span-3'} flex flex-col justify-between p-4 rounded-2xl border border-dashed border-white/30 bg-white/5`}>
|
||||
<label className="mt-4 flex items-center gap-3 text-white text-sm">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={filters.availableToday}
|
||||
onChange={(e) => setFilters({ ...filters, availableToday: e.target.checked })}
|
||||
className="w-5 h-5 text-amber-500 rounded border-gray-300 bg-white"
|
||||
/>
|
||||
<span className="font-medium">{t("availableToday")}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<motion.button
|
||||
onClick={handleSearch}
|
||||
className="w-full bg-amber-500 hover:bg-amber-600 text-white font-bold py-4 px-6 rounded-xl transition-all duration-300 flex items-center justify-center text-base gap-3 shadow-lg hover:shadow-xl"
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
>
|
||||
<Search className="w-5 h-5" />
|
||||
{t("searchButton")}
|
||||
</motion.button>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{showLoginDialog && !isAuthenticated && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 px-4 py-8">
|
||||
<div className="w-full max-w-md rounded-3xl bg-white p-6 shadow-2xl border border-gray-200">
|
||||
<div className="flex items-center gap-3 mb-5">
|
||||
<ShieldCheck className="w-7 h-7 text-amber-500" />
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-900">{t("loginRequired")}</h3>
|
||||
<p className="text-sm text-gray-600">{t("loginRequiredDesc")}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<div className="rounded-2xl bg-gray-50 p-4">
|
||||
<p className="text-sm text-gray-700">{t("loginPrompt")}</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:justify-end">
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowLoginDialog(false)}
|
||||
className="w-full sm:w-auto px-5 py-3 rounded-xl border border-gray-300 text-gray-700 hover:bg-gray-100 transition-colors"
|
||||
key={tab}
|
||||
onClick={() => handleTabClick(tab)}
|
||||
className={`relative px-5 py-2.5 rounded-lg font-medium text-sm transition-colors duration-200 outline-none select-none ${isActive ? "text-white" : "text-amber-50 hover:text-white"}`}
|
||||
>
|
||||
{t("close")}
|
||||
{isActive && <motion.div layoutId="activeTabBackground" className="absolute inset-0 bg-amber-500 rounded-lg shadow-md" transition={{ type: "spring", stiffness: 500, damping: 35 }} />}
|
||||
<span className="relative z-10">{t(`${tab}Tab`)}</span>
|
||||
</button>
|
||||
<Link
|
||||
href="/login"
|
||||
className="w-full sm:w-auto px-5 py-3 rounded-xl bg-amber-500 text-white font-semibold text-center hover:bg-amber-600 transition-colors"
|
||||
>
|
||||
{t("login")}
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{/* city */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<FilterSelect icon={<MapPin className="w-4 h-4" />} value={filters.city} onChange={(e) => setFilters({ ...filters, city: e.target.value })} option={cities} label={t(t("cityStreetLabel"))} />
|
||||
{activeTab === "rent" && (
|
||||
<FilterSelect
|
||||
icon={<Home className="w-4 h-4" />}
|
||||
value={filters.propertyType}
|
||||
onChange={(e) => setFilters({ ...filters, city: e.target.value })}
|
||||
option={propertyTypes}
|
||||
label={t("rentTypeLabel")}
|
||||
/>
|
||||
)}
|
||||
{/* price */}
|
||||
<FilterSelect
|
||||
icon={<DollarSign className="w-4 h-4" />}
|
||||
value={filters.priceRange}
|
||||
onChange={(e) => setFilters({ ...filters, priceRange: e.target.value })}
|
||||
option={priceRanges}
|
||||
label={t("priceLabel")}
|
||||
/>
|
||||
{/* type */}
|
||||
<FilterSelect value={filters.identityType} onChange={(e) => setFilters({ ...filters, identityType: e.target.value })} option={identityTypes} label={t("identityTypeLabel")} />
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mt-4">
|
||||
{/* owener */}
|
||||
<FilterSelect value={filters.ownerSource} onChange={(e) => setFilters({ ...filters, ownerSource: e.target.value })} option={ownerSources} label={t("ownerSourceLabel")} />
|
||||
{activeTab === "rent" && <FilterSelect value={filters.rentPeriod} onChange={(e) => setFilters({ ...filters, rentPeriod: e.target.value })} option={rentPeriods} label={t("rentTypeLabel")} />}
|
||||
<div className={`${activeTab === "rent" ? "md:col-span-2" : "md:col-span-3"} flex flex-col justify-between p-4 rounded-2xl border border-dashed border-white/30 bg-white/5`}>
|
||||
{/* checkbox */}
|
||||
<label className="mt-4 flex items-center gap-3 text-amber-50 text-sm">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={filters.availableToday}
|
||||
onChange={(e) => setFilters({ ...filters, availableToday: e.target.checked })}
|
||||
className="w-5 h-5 text-amber-500 rounded border-gray-300 bg-white"
|
||||
/>
|
||||
<span className="font-medium">{t("availableToday")}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-6">
|
||||
<motion.button
|
||||
onClick={handleSearch}
|
||||
className="w-full bg-amber-500 hover:bg-amber-600 text-white font-bold py-4 px-6 rounded-xl transition-all duration-300 flex items-center justify-center text-base gap-3 shadow-lg hover:shadow-xl"
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
>
|
||||
<Search className="w-5 h-5" />
|
||||
{t("searchButton")}
|
||||
</motion.button>
|
||||
</div>
|
||||
</motion.div>
|
||||
{showLoginDialog && !isAuthenticated && <ShowLoginDialog setShowLoginDialog={setShowLoginDialog} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
54
app/components/home/HeroSection.js
Normal file
54
app/components/home/HeroSection.js
Normal file
@ -0,0 +1,54 @@
|
||||
import { motion } from 'framer-motion';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
export default function HeroSection() {
|
||||
const {t} =useTranslation()
|
||||
return (
|
||||
<>
|
||||
<motion.div
|
||||
className="text-center mb-12"
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
variants={{
|
||||
hidden: { opacity: 0 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
transition: { staggerChildren: 0.2 },
|
||||
},
|
||||
}}
|
||||
>
|
||||
<motion.h1
|
||||
className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-bold text-amber-50 mb-6 leading-tight tracking-tight"
|
||||
variants={{
|
||||
hidden: { opacity: 0, y: 20 },
|
||||
visible: { opacity: 1, y: 0 },
|
||||
}}
|
||||
>
|
||||
{t("heroTitleLine1")}
|
||||
<br />
|
||||
<motion.span
|
||||
className="text-amber-300"
|
||||
animate={{
|
||||
y: [0, -10, 0],
|
||||
}}
|
||||
transition={{
|
||||
duration: 2,
|
||||
repeat: Infinity,
|
||||
ease: "easeInOut",
|
||||
}}
|
||||
>
|
||||
{t("heroTitleLine2")}
|
||||
</motion.span>
|
||||
</motion.h1>
|
||||
<motion.p
|
||||
className="text-base sm:text-lg text-amber-50 max-w-2xl mx-auto leading-relaxed"
|
||||
variants={{
|
||||
hidden: { opacity: 0, y: 20 },
|
||||
visible: { opacity: 1, y: 0 },
|
||||
}}
|
||||
>
|
||||
{t("heroSubtitle")}
|
||||
</motion.p>
|
||||
</motion.div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
73
app/components/home/HomeClient.jsx
Normal file
73
app/components/home/HomeClient.jsx
Normal file
@ -0,0 +1,73 @@
|
||||
"use client";
|
||||
import { useState, useRef } from "react";
|
||||
import { AnimatePresence } from "framer-motion";
|
||||
import HeroSearch from "./HeroSearch";
|
||||
import ShowIfOwner from "./ShowIfOwner";
|
||||
import MapSection from "./MapSection";
|
||||
import useApplyFilters from "../../hooks/useApplyFilters";
|
||||
import HeroSection from "./HeroSection";
|
||||
import FeatureSection from "./FeatureSection";
|
||||
import ArrowForScroll from "./ArrowForScroll";
|
||||
import useAuth from "@/app/hooks/useAuth";
|
||||
|
||||
export default function HomeClient() {
|
||||
const mapSectionRef = useRef(null);
|
||||
const [showMap, setShowMap] = useState(false);
|
||||
const [isScrolling, setIsScrolling] = useState(false);
|
||||
|
||||
const { filteredProperties, applyFilters, resetFilters } = useApplyFilters();
|
||||
const { name, isOwner, isAuthenticated, role } = useAuth();
|
||||
console.log(isOwner, name, isAuthenticated, role);
|
||||
const handleSearch = (filters) => {
|
||||
applyFilters(filters);
|
||||
if (!showMap) {
|
||||
setShowMap(true);
|
||||
setTimeout(() => {
|
||||
scrollToMap();
|
||||
}, 300);
|
||||
} else {
|
||||
scrollToMap();
|
||||
}
|
||||
};
|
||||
const scrollToMap = () => {
|
||||
if (mapSectionRef.current) {
|
||||
setIsScrolling(true);
|
||||
mapSectionRef.current.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "center",
|
||||
});
|
||||
setTimeout(() => setIsScrolling(false), 1000);
|
||||
}
|
||||
};
|
||||
const resetSearch = () => {
|
||||
setShowMap(false);
|
||||
resetFilters();
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
behavior: "smooth",
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<section className="relative min-h-screen flex items-center justify-center overflow-hidden">
|
||||
<div className="relative z-10 container mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<HeroSection />
|
||||
{!isOwner && <HeroSearch onSearch={handleSearch} />}
|
||||
{isOwner && <ShowIfOwner name={name} />}
|
||||
</div>
|
||||
</div>
|
||||
{!showMap && !isOwner && <ArrowForScroll />}
|
||||
</section>
|
||||
{!isOwner && (
|
||||
<>
|
||||
<AnimatePresence mode="wait">
|
||||
{showMap && <MapSection mapSectionRef={mapSectionRef} isScrolling={isScrolling} resetSearch={resetSearch} filteredProperties={filteredProperties} />}
|
||||
</AnimatePresence>
|
||||
</>
|
||||
)}
|
||||
<FeatureSection />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
80
app/components/home/MapSection.js
Normal file
80
app/components/home/MapSection.js
Normal file
@ -0,0 +1,80 @@
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
import PropertyMapWithMarkers from "../PropertyMapWithMarkers";
|
||||
export default function MapSection({ mapSectionRef, isScrolling, resetSearch, filteredProperties }) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<>
|
||||
<motion.section
|
||||
ref={mapSectionRef}
|
||||
initial={{ opacity: 0, y: 50 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -50 }}
|
||||
transition={{
|
||||
type: "spring",
|
||||
damping: 20,
|
||||
stiffness: 100,
|
||||
duration: 0.6,
|
||||
}}
|
||||
className="py-12 bg-gray-50 relative"
|
||||
>
|
||||
{isScrolling && <motion.div className="absolute top-0 left-0 right-0 h-1 bg-amber-500 z-10" initial={{ scaleX: 0 }} animate={{ scaleX: 1 }} transition={{ duration: 1, ease: "easeInOut" }} />}
|
||||
|
||||
<div className="container mx-auto px-4">
|
||||
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.2 }} className="text-center mb-8">
|
||||
<div className="flex items-center justify-center gap-4 mb-2">
|
||||
<h2 className="text-3xl font-bold text-gray-900">{filteredProperties.length > 0 ? t("searchResults") : t("no-properties")}</h2>
|
||||
<motion.button
|
||||
onClick={resetSearch}
|
||||
className="px-4 py-2 bg-white border border-gray-300 rounded-full text-sm font-medium text-gray-700 hover:bg-gray-50 shadow-sm flex items-center gap-2"
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 19l-7-7m0 0l7-7m-7 7h18" />
|
||||
</svg>
|
||||
{t("newSearch")}
|
||||
</motion.button>
|
||||
</div>
|
||||
|
||||
{filteredProperties.length > 0 ? (
|
||||
<p className="text-gray-600">{t("foundPropertiesCount", { count: filteredProperties.length })}</p>
|
||||
) : (
|
||||
<p className="text-gray-600">{t("noPropertiesMatchFilters")}</p>
|
||||
)}
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
className="bg-white rounded-2xl shadow-xl overflow-hidden border border-gray-200"
|
||||
initial={{ scale: 0.95, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
transition={{ delay: 0.3, type: "spring" }}
|
||||
>
|
||||
{filteredProperties.length > 0 ? (
|
||||
<PropertyMapWithMarkers
|
||||
properties={filteredProperties.map((p) => ({
|
||||
...p,
|
||||
lat: p.location.lat,
|
||||
lng: p.location.lng,
|
||||
address: p.location.address,
|
||||
}))}
|
||||
onPropertyClick={() => {}}
|
||||
/>
|
||||
) : (
|
||||
<div className="h-[400px] flex flex-col items-center justify-center bg-gray-50">
|
||||
<div className="w-24 h-24 bg-gray-200 rounded-full flex items-center justify-center mb-4">
|
||||
<svg className="w-12 h-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-gray-700 mb-2">{t("no-properties")}</h3>
|
||||
<p className="text-gray-500">{t("try-changing-search-criteria")}</p>
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
</div>
|
||||
</motion.section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
26
app/components/home/ShowIfOwner.js
Normal file
26
app/components/home/ShowIfOwner.js
Normal file
@ -0,0 +1,26 @@
|
||||
import { motion } from "framer-motion";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Link from "next/link";
|
||||
import { Building } from "lucide-react";
|
||||
export default function ShowIfOwner({ name }) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.5 }}
|
||||
className="bg-white/10 backdrop-blur-lg rounded-2xl p-8 text-center border border-white/20"
|
||||
>
|
||||
<h2 className="text-2xl font-bold text-amber-50 mb-2">
|
||||
{t("ownerGreeting")} {name ?? " notFound "}!
|
||||
</h2>
|
||||
<p className="text-amber-50/80 mb-4">{t("ownerDescription")}</p>
|
||||
<Link href="/owner/properties" className="inline-flex items-center gap-2 bg-amber-500 text-white px-6 py-3 rounded-xl font-medium hover:bg-amber-600 transition-colors">
|
||||
<Building className="w-5 h-5" />
|
||||
{t("manageMyProperties")}
|
||||
</Link>
|
||||
</motion.div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
38
app/components/home/showLoginDialog.js
Normal file
38
app/components/home/showLoginDialog.js
Normal file
@ -0,0 +1,38 @@
|
||||
import { ShieldCheck } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useTranslation } from "react-i18next";
|
||||
export default function ShowLoginDialog({setShowLoginDialog}) {
|
||||
const {t}=useTranslation()
|
||||
return (
|
||||
<>
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 px-4 py-8">
|
||||
<div className="w-full max-w-md rounded-3xl bg-white p-6 shadow-2xl border border-gray-200">
|
||||
<div className="flex items-center gap-3 mb-5">
|
||||
<ShieldCheck className="w-7 h-7 text-amber-500" />
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-900">{t("loginRequired")}</h3>
|
||||
<p className="text-sm text-gray-600">{t("loginRequiredDesc")}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<div className="rounded-2xl bg-gray-50 p-4">
|
||||
<p className="text-sm text-gray-700">{t("loginPrompt")}</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:justify-end">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowLoginDialog(false)}
|
||||
className="w-full sm:w-auto px-5 py-3 rounded-xl border border-gray-300 text-gray-700 hover:bg-gray-100 transition-colors"
|
||||
>
|
||||
{t("close")}
|
||||
</button>
|
||||
<Link href="/login" className="w-full sm:w-auto px-5 py-3 rounded-xl bg-amber-500 text-white font-semibold text-center hover:bg-amber-600 transition-colors">
|
||||
{t("login")}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user