Update booking summary page layout and update notification service

This commit is contained in:
Roba Boru
2026-06-17 16:49:05 +03:00
parent d67f4a8358
commit 2bfce8beb1
9 changed files with 287 additions and 163 deletions

View File

@@ -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;
} }

View File

@@ -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[];
} }

View File

@@ -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')

View File

@@ -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 },
},
}, },
]), ]),
], ],

View File

@@ -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),
}); });
} }

View File

@@ -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) {

View File

@@ -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 dot */}
<div className="w-4 h-4 rounded-full border-4 border-primary bg-white dark:bg-gray-900 z-10" />
{/* Vertical line */}
<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" />
{/* Destination dot */}
<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>
{/* Origin */} {/* Right column: Content */}
<div className="relative pb-16"> <div className="flex-1 flex flex-col">
<div className="absolute left-[-1.625rem] top-0 w-4 h-4 rounded-full border-4 border-primary bg-white dark:bg-gray-900" /> {/* Origin */}
<div className="text-2xl font-bold text-gray-900 dark:text-white"> <div className="pb-8">
{outboundSchedule?.departureTime ? format(new Date(outboundSchedule.departureTime), 'HH:mm') : '--:--'} <div className="text-2xl font-bold text-gray-900 dark:text-white">
</div> {outboundSchedule?.departureTime ? format(new Date(outboundSchedule.departureTime), 'HH:mm') : '--:--'}
<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>
{/* Journey Info */}
<div className="relative pb-16 -mt-8">
<div className="flex items-center gap-6 text-sm text-gray-600 dark:text-gray-400">
<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="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</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 dot */}
<div className="w-4 h-4 rounded-full border-4 border-blue-500 bg-white dark:bg-gray-900 z-10" />
{/* Vertical line */}
<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" />
{/* Destination dot */}
<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>
{/* Origin */} {/* Right column: Content */}
<div className="relative pb-16"> <div className="flex-1 flex flex-col">
<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" /> {/* Origin */}
<div className="text-2xl font-bold text-gray-900 dark:text-white"> <div className="pb-8">
{inboundSchedule?.departureTime ? format(new Date(inboundSchedule.departureTime), 'HH:mm') : '--:--'} <div className="text-2xl font-bold text-gray-900 dark:text-white">
</div> {inboundSchedule?.departureTime ? format(new Date(inboundSchedule.departureTime), 'HH:mm') : '--:--'}
<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>
{/* Journey Info */}
<div className="relative pb-16 -mt-8">
<div className="flex items-center gap-6 text-sm text-gray-600 dark:text-gray-400">
<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="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</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 dot */}
<div className="w-4 h-4 rounded-full border-4 border-primary bg-white dark:bg-gray-900 z-10" />
{/* Vertical line */}
<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" />
{/* Destination dot */}
<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>
{/* Origin */} {/* Right column: Content */}
<div className="relative pb-16"> <div className="flex-1 flex flex-col">
<div className="absolute left-[-1.625rem] top-0 w-4 h-4 rounded-full border-4 border-primary bg-white dark:bg-gray-900" /> {/* Origin */}
<div className="text-2xl font-bold text-gray-900 dark:text-white"> <div className="pb-8">
{selectedSchedule?.departureTime ? format(new Date(selectedSchedule.departureTime), 'HH:mm') : '--:--'} <div className="text-2xl font-bold text-gray-900 dark:text-white">
</div> {selectedSchedule?.departureTime ? format(new Date(selectedSchedule.departureTime), 'HH:mm') : '--:--'}
<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>
{/* Journey Info */}
<div className="relative pb-16 -mt-8">
<div className="flex items-center gap-6 text-sm text-gray-600 dark:text-gray-400">
<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="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</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>

View File

@@ -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);
} }

View File

@@ -85,3 +85,4 @@ export default {
}, },
}, },
plugins: [], plugins: [],
};