First passenger and back office portal commit

This commit is contained in:
Stephanos A
2026-05-31 13:15:44 +03:00
parent a0b5eb1e92
commit 5059a1fe58
181 changed files with 14249 additions and 13949 deletions

View File

@@ -0,0 +1,84 @@
'use client';
import { ReactNode, useState } from 'react';
import { LucideIcon, Loader2 } from 'lucide-react';
import { cn } from '@/lib/utils';
interface ActionButtonProps {
children: ReactNode;
onClick?: () => void | Promise<void>;
variant?: 'primary' | 'secondary' | 'danger' | 'success' | 'export';
size?: 'sm' | 'md' | 'lg';
icon?: LucideIcon;
disabled?: boolean;
loading?: boolean;
className?: string;
type?: 'button' | 'submit' | 'reset';
}
const variants = {
primary: 'bg-[rgb(20,113,76)] text-white hover:bg-[rgb(16,90,61)] shadow-sm',
secondary: 'bg-gray-200 text-gray-700 hover:bg-gray-300 dark:bg-gray-700 dark:text-gray-200 dark:hover:bg-gray-600',
danger: 'bg-[rgb(20,113,76)] text-white hover:bg-[rgb(16,90,61)] shadow-sm',
success: 'bg-green-600 text-white hover:bg-green-700 shadow-sm',
export: 'bg-[rgb(20,113,76)] text-white hover:bg-[rgb(16,90,61)] shadow-sm',
};
const sizes = {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2 text-sm',
lg: 'px-6 py-3 text-base',
};
export default function ActionButton({
children,
onClick,
variant = 'primary',
size = 'md',
icon: Icon,
disabled = false,
loading = false,
className,
type = 'button',
}: ActionButtonProps) {
const [isLoading, setIsLoading] = useState(false);
const handleClick = async () => {
if (!onClick || disabled || loading || isLoading) return;
try {
setIsLoading(true);
await onClick();
} catch (error) {
console.error('Action failed:', error);
} finally {
setIsLoading(false);
}
};
const isDisabled = disabled || loading || isLoading;
const showLoading = loading || isLoading;
return (
<button
type={type}
onClick={handleClick}
disabled={isDisabled}
className={cn(
'inline-flex items-center justify-center gap-2 rounded-lg font-medium transition-all duration-200',
'focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[rgb(20,113,76)]',
'disabled:opacity-50 disabled:cursor-not-allowed',
variants[variant],
sizes[size],
className
)}
>
{showLoading ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
Icon && <Icon className="h-4 w-4" />
)}
{children}
</button>
);
}

View File

@@ -0,0 +1,26 @@
import { cn, getStatusColor } from '@/lib/utils';
interface BadgeProps {
children: React.ReactNode;
variant?: 'default' | 'status';
status?: string;
className?: string;
}
export default function Badge({ children, variant = 'default', status, className }: BadgeProps) {
const baseClasses = 'inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium';
if (variant === 'status' && status) {
return (
<span className={cn(baseClasses, getStatusColor(status), className)}>
{children}
</span>
);
}
return (
<span className={cn(baseClasses, 'bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-300', className)}>
{children}
</span>
);
}

View File

@@ -0,0 +1,40 @@
'use client';
import { ReactNode, ButtonHTMLAttributes } from 'react';
import { cn } from '@/lib/utils';
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
children: ReactNode;
variant?: 'primary' | 'secondary' | 'outline' | 'danger';
size?: 'sm' | 'md' | 'lg';
}
export function Button({
children,
variant = 'primary',
size = 'md',
className,
...props
}: ButtonProps) {
const variants = {
primary: 'btn-primary',
secondary: 'btn-secondary',
outline: 'border border-input bg-background hover:bg-accent',
danger: 'btn-danger',
};
const sizes = {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2 text-sm',
lg: 'px-6 py-3 text-base',
};
return (
<button
className={cn('btn', variants[variant], sizes[size], className)}
{...props}
>
{children}
</button>
);
}

View File

