mirror of
https://github.com/velocitatem/PHANTOM.git
synced 2026-05-31 16:43:36 +00:00
fixed alignment and building
This commit is contained in:
156
web/src/components/feats/airline/AirlineHero.tsx
Normal file
156
web/src/components/feats/airline/AirlineHero.tsx
Normal file
@@ -0,0 +1,156 @@
|
||||
'use client';
|
||||
|
||||
import { useState, FormEvent } from 'react';
|
||||
import { Button, Label, Input, DateInput, RadioGroup, Dropdown, DropdownCounter } from '@/components/ui';
|
||||
|
||||
type TripType = 'roundtrip' | 'oneway' | 'multicity';
|
||||
|
||||
const PlaneIcon = () => (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
const LocationIcon = () => (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default function AirlineHero() {
|
||||
const [tripType, setTripType] = useState<TripType>('roundtrip');
|
||||
const [origin, setOrigin] = useState('');
|
||||
const [destination, setDestination] = useState('');
|
||||
const [departDate, setDepartDate] = useState('');
|
||||
const [returnDate, setReturnDate] = useState('');
|
||||
const [passengers, setPassengers] = useState({ adults: 1, children: 0, infants: 0 });
|
||||
|
||||
const handleSearch = (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
console.log({ tripType, origin, destination, departDate, returnDate, passengers });
|
||||
};
|
||||
|
||||
const totalPax = passengers.adults + passengers.children + passengers.infants;
|
||||
|
||||
return (
|
||||
<div className="hero-section min-h-[70vh] flex items-center justify-center">
|
||||
<div className="w-full max-w-5xl px-4">
|
||||
<div className="text-center mb-8">
|
||||
<h1 className="text-4xl md:text-5xl font-bold mb-4">
|
||||
Book flights at the best prices
|
||||
</h1>
|
||||
<p className="text-lg">
|
||||
Compare hundreds of airlines and find the perfect flight for your journey
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="search-form">
|
||||
<form onSubmit={handleSearch}>
|
||||
<div className="mb-6">
|
||||
<RadioGroup
|
||||
name="tripType"
|
||||
value={tripType}
|
||||
onChange={setTripType}
|
||||
options={[
|
||||
{ value: 'roundtrip', label: 'Round-trip' },
|
||||
{ value: 'oneway', label: 'One-way' },
|
||||
{ value: 'multicity', label: 'Multi-city' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="origin">From</Label>
|
||||
<Input
|
||||
type="text"
|
||||
id="origin"
|
||||
value={origin}
|
||||
onChange={(e) => setOrigin(e.target.value)}
|
||||
placeholder="Airport or city"
|
||||
icon={<PlaneIcon />}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="destination">To</Label>
|
||||
<Input
|
||||
type="text"
|
||||
id="destination"
|
||||
value={destination}
|
||||
onChange={(e) => setDestination(e.target.value)}
|
||||
placeholder="Airport or city"
|
||||
icon={<LocationIcon />}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="departDate">Departure</Label>
|
||||
<DateInput
|
||||
id="departDate"
|
||||
value={departDate}
|
||||
onChange={(e) => setDepartDate(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="returnDate">Return</Label>
|
||||
{tripType === 'roundtrip' ? (
|
||||
<DateInput
|
||||
id="returnDate"
|
||||
value={returnDate}
|
||||
onChange={(e) => setReturnDate(e.target.value)}
|
||||
required
|
||||
/>
|
||||
) : (
|
||||
<DateInput id="returnDate" disabled />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-4 sm:grid-cols-3 lg:grid-cols-4 gap-4 mt-4">
|
||||
<div className="sm:col-span-1 lg:col-span-1">
|
||||
<Label htmlFor="passengers">Passengers</Label>
|
||||
<Dropdown label={`${totalPax} ${totalPax === 1 ? 'passenger' : 'passengers'}`}>
|
||||
<DropdownCounter
|
||||
label="Adults"
|
||||
sublabel="12+ years"
|
||||
value={passengers.adults}
|
||||
min={1}
|
||||
onChange={(v) => setPassengers({ ...passengers, adults: v })}
|
||||
/>
|
||||
<DropdownCounter
|
||||
label="Children"
|
||||
sublabel="2-11 years"
|
||||
value={passengers.children}
|
||||
onChange={(v) => setPassengers({ ...passengers, children: v })}
|
||||
/>
|
||||
<DropdownCounter
|
||||
label="Infants"
|
||||
sublabel="Under 2"
|
||||
value={passengers.infants}
|
||||
onChange={(v) => setPassengers({ ...passengers, infants: v })}
|
||||
/>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<Button type="submit" fullWidth>
|
||||
Search Flights
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 text-center text-sm">
|
||||
<p>Direct flights available · Flexible booking · Compare 500+ airlines worldwide</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
103
web/src/components/feats/hotel/HotelHero.tsx
Normal file
103
web/src/components/feats/hotel/HotelHero.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
'use client';
|
||||
|
||||
import { useState, FormEvent } from 'react';
|
||||
import { Button, Label, Input, DateInput, Dropdown, DropdownCounter } from '@/components/ui';
|
||||
|
||||
const LocationIcon = () => (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default function HotelHero() {
|
||||
const [destination, setDestination] = useState('');
|
||||
const [checkIn, setCheckIn] = useState('');
|
||||
const [checkOut, setCheckOut] = useState('');
|
||||
const [guests, setGuests] = useState({ adults: 2, rooms: 1 });
|
||||
|
||||
const handleSearch = (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
console.log({ destination, checkIn, checkOut, guests });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="hero-section min-h-[70vh] flex items-center justify-center">
|
||||
<div className="w-full max-w-4xl px-4">
|
||||
<div className="text-center mb-8">
|
||||
<h1 className="text-4xl md:text-5xl font-bold mb-4">
|
||||
Find your perfect stay
|
||||
</h1>
|
||||
<p className="text-lg">
|
||||
Search hotels, compare prices, and book with confidence
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSearch} className="search-form">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<div className="sm:col-span-2">
|
||||
<Label htmlFor="destination">Where to?</Label>
|
||||
<Input
|
||||
type="text"
|
||||
id="destination"
|
||||
value={destination}
|
||||
onChange={(e) => setDestination(e.target.value)}
|
||||
placeholder="City, hotel, or landmark"
|
||||
icon={<LocationIcon />}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="checkIn">Check-in</Label>
|
||||
<DateInput
|
||||
id="checkIn"
|
||||
value={checkIn}
|
||||
onChange={(e) => setCheckIn(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="checkOut">Check-out</Label>
|
||||
<DateInput
|
||||
id="checkOut"
|
||||
value={checkOut}
|
||||
onChange={(e) => setCheckOut(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="sm:col-span-2 lg:col-span-4">
|
||||
<Label htmlFor="guests">Guests & Rooms</Label>
|
||||
<Dropdown label={`${guests.adults} ${guests.adults === 1 ? 'adult' : 'adults'}, ${guests.rooms} ${guests.rooms === 1 ? 'room' : 'rooms'}`}>
|
||||
<DropdownCounter
|
||||
label="Adults"
|
||||
value={guests.adults}
|
||||
min={1}
|
||||
onChange={(v) => setGuests({ ...guests, adults: v })}
|
||||
/>
|
||||
<DropdownCounter
|
||||
label="Rooms"
|
||||
value={guests.rooms}
|
||||
min={1}
|
||||
onChange={(v) => setGuests({ ...guests, rooms: v })}
|
||||
/>
|
||||
</Dropdown>
|
||||
</div>
|
||||
|
||||
<div className="sm:col-span-2 lg:col-span-4">
|
||||
<Button type="submit" fullWidth>
|
||||
Search Hotels
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div className="mt-6 text-center text-sm">
|
||||
<p>Over 2 million hotels worldwide · Best price guarantee · Free cancellation on most bookings</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
20
web/src/components/ui/Button.tsx
Normal file
20
web/src/components/ui/Button.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { ReactNode, ButtonHTMLAttributes } from 'react';
|
||||
|
||||
type BtnVariant = 'primary' | 'secondary';
|
||||
|
||||
interface BtnProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
variant?: BtnVariant;
|
||||
children: ReactNode;
|
||||
fullWidth?: boolean;
|
||||
}
|
||||
|
||||
export default function Button({ variant = 'primary', children, fullWidth, className = '', ...props }: BtnProps) {
|
||||
const baseClass = variant === 'primary' ? 'btn-primary' : 'btn-secondary';
|
||||
const widthClass = fullWidth ? 'w-full' : '';
|
||||
|
||||
return (
|
||||
<button className={`${baseClass} ${widthClass} ${className}`.trim()} {...props}>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
7
web/src/components/ui/DateInput.tsx
Normal file
7
web/src/components/ui/DateInput.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
import { InputHTMLAttributes } from 'react';
|
||||
|
||||
interface DateInpProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'type'> {}
|
||||
|
||||
export default function DateInput({ className = '', ...props }: DateInpProps) {
|
||||
return <input type="date" className={`input-field ${className}`.trim()} {...props} />;
|
||||
}
|
||||
83
web/src/components/ui/Dropdown.tsx
Normal file
83
web/src/components/ui/Dropdown.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
'use client';
|
||||
|
||||
import { ReactNode, useState, useRef, useEffect } from 'react';
|
||||
|
||||
interface DropdownProps {
|
||||
label: string;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export default function Dropdown({ label, children }: DropdownProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClick = (e: MouseEvent) => {
|
||||
if (ref.current && !ref.current.contains(e.target as Node)) {
|
||||
setOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClick);
|
||||
return () => document.removeEventListener('mousedown', handleClick);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="relative" ref={ref}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen(!open)}
|
||||
className="input-field flex justify-between items-center w-full"
|
||||
>
|
||||
<span>{label}</span>
|
||||
<svg className="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
{open && (
|
||||
<div className="absolute z-10 mt-2 w-full bg-white border border-gray-200 rounded-lg shadow-lg p-4">
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface CounterProps {
|
||||
label: string;
|
||||
sublabel?: string;
|
||||
value: number;
|
||||
min?: number;
|
||||
max?: number;
|
||||
onChange: (val: number) => void;
|
||||
}
|
||||
|
||||
export function DropdownCounter({ label, sublabel, value, min = 0, max = 99, onChange }: CounterProps) {
|
||||
return (
|
||||
<div className="flex justify-between items-center py-3 border-b border-gray-100 last:border-b-0">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-sm font-medium text-gray-900">{label}</span>
|
||||
{sublabel && <span className="text-xs text-gray-500 mt-0.5">{sublabel}</span>}
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onChange(Math.max(min, value - 1))}
|
||||
disabled={value <= min}
|
||||
className="w-9 h-9 rounded-full border-2 border-gray-300 flex items-center justify-center hover:border-blue-500 hover:bg-blue-50 disabled:opacity-40 disabled:cursor-not-allowed disabled:hover:border-gray-300 disabled:hover:bg-transparent transition-colors text-lg font-medium text-gray-700"
|
||||
>
|
||||
−
|
||||
</button>
|
||||
<span className="w-10 text-center font-semibold text-gray-900">{value}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onChange(Math.min(max, value + 1))}
|
||||
disabled={value >= max}
|
||||
className="w-9 h-9 rounded-full border-2 border-gray-300 flex items-center justify-center hover:border-blue-500 hover:bg-blue-50 disabled:opacity-40 disabled:cursor-not-allowed disabled:hover:border-gray-300 disabled:hover:bg-transparent transition-colors text-lg font-medium text-gray-700"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
29
web/src/components/ui/Input.tsx
Normal file
29
web/src/components/ui/Input.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { InputHTMLAttributes, ReactNode } from 'react';
|
||||
|
||||
interface InpProps extends InputHTMLAttributes<HTMLInputElement> {
|
||||
icon?: ReactNode;
|
||||
}
|
||||
|
||||
export default function Input({ icon, className = '', style, ...props }: InpProps) {
|
||||
const padClass = icon ? 'pl-10' : '';
|
||||
// Fallback if a custom CSS rule still overrides Tailwind
|
||||
const mergedStyle = icon ? { paddingInlineStart: '2.5rem', ...style } : style;
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
{icon && (
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3 text-gray-400 z-10"
|
||||
>
|
||||
{icon}
|
||||
</div>
|
||||
)}
|
||||
<input
|
||||
className={`input-field ${className} ${padClass}`}
|
||||
style={mergedStyle}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
13
web/src/components/ui/Label.tsx
Normal file
13
web/src/components/ui/Label.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { ReactNode, LabelHTMLAttributes } from 'react';
|
||||
|
||||
interface LblProps extends LabelHTMLAttributes<HTMLLabelElement> {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export default function Label({ children, className = '', ...props }: LblProps) {
|
||||
return (
|
||||
<label className={`block text-sm font-medium mb-2 ${className}`.trim()} {...props}>
|
||||
{children}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
33
web/src/components/ui/RadioGroup.tsx
Normal file
33
web/src/components/ui/RadioGroup.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
'use client';
|
||||
|
||||
interface RadioOpt<T extends string> {
|
||||
value: T;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface RadioGrpProps<T extends string> {
|
||||
name: string;
|
||||
options: RadioOpt<T>[];
|
||||
value: T;
|
||||
onChange: (val: T) => void;
|
||||
}
|
||||
|
||||
export default function RadioGroup<T extends string>({ name, options, value, onChange }: RadioGrpProps<T>) {
|
||||
return (
|
||||
<div className="flex gap-4">
|
||||
{options.map((opt) => (
|
||||
<label key={opt.value} className="flex items-center cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
name={name}
|
||||
value={opt.value}
|
||||
checked={value === opt.value}
|
||||
onChange={(e) => onChange(e.target.value as T)}
|
||||
className="mr-2"
|
||||
/>
|
||||
<span className="text-sm">{opt.label}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
6
web/src/components/ui/index.ts
Normal file
6
web/src/components/ui/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export { default as Button } from './Button';
|
||||
export { default as Label } from './Label';
|
||||
export { default as Input } from './Input';
|
||||
export { default as DateInput } from './DateInput';
|
||||
export { default as RadioGroup } from './RadioGroup';
|
||||
export { default as Dropdown, DropdownCounter } from './Dropdown';
|
||||
Reference in New Issue
Block a user