Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
965 changes: 606 additions & 359 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions postcss.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ export default {
plugins: {
'@tailwindcss/postcss': {},
autoprefixer: {},
},
};
}
}
7 changes: 7 additions & 0 deletions src/entities/Ticket/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export { TicketCard } from './ui/TicketCard';
export { TariffsSection } from './ui/TariffsSection';
export { RouteInfo } from './ui/RouteInfo';
export { TicketHeader } from './ui/TicketHeader';

export type { ITicket } from './model';
export { mockTicket } from './model';
33 changes: 33 additions & 0 deletions src/entities/Ticket/model/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type { ITicket } from './types.ts';

export const mockTicket: ITicket = {
id: '1',
airline: 'Q Globus LLC',
flightNumber: 'GF-2025',
isDirect: true,
departure: {
time: '05:00',
city: 'Санкт-Петербург',
date: '30 июн, Пт',
airport: 'LED'
},
arrival: {
time: '06:40',
city: 'Москва',
date: '30 июн, Пт',
airport: 'DME'
},
duration: '1ч 40м',
tariffs: {
economy: {
basic: 3787,
standard: 5887,
plus: 12437
}
},
baggage: {
included: false,
price: 1500
},
seatsLeft: 2
};
2 changes: 2 additions & 0 deletions src/entities/Ticket/model/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export type { ITicket } from './types';
export { mockTicket } from './constants';
31 changes: 31 additions & 0 deletions src/entities/Ticket/model/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
export interface ITicket {
id: string;
airline: string;
flightNumber: string;
isDirect: boolean;
departure: {
time: string;
city: string;
date: string;
airport: string;
};
arrival: {
time: string;
city: string;
date: string;
airport: string;
};
duration: string;
tariffs: {
economy: {
basic: number;
standard: number;
plus: number;
};
};
baggage: {
included: boolean;
price?: number;
};
seatsLeft: number;
}
56 changes: 56 additions & 0 deletions src/entities/Ticket/ui/RouteInfo/RouteInfo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { AirplaneLandingIcon, AirplaneTakeoffIcon, LineIcon } from '@shared/ui/icons';
import type { ITicket } from '@entities/Ticket';

export type RouteInfoProps = Pick<ITicket, 'departure' | 'arrival' | 'duration'>;

export const RouteInfo = ({ departure, arrival, duration }: RouteInfoProps) => {
return (
<div className="flex flex-col">
<div className="flex items-center justify-center text-sm text-gray-500">
<AirplaneTakeoffIcon className="w-4 h-4 mr-2" />
<span className="font-roboto font-medium text-base leading-5 text-[#808080]">
в пути {duration}
</span>
<AirplaneLandingIcon className="w-4 h-4 ml-2" />
</div>

<div className="flex items-center justify-between">
<div className="flex items-center">
<span className="font-roboto font-semibold text-[28px] leading-[32px] text-[#2E2E2E] text-right">
{departure.time}
</span>
<span className="text-base font-medium text-[#2E2E2E] ml-4">{departure.airport}</span>
</div>

<LineIcon className="flex-1 mx-4 text-gray-300" />

<div className="flex items-center">
<span className="text-medium font-medium text-[#2E2E2E] mr-4">{arrival.airport}</span>
<span className="font-roboto font-semibold text-[28px] leading-[32px] text-[#2E2E2E] text-right">
{arrival.time}
</span>
</div>
</div>

<div className="flex items-center justify-between mt-[4px]">
<div className="flex flex-col">
<span className="font-roboto font-medium text-base leading-5 text-[#808080]">
{departure.city}
</span>
<span className="font-roboto font-medium text-base leading-5 text-[#808080]">
{departure.date}
</span>
</div>

<div className="flex flex-col items-end">
<span className="font-roboto font-medium text-base leading-5 text-[#808080] text-right">
{arrival.city}
</span>
<span className="font-roboto font-medium text-base leading-5 text-[#808080] text-right">
{arrival.date}
</span>
</div>
</div>
</div>
);
};
2 changes: 2 additions & 0 deletions src/entities/Ticket/ui/RouteInfo/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { RouteInfo } from './RouteInfo';
export type { RouteInfoProps} from './RouteInfo';
57 changes: 57 additions & 0 deletions src/entities/Ticket/ui/TariffCard/TariffCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import type { ReactNode } from 'react';
import { getSeatText } from '@shared/utils/getSeatText.ts';
import type { ITicket } from '@entities/Ticket';
import { WarningCircleIcon } from '@shared/ui/icons';