@@ -0,0 +1,223 @@
'use client';
import { ReactNode, useState } from 'react';
import { ChevronUp, ChevronDown, MoreHorizontal } from 'lucide-react';
import { cn } from '@/lib/utils';
import ActionButton from './ActionButton';
interface Column<T> {
key: string;
label: string;
sortable?: boolean;
render?: (item: T) => ReactNode;
width?: string;
}
interface Action<T> {
label: string;
onClick: (item: T) => void | Promise<void>;
variant?: 'primary' | 'secondary' | 'danger';
icon?: any;
show?: (item: T) => boolean;
}
interface DataTableProps<T> {
data: T[];
columns: Column<T>[];
actions?: Action<T>[];
onRowClick?: (item: T) => void;
loading?: boolean;
emptyMessage?: string;
className?: string;
}
export default function DataTable<T extends Record<string, any>>({
data,
columns,
actions,
onRowClick,
loading = false,
emptyMessage = 'No data available',
className,
}: DataTableProps<T>) {
const [sortConfig, setSortConfig] = useState<{ key: string; direction: 'asc' | 'desc' } | null>(null);
const [expandedActions, setExpandedActions] = useState<string | null>(null);
// Ensure data is always an array
const safeData = Array.isArray(data) ? data : [];
const handleSort = (key: string) => {
let direction: 'asc' | 'desc' = 'asc';
if (sortConfig && sortConfig.key === key && sortConfig.direction === 'asc') {
direction = 'desc';
}
setSortConfig({ key, direction });
};
const sortedData = [...safeData].sort((a, b) => {
if (!sortConfig) return 0;
const aValue = a[sortConfig.key];
const bValue = b[sortConfig.key];
if (aValue < bValue) return sortConfig.direction === 'asc' ? -1 : 1;
if (aValue > bValue) return sortConfig.direction === 'asc' ? 1 : -1;
return 0;
});
if (loading) {
return (
<div className={cn('card', className)}>
<div className="animate-pulse">
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded mb-4"></div>
<div className="space-y-3">
{[...Array(5)].map((_, i) => (
<div key={i} className="h-4 bg-gray-200 dark:bg-gray-700 rounded"></div>
))}
</div>
</div>
</div>
);
}
return (
<div className={cn('card p-0 overflow-visible', className)}>
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-gray-50 dark:bg-gray-800">
<tr>
{columns.map((column) => (
<th
key={column.key}
className={cn(
'px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400',
column.sortable && 'cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700',
column.width && `w-${column.width}`
)}
onClick={() => column.sortable && handleSort(column.key)}
>
<div className="flex items-center gap-1">
{column.label}
{column.sortable && (
<div className="flex flex-col">
<ChevronUp
className={cn(
'h-3 w-3',
sortConfig?.key === column.key && sortConfig.direction === 'asc'
? 'text-edr-blue-600'
: 'text-gray-400'
)}
/>
<ChevronDown
className={cn(
'h-3 w-3 -mt-1',
sortConfig?.key === column.key && sortConfig.direction === 'desc'
? 'text-edr-blue-600'
: 'text-gray-400'
)}
/>
</div>
)}
</div>
</th>
))}
{actions && actions.length > 0 && (
<th className="px-6 py-3 text-right text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
Actions
</th>
)}
</tr>
</thead>
<tbody className="bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-700">
{sortedData.map((item, index) => (
<tr
key={item.id || index}
onClick={() => onRowClick?.(item)}
className={cn(
'transition-colors',
onRowClick && 'cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800'
)}
>
{columns.map((column) => (
<td key={column.key} className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
{column.render ? column.render(item) : item[column.key]}
</td>
))}
{actions && actions.length > 0 && (
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<div className="relative">
{(() => {
const visibleActions = actions.filter(action => !action.show || action.show(item));
if (visibleActions.length === 0) {
return null;
}
if (visibleActions.length === 1) {
const action = visibleActions[0];
return (
<ActionButton
onClick={() => action.onClick(item)}
variant={action.variant || 'secondary'}
size="sm"
icon={action.icon}
>
{action.label}
</ActionButton>
);
}
return (
<>
<button
onClick={(e) => {
e.stopPropagation();
setExpandedActions(expandedActions === item.id ? null : item.id);
}}
className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
>
<MoreHorizontal className="h-4 w-4" />
</button>
{expandedActions === item.id && (
<div className="absolute right-0 top-full mt-1 w-48 bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 z-10">
<div className="py-1">
{visibleActions.map((action, actionIndex) => (
<button
key={actionIndex}
onClick={(e) => {
e.stopPropagation();
action.onClick(item);
setExpandedActions(null);
}}
className={cn(
'w-full text-left px-4 py-2 text-sm hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors flex items-center gap-2',
action.variant === 'danger' && 'text-red-600 hover:bg-red-50 dark:hover:bg-red-900/20'
)}
>
{action.icon && <action.icon className="h-4 w-4" />}
{action.label}
</button>
))}
</div>
</div>
)}
</>
);
})()}
</div>
</td>
)}
</tr>
))}
</tbody>
</table>
</div>
{sortedData.length === 0 && (
<div className="py-12 text-center text-gray-500 dark:text-gray-400">
{emptyMessage}
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,20 @@
'use client';
import { InputHTMLAttributes, forwardRef } from 'react';
import { cn } from '@/lib/utils';
interface InputProps extends InputHTMLAttributes<HTMLInputElement> {}
export const Input = forwardRef<HTMLInputElement, InputProps>(
({ className, ...props }, ref) => {
return (
<input
ref={ref}
className={cn('input', className)}
{...props}
/>
);
}
);
Input.displayName = 'Input';

View File

@@ -0,0 +1,52 @@
'use client';
import { ReactNode, useEffect } from 'react';
import { X } from 'lucide-react';
interface ModalProps {
isOpen: boolean;
onClose: () => void;
title: string;
children: ReactNode;
size?: 'sm' | 'md' | 'lg' | 'xl';
}
const sizeClasses = {
sm: 'max-w-md',
md: 'max-w-lg',
lg: 'max-w-2xl',
xl: 'max-w-4xl',
};
export default function Modal({ isOpen, onClose, title, children, size = 'md' }: ModalProps) {
useEffect(() => {
if (isOpen) {
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow = 'unset';
}
return () => {
document.body.style.overflow = 'unset';
};
}, [isOpen]);
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center">
<div className="fixed inset-0 bg-black bg-opacity-50" onClick={onClose} />
<div className={`relative w-full ${sizeClasses[size]} rounded-lg bg-background p-6 shadow-xl`}>
<div className="mb-4 flex items-center justify-between">
<h2 className="text-xl font-semibold text-foreground">{title}</h2>
<button
onClick={onClose}
className="rounded-lg p-1 hover:bg-muted"
>
<X className="h-5 w-5 text-muted-foreground" />
</button>
</div>
<div>{children}</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,54 @@
import { ChevronLeft, ChevronRight } from 'lucide-react';
interface PaginationProps {
currentPage: number;
totalPages: number;
onPageChange: (page: number) => void;
}
export default function Pagination({ currentPage, totalPages, onPageChange }: PaginationProps) {
return (
<div className="flex items-center justify-between border-t border-gray-200 bg-white px-4 py-3 sm:px-6">
<div className="flex flex-1 justify-between sm:hidden">
<button
onClick={() => onPageChange(currentPage - 1)}
disabled={currentPage === 1}
className="btn btn-secondary disabled:opacity-50"
>
Previous
</button>
<button
onClick={() => onPageChange(currentPage + 1)}
disabled={currentPage === totalPages}
className="btn btn-secondary disabled:opacity-50"
>
Next
</button>
</div>
<div className="hidden sm:flex sm:flex-1 sm:items-center sm:justify-between">
<div>
<p className="text-sm text-gray-700">
Page <span className="font-medium">{currentPage}</span> of{' '}
<span className="font-medium">{totalPages}</span>
</p>
</div>
<div className="flex gap-2">
<button
onClick={() => onPageChange(currentPage - 1)}
disabled={currentPage === 1}
className="btn btn-secondary disabled:opacity-50"
>
<ChevronLeft className="h-4 w-4" />
</button>
<button
onClick={() => onPageChange(currentPage + 1)}
disabled={currentPage === totalPages}
className="btn btn-secondary disabled:opacity-50"
>
<ChevronRight className="h-4 w-4" />
</button>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,52 @@
import { ReactNode } from 'react';
interface Column<T> {
key: string;
label: string;
render?: (item: T) => ReactNode;
}
interface TableProps<T> {
data: T[];
columns: Column<T>[];
onRowClick?: (item: T) => void;
}
export default function Table<T extends Record<string, any>>({ data, columns, onRowClick }: TableProps<T>) {
return (
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-gray-50 dark:bg-gray-800">
<tr>
{columns.map((column) => (
<th
key={column.key}
className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400"
>
{column.label}
</th>
))}
</tr>
</thead>
<tbody className="divide-y divide-gray-200 dark:divide-gray-700 bg-white dark:bg-gray-900">
{data.map((item, index) => (
<tr
key={item.id || index}
onClick={() => onRowClick?.(item)}
className={onRowClick ? 'cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800' : ''}
>
{columns.map((column) => (
<td key={column.key} className="whitespace-nowrap px-6 py-4 text-sm text-gray-900 dark:text-gray-100">
{column.render ? column.render(item) : item[column.key]}
</td>
))}
</tr>
))}
</tbody>
</table>
{data.length === 0 && (
<div className="py-12 text-center text-gray-500 dark:text-gray-400">No data available</div>
)}
</div>
);
}