banner of desktop completed
This commit is contained in:
@@ -38,6 +38,7 @@
|
||||
"react": "19.2.3",
|
||||
"react-day-picker": "^9.13.0",
|
||||
"react-dom": "19.2.3",
|
||||
"react-hook-form": "^7.71.1",
|
||||
"react-hot-toast": "^2.6.0",
|
||||
"tailwind-merge": "^3.2.0",
|
||||
"tailwind-scrollbar": "^4.0.2",
|
||||
|
||||
9821
pnpm-lock.yaml
generated
9821
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
20
public/icons/marker-preview.svg
Normal file
20
public/icons/marker-preview.svg
Normal file
@@ -0,0 +1,20 @@
|
||||
<svg width="30" height="35" viewBox="0 0 30 35" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.4453 1.5127C24.4711 1.08082 31.4173 11.6767 27.2842 20.9209L27.0752 21.3672C25.9614 23.6466 24.4616 25.0302 23.0117 26.2031C21.6264 27.3238 20.4204 28.1673 19.3184 29.4482L19.0986 29.71C18.9038 29.9504 18.5282 30.5355 18.0273 31.2695C17.8006 31.6018 17.5566 31.95 17.332 32.2393C17.2202 32.3833 17.0974 32.5329 16.9727 32.666C16.8723 32.7731 16.6754 32.9748 16.4141 33.124L16.4131 33.123C15.7088 33.5252 14.941 33.6027 14.2256 33.3691C13.5676 33.1543 13.0766 32.7195 12.7383 32.3145V32.3135C12.3056 31.795 11.9245 31.2168 11.5986 30.7314C11.3403 30.3467 11.107 30.0062 10.8682 29.7012L10.626 29.4072C9.78615 28.4447 8.79047 27.6829 7.65723 26.7939C6.63272 25.9903 5.49745 25.0829 4.54102 23.9053L4.35254 23.666C-2.23923 15.0669 3.27657 2.37649 13.958 1.54199L14.4453 1.5127ZM14.4834 10.8291H14.4795C11.4118 11.1538 9.65384 14.8452 11.3496 17.6201L11.5254 17.8857L11.6885 18.1016C13.3725 20.1841 16.6502 20.1828 18.334 18.1006L18.4961 17.8857C20.762 14.6868 18.2266 10.4356 14.4834 10.8291Z" stroke="white" stroke-width="3"/>
|
||||
<foreignObject x="-62" y="-62" width="154" height="159"></foreignObject><g filter="url(#filter0_d_225_89)" data-figma-bg-blur-radius="65">
|
||||
<path d="M14.0746 3.0373C23.3145 2.3154 29.8796 12.2097 25.7277 20.7091C23.782 24.6909 20.5139 25.583 17.9337 28.7657C17.4406 29.3741 16.126 31.5614 15.6708 31.8213C14.974 32.2197 14.3495 31.903 13.8898 31.3525C13.1462 30.4615 12.5552 29.3355 11.756 28.4207C9.85145 26.2379 7.27127 25.0075 5.54262 22.7533C-0.347943 15.0689 4.61651 3.77623 14.0746 3.0373ZM14.325 9.33689C9.9238 9.79999 7.70872 15.095 10.3011 18.7534C12.5786 21.9678 17.444 21.9644 19.7203 18.7534C22.7123 14.5298 19.3874 8.80455 14.3261 9.33689H14.325Z" fill="currentColor"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_d_225_89" x="-62" y="-62" width="154" height="159" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="1"/>
|
||||
<feGaussianBlur stdDeviation="1"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.235294 0 0 0 0 0.25098 0 0 0 0 0.262745 0 0 0 0.3 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_225_89"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_225_89" result="shape"/>
|
||||
</filter>
|
||||
<clipPath id="bgblur_0_225_89_clip_path" transform="translate(62 62)"><path d="M14.0746 3.0373C23.3145 2.3154 29.8796 12.2097 25.7277 20.7091C23.782 24.6909 20.5139 25.583 17.9337 28.7657C17.4406 29.3741 16.126 31.5614 15.6708 31.8213C14.974 32.2197 14.3495 31.903 13.8898 31.3525C13.1462 30.4615 12.5552 29.3355 11.756 28.4207C9.85145 26.2379 7.27127 25.0075 5.54262 22.7533C-0.347943 15.0689 4.61651 3.77623 14.0746 3.0373ZM14.325 9.33689C9.9238 9.79999 7.70872 15.095 10.3011 18.7534C12.5786 21.9678 17.444 21.9644 19.7203 18.7534C22.7123 14.5298 19.3874 8.80455 14.3261 9.33689H14.325Z"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.2 KiB |
@@ -11,6 +11,7 @@ import ToastProvider from "@/utils/ToastProvider";
|
||||
import {Sidebar} from "@/components/SideBar";
|
||||
import {AppShell} from "@/providers/AppShell";
|
||||
import ServiceWorkerRegister from "@/components/ServiceWorkerRegister";
|
||||
import DeviceProvider from "@/providers/DeviceProvider";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
@@ -58,6 +59,7 @@ export default async function RootLayout({ children }: { children: React.ReactNo
|
||||
<ServiceWorkerRegister/>
|
||||
<NextTopLoader color="#16a795"/>
|
||||
<NextIntlClientProvider locale={locale} messages={messages}>
|
||||
<DeviceProvider>
|
||||
<ClientAppProvider>
|
||||
<AppShell>
|
||||
<UserInitializer/>
|
||||
@@ -67,6 +69,7 @@ export default async function RootLayout({ children }: { children: React.ReactNo
|
||||
<Sidebar/>
|
||||
</AppShell>
|
||||
</ClientAppProvider>
|
||||
</DeviceProvider>
|
||||
</NextIntlClientProvider>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -16,3 +16,4 @@ export { default as SearchIcon } from "&/icons/search.svg";
|
||||
export { default as CalenderIcon } from "&/icons/calender.svg";
|
||||
export { default as LocationIcon } from "&/icons/location.svg";
|
||||
export { default as MarkerIcon } from "&/icons/marker-icon.svg";
|
||||
export { default as MarkerPreviewIcon } from "&/icons/marker-preview.svg";
|
||||
|
||||
@@ -1,19 +1,62 @@
|
||||
"use client";
|
||||
import MainMap from "@/components/MainMap";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import LocationMarker from "./LocationMarker";
|
||||
import {useModalStore} from "@/stores/useModalStore";
|
||||
import {CloseIcon} from "@/assets";
|
||||
import MarkerPosition from "@/components/Home/MarkerPosition";
|
||||
|
||||
const backdropVariants = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: { opacity: 1 },
|
||||
exit: { opacity: 0 },
|
||||
};
|
||||
|
||||
const modalVariants = {
|
||||
hidden: { opacity: 0, scale: 0.9 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
transition: { stiffness: 300, damping: 25 },
|
||||
},
|
||||
exit: { opacity: 0, scale: 0.9 },
|
||||
};
|
||||
|
||||
const LocationModal = () => {
|
||||
const closeModal = useModalStore((s) => s.closeModal);
|
||||
return (
|
||||
<div style={{ width: "100%", height: "100dvh", position: "relative" }}>
|
||||
<AnimatePresence>
|
||||
<motion.div
|
||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
|
||||
variants={backdropVariants}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
exit="exit"
|
||||
onClick={closeModal}
|
||||
>
|
||||
<motion.div
|
||||
className="relative h-[50vh] w-[90vw] rounded-lg shadow-lg sm:h-[40vh] sm:w-[40vw] lg:h-[60vh] lg:w-[50vw]"
|
||||
variants={modalVariants}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
exit="exit"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<button
|
||||
onClick={closeModal}
|
||||
className="absolute top-2 left-2 z-50 cursor-pointer rounded-full bg-black/60 p-2 transition hover:bg-black/60"
|
||||
>
|
||||
<CloseIcon className="h-5 w-5 text-white" />
|
||||
</button>
|
||||
|
||||
<div className="relative h-full w-full">
|
||||
<MainMap>
|
||||
<LocationMarker
|
||||
location={{ lat: 36.541, lng: 54.56 }}
|
||||
onDragEnd={() => {
|
||||
console.log("hello");
|
||||
}}
|
||||
/>
|
||||
<MarkerPosition onChange={(loc) => console.log(loc)} />
|
||||
</MainMap>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
);
|
||||
};
|
||||
export default LocationModal;
|
||||
|
||||
@@ -2,6 +2,12 @@ import SearchItem from "./SearchItem";
|
||||
import { CalenderIcon, LocationIcon, SearchIcon } from "@/assets";
|
||||
import { useModalStore } from "@/stores/useModalStore";
|
||||
import LocationModal from "./LocationModal";
|
||||
import NoFormSelect from "@/components/UI/NoFormSelect";
|
||||
import DatePickerField from "@/components/UI/DatePickerField";
|
||||
const options = [
|
||||
{ label: "Home", value: "home" },
|
||||
{ label: "Search", value: "search" },
|
||||
]
|
||||
|
||||
export default function SearchBar() {
|
||||
const openModal = useModalStore((s) => s.openModal);
|
||||
@@ -20,16 +26,22 @@ export default function SearchBar() {
|
||||
|
||||
{/* تاریخ */}
|
||||
<SearchItem icon={<CalenderIcon className="size-5" />}>
|
||||
<input type="date" className="w-full bg-transparent text-gray-500 outline-none" />
|
||||
<DatePickerField name="bill_of_lading_date" setValue={()=>{
|
||||
console.log("hello DatePickerField")}} />
|
||||
</SearchItem>
|
||||
|
||||
{/* دسته بندی */}
|
||||
<SearchItem icon={<SearchIcon className="size-5" />}>
|
||||
<select className="w-full bg-transparent text-gray-500 outline-none">
|
||||
<option>دسته بندی</option>
|
||||
<option>رستوران</option>
|
||||
<option>کافیشاپ</option>
|
||||
</select>
|
||||
<NoFormSelect
|
||||
value={""}
|
||||
onChange={()=>{
|
||||
console.log("hello")}}
|
||||
options={options}
|
||||
loading={false}
|
||||
error={null}
|
||||
placeholder="علت انسداد"
|
||||
// onOpen={fetchReasons}
|
||||
/>
|
||||
</SearchItem>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -5,8 +5,8 @@ interface SearchItemProps {
|
||||
|
||||
function SearchItem({ icon, children }: SearchItemProps) {
|
||||
return (
|
||||
<div className="flex min-w-[180px] items-center gap-2 rounded-full bg-white px-4 py-3 shadow-sm">
|
||||
<div className="text-gray-400">{icon}</div>
|
||||
<div className="flex min-w-[180px] items-center gap-2 rounded-full bg-white px-4 py-2 shadow-sm">
|
||||
<div className="text-black">{icon}</div>
|
||||
|
||||
<div className="flex-1">{children}</div>
|
||||
</div>
|
||||
|
||||
0
src/components/Home/Desktop/Categories/index.tsx
Normal file
0
src/components/Home/Desktop/Categories/index.tsx
Normal file
66
src/components/Home/MarkerPosition.tsx
Normal file
66
src/components/Home/MarkerPosition.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import { MarkerIcon, MarkerPreviewIcon } from "@/assets";
|
||||
import { useMap } from "@vis.gl/react-maplibre";
|
||||
import maplibregl from "maplibre-gl";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
|
||||
const createMarkerElement = (preview: boolean) => {
|
||||
const el = document.createElement("div");
|
||||
const Icon = preview ? MarkerPreviewIcon : MarkerIcon;
|
||||
|
||||
el.innerHTML = renderToStaticMarkup(
|
||||
<Icon className="size-10 text-neo-aqua" />
|
||||
);
|
||||
|
||||
return el;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
preview?: boolean;
|
||||
onChange?: (location: { lat: number; lng: number }) => void;
|
||||
};
|
||||
|
||||
const MarkerPosition = ({ preview = false, onChange }: Props) => {
|
||||
const { homePageMap } = useMap();
|
||||
const markerRef = useRef<maplibregl.Marker | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!homePageMap) return;
|
||||
const map = homePageMap.getMap();
|
||||
if (!map) return;
|
||||
|
||||
// create marker once
|
||||
if (!markerRef.current) {
|
||||
const el = createMarkerElement(preview);
|
||||
markerRef.current = new maplibregl.Marker({
|
||||
element: el,
|
||||
anchor: "bottom",
|
||||
subpixelPositioning: true,
|
||||
})
|
||||
.setLngLat(map.getCenter())
|
||||
.addTo(map);
|
||||
}
|
||||
|
||||
const onMove = () => {
|
||||
const center = map.getCenter();
|
||||
markerRef.current?.setLngLat(center);
|
||||
|
||||
onChange?.({
|
||||
lat: center.lat,
|
||||
lng: center.lng,
|
||||
});
|
||||
};
|
||||
|
||||
map.on("moveend", onMove);
|
||||
|
||||
return () => {
|
||||
map.off("moveend", onMove);
|
||||
markerRef.current?.remove();
|
||||
markerRef.current = null;
|
||||
};
|
||||
}, [homePageMap, preview]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default MarkerPosition;
|
||||
79
src/components/Home/Mobile/Banner/LocationMarker.tsx
Normal file
79
src/components/Home/Mobile/Banner/LocationMarker.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
import { MarkerIcon } from "@/assets";
|
||||
import { LatLng } from "@/stores/LocationStore";
|
||||
import { Marker, MarkerDragEvent } from "@vis.gl/react-maplibre";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { useState } from "react";
|
||||
|
||||
type Props = {
|
||||
location: LatLng;
|
||||
onDragEnd: (e: MarkerDragEvent) => void;
|
||||
};
|
||||
|
||||
const dropInVariants = {
|
||||
initial: { y: -50, opacity: 0, scale: 0.8 },
|
||||
animate: {
|
||||
y: 0,
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
transition: {
|
||||
type: "spring" as const,
|
||||
stiffness: 600,
|
||||
damping: 30,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default function LocationMarker({ location, onDragEnd }: Props) {
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
|
||||
const handleDragStart = () => {
|
||||
setIsDragging(true);
|
||||
};
|
||||
|
||||
const handleDragEnd = (e: MarkerDragEvent) => {
|
||||
setIsDragging(false);
|
||||
onDragEnd(e);
|
||||
};
|
||||
|
||||
return (
|
||||
<Marker
|
||||
latitude={location.lat}
|
||||
longitude={location.lng}
|
||||
draggable
|
||||
anchor="bottom"
|
||||
onDragStart={handleDragStart}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
variants={dropInVariants}
|
||||
initial="initial"
|
||||
animate={{
|
||||
y: isDragging ? -20 : 0,
|
||||
opacity: 1,
|
||||
transition: {
|
||||
type: "spring" as const,
|
||||
stiffness: 500,
|
||||
damping: 25,
|
||||
},
|
||||
}}
|
||||
exit="exit"
|
||||
className="relative flex flex-col items-center"
|
||||
>
|
||||
{/* Drop pointer inside the marker (shown only when dragging) */}
|
||||
{isDragging && (
|
||||
<motion.div
|
||||
layoutId="drop-pointer"
|
||||
initial={{ scale: 0, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 0.5, y: 10 }}
|
||||
exit={{ scale: 0, opacity: 0 }}
|
||||
transition={{ type: "spring", stiffness: 300, damping: 20 }}
|
||||
className="absolute -bottom-4.5 z-[-1] size-1.5 rounded-full bg-black shadow-md"
|
||||
/>
|
||||
)}
|
||||
<MarkerIcon className="text-neo-aqua size-8" />
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</Marker>
|
||||
);
|
||||
}
|
||||
62
src/components/Home/Mobile/Banner/LocationModal.tsx
Normal file
62
src/components/Home/Mobile/Banner/LocationModal.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
"use client";
|
||||
import MainMap from "@/components/MainMap";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import LocationMarker from "./LocationMarker";
|
||||
import {useModalStore} from "@/stores/useModalStore";
|
||||
import {CloseIcon} from "@/assets";
|
||||
import MarkerPosition from "@/components/Home/MarkerPosition";
|
||||
|
||||
const backdropVariants = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: { opacity: 1 },
|
||||
exit: { opacity: 0 },
|
||||
};
|
||||
|
||||
const modalVariants = {
|
||||
hidden: { opacity: 0, scale: 0.9 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
transition: { stiffness: 300, damping: 25 },
|
||||
},
|
||||
exit: { opacity: 0, scale: 0.9 },
|
||||
};
|
||||
|
||||
const LocationModal = () => {
|
||||
const closeModal = useModalStore((s) => s.closeModal);
|
||||
return (
|
||||
<AnimatePresence>
|
||||
<motion.div
|
||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
|
||||
variants={backdropVariants}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
exit="exit"
|
||||
onClick={closeModal}
|
||||
>
|
||||
<motion.div
|
||||
className="relative h-[50vh] w-[90vw] rounded-lg shadow-lg sm:h-[40vh] sm:w-[40vw] lg:h-[60vh] lg:w-[50vw]"
|
||||
variants={modalVariants}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
exit="exit"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<button
|
||||
onClick={closeModal}
|
||||
className="absolute top-2 left-2 z-50 cursor-pointer rounded-full bg-black/60 p-2 transition hover:bg-black/60"
|
||||
>
|
||||
<CloseIcon className="h-5 w-5 text-white" />
|
||||
</button>
|
||||
|
||||
<div className="relative h-full w-full">
|
||||
<MainMap>
|
||||
<MarkerPosition onChange={(loc) => console.log(loc)} />
|
||||
</MainMap>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
);
|
||||
};
|
||||
export default LocationModal;
|
||||
48
src/components/Home/Mobile/Banner/SearchBar.tsx
Normal file
48
src/components/Home/Mobile/Banner/SearchBar.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import SearchItem from "./SearchItem";
|
||||
import { CalenderIcon, LocationIcon, SearchIcon } from "@/assets";
|
||||
import { useModalStore } from "@/stores/useModalStore";
|
||||
import LocationModal from "./LocationModal";
|
||||
import NoFormSelect from "@/components/UI/NoFormSelect";
|
||||
import DatePickerField from "@/components/UI/DatePickerField";
|
||||
const options = [
|
||||
{ label: "Home", value: "home" },
|
||||
{ label: "Search", value: "search" },
|
||||
]
|
||||
|
||||
export default function SearchBar() {
|
||||
const openModal = useModalStore((s) => s.openModal);
|
||||
return (
|
||||
<div className="bg-card flex max-w-2xl items-center justify-between gap-1 rounded-full p-4">
|
||||
{/* آدرس */}
|
||||
<SearchItem icon={<LocationIcon className="size-5" />}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openModal(<LocationModal />)}
|
||||
className="w-full text-right text-gray-500"
|
||||
>
|
||||
آدرس
|
||||
</button>
|
||||
</SearchItem>
|
||||
|
||||
{/* تاریخ */}
|
||||
<SearchItem icon={<CalenderIcon className="size-5" />}>
|
||||
<DatePickerField name="bill_of_lading_date" setValue={()=>{
|
||||
console.log("hello DatePickerField")}} />
|
||||
</SearchItem>
|
||||
|
||||
{/* دسته بندی */}
|
||||
<SearchItem icon={<SearchIcon className="size-5" />}>
|
||||
<NoFormSelect
|
||||
value={""}
|
||||
onChange={()=>{
|
||||
console.log("hello")}}
|
||||
options={options}
|
||||
loading={false}
|
||||
error={null}
|
||||
placeholder="علت انسداد"
|
||||
// onOpen={fetchReasons}
|
||||
/>
|
||||
</SearchItem>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
15
src/components/Home/Mobile/Banner/SearchItem.tsx
Normal file
15
src/components/Home/Mobile/Banner/SearchItem.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
interface SearchItemProps {
|
||||
icon: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
function SearchItem({ icon, children }: SearchItemProps) {
|
||||
return (
|
||||
<div className="flex min-w-[180px] items-center gap-2 rounded-full bg-white px-4 py-2 shadow-sm">
|
||||
<div className="text-black">{icon}</div>
|
||||
|
||||
<div className="flex-1">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default SearchItem;
|
||||
169
src/components/Home/Mobile/Banner/index.tsx
Normal file
169
src/components/Home/Mobile/Banner/index.tsx
Normal file
@@ -0,0 +1,169 @@
|
||||
"use client";
|
||||
|
||||
import { ArrowTopLeft, AvatarIcon, BannerImage, BannerLine, BannerStar, LogoIcon, MyOrderIcon } from "@/assets";
|
||||
import { useTranslations } from "next-intl";
|
||||
import userStore from "@/stores/userStore";
|
||||
import Image from "next/image";
|
||||
import ThemeToggle from "@/components/ThemeToggle";
|
||||
import { motion } from "framer-motion";
|
||||
import GroupAvatar from "@/components/UI/GroupAvatar";
|
||||
import SearchBar from "@/components/Home/Desktop/Banner/SearchBar";
|
||||
|
||||
export default function BannerComponent() {
|
||||
const t = useTranslations("HomePage");
|
||||
const isAuth = userStore((s) => s.isAuth);
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 60 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.9, ease: "easeOut" }}
|
||||
className="bg-home-page-banner relative mt-16 rounded-tr-[200px] rounded-bl-[200px] px-2 pt-2"
|
||||
>
|
||||
{/* ================= Header ================= */}
|
||||
<header className="flex w-full items-center justify-center pt-8">
|
||||
<div className="flex w-full items-center justify-center space-x-5">
|
||||
<motion.div
|
||||
initial={{ scale: 0 }}
|
||||
animate={{ scale: 1 }}
|
||||
transition={{ type: "spring", stiffness: 200 }}
|
||||
>
|
||||
<LogoIcon className="size-12" />
|
||||
</motion.div>
|
||||
|
||||
<ThemeToggle />
|
||||
|
||||
{["home", "search", "salons", "about_us", "contact_us"].map((item, index) => (
|
||||
<motion.p
|
||||
key={item}
|
||||
initial={{ opacity: 0, y: -10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.1 * index }}
|
||||
className="text-text-primary hover:text-primary-200 cursor-pointer transition"
|
||||
>
|
||||
{t(item)}
|
||||
</motion.p>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex w-full items-center justify-center">
|
||||
{isAuth ? (
|
||||
<motion.div whileHover={{ scale: 1.1 }} className="cursor-pointer">
|
||||
<AvatarIcon className="h-6 w-6" />
|
||||
</motion.div>
|
||||
) : (
|
||||
<span className="flex w-full items-center justify-center gap-x-2">
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
className="bg-bg flex items-center justify-center gap-x-2 rounded-lg p-3"
|
||||
>
|
||||
<MyOrderIcon className="h-6 w-6" />
|
||||
<p>{t("my_order")}</p>
|
||||
</motion.button>
|
||||
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
className="bg-primary-200 flex items-center justify-center gap-x-2 rounded-lg p-3"
|
||||
>
|
||||
<AvatarIcon className="h-6 w-6" />
|
||||
<p>{t("auth")}</p>
|
||||
</motion.button>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* ================= Main ================= */}
|
||||
<motion.main initial="hidden" animate="visible" className="flex w-full items-center justify-center py-12">
|
||||
{/* ===== Left Content ===== */}
|
||||
<div className="flex w-full flex-col items-center justify-center space-y-7">
|
||||
{/* Title */}
|
||||
<motion.div className="text-4xl font-bold">
|
||||
<motion.span
|
||||
variants={{
|
||||
hidden: { opacity: 0, y: 20 },
|
||||
visible: { opacity: 1, y: 0 },
|
||||
}}
|
||||
className="block"
|
||||
>
|
||||
{t("main_title_line1")}
|
||||
</motion.span>
|
||||
|
||||
<motion.div
|
||||
variants={{
|
||||
hidden: { opacity: 0, y: 20 },
|
||||
visible: { opacity: 1, y: 0 },
|
||||
}}
|
||||
className="flex items-center justify-between"
|
||||
>
|
||||
{t("main_title_line2")}
|
||||
|
||||
<motion.div
|
||||
initial={{ scaleX: 0 }}
|
||||
animate={{ scaleX: 1 }}
|
||||
transition={{ delay: 0.6, duration: 0.4 }}
|
||||
className="origin-right"
|
||||
>
|
||||
<BannerLine className="h-1 w-28" />
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
animate={{ rotate: [0, 10, -10, 0] }}
|
||||
transition={{ repeat: Infinity, duration: 3 }}
|
||||
>
|
||||
<BannerStar className="h-6 w-6" />
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
|
||||
{/* Description */}
|
||||
<motion.p
|
||||
variants={{
|
||||
hidden: { opacity: 0 },
|
||||
visible: { opacity: 1 },
|
||||
}}
|
||||
className="text-center text-xs"
|
||||
>
|
||||
{t("des")}
|
||||
</motion.p>
|
||||
|
||||
{/* CTA Button */}
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.07 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
transition={{ type: "spring", stiffness: 300 }}
|
||||
className="bg-primary-200 flex items-center justify-center gap-x-3 rounded-lg p-3"
|
||||
>
|
||||
<p className="text-white">{t("see_salons")}</p>
|
||||
|
||||
<motion.div whileHover={{ x: -4, y: -4 }}>
|
||||
<ArrowTopLeft className="size-5 text-white" />
|
||||
</motion.div>
|
||||
</motion.button>
|
||||
<GroupAvatar />
|
||||
</div>
|
||||
|
||||
{/* ===== Right Image ===== */}
|
||||
<motion.div
|
||||
animate={{ y: [0, -12, 0] }}
|
||||
transition={{ repeat: Infinity, duration: 4, ease: "easeInOut" }}
|
||||
className="flex w-full items-center justify-center"
|
||||
>
|
||||
<Image
|
||||
src={BannerImage}
|
||||
alt="main_image"
|
||||
className="object-cover"
|
||||
width={400}
|
||||
height={200}
|
||||
priority
|
||||
/>
|
||||
</motion.div>
|
||||
</motion.main>
|
||||
<div className="absolute -bottom-10 left-24">
|
||||
<SearchBar />
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,7 @@
|
||||
|
||||
export default function MobileHome() {
|
||||
return <></>;
|
||||
return (
|
||||
<div className="h-full w-full">
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
76
src/components/UI/DatePickerField.tsx
Normal file
76
src/components/UI/DatePickerField.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
"use client";
|
||||
|
||||
import { CalendarHijri } from "@/components/UI/Calendar";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/UI/Popover";
|
||||
import { motion } from "framer-motion";
|
||||
import { useState } from "react";
|
||||
import { Path, UseFormSetValue } from "react-hook-form";
|
||||
|
||||
type Props<T extends Record<string, any>> = {
|
||||
name: Path<T>;
|
||||
label?: string | null;
|
||||
setValue: UseFormSetValue<T>;
|
||||
defaultValue?: string | null;
|
||||
};
|
||||
|
||||
export default function DatePickerField<T extends Record<string, any>>({
|
||||
name,
|
||||
label,
|
||||
setValue,
|
||||
defaultValue,
|
||||
}: Props<T>) {
|
||||
const [date, setDate] = useState<Date | undefined>(defaultValue ? new Date(defaultValue) : undefined);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
// format date into Persian numerals
|
||||
const formatJalali = (date?: Date) => {
|
||||
if (!date) return "";
|
||||
const formatter = new Intl.DateTimeFormat("fa-IR", {
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
});
|
||||
return formatter.format(date);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative flex flex-col justify-between gap-1">
|
||||
<motion.label
|
||||
className="text-sm font-medium"
|
||||
initial={{ opacity: 0, y: -10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
>
|
||||
{label}
|
||||
</motion.label>
|
||||
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen(true)}
|
||||
className={
|
||||
"text-black rounded-lg px-3 text-start focus:outline-none"
|
||||
}
|
||||
>
|
||||
{date ? formatJalali(date) : "تاریخ"}
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
|
||||
<PopoverContent align="center" className="z-50 w-auto p-0" sideOffset={8}>
|
||||
<div>
|
||||
<CalendarHijri
|
||||
selected={date}
|
||||
onSelect={(selectedDate) => {
|
||||
setDate(selectedDate);
|
||||
if (selectedDate) {
|
||||
setValue(name, selectedDate.toISOString() as any, { shouldValidate: true });
|
||||
setOpen(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
91
src/components/UI/NoFormSelect.tsx
Normal file
91
src/components/UI/NoFormSelect.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
"use client";
|
||||
import React from "react";
|
||||
import {Select, SelectTrigger, SelectValue, SelectContent, SelectItem} from "@/components/UI/Select";
|
||||
import {motion} from "framer-motion";
|
||||
|
||||
interface Option {
|
||||
value: string | number;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface ReusableSelectProps {
|
||||
value: string | number;
|
||||
onChange: (value: string | number) => void;
|
||||
options: Option[];
|
||||
loading?: boolean;
|
||||
error?: string | null;
|
||||
placeholder?: string;
|
||||
onOpen?: () => void;
|
||||
}
|
||||
|
||||
const NoFormSelect: React.FC<ReusableSelectProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
options,
|
||||
loading = false,
|
||||
error = null,
|
||||
placeholder = "انتخاب کنید",
|
||||
onOpen,
|
||||
}) => {
|
||||
return (
|
||||
<Select
|
||||
value={value?.toString() || ""}
|
||||
onValueChange={(val) => onChange(val)}
|
||||
onOpenChange={(open) => {
|
||||
if (open && onOpen) onOpen();
|
||||
}}
|
||||
>
|
||||
<SelectTrigger
|
||||
className={`"text-black placeholder-black w-full overflow-x-scroll rounded-lg `}
|
||||
>
|
||||
<SelectValue placeholder={placeholder}/>
|
||||
</SelectTrigger>
|
||||
<SelectContent
|
||||
className="bg-card overflow-y-auto rounded-xl border-0 shadow-lg">
|
||||
{loading && (
|
||||
<SelectItem value="loading" disabled>
|
||||
در حال بارگذاری...
|
||||
</SelectItem>
|
||||
)}
|
||||
{error && (
|
||||
<SelectItem value="error" disabled>
|
||||
{error}
|
||||
</SelectItem>
|
||||
)}
|
||||
{!loading && !error && (
|
||||
<>
|
||||
<motion.div
|
||||
whileHover={{scale: 1.05}}
|
||||
whileTap={{scale: 0.95}}
|
||||
className="rounded px-2 py-1"
|
||||
>
|
||||
<SelectItem
|
||||
value="all"
|
||||
className="hover:from-primary-400 hover:to-primary-500 cursor-pointer hover:bg-linear-to-l"
|
||||
>
|
||||
تمامی موارد
|
||||
</SelectItem>
|
||||
</motion.div>
|
||||
{options.map((opt) => (
|
||||
<motion.div
|
||||
key={opt.value}
|
||||
whileHover={{scale: 1.05}}
|
||||
whileTap={{scale: 0.95}}
|
||||
className="rounded px-2 py-1"
|
||||
>
|
||||
<SelectItem
|
||||
value={opt.value.toString()}
|
||||
className="hover:from-primary-400 hover:to-primary-500 cursor-pointer hover:bg-linear-to-l"
|
||||
>
|
||||
{opt.label}
|
||||
</SelectItem>
|
||||
</motion.div>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
|
||||
export default NoFormSelect;
|
||||
@@ -26,7 +26,7 @@ function PopoverContent({
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
|
||||
"bg-card text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md p-4 shadow-md outline-hidden",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -25,14 +25,14 @@ function SelectTrigger({
|
||||
data-slot="select-trigger"
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
"data-[placeholder]:text-black [&_svg:not([class*='text-'])]:text-black dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md bg-transparent px-3 text-sm whitespace-nowrap transition-[color,box-shadow] outline-none disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-7 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronIcon className="text-text size-4 rotate-270" />
|
||||
<ChevronIcon className="text-text size-3 rotate-270" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
);
|
||||
@@ -48,7 +48,7 @@ function SelectContent({
|
||||
<SelectPrimitive.Content
|
||||
data-slot="select-content"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className
|
||||
|
||||
Reference in New Issue
Block a user