cleanup: ticket and legal stuff
This commit is contained in:
@@ -1,80 +1,110 @@
|
||||
<script lang="ts">
|
||||
import { passengerInfoVM } from "$lib/domains/passengerinfo/view/passenger.info.vm.svelte";
|
||||
import Icon from "$lib/components/atoms/icon.svelte";
|
||||
import { capitalize } from "$lib/core/string.utils";
|
||||
import BackpackIcon from "~icons/solar/backpack-linear";
|
||||
import BagIcon from "~icons/lucide/briefcase";
|
||||
import SuitcaseIcon from "~icons/bi/suitcase2";
|
||||
import SeatIcon from "~icons/solar/armchair-2-linear";
|
||||
import Title from "$lib/components/atoms/title.svelte";
|
||||
import { customerInfoVM } from "$lib/domains/customerinfo/view/customerinfo.vm.svelte";
|
||||
import { productStore } from "$lib/domains/product/store";
|
||||
import MailIcon from "~icons/lucide/mail";
|
||||
import MapPinIcon from "~icons/lucide/map-pin";
|
||||
import PackageIcon from "~icons/lucide/package";
|
||||
import PhoneIcon from "~icons/lucide/phone";
|
||||
import UserIcon from "~icons/lucide/user";
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-6">
|
||||
{#each passengerInfoVM.passengerInfos as passenger, index}
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="font-semibold">
|
||||
Passenger {index + 1} ({capitalize(passenger.passengerType)})
|
||||
</span>
|
||||
<!-- Product Summary -->
|
||||
{#if $productStore}
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<Icon icon={PackageIcon} cls="h-5 w-5 text-gray-600" />
|
||||
<Title size="p" weight="semibold">Product</Title>
|
||||
</div>
|
||||
<div class="rounded-lg border bg-gray-50 p-4">
|
||||
<p class="font-medium">{$productStore.title}</p>
|
||||
<p class="mt-1 text-sm text-gray-600">
|
||||
{$productStore.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Customer Information Summary -->
|
||||
{#if customerInfoVM.customerInfo}
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<Icon icon={UserIcon} cls="h-5 w-5 text-gray-600" />
|
||||
<Title size="p" weight="semibold">Customer Information</Title>
|
||||
</div>
|
||||
|
||||
<!-- Personal Info -->
|
||||
<div class="rounded-lg border bg-gray-50 p-4">
|
||||
<div class="grid grid-cols-2 gap-3 text-sm md:grid-cols-3">
|
||||
<div class="flex flex-col gap-3">
|
||||
<!-- Name -->
|
||||
<div>
|
||||
<span class="text-gray-500">Name</span>
|
||||
<span class="text-xs text-gray-500">Full Name</span>
|
||||
<p class="font-medium">
|
||||
{passenger.passengerPii.firstName}
|
||||
{passenger.passengerPii.lastName}
|
||||
{customerInfoVM.customerInfo.firstName}
|
||||
{#if customerInfoVM.customerInfo.middleName}
|
||||
{customerInfoVM.customerInfo.middleName}
|
||||
{/if}
|
||||
{customerInfoVM.customerInfo.lastName}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-gray-500">Nationality</span>
|
||||
<p class="font-medium">
|
||||
{passenger.passengerPii.nationality}
|
||||
|
||||
<!-- Email -->
|
||||
<div class="flex items-center gap-2">
|
||||
<Icon icon={MailIcon} cls="h-4 w-4 text-gray-500" />
|
||||
<div class="flex-1">
|
||||
<span class="text-xs text-gray-500">Email</span>
|
||||
<p class="text-sm">
|
||||
{customerInfoVM.customerInfo.email}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-gray-500">Date of Birth</span>
|
||||
<p class="font-medium">{passenger.passengerPii.dob}</p>
|
||||
</div>
|
||||
|
||||
<!-- Phone -->
|
||||
<div class="flex items-center gap-2">
|
||||
<Icon icon={PhoneIcon} cls="h-4 w-4 text-gray-500" />
|
||||
<div class="flex-1">
|
||||
<span class="text-xs text-gray-500">Phone</span>
|
||||
<p class="text-sm">
|
||||
{customerInfoVM.customerInfo.phoneCountryCode}
|
||||
{customerInfoVM.customerInfo.phoneNumber}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Baggage Selection -->
|
||||
<div class="flex flex-wrap gap-4 text-sm">
|
||||
{#if passenger.bagSelection.personalBags > 0}
|
||||
<div class="flex items-center gap-2">
|
||||
<Icon icon={BackpackIcon} cls="h-5 w-5 text-gray-600" />
|
||||
<span>Personal Item</span>
|
||||
<!-- Address -->
|
||||
<div class="flex items-start gap-2">
|
||||
<Icon
|
||||
icon={MapPinIcon}
|
||||
cls="h-4 w-4 text-gray-500 mt-1"
|
||||
/>
|
||||
<div class="flex-1">
|
||||
<span class="text-xs text-gray-500">Address</span>
|
||||
<p class="text-sm">
|
||||
{customerInfoVM.customerInfo.address}
|
||||
{#if customerInfoVM.customerInfo.address2}
|
||||
, {customerInfoVM.customerInfo.address2}
|
||||
{/if}
|
||||
</p>
|
||||
<p class="text-sm text-gray-600">
|
||||
{customerInfoVM.customerInfo.city},
|
||||
{customerInfoVM.customerInfo.state}
|
||||
{customerInfoVM.customerInfo.zipCode}
|
||||
</p>
|
||||
<p class="text-sm text-gray-600">
|
||||
{customerInfoVM.customerInfo.country}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="rounded-lg border border-dashed p-6 text-center">
|
||||
<p class="text-sm text-gray-500">
|
||||
Customer information not yet provided
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
{#if passenger.bagSelection.handBags > 0}
|
||||
<div class="flex items-center gap-2">
|
||||
<Icon icon={SuitcaseIcon} cls="h-5 w-5 text-gray-600" />
|
||||
<span>{passenger.bagSelection.handBags} x Cabin Bag</span>
|
||||
</div>
|
||||
{/if}
|
||||
{#if passenger.bagSelection.checkedBags > 0}
|
||||
<div class="flex items-center gap-2">
|
||||
<Icon icon={BagIcon} cls="h-5 w-5 text-gray-600" />
|
||||
<span>
|
||||
{passenger.bagSelection.checkedBags} x Checked Bag
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Seat Selection -->
|
||||
{#if passenger.seatSelection.number}
|
||||
<div class="flex items-center gap-2 text-sm">
|
||||
<Icon icon={SeatIcon} cls="h-5 w-5 text-gray-600" />
|
||||
<span>Seat {passenger.seatSelection.number}</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if index < passengerInfoVM.passengerInfos.length - 1}
|
||||
<div class="border-b border-dashed"></div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
<script lang="ts">
|
||||
import Input from "$lib/components/ui/input/input.svelte";
|
||||
import LabelWrapper from "$lib/components/atoms/label-wrapper.svelte";
|
||||
import Input from "$lib/components/ui/input/input.svelte";
|
||||
import { paymentInfoVM } from "./payment.info.vm.svelte";
|
||||
import { chunk } from "$lib/core/array.utils";
|
||||
|
||||
function formatCardNumberForDisplay(value: string) {
|
||||
// return in format "XXXX XXXX XXXX XXXX" from "XXXXXXXXXXXXXXXX"
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
<script lang="ts">
|
||||
import * as Select from "$lib/components/ui/select";
|
||||
import type { SelectOption } from "$lib/core/data.types";
|
||||
import { capitalize } from "$lib/core/string.utils";
|
||||
|
||||
let {
|
||||
opts,
|
||||
onselect,
|
||||
}: { opts: SelectOption[]; onselect: (e: string) => void } = $props();
|
||||
|
||||
let chosenOpt = $state<SelectOption | undefined>(undefined);
|
||||
|
||||
function setOpt(val: string) {
|
||||
chosenOpt = opts.find((e) => e.value === val);
|
||||
onselect(val);
|
||||
}
|
||||
</script>
|
||||
|
||||
<Select.Root type="single" required onValueChange={(e) => setOpt(e)} name="role">
|
||||
<Select.Trigger class="w-full border-border/20">
|
||||
{capitalize(
|
||||
opts?.find((e) => e.value === chosenOpt?.value)?.label ??
|
||||
"Cabin Class",
|
||||
)}
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
{#each opts as each}
|
||||
<Select.Item value={each.value}>
|
||||
{each.label}
|
||||
</Select.Item>
|
||||
{/each}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
@@ -1,104 +0,0 @@
|
||||
<script lang="ts">
|
||||
import CalendarIcon from "@lucide/svelte/icons/calendar";
|
||||
import {
|
||||
DateFormatter,
|
||||
getLocalTimeZone,
|
||||
today,
|
||||
toCalendarDate,
|
||||
type DateValue,
|
||||
} from "@internationalized/date";
|
||||
import { cn } from "$lib/utils.js";
|
||||
import { buttonVariants } from "$lib/components/ui/button/index.js";
|
||||
import * as Popover from "$lib/components/ui/popover/index.js";
|
||||
|
||||
import Icon from "$lib/components/atoms/icon.svelte";
|
||||
import Calendar from "$lib/components/ui/calendar/calendar.svelte";
|
||||
import {
|
||||
parseCalDateToDateString,
|
||||
makeDateStringISO,
|
||||
} from "$lib/core/date.utils";
|
||||
|
||||
import { ticketSearchStore } from "../data/store";
|
||||
import { onMount } from "svelte";
|
||||
|
||||
const df = new DateFormatter("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
timeZone: getLocalTimeZone(),
|
||||
});
|
||||
|
||||
let contentRef = $state<HTMLElement | null>(null);
|
||||
|
||||
let open = $state(false);
|
||||
|
||||
const todayDate = today(getLocalTimeZone());
|
||||
let value = $state(today(getLocalTimeZone()));
|
||||
|
||||
let startFmt = $derived(value.toDate(getLocalTimeZone()));
|
||||
|
||||
let defaultPlaceholder = "Pick a date";
|
||||
let placeholder = $derived(
|
||||
value ? `${df.format(startFmt)}` : defaultPlaceholder,
|
||||
);
|
||||
|
||||
function updateValInStore() {
|
||||
const val = parseCalDateToDateString(value);
|
||||
ticketSearchStore.update((prev) => {
|
||||
return {
|
||||
...prev,
|
||||
departureDate: makeDateStringISO(val),
|
||||
returnDate: makeDateStringISO(val),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function isDateDisabled(date: DateValue) {
|
||||
return date.compare(todayDate) < 0;
|
||||
}
|
||||
|
||||
function handleDateSelection(v: DateValue | undefined) {
|
||||
if (!v) {
|
||||
value = today(getLocalTimeZone());
|
||||
} else {
|
||||
value = toCalendarDate(v);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
open = false;
|
||||
}, 0);
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
updateValInStore();
|
||||
});
|
||||
|
||||
onMount(() => {
|
||||
updateValInStore();
|
||||
});
|
||||
</script>
|
||||
|
||||
<Popover.Root bind:open>
|
||||
<Popover.Trigger
|
||||
class={cn(
|
||||
buttonVariants({
|
||||
variant: "white",
|
||||
class: "w-full justify-start text-left font-normal",
|
||||
}),
|
||||
!value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
<Icon icon={CalendarIcon} cls="w-auto h-4" />
|
||||
<span class="text-sm md:text-base">{placeholder}</span>
|
||||
</Popover.Trigger>
|
||||
<Popover.Content bind:ref={contentRef} class="w-auto p-0">
|
||||
<Calendar
|
||||
type="single"
|
||||
{value}
|
||||
minValue={todayDate}
|
||||
{isDateDisabled}
|
||||
onValueChange={(v) => handleDateSelection(v)}
|
||||
class="rounded-md border border-white"
|
||||
/>
|
||||
</Popover.Content>
|
||||
</Popover.Root>
|
||||
@@ -1,133 +0,0 @@
|
||||
<script lang="ts">
|
||||
import CalendarIcon from "@lucide/svelte/icons/calendar";
|
||||
import {
|
||||
DateFormatter,
|
||||
getLocalTimeZone,
|
||||
toCalendarDate,
|
||||
today,
|
||||
type DateValue,
|
||||
} from "@internationalized/date";
|
||||
import { cn } from "$lib/utils.js";
|
||||
import { buttonVariants } from "$lib/components/ui/button/index.js";
|
||||
import * as Popover from "$lib/components/ui/popover/index.js";
|
||||
|
||||
import { RangeCalendar } from "$lib/components/ui/range-calendar/index.js";
|
||||
import Icon from "$lib/components/atoms/icon.svelte";
|
||||
import { ticketSearchStore } from "../data/store";
|
||||
import { onMount } from "svelte";
|
||||
import {
|
||||
makeDateStringISO,
|
||||
parseCalDateToDateString,
|
||||
} from "$lib/core/date.utils";
|
||||
import Button from "$lib/components/ui/button/button.svelte";
|
||||
|
||||
const df = new DateFormatter("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
});
|
||||
|
||||
let contentRef = $state<HTMLElement | null>(null);
|
||||
|
||||
let open = $state(false);
|
||||
|
||||
const todayDate = today(getLocalTimeZone());
|
||||
|
||||
const start = today(getLocalTimeZone());
|
||||
const end = start.add({ days: 7 });
|
||||
|
||||
let value = $state({ start, end });
|
||||
|
||||
let startFmt = $derived(value?.start?.toDate(getLocalTimeZone()));
|
||||
let endFmt = $derived(value?.end?.toDate(getLocalTimeZone()));
|
||||
|
||||
let defaultPlaceholder = "Pick a date";
|
||||
let placeholder = $derived(
|
||||
value?.start && value?.end && startFmt && endFmt
|
||||
? `${df.formatRange(startFmt, endFmt)}`
|
||||
: value?.start && startFmt
|
||||
? `${df.format(startFmt)} - Select end date`
|
||||
: defaultPlaceholder,
|
||||
);
|
||||
|
||||
function isDateDisabled(date: DateValue) {
|
||||
return date.compare(todayDate) < 0;
|
||||
}
|
||||
|
||||
function handleStartChange(v: DateValue | undefined) {
|
||||
if (!v) {
|
||||
value.start = today(getLocalTimeZone());
|
||||
return;
|
||||
}
|
||||
|
||||
value.start = toCalendarDate(v);
|
||||
}
|
||||
|
||||
function handleEndChange(v: DateValue | undefined) {
|
||||
if (!v) {
|
||||
value.end = today(getLocalTimeZone());
|
||||
return;
|
||||
}
|
||||
|
||||
value.end = toCalendarDate(v);
|
||||
}
|
||||
|
||||
function updateValsInStore() {
|
||||
const sVal = parseCalDateToDateString(value.start);
|
||||
const eVal = parseCalDateToDateString(value.end);
|
||||
ticketSearchStore.update((prev) => {
|
||||
return {
|
||||
...prev,
|
||||
departureDate: makeDateStringISO(sVal),
|
||||
returnDate: makeDateStringISO(eVal),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
updateValsInStore();
|
||||
});
|
||||
|
||||
onMount(() => {
|
||||
updateValsInStore();
|
||||
});
|
||||
</script>
|
||||
|
||||
<Popover.Root
|
||||
{open}
|
||||
onOpenChange={(o) => {
|
||||
open = o;
|
||||
}}
|
||||
>
|
||||
<Popover.Trigger
|
||||
class={cn(
|
||||
buttonVariants({
|
||||
variant: "white",
|
||||
class: "w-full justify-start text-left font-normal",
|
||||
}),
|
||||
!value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
<Icon icon={CalendarIcon} cls="w-auto h-4" />
|
||||
<span class="text-sm md:text-base">{placeholder}</span>
|
||||
</Popover.Trigger>
|
||||
<Popover.Content bind:ref={contentRef} class="w-auto p-0">
|
||||
<RangeCalendar
|
||||
{value}
|
||||
{isDateDisabled}
|
||||
onStartValueChange={handleStartChange}
|
||||
onEndValueChange={handleEndChange}
|
||||
class="rounded-md border border-white"
|
||||
/>
|
||||
<div class="w-full p-2">
|
||||
<Button
|
||||
class="w-full"
|
||||
onclick={() => {
|
||||
open = false;
|
||||
}}
|
||||
>
|
||||
Confirm
|
||||
</Button>
|
||||
</div>
|
||||
</Popover.Content>
|
||||
</Popover.Root>
|
||||
@@ -1,116 +0,0 @@
|
||||
<script lang="ts">
|
||||
import Counter from "$lib/components/atoms/counter.svelte";
|
||||
import Title from "$lib/components/atoms/title.svelte";
|
||||
import { buttonVariants } from "$lib/components/ui/button";
|
||||
import * as Popover from "$lib/components/ui/popover/index.js";
|
||||
import { cn } from "$lib/utils";
|
||||
import { CabinClass } from "../data/entities";
|
||||
import { ticketSearchStore } from "../data/store";
|
||||
import { snakeToSpacedPascal } from "$lib/core/string.utils";
|
||||
import Icon from "$lib/components/atoms/icon.svelte";
|
||||
import ChevronDownIcon from "~icons/lucide/chevron-down";
|
||||
import CheckIcon from "~icons/lucide/check";
|
||||
|
||||
let cabinClass = $state($ticketSearchStore.cabinClass);
|
||||
|
||||
$effect(() => {
|
||||
ticketSearchStore.update((prev) => {
|
||||
return { ...prev, cabinClass: cabinClass };
|
||||
});
|
||||
});
|
||||
|
||||
let adultCount = $state(1);
|
||||
let childCount = $state(0);
|
||||
|
||||
$effect(() => {
|
||||
ticketSearchStore.update((prev) => {
|
||||
return {
|
||||
...prev,
|
||||
passengerCounts: { adults: adultCount, children: childCount },
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
let passengerCounts = $derived(adultCount + childCount);
|
||||
|
||||
let cabinClassOpen = $state(false);
|
||||
</script>
|
||||
|
||||
<div class="flex w-full flex-col items-center justify-end gap-2 sm:flex-row">
|
||||
<Popover.Root bind:open={cabinClassOpen}>
|
||||
<Popover.Trigger
|
||||
class={cn(
|
||||
buttonVariants({ variant: "white" }),
|
||||
"w-full justify-between",
|
||||
)}
|
||||
>
|
||||
{snakeToSpacedPascal(cabinClass.toLowerCase() ?? "Select")}
|
||||
<Icon icon={ChevronDownIcon} cls="w-auto h-4" />
|
||||
</Popover.Trigger>
|
||||
|
||||
<Popover.Content>
|
||||
<div class="flex flex-col gap-2">
|
||||
{#each Object.values(CabinClass) as each}
|
||||
<button
|
||||
onclick={() => {
|
||||
cabinClass = each;
|
||||
cabinClassOpen = false;
|
||||
}}
|
||||
class={cn(
|
||||
"flex items-center gap-2 rounded-md p-2 px-4 text-start hover:bg-gray-200",
|
||||
)}
|
||||
>
|
||||
{#if cabinClass === each}
|
||||
<Icon
|
||||
icon={CheckIcon}
|
||||
cls="w-auto h-4 text-brand-600"
|
||||
/>
|
||||
{:else}
|
||||
<div
|
||||
class="h-4 w-4 rounded-full bg-transparent"
|
||||
></div>
|
||||
{/if}
|
||||
{snakeToSpacedPascal(each.toLowerCase())}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</Popover.Content>
|
||||
</Popover.Root>
|
||||
|
||||
<Popover.Root>
|
||||
<Popover.Trigger
|
||||
class={cn(
|
||||
buttonVariants({ variant: "white" }),
|
||||
"w-full justify-between",
|
||||
)}
|
||||
>
|
||||
{passengerCounts} Passenger(s)
|
||||
<Icon icon={ChevronDownIcon} cls="w-auto h-4" />
|
||||
</Popover.Trigger>
|
||||
|
||||
<Popover.Content>
|
||||
<div class="flex flex-col gap-8">
|
||||
<Title size="h5" weight="normal" color="black"
|
||||
>Passenger Selection</Title
|
||||
>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div class="flex flex-col gap-1">
|
||||
<p>Adults</p>
|
||||
<p class="text-gray-500">Aged 16+</p>
|
||||
</div>
|
||||
<Counter bind:value={adultCount} />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div class="flex flex-col gap-1">
|
||||
<p>Children</p>
|
||||
<p class="text-gray-500">Aged 0-16</p>
|
||||
</div>
|
||||
<Counter bind:value={childCount} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Popover.Content>
|
||||
</Popover.Root>
|
||||
</div>
|
||||
@@ -1,41 +0,0 @@
|
||||
<script lang="ts">
|
||||
import Badge from "$lib/components/ui/badge/badge.svelte";
|
||||
import Button from "$lib/components/ui/button/button.svelte";
|
||||
import { flightTicketVM } from "./ticket.vm.svelte";
|
||||
import TicketCard from "./ticket/ticket-card.svelte";
|
||||
</script>
|
||||
|
||||
<div class="flex w-full flex-col gap-4">
|
||||
{#if flightTicketVM.searching}
|
||||
{#each Array(5) as _}
|
||||
<div
|
||||
class="h-64 w-full animate-pulse rounded-lg bg-gray-300 shadow-lg"
|
||||
></div>
|
||||
{/each}
|
||||
{:else if flightTicketVM.renderedTickets.length > 0}
|
||||
<Badge variant="outline" class="w-max">
|
||||
Showing {flightTicketVM.renderedTickets.length} tickets
|
||||
</Badge>
|
||||
{#each flightTicketVM.renderedTickets as each}
|
||||
<TicketCard data={each} />
|
||||
{/each}
|
||||
<Button
|
||||
class="text-center"
|
||||
variant="white"
|
||||
onclick={() => {
|
||||
flightTicketVM.searchForTickets(true);
|
||||
}}
|
||||
>
|
||||
Load More
|
||||
</Button>
|
||||
{:else}
|
||||
<div
|
||||
class="flex flex-col items-center justify-center gap-4 p-8 py-32 text-center"
|
||||
>
|
||||
<div class="text-2xl font-bold">No tickets found</div>
|
||||
<div class="text-sm text-gray-500">
|
||||
Try searching for a different flight
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -1,260 +0,0 @@
|
||||
<script lang="ts">
|
||||
import Title from "$lib/components/atoms/title.svelte";
|
||||
import { Slider } from "$lib/components/ui/slider/index.js";
|
||||
import {
|
||||
ticketFiltersStore,
|
||||
MaxStops,
|
||||
SortOption,
|
||||
} from "$lib/domains/ticket/view/ticket-filters.vm.svelte";
|
||||
import { flightTicketVM } from "./ticket.vm.svelte";
|
||||
import { RadioGroup, RadioGroupItem } from "$lib/components/ui/radio-group";
|
||||
import { Checkbox } from "$lib/components/ui/checkbox";
|
||||
import { Label } from "$lib/components/ui/label";
|
||||
import Button from "$lib/components/ui/button/button.svelte";
|
||||
import {
|
||||
convertAndFormatCurrency,
|
||||
currencyStore,
|
||||
currencyVM,
|
||||
} from "$lib/domains/currency/view/currency.vm.svelte";
|
||||
|
||||
let { onApplyClick }: { onApplyClick?: () => void } = $props();
|
||||
|
||||
function onApply() {
|
||||
flightTicketVM.applyFilters();
|
||||
if (onApplyClick) {
|
||||
onApplyClick();
|
||||
}
|
||||
}
|
||||
|
||||
let maxPrice = $state(
|
||||
flightTicketVM.tickets.length > 0
|
||||
? flightTicketVM.tickets.sort(
|
||||
(a, b) =>
|
||||
b.priceDetails.displayPrice - a.priceDetails.displayPrice,
|
||||
)[0]?.priceDetails.displayPrice
|
||||
: 100,
|
||||
);
|
||||
|
||||
let priceRange = $state([
|
||||
0,
|
||||
currencyVM.convertFromUsd(
|
||||
$ticketFiltersStore.priceRange.max,
|
||||
$currencyStore.code,
|
||||
),
|
||||
]);
|
||||
|
||||
// Time ranges
|
||||
let departureTimeRange = $state([0, $ticketFiltersStore.time.departure.max]);
|
||||
let arrivalTimeRange = $state([0, $ticketFiltersStore.time.arrival.max]);
|
||||
let durationRange = $state([0, $ticketFiltersStore.duration.max]);
|
||||
|
||||
let maxStops = $state($ticketFiltersStore.maxStops);
|
||||
let allowOvernight = $state($ticketFiltersStore.allowOvernight);
|
||||
|
||||
$effect(() => {
|
||||
maxPrice =
|
||||
flightTicketVM.tickets.length > 0
|
||||
? flightTicketVM.tickets.sort(
|
||||
(a, b) =>
|
||||
b.priceDetails.displayPrice -
|
||||
a.priceDetails.displayPrice,
|
||||
)[0]?.priceDetails.displayPrice
|
||||
: 100;
|
||||
if (priceRange[0] > maxPrice || priceRange[1] > maxPrice) {
|
||||
priceRange = [
|
||||
0,
|
||||
currencyVM.convertFromUsd(maxPrice, $currencyStore.code),
|
||||
];
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
ticketFiltersStore.update((prev) => ({
|
||||
...prev,
|
||||
priceRange: { min: priceRange[0], max: priceRange[1] },
|
||||
maxStops,
|
||||
allowOvernight,
|
||||
time: {
|
||||
departure: {
|
||||
min: departureTimeRange[0],
|
||||
max: departureTimeRange[1],
|
||||
},
|
||||
arrival: { min: arrivalTimeRange[0], max: arrivalTimeRange[1] },
|
||||
},
|
||||
duration: { min: durationRange[0], max: durationRange[1] },
|
||||
}));
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex w-full max-w-sm flex-col gap-6">
|
||||
<Title size="h5" color="black">Sort By</Title>
|
||||
|
||||
{#if flightTicketVM.searching}
|
||||
<div class="h-16 w-full animate-pulse rounded-lg bg-gray-300"></div>
|
||||
{:else}
|
||||
<RadioGroup
|
||||
value={$ticketFiltersStore.sortBy}
|
||||
onValueChange={(value) => {
|
||||
ticketFiltersStore.update((prev) => ({
|
||||
...prev,
|
||||
sortBy: value as SortOption,
|
||||
}));
|
||||
}}
|
||||
>
|
||||
<div class="flex items-center space-x-2">
|
||||
<RadioGroupItem
|
||||
value={SortOption.Default}
|
||||
id="price_low_to_high"
|
||||
/>
|
||||
<Label for="default">Price: Default</Label>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<RadioGroupItem
|
||||
value={SortOption.PriceLowToHigh}
|
||||
id="price_low_to_high"
|
||||
/>
|
||||
<Label for="price_low_to_high">Price: Low to High</Label>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<RadioGroupItem
|
||||
value={SortOption.PriceHighToLow}
|
||||
id="price_high_to_low"
|
||||
/>
|
||||
<Label for="price_high_to_low">Price: High to Low</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
{/if}
|
||||
|
||||
<Title size="h5" color="black">Price</Title>
|
||||
|
||||
{#if flightTicketVM.searching}
|
||||
<div class="h-6 w-full animate-pulse rounded-full bg-gray-300"></div>
|
||||
{:else}
|
||||
<div class="flex w-full flex-col gap-2">
|
||||
<Slider
|
||||
type="multiple"
|
||||
bind:value={priceRange}
|
||||
min={0}
|
||||
max={maxPrice}
|
||||
step={1}
|
||||
/>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<div class="flex flex-col gap-1">
|
||||
<p class="text-sm font-medium">Min</p>
|
||||
<p class="text-sm text-gray-500">{priceRange[0]}</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<p class="text-sm font-medium">Max</p>
|
||||
<p class="text-sm text-gray-500">{priceRange[1]}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Title size="h5" color="black">Max Stops</Title>
|
||||
|
||||
{#if flightTicketVM.searching}
|
||||
<div class="h-24 w-full animate-pulse rounded-lg bg-gray-300"></div>
|
||||
{:else}
|
||||
<RadioGroup
|
||||
value={$ticketFiltersStore.maxStops}
|
||||
onValueChange={(value) => {
|
||||
ticketFiltersStore.update((prev) => ({
|
||||
...prev,
|
||||
maxStops: value as MaxStops,
|
||||
}));
|
||||
}}
|
||||
>
|
||||
{#each Object.entries(MaxStops) as [label, value]}
|
||||
<div class="flex items-center space-x-2">
|
||||
<RadioGroupItem {value} id={value} />
|
||||
<Label for={value}>{label}</Label>
|
||||
</div>
|
||||
{/each}
|
||||
</RadioGroup>
|
||||
{/if}
|
||||
|
||||
{#if flightTicketVM.searching}
|
||||
<div class="h-8 w-full animate-pulse rounded-lg bg-gray-300"></div>
|
||||
{:else}
|
||||
<div class="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
checked={$ticketFiltersStore.allowOvernight}
|
||||
onCheckedChange={(checked) => {
|
||||
ticketFiltersStore.update((prev) => ({
|
||||
...prev,
|
||||
allowOvernight: checked,
|
||||
}));
|
||||
}}
|
||||
id="overnight"
|
||||
/>
|
||||
<Label for="overnight">Allow Overnight Flights</Label>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Title size="h5" color="black">Time</Title>
|
||||
|
||||
{#if flightTicketVM.searching}
|
||||
<div class="space-y-4">
|
||||
<div class="h-16 w-full animate-pulse rounded-lg bg-gray-300"></div>
|
||||
<div class="h-16 w-full animate-pulse rounded-lg bg-gray-300"></div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="space-y-4">
|
||||
<div class="flex flex-col gap-2">
|
||||
<p class="mb-2 text-sm font-medium">Departure Time</p>
|
||||
<Slider
|
||||
type="multiple"
|
||||
bind:value={departureTimeRange}
|
||||
min={0}
|
||||
max={24}
|
||||
step={1}
|
||||
/>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<small>{departureTimeRange[0]}:00</small>
|
||||
<small>{departureTimeRange[1]}:00</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<p class="mb-2 text-sm font-medium">Arrival Time</p>
|
||||
<Slider
|
||||
type="multiple"
|
||||
bind:value={arrivalTimeRange}
|
||||
min={0}
|
||||
max={24}
|
||||
step={1}
|
||||
/>
|
||||
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<small>{arrivalTimeRange[0]}:00</small>
|
||||
<small>{arrivalTimeRange[1]}:00</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Title size="h5" color="black">Duration</Title>
|
||||
|
||||
{#if flightTicketVM.searching}
|
||||
<div class="h-16 w-full animate-pulse rounded-lg bg-gray-300"></div>
|
||||
{:else}
|
||||
<div class="flex flex-col gap-2">
|
||||
<p class="mb-2 text-sm font-medium">Max Duration</p>
|
||||
<Slider
|
||||
type="multiple"
|
||||
bind:value={durationRange}
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
/>
|
||||
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<small>{durationRange[0]} hours</small>
|
||||
<small>{durationRange[1]} hours</small>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Button onclick={() => onApply()} class="w-full">Apply Changes</Button>
|
||||
</div>
|
||||
@@ -1,24 +0,0 @@
|
||||
import { writable } from "svelte/store";
|
||||
|
||||
export enum SortOption {
|
||||
Default = "default",
|
||||
PriceLowToHigh = "price_low_to_high",
|
||||
PriceHighToLow = "price_high_to_low",
|
||||
}
|
||||
|
||||
export enum MaxStops {
|
||||
Any = "any",
|
||||
Direct = "direct",
|
||||
One = "one",
|
||||
Two = "two",
|
||||
}
|
||||
|
||||
export const ticketFiltersStore = writable({
|
||||
priceRange: { min: 0, max: 0 },
|
||||
excludeCountries: [] as string[],
|
||||
maxStops: MaxStops.Any,
|
||||
allowOvernight: true,
|
||||
time: { departure: { min: 0, max: 0 }, arrival: { min: 0, max: 0 } },
|
||||
duration: { min: 0, max: 0 },
|
||||
sortBy: SortOption.Default,
|
||||
});
|
||||
@@ -1,150 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { TicketType } from "../data/entities/index";
|
||||
import AirportSearchInput from "$lib/domains/airport/view/airport-search-input.svelte";
|
||||
import { ticketSearchStore } from "../data/store";
|
||||
import FlightDateInput from "./flight-date-input.svelte";
|
||||
import FlightDateRangeInput from "./flight-date-range-input.svelte";
|
||||
import Icon from "$lib/components/atoms/icon.svelte";
|
||||
import SearchIcon from "~icons/solar/magnifer-linear";
|
||||
import SwapIcon from "~icons/ant-design/swap-outlined";
|
||||
import Button from "$lib/components/ui/button/button.svelte";
|
||||
import PassengerAndCabinClassSelect from "./passenger-and-cabin-class-select.svelte";
|
||||
import { Label } from "$lib/components/ui/label/index.js";
|
||||
import * as RadioGroup from "$lib/components/ui/radio-group/index.js";
|
||||
import { airportVM } from "$lib/domains/airport/view/airport.vm.svelte";
|
||||
import { browser } from "$app/environment";
|
||||
import { onDestroy, onMount } from "svelte";
|
||||
|
||||
let { onSubmit, rowify = false }: { onSubmit: () => void; rowify?: boolean } =
|
||||
$props();
|
||||
|
||||
let currTicketType = $state($ticketSearchStore.ticketType);
|
||||
let isMobileView = $state(false);
|
||||
|
||||
function checkIfMobile() {
|
||||
if (browser) {
|
||||
isMobileView = window.innerWidth < 768;
|
||||
}
|
||||
}
|
||||
|
||||
function setupResizeListener() {
|
||||
if (browser) {
|
||||
window.addEventListener("resize", checkIfMobile);
|
||||
checkIfMobile(); // Initial check
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("resize", checkIfMobile);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
ticketSearchStore.update((prev) => {
|
||||
return { ...prev, ticketType: currTicketType };
|
||||
});
|
||||
});
|
||||
|
||||
let cleanup: any | undefined = $state(undefined);
|
||||
|
||||
onMount(() => {
|
||||
cleanup = setupResizeListener();
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
cleanup?.();
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex w-full flex-col gap-4">
|
||||
<div
|
||||
class="flex w-full flex-col items-center justify-between gap-4 lg:grid lg:grid-cols-2 lg:gap-4"
|
||||
>
|
||||
<RadioGroup.Root
|
||||
bind:value={currTicketType}
|
||||
class="flex w-full flex-row gap-6 p-2 lg:p-4"
|
||||
>
|
||||
<div class="flex items-center space-x-2">
|
||||
<RadioGroup.Item
|
||||
value={TicketType.Return}
|
||||
id={TicketType.Return}
|
||||
onselect={() => {
|
||||
ticketSearchStore.update((prev) => {
|
||||
return { ...prev, ticketType: TicketType.Return };
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Label for={TicketType.Return} class="md:text-lg">Return</Label>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<RadioGroup.Item
|
||||
value={TicketType.OneWay}
|
||||
id={TicketType.OneWay}
|
||||
onselect={() => {
|
||||
ticketSearchStore.update((prev) => {
|
||||
return { ...prev, ticketType: TicketType.OneWay };
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Label for={TicketType.OneWay} class="md:text-lg">One Way</Label>
|
||||
</div>
|
||||
</RadioGroup.Root>
|
||||
|
||||
<PassengerAndCabinClassSelect />
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex w-full flex-col items-center justify-between gap-2 lg:grid lg:grid-cols-2 lg:gap-4"
|
||||
>
|
||||
<div
|
||||
class="flex w-full flex-col items-center justify-between gap-2 lg:flex-row"
|
||||
>
|
||||
<AirportSearchInput
|
||||
currentValue={airportVM.departure}
|
||||
onChange={(e) => {
|
||||
airportVM.setDepartureAirport(e);
|
||||
}}
|
||||
placeholder="Depart from"
|
||||
searchPlaceholder="Departure airport search"
|
||||
isMobile={isMobileView}
|
||||
fieldType="departure"
|
||||
/>
|
||||
<div class="hidden w-full max-w-fit md:block">
|
||||
<Button
|
||||
size="icon"
|
||||
variant={rowify ? "outlineWhite" : "defaultInverted"}
|
||||
onclick={() => {
|
||||
airportVM.swapDepartureAndArrival();
|
||||
}}
|
||||
>
|
||||
<Icon icon={SwapIcon} cls="w-auto h-6" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<AirportSearchInput
|
||||
currentValue={airportVM.arrival}
|
||||
onChange={(e) => {
|
||||
airportVM.setArrivalAirport(e);
|
||||
}}
|
||||
placeholder="Arrive at"
|
||||
searchPlaceholder="Arrival airport search"
|
||||
isMobile={isMobileView}
|
||||
fieldType="arrival"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex w-full flex-col items-center justify-between gap-2 lg:flex-row lg:gap-2"
|
||||
>
|
||||
{#if $ticketSearchStore.ticketType === TicketType.Return}
|
||||
<FlightDateRangeInput />
|
||||
{:else}
|
||||
<FlightDateInput />
|
||||
{/if}
|
||||
|
||||
<Button onclick={() => onSubmit()} class={"w-full"} variant="default">
|
||||
<Icon icon={SearchIcon} cls="w-auto h-4" />
|
||||
Search flights
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,35 +0,0 @@
|
||||
<script lang="ts">
|
||||
import Title from "$lib/components/atoms/title.svelte";
|
||||
import TableOfContents from "../table-of-contents.svelte";
|
||||
|
||||
const toc = [{ title: "Introduction", link: "#introduction" }];
|
||||
</script>
|
||||
|
||||
<section class="flex w-full flex-col-reverse gap-4 md:gap-8 lg:flex-row">
|
||||
<div
|
||||
class="col-span-3 flex w-full flex-col gap-4 rounded-lg border border-brand-200 bg-gradient-to-b from-white/50 to-brand-50 p-4 md:p-8"
|
||||
>
|
||||
<Title size="h2" weight="medium">Introduction</Title>
|
||||
|
||||
<p>
|
||||
Welcome to FlyTicketTravel's legal documentation. As a flight booking
|
||||
platform dedicated to providing seamless travel experiences, we take
|
||||
our legal obligations seriously, particularly regarding payment
|
||||
processing, data protection, and user privacy. This section outlines
|
||||
our Privacy Policy, Terms and Conditions, Cookie Policy, and Refund
|
||||
Policy, ensuring transparency in how we handle your personal data and
|
||||
transactions.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Our policies are designed to comply with international data protection
|
||||
laws, including GDPR. They cover everything from how we process your
|
||||
flight bookings to how we secure your payment information. We
|
||||
encourage you to read each section carefully to understand your rights
|
||||
and our commitments to protecting your privacy while using
|
||||
FlyTicketTravel's services.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<TableOfContents {toc} />
|
||||
</section>
|
||||
@@ -1,36 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { cn } from "$lib/utils";
|
||||
import { TRANSITION_COLORS } from "$lib/core/constants";
|
||||
import { legalArticles } from "./legal.articles";
|
||||
import MaxWidthWrapper from "$lib/components/molecules/max-width-wrapper.svelte";
|
||||
|
||||
interface Props {
|
||||
children?: import("svelte").Snippet;
|
||||
}
|
||||
|
||||
let { children }: Props = $props();
|
||||
</script>
|
||||
|
||||
<MaxWidthWrapper
|
||||
cls="my-32 grid grid-cols-1 gap-4 px-4 md:px-8 lg:grid-cols-5 lg:gap-8"
|
||||
>
|
||||
<div
|
||||
class="flex h-max flex-col gap-4 rounded-lg border border-brand-200 bg-brand-50/50 p-4 lg:col-span-1"
|
||||
>
|
||||
{#each legalArticles as article}
|
||||
<a
|
||||
href={article.link}
|
||||
class={cn(
|
||||
"cursor-pointer text-brand-950 hover:text-brand-600",
|
||||
TRANSITION_COLORS,
|
||||
)}
|
||||
>
|
||||
{article.title}
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="lg:col-span-4">
|
||||
{@render children?.()}
|
||||
</div>
|
||||
</MaxWidthWrapper>
|
||||
@@ -1,93 +0,0 @@
|
||||
<script lang="ts">
|
||||
import Title from "$lib/components/atoms/title.svelte";
|
||||
import { CONTACT_INFO } from "$lib/core/constants";
|
||||
import TableOfContents from "../table-of-contents.svelte";
|
||||
|
||||
const toc = [
|
||||
{ title: "What Are Cookies?", link: "#what-are-cookies" },
|
||||
{ title: "How We Use Cookies", link: "#how-we-use-cookies" },
|
||||
{ title: "Types of Cookies We Use", link: "#types-of-cookies" },
|
||||
{ title: "Managing Cookies", link: "#managing-cookies" },
|
||||
{
|
||||
title: "Changes to This Cookie Policy",
|
||||
link: "#changes-to-this-cookie-policy",
|
||||
},
|
||||
{ title: "Contact Us", link: "#contact-us" },
|
||||
];
|
||||
|
||||
const lastUpdated = "December 3, 2024";
|
||||
</script>
|
||||
|
||||
<section class="flex w-full flex-col-reverse gap-6 md:gap-8 lg:flex-row">
|
||||
<div
|
||||
class="col-span-3 flex w-full flex-col gap-4 rounded-lg border border-brand-200 bg-gradient-to-b from-white/50 to-brand-50 p-4 md:p-8"
|
||||
>
|
||||
<Title size="h2" weight="medium">Cookie Policy</Title>
|
||||
|
||||
<p>
|
||||
FlyTicketTravel ("we", "our", "us") uses cookies and similar
|
||||
technologies to provide you with a better experience while using our
|
||||
flight booking platform. This policy explains how we use cookies and
|
||||
your choices regarding them.
|
||||
</p>
|
||||
|
||||
<Title size="h3" weight="medium" id="what-are-cookies"
|
||||
>1. What Are Cookies?</Title
|
||||
>
|
||||
<p>
|
||||
Cookies are small text files stored on your device when you visit our
|
||||
platform. They help us provide essential features and improve your
|
||||
experience with FlyTicketTravel.
|
||||
</p>
|
||||
|
||||
<Title size="h3" weight="medium" id="how-we-use-cookies"
|
||||
>2. How We Use Cookies</Title
|
||||
>
|
||||
<p>We use cookies to:</p>
|
||||
<ul class="list-disc pl-4">
|
||||
<li>Keep you signed in to your FlyTicketTravel account securely</li>
|
||||
<li>Remember your flight search preferences and settings</li>
|
||||
<li>Ensure the security of your payment information</li>
|
||||
<li>Improve our platform's performance</li>
|
||||
<li>Analyze how our services are used</li>
|
||||
</ul>
|
||||
|
||||
<Title size="h3" weight="medium" id="types-of-cookies"
|
||||
>3. Types of Cookies We Use</Title
|
||||
>
|
||||
<p>Our platform uses:</p>
|
||||
<ul class="list-disc pl-4">
|
||||
<li>Essential cookies: Required for basic platform functionality</li>
|
||||
<li>Authentication cookies: To keep you securely logged in</li>
|
||||
<li>Preference cookies: To remember your settings</li>
|
||||
<li>Analytics cookies: To improve our services</li>
|
||||
</ul>
|
||||
|
||||
<Title size="h3" weight="medium" id="managing-cookies"
|
||||
>4. Managing Cookies</Title
|
||||
>
|
||||
<p>
|
||||
You can control cookies through your browser settings. Note that
|
||||
disabling certain cookies may limit your access to some features of
|
||||
FlyTicketTravel, particularly those related to secure booking and
|
||||
payment processing.
|
||||
</p>
|
||||
|
||||
<Title size="h3" weight="medium" id="changes-to-this-cookie-policy"
|
||||
>5. Changes to This Cookie Policy</Title
|
||||
>
|
||||
<p>
|
||||
We may update this policy as we enhance our platform. Any changes will
|
||||
be posted here with an updated revision date.
|
||||
</p>
|
||||
|
||||
<Title size="h3" weight="medium" id="contact-us">6. Contact Us</Title>
|
||||
<p>
|
||||
For questions about our cookie practices, contact us at {CONTACT_INFO.email}.
|
||||
</p>
|
||||
|
||||
<small class="text-gray-500">Last Updated: {lastUpdated}</small>
|
||||
</div>
|
||||
|
||||
<TableOfContents {toc} />
|
||||
</section>
|
||||
@@ -1,27 +0,0 @@
|
||||
export const legalArticles = [
|
||||
{
|
||||
id: "introduction",
|
||||
title: "Introduction",
|
||||
link: "/legal",
|
||||
},
|
||||
{
|
||||
id: "privacy-policy",
|
||||
title: "Privacy Policy",
|
||||
link: "/legal/privacy-policy",
|
||||
},
|
||||
{
|
||||
id: "terms-and-conditions",
|
||||
title: "Terms & Conditions",
|
||||
link: "/legal/terms-and-conditions",
|
||||
},
|
||||
{
|
||||
id: "refund-policy",
|
||||
title: "Refund Policy",
|
||||
link: "/legal/refund-policy",
|
||||
},
|
||||
{
|
||||
id: "cookie-policy",
|
||||
title: "Cookie Policy",
|
||||
link: "/legal/cookie-policy",
|
||||
},
|
||||
];
|
||||
@@ -1,133 +0,0 @@
|
||||
<script lang="ts">
|
||||
import Title from "$lib/components/atoms/title.svelte";
|
||||
import { CONTACT_INFO } from "$lib/core/constants";
|
||||
import TableOfContents from "../table-of-contents.svelte";
|
||||
|
||||
const toc = [
|
||||
{ title: "Information We Collect", link: "#information-we-collect" },
|
||||
{ title: "Booking Data", link: "#booking-data" },
|
||||
{
|
||||
title: "How We Use Your Information",
|
||||
link: "#how-we-use-your-information",
|
||||
},
|
||||
{ title: "Data Protection", link: "#data-protection" },
|
||||
{ title: "International Transfers", link: "#international-transfers" },
|
||||
{ title: "Your Rights", link: "#your-rights" },
|
||||
{ title: "Changes to Policy", link: "#changes-to-policy" },
|
||||
{ title: "Contact Us", link: "#contact-us" },
|
||||
];
|
||||
|
||||
const lastUpdated = "December 3, 2024";
|
||||
</script>
|
||||
|
||||
<section class="flex w-full flex-col-reverse gap-6 md:gap-8 lg:flex-row">
|
||||
<div
|
||||
class="col-span-3 flex w-full flex-col gap-4 rounded-lg border border-brand-200 bg-gradient-to-b from-white/50 to-brand-50 p-4 md:p-8"
|
||||
>
|
||||
<Title size="h2" weight="medium">Privacy Policy</Title>
|
||||
<p>
|
||||
FlyTicketTravel is committed to protecting your privacy and ensuring
|
||||
the security of your personal information. This Privacy Policy
|
||||
explains how we collect, use, and protect your data when you use our
|
||||
flight booking platform.
|
||||
</p>
|
||||
|
||||
<Title size="h3" weight="medium" id="information-we-collect"
|
||||
>1. Information We Collect</Title
|
||||
>
|
||||
<p>We collect the following types of information:</p>
|
||||
<ul class="list-disc pl-4">
|
||||
<li>
|
||||
Personal Information:
|
||||
<ul class="list-disc pl-4">
|
||||
<li>Full name and contact details</li>
|
||||
<li>Travel document information (passport/ID)</li>
|
||||
<li>Payment details</li>
|
||||
<li>Date of birth and nationality</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
Usage Information:
|
||||
<ul class="list-disc pl-4">
|
||||
<li>How you use our platform</li>
|
||||
<li>Search history and preferences</li>
|
||||
<li>Device and browser information</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<Title size="h3" weight="medium" id="booking-data">2. Booking Data</Title>
|
||||
<p>We handle travel booking information including:</p>
|
||||
<ul class="list-disc pl-4">
|
||||
<li>Flight reservation details</li>
|
||||
<li>Itinerary information</li>
|
||||
<li>Special requests (meals, seating, etc.)</li>
|
||||
</ul>
|
||||
<p>
|
||||
This data is processed in accordance with GDPR and relevant travel
|
||||
industry regulations. We implement additional security measures for
|
||||
payment data protection.
|
||||
</p>
|
||||
|
||||
<Title size="h3" weight="medium" id="how-we-use-your-information"
|
||||
>3. How We Use Your Information</Title
|
||||
>
|
||||
<p>We use your information to:</p>
|
||||
<ul class="list-disc pl-4">
|
||||
<li>Process flight bookings</li>
|
||||
<li>Provide customer support</li>
|
||||
<li>Send booking confirmations and updates</li>
|
||||
<li>Improve our platform</li>
|
||||
<li>Comply with legal obligations</li>
|
||||
</ul>
|
||||
|
||||
<Title size="h3" weight="medium" id="data-protection"
|
||||
>4. Data Protection</Title
|
||||
>
|
||||
<p>We implement robust security measures including:</p>
|
||||
<ul class="list-disc pl-4">
|
||||
<li>Encryption for payment data</li>
|
||||
<li>Secure access controls</li>
|
||||
<li>Regular security audits</li>
|
||||
<li>Staff training on data protection</li>
|
||||
</ul>
|
||||
|
||||
<Title size="h3" weight="medium" id="international-transfers"
|
||||
>5. International Transfers</Title
|
||||
>
|
||||
<p>
|
||||
As a flight booking service, we may transfer data across borders. All
|
||||
transfers comply with GDPR and relevant data protection laws, using
|
||||
appropriate safeguards and security measures.
|
||||
</p>
|
||||
|
||||
<Title size="h3" weight="medium" id="your-rights">6. Your Rights</Title>
|
||||
<p>Under GDPR, you have the right to:</p>
|
||||
<ul class="list-disc pl-4">
|
||||
<li>Access your personal data</li>
|
||||
<li>Correct inaccurate data</li>
|
||||
<li>Request data deletion</li>
|
||||
<li>Restrict processing</li>
|
||||
<li>Data portability</li>
|
||||
<li>Object to processing</li>
|
||||
</ul>
|
||||
|
||||
<Title size="h3" weight="medium" id="changes-to-policy"
|
||||
>7. Changes to Policy</Title
|
||||
>
|
||||
<p>
|
||||
We may update this policy as our services evolve. Significant changes
|
||||
will be notified to users directly through the platform.
|
||||
</p>
|
||||
|
||||
<Title size="h3" weight="medium" id="contact-us">8. Contact Us</Title>
|
||||
<p>
|
||||
For privacy-related queries or to exercise your rights, contact our
|
||||
Data Protection Officer at {CONTACT_INFO.email}.
|
||||
</p>
|
||||
|
||||
<small class="text-gray-500">Last Updated: {lastUpdated}</small>
|
||||
</div>
|
||||
|
||||
<TableOfContents {toc} />
|
||||
</section>
|
||||
@@ -1,177 +0,0 @@
|
||||
<script lang="ts">
|
||||
import Title from "$lib/components/atoms/title.svelte";
|
||||
import { CONTACT_INFO } from "$lib/core/constants";
|
||||
import TableOfContents from "../table-of-contents.svelte";
|
||||
|
||||
const toc = [
|
||||
{ title: "Booking Cancellations", link: "#booking-cancellations" },
|
||||
{
|
||||
title: "Airline-Controlled Refunds",
|
||||
link: "#airline-controlled-refunds",
|
||||
},
|
||||
{
|
||||
title: "FlyTicketTravel Service Fees",
|
||||
link: "#FlyTicketTravel-service-fees",
|
||||
},
|
||||
{ title: "Refund Processing", link: "#refund-processing" },
|
||||
{ title: "Flight Changes", link: "#flight-changes" },
|
||||
{ title: "Special Circumstances", link: "#special-circumstances" },
|
||||
{ title: "Contact Us", link: "#contact-us" },
|
||||
];
|
||||
|
||||
const lastUpdated = "December 3, 2024";
|
||||
</script>
|
||||
|
||||
<section class="flex w-full flex-col-reverse gap-6 md:gap-8 lg:flex-row">
|
||||
<div
|
||||
class="col-span-3 flex w-full flex-col gap-4 rounded-lg border border-brand-200 bg-gradient-to-b from-white/50 to-brand-50 p-4 md:p-8"
|
||||
>
|
||||
<Title size="h2" weight="medium">Refund Policy</Title>
|
||||
|
||||
<p>
|
||||
At FlyTicketTravel, we understand that travel plans can change. This
|
||||
policy outlines our approach to refunds and cancellations for flight
|
||||
bookings made through our platform.
|
||||
</p>
|
||||
|
||||
<Title size="h3" weight="medium" id="booking-cancellations"
|
||||
>1. Booking Cancellations</Title
|
||||
>
|
||||
<p>Our cancellation policy varies based on ticket type:</p>
|
||||
<ul class="list-disc pl-4">
|
||||
<li>
|
||||
<strong>Refundable Tickets:</strong> Eligible for full or partial refunds
|
||||
as per airline terms
|
||||
</li>
|
||||
<li>
|
||||
<strong>Non-Refundable Tickets:</strong> Generally not eligible for
|
||||
refunds, but may receive airline credits or partial refunds in special
|
||||
circumstances
|
||||
</li>
|
||||
<li>
|
||||
<strong>Flexible Tickets:</strong> Can be changed with minimal or no
|
||||
fees, subject to fare difference
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
The specific refund terms for your booking are displayed during the
|
||||
booking process and in your confirmation email.
|
||||
</p>
|
||||
|
||||
<Title size="h3" weight="medium" id="airline-controlled-refunds"
|
||||
>2. Airline-Controlled Refunds</Title
|
||||
>
|
||||
<p>Important information about airline policies:</p>
|
||||
<ul class="list-disc pl-4">
|
||||
<li>
|
||||
Most refunds are subject to the airline's fare rules and
|
||||
conditions of carriage
|
||||
</li>
|
||||
<li>Each airline has its own cancellation fees and policies</li>
|
||||
<li>
|
||||
Some low-cost carriers offer no refunds under any circumstances
|
||||
</li>
|
||||
<li>Premium airlines typically offer more flexible refund options</li>
|
||||
</ul>
|
||||
<p>
|
||||
FlyTicketTravel will advocate on your behalf with airlines, but we
|
||||
cannot override their refund policies.
|
||||
</p>
|
||||
|
||||
<Title size="h3" weight="medium" id="FlyTicketTravel-service-fees"
|
||||
>3. FlyTicketTravel Service Fees</Title
|
||||
>
|
||||
<p>Our policy regarding service fees:</p>
|
||||
<ul class="list-disc pl-4">
|
||||
<li>
|
||||
FlyTicketTravel booking service fees are non-refundable after 24
|
||||
hours from booking
|
||||
</li>
|
||||
<li>
|
||||
Within 24 hours of booking, service fees are fully refundable if
|
||||
you cancel
|
||||
</li>
|
||||
<li>
|
||||
Premium support fees are non-refundable once the service has been
|
||||
provided
|
||||
</li>
|
||||
<li>
|
||||
Seat selection and other ancillary fees follow the airline's
|
||||
refund policy
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<Title size="h3" weight="medium" id="refund-processing"
|
||||
>4. Refund Processing</Title
|
||||
>
|
||||
<p>When you're eligible for a refund:</p>
|
||||
<ul class="list-disc pl-4">
|
||||
<li>
|
||||
Refunds are processed to the original payment method used for
|
||||
booking
|
||||
</li>
|
||||
<li>
|
||||
Processing time is typically 7-14 business days after airline
|
||||
approval
|
||||
</li>
|
||||
<li>
|
||||
Credit card refunds may take an additional 1-2 billing cycles to
|
||||
appear
|
||||
</li>
|
||||
<li>
|
||||
You'll receive email confirmation when your refund is processed
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<Title size="h3" weight="medium" id="flight-changes"
|
||||
>5. Flight Changes</Title
|
||||
>
|
||||
<p>Our policy for changing flights:</p>
|
||||
<ul class="list-disc pl-4">
|
||||
<li>
|
||||
Most tickets allow date/time changes for a fee plus any fare
|
||||
difference
|
||||
</li>
|
||||
<li>
|
||||
Changes must be made at least 24 hours before departure (varies by
|
||||
airline)
|
||||
</li>
|
||||
<li>Name changes are generally not permitted by airlines</li>
|
||||
<li>Route changes are treated as a cancellation and new booking</li>
|
||||
</ul>
|
||||
|
||||
<Title size="h3" weight="medium" id="special-circumstances"
|
||||
>6. Special Circumstances</Title
|
||||
>
|
||||
<p>We offer additional flexibility for:</p>
|
||||
<ul class="list-disc pl-4">
|
||||
<li>
|
||||
<strong>Flight Cancellations by Airline:</strong> Full refund or rebooking
|
||||
assistance
|
||||
</li>
|
||||
<li>
|
||||
<strong>Significant Schedule Changes:</strong> Option to accept change
|
||||
or request refund
|
||||
</li>
|
||||
<li>
|
||||
<strong>Medical Emergencies:</strong> We'll help you request special
|
||||
consideration from airlines (documentation required)
|
||||
</li>
|
||||
<li>
|
||||
<strong>COVID-19 Related Changes:</strong> Subject to current airline
|
||||
and regulatory policies
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<Title size="h3" weight="medium" id="contact-us">7. Contact Us</Title>
|
||||
<p>
|
||||
To request a refund or for questions about our refund policy, please
|
||||
contact our customer support team at {CONTACT_INFO.email} or through the
|
||||
support section in your FlyTicketTravel account.
|
||||
</p>
|
||||
|
||||
<small class="text-gray-500">Last Updated: {lastUpdated}</small>
|
||||
</div>
|
||||
|
||||
<TableOfContents {toc} />
|
||||
</section>
|
||||
@@ -1,31 +0,0 @@
|
||||
<script lang="ts">
|
||||
import Title from "$lib/components/atoms/title.svelte";
|
||||
import { cn } from "$lib/utils";
|
||||
import { TRANSITION_COLORS } from "$lib/core/constants";
|
||||
|
||||
interface Props {
|
||||
toc?: any;
|
||||
}
|
||||
|
||||
let { toc = [] }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="flex w-full flex-col gap-4 lg:max-w-[15rem]">
|
||||
<div
|
||||
class="flex w-full flex-col gap-4 rounded-lg border border-brand-200 bg-brand-50/50 p-4"
|
||||
>
|
||||
<Title capitalize size="h5" weight="medium">Table of contents</Title>
|
||||
|
||||
{#each toc as each}
|
||||
<a
|
||||
class={cn(
|
||||
"cursor-pointer text-brand-950 hover:text-brand-600",
|
||||
TRANSITION_COLORS,
|
||||
)}
|
||||
href={each.link}
|
||||
>
|
||||
{each.title}
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,119 +0,0 @@
|
||||
<script lang="ts">
|
||||
import Title from "$lib/components/atoms/title.svelte";
|
||||
import { CONTACT_INFO } from "$lib/core/constants";
|
||||
import TableOfContents from "../table-of-contents.svelte";
|
||||
|
||||
const toc = [
|
||||
{ title: "Platform Use", link: "#platform-use" },
|
||||
{ title: "Booking Disclaimer", link: "#booking-disclaimer" },
|
||||
{ title: "User Accounts", link: "#user-accounts" },
|
||||
{ title: "Booking & Payment", link: "#booking-payment" },
|
||||
{ title: "Flight Information", link: "#flight-information" },
|
||||
{ title: "Liability", link: "#liability" },
|
||||
{ title: "Changes to Terms", link: "#changes-to-terms" },
|
||||
{ title: "Contact Us", link: "#contact-us" },
|
||||
];
|
||||
|
||||
const lastUpdated = "December 3, 2024";
|
||||
</script>
|
||||
|
||||
<section class="flex w-full flex-col-reverse gap-6 md:gap-8 lg:flex-row">
|
||||
<div
|
||||
class="col-span-3 flex w-full flex-col gap-4 rounded-lg border border-brand-200 bg-gradient-to-b from-white/50 to-brand-50 p-4 md:p-8"
|
||||
>
|
||||
<Title size="h2" weight="medium">Terms & Conditions</Title>
|
||||
|
||||
<p>
|
||||
Welcome to FlyTicketTravel. By using our platform, you agree to these
|
||||
terms. Please read them carefully as they govern your use of our
|
||||
flight booking services.
|
||||
</p>
|
||||
|
||||
<Title size="h3" weight="medium" id="platform-use">1. Platform Use</Title>
|
||||
<p>
|
||||
FlyTicketTravel provides tools for flight search, booking, and
|
||||
management. You agree to:
|
||||
</p>
|
||||
<ul class="list-disc pl-4">
|
||||
<li>Provide accurate information</li>
|
||||
<li>Use the platform legally and appropriately</li>
|
||||
<li>Not misuse or attempt to manipulate our services</li>
|
||||
<li>Maintain the confidentiality of your account</li>
|
||||
</ul>
|
||||
|
||||
<Title size="h3" weight="medium" id="booking-disclaimer"
|
||||
>2. Booking Disclaimer</Title
|
||||
>
|
||||
<p>FlyTicketTravel is a flight booking platform:</p>
|
||||
<ul class="list-disc pl-4">
|
||||
<li>We facilitate bookings between you and airlines</li>
|
||||
<li>The airline's conditions of carriage apply to your travel</li>
|
||||
<li>
|
||||
We are not responsible for airline schedule changes or
|
||||
cancellations
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<Title size="h3" weight="medium" id="user-accounts"
|
||||
>3. User Accounts</Title
|
||||
>
|
||||
<p>To use FlyTicketTravel, you must:</p>
|
||||
<ul class="list-disc pl-4">
|
||||
<li>Be at least 18 years old or have guardian consent</li>
|
||||
<li>Provide valid identification when required</li>
|
||||
<li>Maintain accurate account information</li>
|
||||
<li>Protect your account credentials</li>
|
||||
</ul>
|
||||
|
||||
<Title size="h3" weight="medium" id="booking-payment"
|
||||
>4. Booking & Payment</Title
|
||||
>
|
||||
<p>When making bookings through our platform:</p>
|
||||
<ul class="list-disc pl-4">
|
||||
<li>All prices are displayed in the selected currency</li>
|
||||
<li>Payment processing is secure and PCI-DSS compliant</li>
|
||||
<li>Booking is confirmed only after payment is processed</li>
|
||||
<li>Refunds are subject to our Refund Policy and airline terms</li>
|
||||
</ul>
|
||||
|
||||
<Title size="h3" weight="medium" id="flight-information"
|
||||
>5. Flight Information</Title
|
||||
>
|
||||
<p>Regarding flight details:</p>
|
||||
<ul class="list-disc pl-4">
|
||||
<li>We strive to provide accurate and up-to-date information</li>
|
||||
<li>Flight schedules are subject to change by airlines</li>
|
||||
<li>It is your responsibility to check final departure details</li>
|
||||
<li>We will notify you of major changes when possible</li>
|
||||
</ul>
|
||||
|
||||
<Title size="h3" weight="medium" id="liability"
|
||||
>6. Limitation of Liability</Title
|
||||
>
|
||||
<p>FlyTicketTravel is not liable for:</p>
|
||||
<ul class="list-disc pl-4">
|
||||
<li>Airline service quality or performance</li>
|
||||
<li>Flight delays, cancellations, or schedule changes</li>
|
||||
<li>Lost or damaged baggage</li>
|
||||
<li>Accuracy of third-party information</li>
|
||||
<li>Service interruptions or technical issues</li>
|
||||
</ul>
|
||||
|
||||
<Title size="h3" weight="medium" id="changes-to-terms"
|
||||
>7. Changes to Terms</Title
|
||||
>
|
||||
<p>
|
||||
We may update these terms as our services evolve. Continued use after
|
||||
changes constitutes acceptance of new terms.
|
||||
</p>
|
||||
|
||||
<Title size="h3" weight="medium" id="contact-us">8. Contact Us</Title>
|
||||
<p>
|
||||
For questions about these terms, contact us at {CONTACT_INFO.email}.
|
||||
</p>
|
||||
|
||||
<small class="text-gray-500">Last Updated: {lastUpdated}</small>
|
||||
</div>
|
||||
|
||||
<TableOfContents {toc} />
|
||||
</section>
|
||||
Reference in New Issue
Block a user