add: rating page
This commit is contained in:
Generated
+81
@@ -10,6 +10,7 @@
|
||||
"dependencies": {
|
||||
"@radix-ui/react-dialog": "^1.1.14",
|
||||
"@radix-ui/react-popover": "^1.1.14",
|
||||
"@radix-ui/react-slider": "^1.3.5",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-switch": "^1.2.5",
|
||||
"@tanstack/react-query": "^5.81.5",
|
||||
@@ -1031,6 +1032,12 @@
|
||||
"node": ">=12.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/number": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz",
|
||||
"integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@radix-ui/primitive": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz",
|
||||
@@ -1060,6 +1067,32 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-collection": {
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz",
|
||||
"integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-context": "1.1.2",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-slot": "1.2.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-compose-refs": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
|
||||
@@ -1126,6 +1159,21 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-direction": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
|
||||
"integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dismissable-layer": {
|
||||
"version": "1.1.10",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.10.tgz",
|
||||
@@ -1351,6 +1399,39 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-slider": {
|
||||
"version": "1.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.3.5.tgz",
|
||||
"integrity": "sha512-rkfe2pU2NBAYfGaxa3Mqosi7VZEWX5CxKaanRv0vZd4Zhl9fvQrg0VM93dv3xGLGfrHuoTRF3JXH8nb9g+B3fw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/number": "1.1.1",
|
||||
"@radix-ui/primitive": "1.1.2",
|
||||
"@radix-ui/react-collection": "1.1.7",
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-context": "1.1.2",
|
||||
"@radix-ui/react-direction": "1.1.1",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-use-controllable-state": "1.2.2",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1",
|
||||
"@radix-ui/react-use-previous": "1.1.1",
|
||||
"@radix-ui/react-use-size": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"dependencies": {
|
||||
"@radix-ui/react-dialog": "^1.1.14",
|
||||
"@radix-ui/react-popover": "^1.1.14",
|
||||
"@radix-ui/react-slider": "^1.3.5",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-switch": "^1.2.5",
|
||||
"@tanstack/react-query": "^5.81.5",
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
'use client';
|
||||
|
||||
import ToggleButton from '@/components/button/ToggleButton';
|
||||
import TabContainer, { TabContainerClassNames, TabContainerRenderType } from '@/components/tab/TabContainer';
|
||||
import { TabHeader } from '@/components/tab/TabHeader';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import RateSelectionBar from '@/components/utils/RateSelectionBar';
|
||||
import clsx from 'clsx';
|
||||
import { useParams } from 'next/navigation';
|
||||
import React from 'react'
|
||||
|
||||
type Props = object
|
||||
|
||||
type Params = {
|
||||
orderId: string;
|
||||
}
|
||||
|
||||
function RatingOrderIndex({ }: Props) {
|
||||
const params: Params = useParams();
|
||||
console.log(params)
|
||||
|
||||
const badPoitns = () => {
|
||||
return (
|
||||
<>
|
||||
<div className="grid grid-cols-2 gap-x-4.5 gap-y-4 mt-8">
|
||||
<ToggleButton name='minus1'>
|
||||
اماده سازی زمانبر
|
||||
</ToggleButton>
|
||||
<ToggleButton name='minus2'>
|
||||
بهداشت محیطی بد
|
||||
</ToggleButton>
|
||||
<ToggleButton name='minus3'>
|
||||
کیفیت منو پایین
|
||||
</ToggleButton>
|
||||
<ToggleButton name='minus4'>
|
||||
تنوع پایین منو
|
||||
</ToggleButton>
|
||||
<ToggleButton name='minus5'>
|
||||
برخورد نامناسب کارکنان
|
||||
</ToggleButton>
|
||||
<ToggleButton name='minus6'>
|
||||
قیمت نامناسب
|
||||
</ToggleButton>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const goodPoitns = () => {
|
||||
return (
|
||||
<>
|
||||
|
||||
<div className="grid grid-cols-2 gap-x-4.5 gap-y-4 mt-8">
|
||||
<ToggleButton name='plus1'>
|
||||
اماده سازی سریع
|
||||
</ToggleButton>
|
||||
<ToggleButton name='plus2'>
|
||||
بهداشت محیطی خوب
|
||||
</ToggleButton>
|
||||
<ToggleButton name='plus3'>
|
||||
کیفیت منو خوب
|
||||
</ToggleButton>
|
||||
<ToggleButton name='plus4'>
|
||||
تنوع بالا منو
|
||||
</ToggleButton>
|
||||
<ToggleButton name='plus5'>
|
||||
برخورد مناسب کارکنان
|
||||
</ToggleButton>
|
||||
<ToggleButton name='plus6'>
|
||||
قیمت مناسب
|
||||
</ToggleButton>
|
||||
</div>
|
||||
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const submitForm = (formData: FormData) => {
|
||||
console.log(formData.get('rating'))
|
||||
}
|
||||
|
||||
return (
|
||||
<section className='min-h-full'>
|
||||
<h1 className='font-medium'>امتیاز شما به رستوران</h1>
|
||||
|
||||
<form action={submitForm} className='bg-container h-full flex flex-col justify-between rounded-[30px] pt-10 px-6 pb-7.5 mt-4'>
|
||||
|
||||
<div>
|
||||
<div className='w-full'>
|
||||
<RateSelectionBar name='rating'/>
|
||||
</div>
|
||||
|
||||
<div className='mt-10'>
|
||||
<TabContainer
|
||||
defaultIndex={1}
|
||||
changeType={TabContainerRenderType.VISIBILITY}
|
||||
className={{
|
||||
...TabContainerClassNames,
|
||||
wrapper: 'p-2!',
|
||||
scrollView: clsx(
|
||||
'border-none! rounded-xl! gap-0! h-full bg-[#EAECF0]! grid! grid-cols-2 px-2! py-2! pt-2!', TabContainerClassNames.scrollView
|
||||
),
|
||||
title: 'text-sm2! font-normal mt-1',
|
||||
header: 'rounded-lg h-7! w-full',
|
||||
headerActive: 'bg-primary',
|
||||
titleActive: 'text-accent'
|
||||
}}>
|
||||
<TabHeader title='نقاط ضعف' viewRenderer={badPoitns()}></TabHeader>
|
||||
<TabHeader title='نقاط قوت' viewRenderer={goodPoitns()}></TabHeader>
|
||||
</TabContainer>
|
||||
<div className="w-full mt-6">
|
||||
<label htmlFor='userOpinion'>نظر شما</label>
|
||||
<br />
|
||||
<textarea name='userOpinion' id='userOpinion' placeholder='نظر شما' className='mt-2 w-full h-21 px-4 py-2.5 text-xs text-[#888888] rounded-xl border border-[#D0D0D0]' />
|
||||
<br />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4 mt-6">
|
||||
<Button type='submit' className='mt-auto rounded-xl font-normal cursor-pointer'>ثبت بازخورد</Button>
|
||||
<Button type='submit' className='mt-auto rounded-xl bg-white cursor-pointer hover:bg-neutral-100 border border-foreground text-foreground font-normal'>بازگشت</Button>
|
||||
</div>
|
||||
|
||||
|
||||
</form>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export default RatingOrderIndex
|
||||
@@ -0,0 +1,128 @@
|
||||
import clsx from 'clsx';
|
||||
import { motion } from 'framer-motion';
|
||||
import { Refresh } from 'iconsax-react';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
|
||||
type Props = {
|
||||
className?: ToggleButtonClassName;
|
||||
pending?: boolean;
|
||||
defaultChecked?: boolean;
|
||||
onToggle?: (checked: boolean) => void;
|
||||
name: string; // required for form data
|
||||
children?: React.ReactNode;
|
||||
disabled?: boolean;
|
||||
} & Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'className' | 'onClick' | 'type'>;
|
||||
|
||||
export type ToggleButtonClassName = {
|
||||
wrapper?: string;
|
||||
wrapperDeactive?: string;
|
||||
wrapperActive?: string;
|
||||
wrapperDisabled?: string;
|
||||
wrapperEnabled?: string;
|
||||
wrapperPending?: string;
|
||||
icon?: string;
|
||||
iconDeactive?: string;
|
||||
iconActive?: string;
|
||||
content?: string;
|
||||
contentDeactive?: string;
|
||||
contentActive?: string;
|
||||
};
|
||||
|
||||
export const ToggleButtonClassNames: ToggleButtonClassName = {
|
||||
wrapper:
|
||||
'relative transition-all border border-solid duration-200 cursor-pointer w-full rounded-normal p-3 font-normal text-sm overflow-clip',
|
||||
wrapperDeactive: 'bg-white border-neutral-200',
|
||||
wrapperActive: 'border-[#439C46]',
|
||||
wrapperDisabled: 'bg-disabled text-disabled-text',
|
||||
wrapperEnabled: 'bg-container text-white hover:bg-gray-100 active:brightness-110',
|
||||
wrapperPending: 'bg-transparent text-transparent cursor-auto',
|
||||
content: 'text-xs transition-all duration-150',
|
||||
contentDeactive: 'text-foreground',
|
||||
contentActive: 'text-[#439C46]',
|
||||
};
|
||||
|
||||
function ToggleButton({
|
||||
className = ToggleButtonClassNames,
|
||||
disabled = false,
|
||||
pending = false,
|
||||
defaultChecked = false,
|
||||
onToggle,
|
||||
name,
|
||||
children,
|
||||
...rest
|
||||
}: Props) {
|
||||
const [checked, setChecked] = useState(defaultChecked);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleClick = () => {
|
||||
if (disabled || pending) return;
|
||||
const newChecked = !checked;
|
||||
setChecked(newChecked);
|
||||
onToggle?.(newChecked);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const form = inputRef.current?.form;
|
||||
if (!form) return;
|
||||
|
||||
const onReset = () => setChecked(defaultChecked);
|
||||
form.addEventListener('reset', onReset);
|
||||
return () => form.removeEventListener('reset', onReset);
|
||||
}, [defaultChecked]);
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<input
|
||||
type="checkbox"
|
||||
name={name}
|
||||
checked={checked}
|
||||
readOnly
|
||||
hidden
|
||||
ref={inputRef}
|
||||
/>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
onClick={handleClick}
|
||||
className={clsx(
|
||||
className.wrapper,
|
||||
disabled ? className.wrapperDisabled : className.wrapperEnabled,
|
||||
pending && className.wrapperPending,
|
||||
checked ? className.wrapperActive : className.wrapperDeactive
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
{pending && (
|
||||
<motion.div
|
||||
className="absolute left-0 top-0 h-full w-full bg-neutral-200"
|
||||
initial={{ y: '100%' }}
|
||||
animate={{ y: 0 }}
|
||||
>
|
||||
<motion.div
|
||||
animate={{ rotateZ: [0, 360] }}
|
||||
transition={{ duration: 1.2, repeat: Infinity, ease: 'linear' }}
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2"
|
||||
>
|
||||
<Refresh stroke="#333333" size={24} />
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
<span
|
||||
className={clsx(
|
||||
className.content,
|
||||
disabled || !checked ? className.contentDeactive : className.contentActive
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default ToggleButton;
|
||||
@@ -11,8 +11,15 @@ type Props = {
|
||||
itemRenderer?: (child: ReactNode, index: number) => ReactElement;
|
||||
children: ReactNode;
|
||||
className?: TabContainerClassName;
|
||||
changeType?: TabContainerRenderType;
|
||||
defaultIndex?: number;
|
||||
} & Omit<React.OlHTMLAttributes<HTMLUListElement>, 'className'>;
|
||||
|
||||
export enum TabContainerRenderType {
|
||||
RENDERER,
|
||||
VISIBILITY,
|
||||
}
|
||||
|
||||
export type TabContainerClassName = {
|
||||
wrapper?: string;
|
||||
scrollView?: string;
|
||||
@@ -41,9 +48,9 @@ export const TabContainerClassNames: TabContainerClassName = {
|
||||
titleActive: 'text-black'
|
||||
}
|
||||
|
||||
function TabContainer({ className = TabContainerClassNames, itemRenderer, children, ...restProps }: Props) {
|
||||
function TabContainer({ className = TabContainerClassNames, changeType = TabContainerRenderType.RENDERER, defaultIndex = 0, itemRenderer, children, ...restProps }: Props) {
|
||||
|
||||
const [selectedTab, setSelectedTab] = useState(0);
|
||||
const [selectedTab, setSelectedTab] = useState(defaultIndex);
|
||||
|
||||
const updateTab = useCallback((index: number) => {
|
||||
setSelectedTab(index);
|
||||
@@ -99,9 +106,24 @@ function TabContainer({ className = TabContainerClassNames, itemRenderer, childr
|
||||
})}
|
||||
</HorizontalScrollView>
|
||||
|
||||
<div>
|
||||
{items[selectedTab]?.props?.viewRenderer ?? <div></div>}
|
||||
</div>
|
||||
{changeType === TabContainerRenderType.RENDERER &&
|
||||
<div>
|
||||
{items[selectedTab]?.props?.viewRenderer ?? <div></div>}
|
||||
</div>
|
||||
}
|
||||
{changeType === TabContainerRenderType.VISIBILITY &&
|
||||
<>
|
||||
{items.map((item, index) => {
|
||||
return (
|
||||
<div key={index} className={clsx(
|
||||
index === selectedTab ? 'visible' : 'hidden'
|
||||
)}>
|
||||
{item.props.viewRenderer}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SliderPrimitive from "@radix-ui/react-slider"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Slider({
|
||||
className,
|
||||
defaultValue,
|
||||
value,
|
||||
min = 0,
|
||||
max = 100,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SliderPrimitive.Root>) {
|
||||
const _values = React.useMemo(
|
||||
() =>
|
||||
Array.isArray(value)
|
||||
? value
|
||||
: Array.isArray(defaultValue)
|
||||
? defaultValue
|
||||
: [min, max],
|
||||
[value, defaultValue, min, max]
|
||||
)
|
||||
|
||||
return (
|
||||
<SliderPrimitive.Root
|
||||
data-slot="slider"
|
||||
defaultValue={defaultValue}
|
||||
value={value}
|
||||
min={min}
|
||||
max={max}
|
||||
className={cn(
|
||||
"relative flex w-full touch-none items-center select-none data-[disabled]:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<SliderPrimitive.Track
|
||||
data-slot="slider-track"
|
||||
className={cn(
|
||||
"bg-muted relative grow overflow-hidden rounded-full data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5"
|
||||
)}
|
||||
>
|
||||
<SliderPrimitive.Range
|
||||
data-slot="slider-range"
|
||||
className={cn(
|
||||
"bg-primary absolute data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full"
|
||||
)}
|
||||
/>
|
||||
</SliderPrimitive.Track>
|
||||
{Array.from({ length: _values.length }, (_, index) => (
|
||||
<SliderPrimitive.Thumb
|
||||
data-slot="slider-thumb"
|
||||
key={index}
|
||||
className="border-primary bg-background ring-ring/50 block size-3 shrink-0 rounded-full border shadow-sm transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50"
|
||||
/>
|
||||
))}
|
||||
</SliderPrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
export { Slider }
|
||||
@@ -0,0 +1,43 @@
|
||||
import { Star, Star1 } from 'iconsax-react'
|
||||
import React from 'react'
|
||||
import { Slider } from '../ui/slider';
|
||||
|
||||
type Props = {
|
||||
content?: Array<{ value: string, icon: React.ReactElement }>;
|
||||
className?: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
const defaultContent: Array<{ value: string, icon: React.ReactElement }> = [
|
||||
{ value: '5', icon: <Star className='fill-black me-1 mb-1' size={24} /> },
|
||||
{ value: '4', icon: <Star className='fill-black me-1 mb-1' size={24} /> },
|
||||
{ value: '3', icon: <Star className='fill-black me-1 mb-1' size={24} /> },
|
||||
{ value: '2', icon: <Star className='fill-black me-1 mb-1' size={24} /> },
|
||||
{ value: '1', icon: <Star className='fill-black me-1 mb-1' size={24} /> },
|
||||
]
|
||||
|
||||
function RateSelectionBar({ name, content = defaultContent, className = '' }: Props) {
|
||||
return (
|
||||
<div className={`${className}`}>
|
||||
<div className='relative w-full h-3 rounded-full'>
|
||||
<Slider
|
||||
min={1}
|
||||
max={content.length}
|
||||
name={name}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-between items-center mt-2">
|
||||
{content?.map((value, index) => {
|
||||
return (
|
||||
<span key={index} className='flex items-center gap-1'>
|
||||
<Star1 className='fill-black mb-1' size={12} />
|
||||
<span className='text-xs font-medium'>{value.value}</span>
|
||||
</span>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default RateSelectionBar
|
||||
Reference in New Issue
Block a user