interface TariffCardProps extends Pick<ITicket, 'seatsLeft'> {
title: string;
price: number;
isSelected: boolean;
onClick: () => void;
icons: ReactNode;
showSeatsLeft?: boolean;
bgColor?: string;
dimensions?: string;
}

export const TariffCard = ({
title,
price,
isSelected,
onClick,
icons,
showSeatsLeft = false,
seatsLeft = 0,
bgColor = 'bg-[#EBF3FF]',
dimensions = ''
}: TariffCardProps) => {
const shadowClass = 'shadow-[0_4px_4px_rgba(0,0,0,0.25)]';

return (
<button
type="button"
onClick={onClick}
className={`flex flex-col text-left ${bgColor} rounded-lg pt-[20px] p-4 relative transition-shadow ${dimensions} ${
isSelected ? shadowClass : ''
}`}
>
<div className="flex justify-between items-center mb-[17px]">
<span className="font-medium text-[16px] leading-[20px] tracking-normal text-[#2E2E2E] font-roboto">
{title}
</span>
<div className="flex items-center">{icons}</div>
</div>

<span className="font-semibold text-[28px] leading-[32px] tracking-normal font-roboto">
{new Intl.NumberFormat('ru-RU').format(price)} ₽
</span>

{showSeatsLeft && (
<div className=" absolute bottom-4 left-4 right-4 text-sm font-medium text-[#808080] flex items-center gap-1 mt-[22px]">
<WarningCircleIcon />
{`Осталось ${seatsLeft} ${getSeatText(seatsLeft)}`}
</div>
)}
</button>
);
};
2 changes: 2 additions & 0 deletions src/entities/Ticket/ui/TariffCard/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { TariffCard } from './TariffCard';
export type { TariffCardProps } from './TariffCard';
111 changes: 111 additions & 0 deletions src/entities/Ticket/ui/TariffsSection/TariffsSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { useState } from 'react';
import {
BackpackIcon,
SuitcaseIcon,
ArrowsClockwiseIcon,
ArmchairIcon
} from '@shared/ui/icons';
import { TariffCard } from '@entities/Ticket/ui/TariffCard';
import type { ITicket } from '@entities/Ticket';

export type TariffsSectionProps = Pick<ITicket, 'tariffs' | 'seatsLeft'>;

export const TariffsSection = ({ tariffs, seatsLeft }: TariffsSectionProps) => {
const [selectedTariff, setSelectedTariff] = useState<string | null>(null);

const servicesAvailability = {
basic: {
backpack: true,
suitcase: false,
arrows: false,
armchair: false
},
standard: {
backpack: true,
suitcase: true,
arrows: false,
armchair: false
},
plus: {
backpack: true,
suitcase: true,
arrows: true,
armchair: true
}
};

const getColorClass = (
tariffType: keyof typeof servicesAvailability,
icon: keyof typeof servicesAvailability.basic
) => {
return servicesAvailability[tariffType][icon]
? 'text-[#227420]'
: 'text-[#808080]';
};

const tariffConfigs = [
{
type: 'basic' as const,
title: 'Эконом Базовый',
price: tariffs.economy.basic,
bgColor: 'bg-[#EBF3FF]',
icons: (
<>
<BackpackIcon className={`w-5 h-4.5 mr-[3px] ml-[10px] ${getColorClass('basic', 'backpack')}`} />
<SuitcaseIcon className={`mr-[3px] ${getColorClass('basic', 'suitcase')}`} />
<ArrowsClockwiseIcon className={`mr-[3px] ${getColorClass('basic', 'arrows')}`} />
<ArmchairIcon className={`${getColorClass('basic', 'armchair')}`} />
</>
),
showSeatsLeft: true
},
{
type: 'standard' as const,
title: 'Эконом Стандарт',
price: tariffs.economy.standard,
bgColor: 'bg-[#EBF3FF]',
icons: (
<>
<BackpackIcon className={`w-4.5 h-4.5 mr-[2px] ml-[10px] ${getColorClass('standard', 'backpack')}`} />
<SuitcaseIcon className={`mr-[4px] ${getColorClass('standard', 'suitcase')}`} />
<ArrowsClockwiseIcon className={`mr-[2px] ${getColorClass('standard', 'arrows')}`} />
<ArmchairIcon className={`${getColorClass('standard', 'armchair')}`} />
</>
)
},
{
type: 'plus' as const,
title: 'Эконом Плюс',
price: tariffs.economy.plus,
bgColor: 'bg-[#C2DCFF]',
icons: (
<>
<BackpackIcon className={`w-5 h-4.5 mr-[3px] ${getColorClass('plus', 'backpack')}`} />
<SuitcaseIcon className={`mr-[4.88px] ${getColorClass('plus', 'suitcase')}`} />
<ArrowsClockwiseIcon className={`mr-[3px] ${getColorClass('plus', 'arrows')}`} />
<ArmchairIcon className={`${getColorClass('plus', 'armchair')}`} />
</>
),
dimensions: 'w-[260px] h-[165px]'
}
];

return (
<div className="flex gap-4 w-full">
{tariffConfigs.map((config) => (
<TariffCard
key={config.type}
title={config.title}
price={config.price}
isSelected={selectedTariff === config.type}
onClick={() => setSelectedTariff(config.type)}
icons={config.icons}
showSeatsLeft={config.showSeatsLeft}
seatsLeft={seatsLeft}
bgColor={config.bgColor}
dimensions={config.dimensions}
/>
))}
</div>
);
};
2 changes: 2 additions & 0 deletions src/entities/Ticket/ui/TariffsSection/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { TariffsSection } from './TariffsSection';
export type { TariffsSectionProps } from './TariffsSection';
31 changes: 31 additions & 0 deletions src/entities/Ticket/ui/TicketCard/TicketCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { ITicket } from '@entities/Ticket';
import { TicketHeader } from '@entities/Ticket';
import { RouteInfo } from '@entities/Ticket';
import { TariffsSection } from '@entities/Ticket';

