banner of desktop completed
This commit is contained in:
@@ -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" }}>
|
||||
<MainMap>
|
||||
<LocationMarker
|
||||
location={{ lat: 36.541, lng: 54.56 }}
|
||||
onDragEnd={() => {
|
||||
console.log("hello");
|
||||
}}
|
||||
/>
|
||||
</MainMap>
|
||||
</div>
|
||||
<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;
|
||||
|
||||
@@ -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