- Introduced NumberField for price input and TextareaField for service description in the ServiceDataCard component. - Updated ServiceCard component to display the new description and price fields, enhancing service details visibility. - Added formatMoney utility for consistent currency formatting across the application. - Updated GraphQL fragments and types to include price and description fields for services.
120 lines
4.1 KiB
TypeScript
120 lines
4.1 KiB
TypeScript
'use client';
|
||
|
||
import { type ServiceFieldsFragment } from '@repo/graphql/types';
|
||
import { Badge } from '@repo/ui/components/ui/badge';
|
||
import { Button } from '@repo/ui/components/ui/button';
|
||
import { cn } from '@repo/ui/lib/utils';
|
||
import { getMinutes } from '@repo/utils/datetime-format';
|
||
import { formatMoney } from '@repo/utils/money';
|
||
import { useState } from 'react';
|
||
|
||
type ServiceCardProps = ServiceFieldsFragment;
|
||
|
||
export function ServiceCard({
|
||
active,
|
||
description,
|
||
duration,
|
||
name,
|
||
price,
|
||
}: Readonly<ServiceCardProps>) {
|
||
const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false);
|
||
|
||
return (
|
||
<div
|
||
className={cn(
|
||
'w-full rounded-2xl bg-background p-4 dark:bg-primary/5 transition-all duration-200',
|
||
active ? '' : 'opacity-75',
|
||
)}
|
||
>
|
||
<div className="flex w-full items-center justify-between gap-2">
|
||
<span className="truncate text-base font-semibold text-foreground">{name}</span>
|
||
{active ? (
|
||
<span
|
||
className={cn(
|
||
'inline-flex items-center gap-1 px-4 p-2 rounded-full bg-secondary dark:bg-background text-primary text-xs font-medium',
|
||
)}
|
||
>
|
||
<svg
|
||
className="size-4 opacity-70"
|
||
fill="currentColor"
|
||
viewBox="0 0 16 16"
|
||
xmlns="http://www.w3.org/2000/svg"
|
||
>
|
||
<path d="M8 1.5a6.5 6.5 0 1 0 0 13a6.5 6.5 0 0 0 0-13ZM2.5 8a5.5 5.5 0 1 1 11 0a5.5 5.5 0 0 1-11 0Zm6-2.75a.5.5 0 0 0-1 0v3c0 .28.22.5.5.5h2a.5.5 0 0 0 0-1H8.5V5.25Z" />
|
||
</svg>
|
||
{getMinutes(duration) + ' мин'}
|
||
</span>
|
||
) : (
|
||
<Badge className="px-3 py-1" variant="destructive">
|
||
Закрыта
|
||
</Badge>
|
||
)}
|
||
</div>
|
||
|
||
{(description || typeof price === 'number') && (
|
||
<div className="mt-3 flex flex-col gap-2">
|
||
{description && (
|
||
<div className="space-y-1">
|
||
<div
|
||
className={cn(
|
||
'text-xs text-muted-foreground leading-relaxed whitespace-pre-wrap',
|
||
isDescriptionExpanded ? '' : 'line-clamp-2',
|
||
)}
|
||
>
|
||
{description}
|
||
</div>
|
||
{description.length > 100 && (
|
||
<div
|
||
onClick={(event) => {
|
||
event.preventDefault();
|
||
event.stopPropagation();
|
||
}}
|
||
onMouseDown={(event) => {
|
||
event.preventDefault();
|
||
event.stopPropagation();
|
||
}}
|
||
>
|
||
<Button
|
||
className="h-auto p-0 px-1 text-xs text-primary hover:text-primary/80"
|
||
onClick={(event) => {
|
||
event.preventDefault();
|
||
event.stopPropagation();
|
||
setIsDescriptionExpanded(!isDescriptionExpanded);
|
||
}}
|
||
size="sm"
|
||
variant="ghost"
|
||
>
|
||
{isDescriptionExpanded ? 'Скрыть' : 'Читать далее'}
|
||
<svg
|
||
className={cn(
|
||
'ml-1 size-3 transition-transform duration-200',
|
||
isDescriptionExpanded ? 'rotate-180' : '',
|
||
)}
|
||
fill="none"
|
||
stroke="currentColor"
|
||
viewBox="0 0 24 24"
|
||
xmlns="http://www.w3.org/2000/svg"
|
||
>
|
||
<path
|
||
d="M19 9l-7 7-7-7"
|
||
strokeLinecap="round"
|
||
strokeLinejoin="round"
|
||
strokeWidth={2}
|
||
/>
|
||
</svg>
|
||
</Button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
{typeof price === 'number' && (
|
||
<div className="flex items-center gap-1">
|
||
<span className="text-sm font-semibold text-foreground">{formatMoney(price)}</span>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|