mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-08 21:48:18 +00:00
Update booking summary page layout and update notification service
This commit is contained in:
@@ -1,8 +1,57 @@
|
|||||||
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
import { IsEmail, IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
||||||
|
|
||||||
export class SendEmail {
|
export class SendEmail {
|
||||||
|
@ApiProperty()
|
||||||
|
@IsEmail()
|
||||||
|
@IsNotEmpty()
|
||||||
to: string;
|
to: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
sourceId?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
sourceName?: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
subject: string;
|
subject: string;
|
||||||
body: string;
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
html?: string;
|
html?: string;
|
||||||
templateKey?: string;
|
|
||||||
context?: Record<string, unknown>;
|
@ApiPropertyOptional()
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
text?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
@IsOptional()
|
||||||
|
body?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
@IsOptional()
|
||||||
|
context?: Record<string, any>;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
templateName?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
@IsOptional()
|
||||||
|
@IsEmail()
|
||||||
|
from?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
@IsOptional()
|
||||||
|
@IsEmail()
|
||||||
|
replyTo?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,28 @@
|
|||||||
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
import { IsArray, IsNotEmpty, IsOptional, IsString, ValidateNested } from 'class-validator';
|
||||||
|
import { Type } from 'class-transformer';
|
||||||
|
|
||||||
export class SendMessage {
|
export class SendMessage {
|
||||||
|
@ApiProperty()
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
to: string;
|
to: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
message: string;
|
message: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
from?: string;
|
from?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class BulkMessagesDto {
|
export class BulkMessagesDto {
|
||||||
|
@ApiProperty({ type: [SendMessage] })
|
||||||
|
@IsArray()
|
||||||
|
@ValidateNested({ each: true })
|
||||||
|
@Type(() => SendMessage)
|
||||||
messages: SendMessage[];
|
messages: SendMessage[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { TestNotificationDto } from './notifications.dto';
|
|||||||
import { EmailClientService } from './email-client.service';
|
import { EmailClientService } from './email-client.service';
|
||||||
import { SmsClientService } from './sms-client.service';
|
import { SmsClientService } from './sms-client.service';
|
||||||
import { SendEmail } from './dtos/email.dto';
|
import { SendEmail } from './dtos/email.dto';
|
||||||
import { SendMessage } from './dtos/sms.dto';
|
import { BulkMessagesDto, SendMessage } from './dtos/sms.dto';
|
||||||
|
|
||||||
@ApiTags('Notifications')
|
@ApiTags('Notifications')
|
||||||
@Controller('notifications')
|
@Controller('notifications')
|
||||||
@@ -56,6 +56,15 @@ export class NotificationsController {
|
|||||||
return this.smsClient.sendSms(dto);
|
return this.smsClient.sendSms(dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post('send/sms/bulk')
|
||||||
|
@UseGuards(IamGuard)
|
||||||
|
@IamRoles('ADMIN', 'STAFF')
|
||||||
|
@ApiOperation({ summary: 'Send bulk SMS messages via the SMS microservice' })
|
||||||
|
@ApiBody({ type: BulkMessagesDto })
|
||||||
|
sendBulkSms(@Body() dto: BulkMessagesDto) {
|
||||||
|
return this.smsClient.sendBulkMessages(dto);
|
||||||
|
}
|
||||||
|
|
||||||
@Post('test')
|
@Post('test')
|
||||||
@UseGuards(IamGuard)
|
@UseGuards(IamGuard)
|
||||||
@IamRoles('ADMIN', 'STAFF')
|
@IamRoles('ADMIN', 'STAFF')
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { HttpModule } from '@nestjs/axios';
|
import { HttpModule } from '@nestjs/axios';
|
||||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
|
||||||
import { ClientsModule, Transport } from '@nestjs/microservices';
|
import { ClientsModule, Transport } from '@nestjs/microservices';
|
||||||
import { NotificationsController } from './notifications.controller';
|
import { NotificationsController } from './notifications.controller';
|
||||||
import { NotificationsService } from './notifications.service';
|
import { NotificationsService } from './notifications.service';
|
||||||
@@ -11,34 +10,33 @@ import { SmsClientService } from './sms-client.service';
|
|||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
HttpModule.register({ timeout: 10_000 }),
|
HttpModule.register({ timeout: 10_000 }),
|
||||||
ClientsModule.registerAsync([
|
ClientsModule.register([
|
||||||
{
|
|
||||||
name: 'EMAIL_SERVICE',
|
|
||||||
imports: [ConfigModule],
|
|
||||||
inject: [ConfigService],
|
|
||||||
useFactory: (config: ConfigService) => ({
|
|
||||||
transport: Transport.RMQ,
|
|
||||||
options: {
|
|
||||||
urls: [config.get<string>('RABBITMQ_URL') ?? 'amqp://localhost:5672'],
|
|
||||||
queue: config.get<string>('EMAIL_QUEUE') ?? 'email_queue',
|
|
||||||
queueOptions: { durable: true },
|
|
||||||
noAck: true,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'SMS_SERVICE',
|
name: 'SMS_SERVICE',
|
||||||
imports: [ConfigModule],
|
transport: Transport.RMQ,
|
||||||
inject: [ConfigService],
|
options: {
|
||||||
useFactory: (config: ConfigService) => ({
|
urls: [process.env.RABBITMQ_URL as string],
|
||||||
transport: Transport.RMQ,
|
queue: 'sms_queue',
|
||||||
options: {
|
queueOptions: { durable: true },
|
||||||
urls: [config.get<string>('RABBITMQ_URL') ?? 'amqp://localhost:5672'],
|
},
|
||||||
queue: config.get<string>('SMS_QUEUE') ?? 'sms_queue',
|
},
|
||||||
queueOptions: { durable: true },
|
{
|
||||||
noAck: true,
|
name: 'EMAIL_SERVICE',
|
||||||
},
|
transport: Transport.RMQ,
|
||||||
}),
|
options: {
|
||||||
|
urls: [process.env.RABBITMQ_URL as string],
|
||||||
|
queue: 'email_queue',
|
||||||
|
queueOptions: { durable: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'NOTIFICATION_SERVICE',
|
||||||
|
transport: Transport.RMQ,
|
||||||
|
options: {
|
||||||
|
urls: [process.env.RABBITMQ_URL as string],
|
||||||
|
queue: 'notification_queue',
|
||||||
|
queueOptions: { durable: true },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export class NotificationsService {
|
|||||||
private pushAdapter: PushAdapter,
|
private pushAdapter: PushAdapter,
|
||||||
) {
|
) {
|
||||||
this.channels = new Map<NotificationChannelType, NotificationChannel>([
|
this.channels = new Map<NotificationChannelType, NotificationChannel>([
|
||||||
['EMAIL', { send: (to, subject, body) => this.emailClient.sendEmail({ to, subject, body }).then(() => true) }],
|
['EMAIL', { send: (to, subject, body) => this.emailClient.sendEmail({ to, subject, text: body }).then(() => true) }],
|
||||||
['SMS', { send: (to, _subject, body) => this.smsClient.sendSms({ to, message: body }).then(() => true) }],
|
['SMS', { send: (to, _subject, body) => this.smsClient.sendSms({ to, message: body }).then(() => true) }],
|
||||||
['PUSH', this.pushAdapter as NotificationChannel],
|
['PUSH', this.pushAdapter as NotificationChannel],
|
||||||
]);
|
]);
|
||||||
@@ -107,7 +107,7 @@ export class NotificationsService {
|
|||||||
await this.emailClient.sendEmail({
|
await this.emailClient.sendEmail({
|
||||||
to: passenger.user.email,
|
to: passenger.user.email,
|
||||||
subject: this.sanitize(dto.title),
|
subject: this.sanitize(dto.title),
|
||||||
body: this.sanitize(dto.body),
|
text: this.sanitize(dto.body),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
import { Inject, Injectable, Logger, OnApplicationBootstrap } from '@nestjs/common';
|
import {
|
||||||
|
Inject,
|
||||||
|
Injectable,
|
||||||
|
Logger,
|
||||||
|
OnApplicationBootstrap,
|
||||||
|
} from '@nestjs/common';
|
||||||
import { ClientProxy } from '@nestjs/microservices';
|
import { ClientProxy } from '@nestjs/microservices';
|
||||||
import { BulkMessagesDto, SendMessage } from './dtos/sms.dto';
|
import { BulkMessagesDto, SendMessage } from './dtos/sms.dto';
|
||||||
|
|
||||||
@@ -8,14 +13,18 @@ export class SmsClientService implements OnApplicationBootstrap {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject('SMS_SERVICE')
|
@Inject('SMS_SERVICE')
|
||||||
private readonly smsClient: ClientProxy,
|
private smsClient: ClientProxy,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async onApplicationBootstrap() {
|
async onApplicationBootstrap() {
|
||||||
this.smsClient
|
this.smsClient
|
||||||
.connect()
|
.connect()
|
||||||
.then(() => this.logger.log('Connected to SMS service'))
|
.then(() => {
|
||||||
.catch((err) => this.logger.error('Error connecting to SMS service', err));
|
this.logger.log('connected to SMS service');
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error('Error happened at SMS service', err);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendSms(dto: SendMessage) {
|
async sendSms(dto: SendMessage) {
|
||||||
|
|||||||
@@ -238,52 +238,61 @@ export default function PaymentPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Flight-style timeline */}
|
{/* Flight-style timeline */}
|
||||||
<div className="relative pl-6">
|
<div className="flex">
|
||||||
<div className="absolute left-2 top-2 bottom-2 w-0.5 bg-gradient-to-b from-primary via-gray-300 dark:via-gray-700 to-gray-300 dark:to-gray-700" />
|
{/* Left column: Timeline with dots and line */}
|
||||||
|
<div className="flex flex-col items-center w-8 flex-shrink-0">
|
||||||
{/* Origin */}
|
{/* Origin dot */}
|
||||||
<div className="relative pb-16">
|
<div className="w-4 h-4 rounded-full border-4 border-primary bg-white dark:bg-gray-900 z-10" />
|
||||||
<div className="absolute left-[-1.625rem] top-0 w-4 h-4 rounded-full border-4 border-primary bg-white dark:bg-gray-900" />
|
{/* Vertical line */}
|
||||||
<div className="text-2xl font-bold text-gray-900 dark:text-white">
|
<div className="w-0.5 flex-1 bg-gradient-to-b from-primary via-gray-300 dark:via-gray-700 to-gray-300 dark:to-gray-700 my-2" />
|
||||||
{outboundSchedule?.departureTime ? format(new Date(outboundSchedule.departureTime), 'HH:mm') : '--:--'}
|
{/* Destination dot */}
|
||||||
</div>
|
<div className="w-4 h-4 rounded-full border-4 border-gray-400 dark:border-gray-600 bg-white dark:bg-gray-900 z-10" />
|
||||||
<div className="text-sm text-gray-500 dark:text-gray-400 mt-0.5">
|
|
||||||
{outboundSchedule?.departureTime ? format(new Date(outboundSchedule.departureTime), 'EEE, MMM d') : 'N/A'}
|
|
||||||
</div>
|
|
||||||
<div className="text-base font-semibold text-gray-900 dark:text-white mt-2">
|
|
||||||
{outboundSchedule?.origin}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Journey Info */}
|
{/* Right column: Content */}
|
||||||
<div className="relative pb-16 -mt-8">
|
<div className="flex-1 flex flex-col">
|
||||||
<div className="flex items-center gap-6 text-sm text-gray-600 dark:text-gray-400">
|
{/* Origin */}
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="pb-8">
|
||||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<div className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
{outboundSchedule?.departureTime ? format(new Date(outboundSchedule.departureTime), 'HH:mm') : '--:--'}
|
||||||
</svg>
|
|
||||||
<span className="font-medium">{outboundSchedule?.duration}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="text-sm text-gray-500 dark:text-gray-400 mt-0.5">
|
||||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
{outboundSchedule?.departureTime ? format(new Date(outboundSchedule.departureTime), 'EEE, MMM d') : 'N/A'}
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
|
</div>
|
||||||
</svg>
|
<div className="text-base font-semibold text-gray-900 dark:text-white mt-2">
|
||||||
<span className="font-medium">Train {outboundSchedule?.trainNumber}</span>
|
{outboundSchedule?.origin}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Destination */}
|
{/* Journey Info */}
|
||||||
<div className="relative -mt-8">
|
<div className="pb-8">
|
||||||
<div className="absolute left-[-1.625rem] top-0 w-4 h-4 rounded-full border-4 border-gray-400 dark:border-gray-600 bg-white dark:bg-gray-900" />
|
<div className="flex items-center gap-6 text-sm text-gray-600 dark:text-gray-400">
|
||||||
<div className="text-2xl font-bold text-gray-900 dark:text-white">
|
<div className="flex items-center gap-1.5">
|
||||||
{outboundSchedule?.arrivalTime ? format(new Date(outboundSchedule.arrivalTime), 'HH:mm') : '--:--'}
|
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
<span className="font-medium">{outboundSchedule?.duration}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||||
|
</svg>
|
||||||
|
<span className="font-medium">Train {outboundSchedule?.trainNumber}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-gray-500 dark:text-gray-400 mt-0.5">
|
|
||||||
{outboundSchedule?.arrivalTime ? format(new Date(outboundSchedule.arrivalTime), 'EEE, MMM d') : 'N/A'}
|
{/* Destination */}
|
||||||
</div>
|
<div>
|
||||||
<div className="text-base font-semibold text-gray-900 dark:text-white mt-2">
|
<div className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||||
{outboundSchedule?.destination}
|
{outboundSchedule?.arrivalTime ? format(new Date(outboundSchedule.arrivalTime), 'HH:mm') : '--:--'}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-gray-500 dark:text-gray-400 mt-0.5">
|
||||||
|
{outboundSchedule?.arrivalTime ? format(new Date(outboundSchedule.arrivalTime), 'EEE, MMM d') : 'N/A'}
|
||||||
|
</div>
|
||||||
|
<div className="text-base font-semibold text-gray-900 dark:text-white mt-2">
|
||||||
|
{outboundSchedule?.destination}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -307,52 +316,61 @@ export default function PaymentPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Flight-style timeline */}
|
{/* Flight-style timeline */}
|
||||||
<div className="relative pl-6">
|
<div className="flex">
|
||||||
<div className="absolute left-2 top-2 bottom-2 w-0.5 bg-gradient-to-b from-blue-500 via-gray-300 dark:via-gray-700 to-gray-300 dark:to-gray-700" />
|
{/* Left column: Timeline with dots and line */}
|
||||||
|
<div className="flex flex-col items-center w-8 flex-shrink-0">
|
||||||
{/* Origin */}
|
{/* Origin dot */}
|
||||||
<div className="relative pb-16">
|
<div className="w-4 h-4 rounded-full border-4 border-blue-500 bg-white dark:bg-gray-900 z-10" />
|
||||||
<div className="absolute left-[-1.625rem] top-0 w-4 h-4 rounded-full border-4 border-blue-500 bg-white dark:bg-gray-900" />
|
{/* Vertical line */}
|
||||||
<div className="text-2xl font-bold text-gray-900 dark:text-white">
|
<div className="w-0.5 flex-1 bg-gradient-to-b from-blue-500 via-gray-300 dark:via-gray-700 to-gray-300 dark:to-gray-700 my-2" />
|
||||||
{inboundSchedule?.departureTime ? format(new Date(inboundSchedule.departureTime), 'HH:mm') : '--:--'}
|
{/* Destination dot */}
|
||||||
</div>
|
<div className="w-4 h-4 rounded-full border-4 border-gray-400 dark:border-gray-600 bg-white dark:bg-gray-900 z-10" />
|
||||||
<div className="text-sm text-gray-500 dark:text-gray-400 mt-0.5">
|
|
||||||
{inboundSchedule?.departureTime ? format(new Date(inboundSchedule.departureTime), 'EEE, MMM d') : 'N/A'}
|
|
||||||
</div>
|
|
||||||
<div className="text-base font-semibold text-gray-900 dark:text-white mt-2">
|
|
||||||
{inboundSchedule?.origin}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Journey Info */}
|
{/* Right column: Content */}
|
||||||
<div className="relative pb-16 -mt-8">
|
<div className="flex-1 flex flex-col">
|
||||||
<div className="flex items-center gap-6 text-sm text-gray-600 dark:text-gray-400">
|
{/* Origin */}
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="pb-8">
|
||||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<div className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
{inboundSchedule?.departureTime ? format(new Date(inboundSchedule.departureTime), 'HH:mm') : '--:--'}
|
||||||
</svg>
|
|
||||||
<span className="font-medium">{inboundSchedule?.duration}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="text-sm text-gray-500 dark:text-gray-400 mt-0.5">
|
||||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
{inboundSchedule?.departureTime ? format(new Date(inboundSchedule.departureTime), 'EEE, MMM d') : 'N/A'}
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
|
</div>
|
||||||
</svg>
|
<div className="text-base font-semibold text-gray-900 dark:text-white mt-2">
|
||||||
<span className="font-medium">Train {inboundSchedule?.trainNumber}</span>
|
{inboundSchedule?.origin}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Destination */}
|
{/* Journey Info */}
|
||||||
<div className="relative -mt-8">
|
<div className="pb-8">
|
||||||
<div className="absolute left-[-1.625rem] top-0 w-4 h-4 rounded-full border-4 border-gray-400 dark:border-gray-600 bg-white dark:bg-gray-900" />
|
<div className="flex items-center gap-6 text-sm text-gray-600 dark:text-gray-400">
|
||||||
<div className="text-2xl font-bold text-gray-900 dark:text-white">
|
<div className="flex items-center gap-1.5">
|
||||||
{inboundSchedule?.arrivalTime ? format(new Date(inboundSchedule.arrivalTime), 'HH:mm') : '--:--'}
|
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
<span className="font-medium">{inboundSchedule?.duration}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||||
|
</svg>
|
||||||
|
<span className="font-medium">Train {inboundSchedule?.trainNumber}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-gray-500 dark:text-gray-400 mt-0.5">
|
|
||||||
{inboundSchedule?.arrivalTime ? format(new Date(inboundSchedule.arrivalTime), 'EEE, MMM d') : 'N/A'}
|
{/* Destination */}
|
||||||
</div>
|
<div>
|
||||||
<div className="text-base font-semibold text-gray-900 dark:text-white mt-2">
|
<div className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||||
{inboundSchedule?.destination}
|
{inboundSchedule?.arrivalTime ? format(new Date(inboundSchedule.arrivalTime), 'HH:mm') : '--:--'}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-gray-500 dark:text-gray-400 mt-0.5">
|
||||||
|
{inboundSchedule?.arrivalTime ? format(new Date(inboundSchedule.arrivalTime), 'EEE, MMM d') : 'N/A'}
|
||||||
|
</div>
|
||||||
|
<div className="text-base font-semibold text-gray-900 dark:text-white mt-2">
|
||||||
|
{inboundSchedule?.destination}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -378,52 +396,61 @@ export default function PaymentPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Flight-style timeline */}
|
{/* Flight-style timeline */}
|
||||||
<div className="relative pl-6">
|
<div className="flex">
|
||||||
<div className="absolute left-2 top-2 bottom-2 w-0.5 bg-gradient-to-b from-primary via-gray-300 dark:via-gray-700 to-gray-300 dark:to-gray-700" />
|
{/* Left column: Timeline with dots and line */}
|
||||||
|
<div className="flex flex-col items-center w-8 flex-shrink-0">
|
||||||
{/* Origin */}
|
{/* Origin dot */}
|
||||||
<div className="relative pb-16">
|
<div className="w-4 h-4 rounded-full border-4 border-primary bg-white dark:bg-gray-900 z-10" />
|
||||||
<div className="absolute left-[-1.625rem] top-0 w-4 h-4 rounded-full border-4 border-primary bg-white dark:bg-gray-900" />
|
{/* Vertical line */}
|
||||||
<div className="text-2xl font-bold text-gray-900 dark:text-white">
|
<div className="w-0.5 flex-1 bg-gradient-to-b from-primary via-gray-300 dark:via-gray-700 to-gray-300 dark:to-gray-700 my-2" />
|
||||||
{selectedSchedule?.departureTime ? format(new Date(selectedSchedule.departureTime), 'HH:mm') : '--:--'}
|
{/* Destination dot */}
|
||||||
</div>
|
<div className="w-4 h-4 rounded-full border-4 border-gray-400 dark:border-gray-600 bg-white dark:bg-gray-900 z-10" />
|
||||||
<div className="text-sm text-gray-500 dark:text-gray-400 mt-0.5">
|
|
||||||
{selectedSchedule?.departureTime ? format(new Date(selectedSchedule.departureTime), 'EEE, MMM d') : 'N/A'}
|
|
||||||
</div>
|
|
||||||
<div className="text-base font-semibold text-gray-900 dark:text-white mt-2">
|
|
||||||
{selectedSchedule?.origin}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Journey Info */}
|
{/* Right column: Content */}
|
||||||
<div className="relative pb-16 -mt-8">
|
<div className="flex-1 flex flex-col">
|
||||||
<div className="flex items-center gap-6 text-sm text-gray-600 dark:text-gray-400">
|
{/* Origin */}
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="pb-8">
|
||||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<div className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
{selectedSchedule?.departureTime ? format(new Date(selectedSchedule.departureTime), 'HH:mm') : '--:--'}
|
||||||
</svg>
|
|
||||||
<span className="font-medium">{selectedSchedule?.duration}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="text-sm text-gray-500 dark:text-gray-400 mt-0.5">
|
||||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
{selectedSchedule?.departureTime ? format(new Date(selectedSchedule.departureTime), 'EEE, MMM d') : 'N/A'}
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
|
</div>
|
||||||
</svg>
|
<div className="text-base font-semibold text-gray-900 dark:text-white mt-2">
|
||||||
<span className="font-medium">Train {selectedSchedule?.trainNumber}</span>
|
{selectedSchedule?.origin}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Destination */}
|
{/* Journey Info */}
|
||||||
<div className="relative -mt-8">
|
<div className="pb-8">
|
||||||
<div className="absolute left-[-1.625rem] top-0 w-4 h-4 rounded-full border-4 border-gray-400 dark:border-gray-600 bg-white dark:bg-gray-900" />
|
<div className="flex items-center gap-6 text-sm text-gray-600 dark:text-gray-400">
|
||||||
<div className="text-2xl font-bold text-gray-900 dark:text-white">
|
<div className="flex items-center gap-1.5">
|
||||||
{selectedSchedule?.arrivalTime ? format(new Date(selectedSchedule.arrivalTime), 'HH:mm') : '--:--'}
|
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
<span className="font-medium">{selectedSchedule?.duration}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||||
|
</svg>
|
||||||
|
<span className="font-medium">Train {selectedSchedule?.trainNumber}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-gray-500 dark:text-gray-400 mt-0.5">
|
|
||||||
{selectedSchedule?.arrivalTime ? format(new Date(selectedSchedule.arrivalTime), 'EEE, MMM d') : 'N/A'}
|
{/* Destination */}
|
||||||
</div>
|
<div>
|
||||||
<div className="text-base font-semibold text-gray-900 dark:text-white mt-2">
|
<div className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||||
{selectedSchedule?.destination}
|
{selectedSchedule?.arrivalTime ? format(new Date(selectedSchedule.arrivalTime), 'HH:mm') : '--:--'}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-gray-500 dark:text-gray-400 mt-0.5">
|
||||||
|
{selectedSchedule?.arrivalTime ? format(new Date(selectedSchedule.arrivalTime), 'EEE, MMM d') : 'N/A'}
|
||||||
|
</div>
|
||||||
|
<div className="text-base font-semibold text-gray-900 dark:text-white mt-2">
|
||||||
|
{selectedSchedule?.destination}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -30,7 +30,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.btn-ghost:hover {
|
.btn-ghost:hover {
|
||||||
@apply bg-[rgb(20_113_76)] bg-opacity-10 dark:bg-[rgb(20_113_76)] dark:bg-opacity-20;
|
background-color: rgba(20, 113, 76, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.btn-ghost:hover {
|
||||||
|
background-color: rgba(20, 113, 76, 0.2);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-field {
|
.input-field {
|
||||||
@@ -146,6 +152,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes slide-up {
|
||||||
|
from {
|
||||||
|
transform: translateY(100%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateY(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.animate-bounce-in {
|
.animate-bounce-in {
|
||||||
animation: bounce-in 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
|
animation: bounce-in 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||||
}
|
}
|
||||||
@@ -167,11 +184,6 @@
|
|||||||
animation: slide-in-right 0.5s ease-out;
|
animation: slide-in-right 0.5s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes slide-up {
|
|
||||||
from { transform: translateY(100%); opacity: 0; }
|
|
||||||
to { transform: translateY(0); opacity: 1; }
|
|
||||||
}
|
|
||||||
|
|
||||||
.animate-slide-up {
|
.animate-slide-up {
|
||||||
animation: slide-up 0.25s cubic-bezier(0.32, 0.72, 0, 1);
|
animation: slide-up 0.25s cubic-bezier(0.32, 0.72, 0, 1);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,3 +85,4 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [],
|
plugins: [],
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user