cleanup: ticket and legal stuff

This commit is contained in:
user
2025-10-21 16:07:17 +03:00
parent 8440c6a2dd
commit de2fbd41d6
18 changed files with 92 additions and 1575 deletions

View File

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

View File

@@ -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"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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