diff --git a/package-lock.json b/package-lock.json index 57dd7d8..b620551 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "next": "15.3.4", "next-usequerystate": "^1.20.0", "react": "^19.0.0", + "react-day-picker": "^9.8.0", "react-dom": "^19.0.0", "react-virtualized": "^9.22.6", "tailwind-merge": "^3.3.1", @@ -80,6 +81,12 @@ "node": ">=6.9.0" } }, + "node_modules/@date-fns/tz": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@date-fns/tz/-/tz-1.2.0.tgz", + "integrity": "sha512-LBrd7MiJZ9McsOgxqWX7AaxrDjcFVjWH/tIKJd7pnR7McaslGYOP1QmmiBXdJH/H/yLCT+rcQ7FaPBUxRGUtrg==", + "license": "MIT" + }, "node_modules/@emnapi/core": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.3.tgz", @@ -3179,6 +3186,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/date-fns-jalali": { + "version": "4.1.0-0", + "resolved": "https://registry.npmjs.org/date-fns-jalali/-/date-fns-jalali-4.1.0-0.tgz", + "integrity": "sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==", + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", @@ -5974,6 +5997,27 @@ "node": ">=0.10.0" } }, + "node_modules/react-day-picker": { + "version": "9.8.0", + "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-9.8.0.tgz", + "integrity": "sha512-E0yhhg7R+pdgbl/2toTb0xBhsEAtmAx1l7qjIWYfcxOy8w4rTSVfbtBoSzVVhPwKP/5E9iL38LivzoE3AQDhCQ==", + "license": "MIT", + "dependencies": { + "@date-fns/tz": "1.2.0", + "date-fns": "4.1.0", + "date-fns-jalali": "4.1.0-0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/gpbl" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/react-dom": { "version": "19.1.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", diff --git a/package.json b/package.json index e1105d5..0fb0360 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "next": "15.3.4", "next-usequerystate": "^1.20.0", "react": "^19.0.0", + "react-day-picker": "^9.8.0", "react-dom": "^19.0.0", "react-virtualized": "^9.22.6", "tailwind-merge": "^3.3.1", diff --git a/src/app/[name]/[id]/page.tsx b/src/app/[name]/[id]/page.tsx index f659c1c..d61d91f 100644 --- a/src/app/[name]/[id]/page.tsx +++ b/src/app/[name]/[id]/page.tsx @@ -18,7 +18,7 @@ function FoodPage({ }: Props) { return ( -
+
{
- - (new Date()); + const [selectedGender, setSelectedGender] = useState(""); + const [showCalendar, setShowCalendar] = useState(false); + const dateLib = getDateLib() + + const changeGender = (e: React.MouseEvent, index: number) => { + setSelectedGender(() => String(index)); + } + + // const isAuthenticated = useAuthStore((state) => state.isAuthenticated); + + // const { mutate, data, isPending } = useProfile(); + + // useEffect(() => { + // if (isAuthenticated) { + // mutate(); + // } + // // eslint-disable-next-line react-hooks/exhaustive-deps + // }, [isAuthenticated]) + + const onChange = (e: ChangeEvent) => { + if (e.target.name == PORFILE_EDIT_PAGE_ELEMENT.INPUT_FULLNAME) { + setFullName(() => e.target.value) + } + else if (e.target.name == PORFILE_EDIT_PAGE_ELEMENT.INPUT_EMAIL) { + setEmail(() => e.target.value) + } + else if (e.target.name == PORFILE_EDIT_PAGE_ELEMENT.INPUT_PHONE) { + setPhoneNumber(() => e.target.value) + } + // else if (e.target.name == PORFILE_EDIT_PAGE_ELEMENT.INPUT_DOB) { + // setDateOfBirth(() => e.target.value) + // } + } + + return ( + <> +
+
ویرایش اطلاعات
+
+ +
+ +
+
+ +
+ +
+ + + + + + + {dateOfBirth ? dateLib.format(dateOfBirth, "yyyy/MM/dd") : "انتخاب کنید"} + + تاریخ تولد + + + + { + setDateOfBirth(date) + setShowCalendar(false) + }} + /> + + + + + +
+ + جنسیت +
+
+
+ +
+ + +
+
+ + ) +} + +export default ProfileIndex \ No newline at end of file diff --git a/src/app/profile/page.tsx b/src/app/profile/page.tsx index 7051f2f..03fb787 100644 --- a/src/app/profile/page.tsx +++ b/src/app/profile/page.tsx @@ -5,11 +5,13 @@ import Button from '@/components/button/PrimaryButton'; // import { useAuthStore } from '@/zustand/authStore'; import { ArrowLeft, Calendar2, CallCalling, Profile, Sms, Verify } from 'iconsax-react'; import Image from 'next/image'; +import Link from 'next/link'; import React from 'react' type Props = object function ProfileIndex({ }: Props) { + // const isAuthenticated = useAuthStore((state) => state.isAuthenticated); // const { mutate, data, isPending } = useProfile(); @@ -30,7 +32,7 @@ function ProfileIndex({ }: Props) {
-
+
@@ -78,7 +80,9 @@ function ProfileIndex({ }: Props) {
- + + +
diff --git a/src/components/combobox/Combobox.tsx b/src/components/combobox/Combobox.tsx index b7e8d25..55ecd0e 100644 --- a/src/components/combobox/Combobox.tsx +++ b/src/components/combobox/Combobox.tsx @@ -1,91 +1,153 @@ -"use client" - -import * as React from "react" -import { Check } from "lucide-react" - -import { cn } from "@/lib/utils" -import { Button } from "@/components/ui/button" -import { - Command, - CommandEmpty, - CommandGroup, - CommandInput, - CommandItem, - CommandList, -} from "@/components/ui/command" -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/ui/popover" -import ArrowDownIcon from "../icons/ArrowDownIcon" +import React, { useState, useEffect, useRef } from 'react'; +import ArrowDownIcon from '../icons/ArrowDownIcon'; +import { motion, Variants } from 'framer-motion'; +import { SearchNormal } from 'iconsax-react'; export interface DropdownOption { id: string; - label?: string; title: string; + label?: string; } type Props = { title: string; options: Array; - value: string; - setValue: React.Dispatch> + expanded?: boolean; + selectedId: string; + searchable?: boolean; + onSelectionChange: (e: React.MouseEvent, index: number) => void, } & React.HTMLAttributes -export function Combobox({ options, value, title, setValue, }: Props) { - const [open, setOpen] = React.useState(false) +function ComboBox({ title, options, expanded, selectedId, searchable = true, onSelectionChange, ...props }: Props) { + const [expand, setExpand] = useState(expanded); + const [searchValue, setSearchValue] = useState(''); + const boxRef = useRef(null); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (boxRef.current && !boxRef.current.contains(event.target as Node)) { + setExpand(false); + } + }; + + if (expand) { + document.addEventListener('mousedown', handleClickOutside); + } + + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [expand]); + + const toggleExpand = () => { + setExpand((prev) => !prev); + setSearchValue(''); + }; + + const searchChanged = (e: React.ChangeEvent) => { + setSearchValue(e.target.value); + }; + + const setSelection = (e: React.MouseEvent, index: number) => { + e.stopPropagation(); // prevent toggle + onSelectionChange(e, index); + setExpand(false); + }; + + const variants: Variants = { + collapse: { + opacity: 0, + scale: 0.95, + pointerEvents: 'none', // disable clicks + transition: { + duration: 0.1, + ease: 'easeInOut' + } + }, + expand: { + opacity: 1, + scale: 1, + pointerEvents: 'auto', // re-enable clicks + transition: { + duration: 0.1, + ease: 'easeInOut' + } + } + }; + return ( - - - - - - - - - نتیجه ای یافت نشد - - {options.map((option) => ( - { - setValue(currentValue === value ? "" : currentValue) - setOpen(false) - }} - > - - {option.label} - - ))} - - - - - - ) + + {title} + + + +
+ {options.find((x) => x.id === selectedId)?.title ?? ''} +
+ + +
+ + e.stopPropagation()} + initial="collapse" + animate={expand ? "expand" : "collapse"} + variants={variants} + > + + {searchable && +
+ + +
+ } +
+ {options + .filter((v) => v.title?.includes(searchValue)) + .map((v, i) => ( +
setSelection(e, i)} + key={v.id} + data-selected={v.id === selectedId} + className="flex items-center justify-end px-2 py-1.5 cursor-pointer hover:bg-gray-50 rounded-md last:rounded-b-xl" + tabIndex={0} + role="option" + aria-selected + > + {v.title} +
+ ))} +
+
+
+ ); } + + +export default ComboBox \ No newline at end of file diff --git a/src/components/combobox/SearchComboBox.tsx b/src/components/combobox/SearchComboBox.tsx deleted file mode 100644 index 691e2c2..0000000 --- a/src/components/combobox/SearchComboBox.tsx +++ /dev/null @@ -1,150 +0,0 @@ -import React, { useState, useEffect, useRef } from 'react'; -import ArrowDownIcon from '../icons/ArrowDownIcon'; -import { motion, Variants } from 'framer-motion'; -import { SearchNormal } from 'iconsax-react'; - -export interface DropdownOption { - id: string; - title: string; - label?: string; -} - -type Props = { - title: string; - options: Array; - expanded?: boolean; - selectedId: string; - onSelectionChange: (e: React.MouseEvent, index: number) => void, -} & React.HTMLAttributes - -function SearchComboBox({ title, options, expanded, selectedId, onSelectionChange, ...props }: Props) { - const [expand, setExpand] = useState(expanded); - const [searchValue, setSearchValue] = useState(''); - const boxRef = useRef(null); - - useEffect(() => { - const handleClickOutside = (event: MouseEvent) => { - if (boxRef.current && !boxRef.current.contains(event.target as Node)) { - setExpand(false); - } - }; - - if (expand) { - document.addEventListener('mousedown', handleClickOutside); - } - - return () => { - document.removeEventListener('mousedown', handleClickOutside); - }; - }, [expand]); - - const toggleExpand = () => { - setExpand((prev) => !prev); - setSearchValue(''); - }; - - const searchChanged = (e: React.ChangeEvent) => { - setSearchValue(e.target.value); - }; - - const setSelection = (e: React.MouseEvent, index: number) => { - e.stopPropagation(); // prevent toggle - onSelectionChange(e, index); - setExpand(false); - }; - - const variants: Variants = { - collapse: { - opacity: 0, - scale: 0.95, - pointerEvents: 'none', // disable clicks - transition: { - duration: 0.1, - ease: 'easeInOut' - } - }, - expand: { - opacity: 1, - scale: 1, - pointerEvents: 'auto', // re-enable clicks - transition: { - duration: 0.1, - ease: 'easeInOut' - } - } - }; - - - return ( -
-
- - -
- {options.find((x) => x.id === selectedId)?.title ?? ''} -
- - -
- - e.stopPropagation()} - initial="collapse" - animate={expand ? "expand" : "collapse"} - variants={variants} - > - -
- - -
-
- {options - .filter((v) => v.title?.includes(searchValue)) - .map((v, i) => ( -
setSelection(e, i)} - key={v.id} - data-selected={v.id === selectedId} - className="flex items-center justify-end px-2 py-1.5 cursor-pointer hover:bg-gray-50 rounded-md last:rounded-b-xl" - tabIndex={0} - role="option" - aria-selected - > - {v.title} -
- ))} -
-
-
- ); -} - - -export default SearchComboBox \ No newline at end of file diff --git a/src/components/ui/calendar.tsx b/src/components/ui/calendar.tsx new file mode 100644 index 0000000..a6d9bac --- /dev/null +++ b/src/components/ui/calendar.tsx @@ -0,0 +1,226 @@ +"use client" + +import * as React from "react" +import { + ChevronDownIcon, + ChevronLeftIcon, + ChevronRightIcon, +} from "lucide-react" +import { DayButton, getDefaultClassNames } from "react-day-picker" +import { DayPicker, faIR } from "react-day-picker/persian" + +import { cn } from "@/lib/utils" +import { Button, buttonVariants } from "@/components/ui/button" + +export function CalendarHijri() { + const [date, setDate] = React.useState( + new Date(2025, 5, 12) + ) + + return ( + + ) +} + +// ---------------------------------------------------------------------------- +// The code below is for this example only. +// For your own calendar, you would edit the calendar.tsx component directly. +// ---------------------------------------------------------------------------- +export default function Calendar({ + className, + classNames, + showOutsideDays = true, + captionLayout = "label", + buttonVariant = "ghost", + components, + ...props +}: React.ComponentProps & { + buttonVariant?: React.ComponentProps["variant"] +}) { + const defaultClassNames = getDefaultClassNames() + + return ( + svg]:rotate-180`, + String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`, + className + )} + captionLayout={captionLayout} + + classNames={{ + root: cn("w-fit", defaultClassNames.root), + months: cn( + "flex gap-4 flex-col md:flex-row relative", + defaultClassNames.months + ), + month: cn("flex flex-col w-full gap-4", defaultClassNames.month), + nav: cn( + "flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between", + defaultClassNames.nav + ), + button_previous: cn( + buttonVariants({ variant: buttonVariant }), + "size-(--cell-size) aria-disabled:opacity-50 p-0 select-none", + defaultClassNames.button_previous + ), + button_next: cn( + buttonVariants({ variant: buttonVariant }), + "size-(--cell-size) aria-disabled:opacity-50 p-0 select-none", + defaultClassNames.button_next + ), + month_caption: cn( + "flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)", + defaultClassNames.month_caption + ), + dropdowns: cn( + "w-full flex items-center text-sm font-medium justify-center px-3 h-(--cell-size) gap-1.5", + defaultClassNames.dropdowns + ), + dropdown_root: cn( + "relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md", + defaultClassNames.dropdown_root + ), + dropdown: cn("absolute inset-0 opacity-0 ", defaultClassNames.dropdown), + caption_label: cn( + "select-none font-medium", + captionLayout === "label" + ? "text-sm" + : "rounded-md pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5", + defaultClassNames.caption_label + ), + table: "w-full border-collapse", + weekdays: cn("flex", defaultClassNames.weekdays), + weekday: cn( + "text-muted-foreground px-2 rounded-md flex-1 font-normal text-[0.8rem] select-none", + defaultClassNames.weekday + ), + week: cn("flex w-full mt-2", defaultClassNames.week), + week_number_header: cn( + "select-none w-(--cell-size)", + defaultClassNames.week_number_header + ), + week_number: cn( + "text-[0.8rem] select-none text-muted-foreground", + defaultClassNames.week_number + ), + day: cn( + "relative w-full h-full p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none", + defaultClassNames.day + ), + range_start: cn( + "rounded-l-md bg-accent", + defaultClassNames.range_start + ), + range_middle: cn("rounded-none", defaultClassNames.range_middle), + range_end: cn("rounded-r-md bg-accent", defaultClassNames.range_end), + today: cn( + "bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none", + defaultClassNames.today + ), + outside: cn( + "text-disabled aria-selected:text-muted-foreground", + defaultClassNames.outside + ), + disabled: cn( + "bg-red-500 opacity-50", + defaultClassNames.disabled + ), + hidden: cn("invisible", defaultClassNames.hidden), + ...classNames, + }} + components={{ + Root: ({ className, rootRef, ...props }) => { + return ( +
+ ) + }, + Chevron: ({ className, orientation, ...props }) => { + if (orientation === "left") { + return ( + + ) + } + + if (orientation === "right") { + return ( + + ) + } + + return ( + + ) + }, + DayButton: CalendarDayButton, + WeekNumber: ({ children, ...props }) => { + return ( + +
+ {children} +
+ + ) + }, + ...components, + }} + {...props} + /> + ) +} + +function CalendarDayButton({ + className, + day, + modifiers, + ...props +}: React.ComponentProps) { + const defaultClassNames = getDefaultClassNames() + + const ref = React.useRef(null) + React.useEffect(() => { + if (modifiers.focused) ref.current?.focus() + }, [modifiers.focused]) + + return ( +