export interface TicketCardProps {
ticket: ITicket;
}

export const TicketCard = ({ ticket }: TicketCardProps) => {
return (
<div className="bg-white rounded-lg shadow-[0_4px_4px_0_rgba(0,0,0,0.25)] border border-gray-200 w-[1356px] ml-0">
<div className="flex">
<div className="flex-1 py-[25px] px-[37px] ">
<TicketHeader airline={ticket.airline} isDirect={ticket.isDirect} />
<RouteInfo
departure={ticket.departure}
arrival={ticket.arrival}
duration={ticket.duration}
/>
</div>
<div className="w-[855px] flex-shrink-0 py-[15px]">
<TariffsSection
tariffs={ticket.tariffs}
seatsLeft={ticket.seatsLeft}
/>
</div>
</div>
</div>
);
};
2 changes: 2 additions & 0 deletions src/entities/Ticket/ui/TicketCard/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { TicketCard } from './TicketCard';
export type { TicketCardProps } from './TicketCard';
23 changes: 23 additions & 0 deletions src/entities/Ticket/ui/TicketHeader/TicketHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { CaretDownIcon, LogoIcon } from '@shared/ui/icons';
import type { ITicket } from '@entities/Ticket';

export type TicketHeaderProps = Pick<ITicket, 'airline' | 'isDirect'>;

export const TicketHeader = ({ airline, isDirect }: TicketHeaderProps) => {
return (
<div className="flex items-center justify-between mb-[11px]">
<div className="flex items-center">
<div className="mr-3">
<LogoIcon size={40} />
</div>
<span className="font-medium text-[#808080]">{airline}</span>
</div>
{isDirect && (
<div className="flex items-center gap-1 font-medium text-base leading-5 tracking-normal text-center text-[#4797FF] font-roboto">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Вместо

 {isDirect && (
        <div className="flex items-center gap-1 font-medium text-base leading-5 tracking-normal text-center text-[#4797FF] font-roboto">
           Прямой рейс
          <CaretDownIcon className="text-[#4797FF]" />
        </div>
      )}

Скорее всего здесь нужно использовать элемент выпадающего списка
<select><option/></select>
Но это не точно, лучше спросить у Леонида

Прямой рейс
<CaretDownIcon className="text-[#4797FF]" />
</div>
)}
</div>
);
};
2 changes: 2 additions & 0 deletions src/entities/Ticket/ui/TicketHeader/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { TicketHeader } from './TicketHeader';
export type { TicketHeaderProps } from './TicketHeader';
4 changes: 4 additions & 0 deletions src/entities/Ticket/ui/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export { TicketCard } from './TicketCard';
export { TariffsSection } from './TariffsSection';
export { RouteInfo } from './RouteInfo';
export { TicketHeader } from './TicketHeader';
Loading