UI refactor and some logical fixes

This commit is contained in:
user
2025-10-21 18:41:19 +03:00
parent 1a89236449
commit f9f743eb15
29 changed files with 206 additions and 1070 deletions

View File

@@ -4,4 +4,8 @@ FLIGHT SIMULATOR 💥✈️🏢🏢💥
Ok but fo' real dis wus a project that I made to learn more around monorepos and websocket or well live data sync in general, in particular the checkout data was synced and controllable by the admin for this.
## Domain Wall 🧱
It's called that cuse for instance if you buy the product, e.g a domain, but the wall is the checkout process, and the admin is the one who controls whether you pass or not (Wannabe Gandalf)
---

View File

@@ -7,7 +7,7 @@
import { toast } from "svelte-sonner";
import type { PageData } from "./$types";
pageTitle.set("Passenger Info");
pageTitle.set("Customer Info");
let { data }: { data: PageData } = $props();

View File

@@ -1,4 +1,6 @@
import { getCustomerInfoUseCases } from "$lib/domains/customerinfo/usecases";
import { PaymentInfoRepository } from "$lib/domains/paymentinfo/repository";
import { db } from "@pkg/db";
import { getError } from "@pkg/logger";
import { ERROR_CODES } from "@pkg/result";
import type { PageServerLoad } from "./$types";
@@ -16,5 +18,13 @@ export const load: PageServerLoad = async ({ params }) => {
}),
};
}
return await getCustomerInfoUseCases().getAllCustomerInfo();
const cinfo = await getCustomerInfoUseCases().getAllCustomerInfo();
const piRes = await new PaymentInfoRepository(db).getPaymentInfo(uid);
return {
customerInfo: cinfo.data,
paymentInfo: piRes.data,
error: piRes.error || cinfo.error,
};
};

View File

@@ -8,10 +8,7 @@
import { capitalize } from "$lib/core/string.utils";
import CinfoCard from "$lib/domains/customerinfo/view/cinfo-card.svelte";
import { onMount } from "svelte";
import GenderIcon from "~icons/mdi/gender-male-female";
import PackageIcon from "~icons/solar/box-broken";
import CubeIcon from "~icons/solar/box-minimalistic-broken";
import CalendarIcon from "~icons/solar/calendar-broken";
import CalendarCheckIcon from "~icons/solar/calendar-linear";
import CardNumberIcon from "~icons/solar/card-recive-broken";
import ClipboardIcon from "~icons/solar/clipboard-list-broken";
@@ -19,7 +16,6 @@
import EmailIcon from "~icons/solar/letter-broken";
import LockKeyIcon from "~icons/solar/lock-keyhole-minimalistic-broken";
import LocationIcon from "~icons/solar/map-point-broken";
import PassportIcon from "~icons/solar/passport-broken";
import PhoneIcon from "~icons/solar/phone-broken";
import UserIdIcon from "~icons/solar/user-id-broken";
import CardUserIcon from "~icons/solar/user-id-linear";
@@ -54,31 +50,6 @@
title: "Phone",
value: `${pii?.phoneCountryCode ?? ""} ${pii?.phoneNumber ?? ""}`,
},
{
icon: PassportIcon,
title: "Passport No",
value: pii?.passportNo,
},
{
icon: LocationIcon,
title: "Nationality",
value: capitalize(pii?.nationality ?? ""),
},
{
icon: GenderIcon,
title: "Gender",
value: capitalize(pii?.gender ?? ""),
},
{
icon: CalendarIcon,
title: "Date of Birth",
value: new Date(pii?.dob ?? "").toDateString(),
},
{
icon: CubeIcon,
title: "Passenger Type",
value: capitalize(data.data?.passengerType ?? ""),
},
];
// No icons for this one
@@ -154,7 +125,7 @@
<Breadcrumb.List>
<Breadcrumb.Item>
<Breadcrumb.Link href={adminSiteNavMap.data}>
Passengers Data
Customer Data
</Breadcrumb.Link>
</Breadcrumb.Item>
<Breadcrumb.Separator />

View File

@@ -1,8 +1,6 @@
{
"$schema": "https://next.shadcn-svelte.com/schema.json",
"style": "default",
"tailwind": {
"config": "tailwind.config.ts",
"css": "src/app.css",
"baseColor": "slate"
},
@@ -10,8 +8,9 @@
"components": "$lib/components",
"utils": "$lib/utils",
"ui": "$lib/components/ui",
"hooks": "$lib/hooks"
"hooks": "$lib/hooks",
"lib": "$lib"
},
"typescript": true,
"registry": "https://next.shadcn-svelte.com/registry"
"registry": "https://tw3.shadcn-svelte.com/registry/default"
}

View File

@@ -48,12 +48,12 @@
"devDependencies": {
"@iconify/json": "^2.2.275",
"@internationalized/date": "^3.6.0",
"@lucide/svelte": "^0.503.0",
"@lucide/svelte": "^0.482.0",
"@sveltejs/kit": "^2.0.0",
"@sveltejs/vite-plugin-svelte": "^4.0.0",
"@types/node": "^22.9.3",
"autoprefixer": "^10.4.20",
"bits-ui": "^1.4.1",
"bits-ui": "^1.4.7",
"clsx": "^2.1.1",
"embla-carousel-svelte": "^8.6.0",
"formsnap": "^2.0.0",

View File

@@ -216,6 +216,56 @@
}
}
/* Custom Scrollbar Styling */
@layer base {
/* For Webkit browsers (Chrome, Safari, Edge) */
::-webkit-scrollbar {
width: 12px;
height: 12px;
}
::-webkit-scrollbar-track {
background: white;
border-radius: 10px;
}
::-webkit-scrollbar-thumb {
background: hsl(var(--primary));
border-radius: 10px;
border: 3px solid white;
}
::-webkit-scrollbar-thumb:hover {
background: hsl(var(--primary) / 0.8);
}
/* For Firefox */
* {
scrollbar-width: thin;
scrollbar-color: hsl(var(--primary)) white;
}
/* Thin scrollbar for command lists */
.command-scrollbar::-webkit-scrollbar {
width: 8px;
}
.command-scrollbar::-webkit-scrollbar-track {
background: hsl(var(--muted));
border-radius: 10px;
}
.command-scrollbar::-webkit-scrollbar-thumb {
background: hsl(var(--primary));
border-radius: 10px;
border: 2px solid hsl(var(--muted));
}
.command-scrollbar::-webkit-scrollbar-thumb:hover {
background: hsl(var(--primary) / 0.8);
}
}
.all-reset {
all: unset;
}

View File

@@ -1,181 +0,0 @@
<script lang="ts">
import { PROJECT_NAME, SPECIAL_PARTNERS } from "$lib/core/constants";
import Logo from "$lib/components/atoms/logo.svelte";
import PaymentCardImages from "$lib/components/atoms/payment-card-images.svelte";
import CurrencySelect from "$lib/domains/currency/view/currency-select.svelte";
import MaxWidthWrapper from "../max-width-wrapper.svelte";
import Input from "$lib/components/ui/input/input.svelte";
import Button from "$lib/components/ui/button/button.svelte";
import Facebook from "~icons/ic/baseline-facebook";
import Twitter from "~icons/arcticons/x-twitter";
import Instagram from "~icons/hugeicons/instagram";
import Linkedin from "~icons/tabler/brand-linkedin";
import Youtube from "~icons/uil/youtube";
// Social media links
const SOCIAL_LINKS = [
{ icon: Facebook, url: "#", ariaLabel: "Facebook" },
{ icon: Twitter, url: "#", ariaLabel: "Twitter" },
{ icon: Instagram, url: "#", ariaLabel: "Instagram" },
{ icon: Linkedin, url: "#", ariaLabel: "LinkedIn" },
{ icon: Youtube, url: "#", ariaLabel: "YouTube" },
];
// Company links
const COMPANY_LINKS = [
{ name: "About Us", url: "#" },
{ name: "Careers", url: "#" },
{ name: "Press", url: "#" },
{ name: "Partners", url: "#" },
];
// Support links
const SUPPORT_LINKS = [
{ name: "Help Center", url: "#" },
{ name: "Contact Us", url: "#" },
{ name: "Privacy Policy", url: "#" },
{ name: "Terms of Service", url: "#" },
];
// Legal links
const LEGAL_LINKS = [
{ name: "Privacy Policy", url: "#" },
{ name: "Terms of Service", url: "#" },
{ name: "Cookies", url: "#" },
];
</script>
<footer class="bg-brand-950 text-white">
<MaxWidthWrapper cls="px-4 py-16">
<div class="grid grid-cols-1 gap-8 md:grid-cols-2 lg:grid-cols-5">
<div class="lg:col-span-2">
<Logo intent="white" />
<p class="my-6 max-w-md text-brand-100">
Connecting travelers to their destinations with premium
service, competitive prices, and a seamless booking experience
since 2020.
</p>
<!-- <div class="flex space-x-4">
{#each SOCIAL_LINKS as social}
<a
href={social.url}
class="rounded-full bg-brand-900 p-2 transition-colors hover:bg-brand-700"
aria-label={social.ariaLabel}
>
<svelte:component
this={social.icon}
class="h-4 w-5"
/>
</a>
{/each}
</div> -->
<!-- <div class="mt-6">
<a
class="flex items-center gap-2 text-brand-100 hover:text-white"
href={`mailto:${CONTACT_INFO.email}`}
>
<Icon icon={IconEmail} cls="h-4 w-auto" />
<p>
{CONTACT_INFO.email}
</p>
</a>
</div> -->
</div>
<div>
<h3 class="mb-4 text-lg font-semibold text-white">Company</h3>
<ul class="space-y-2">
{#each COMPANY_LINKS as link}
<li>
<a
href={link.url}
class="text-brand-100 transition-colors hover:text-white"
>{link.name}</a
>
</li>
{/each}
</ul>
</div>
<div>
<h3 class="mb-4 text-lg font-semibold text-white">Support</h3>
<ul class="space-y-2">
{#each SUPPORT_LINKS as link}
<li>
<a
href={link.url}
class="text-brand-100 transition-colors hover:text-white"
>{link.name}</a
>
</li>
{/each}
</ul>
</div>
<div>
<h3 class="mb-4 text-lg font-semibold text-white">Newsletter</h3>
<p class="mb-4 text-brand-100">
Subscribe for travel inspiration and exclusive deals.
</p>
<div class="space-y-2">
<Input
placeholder="Your email address"
class="border-brand-800 bg-brand-900 text-white"
/>
<Button class="w-full bg-brand-600 hover:bg-brand-500">
Subscribe
</Button>
</div>
</div>
</div>
<div
class="Md:items-center mt-12 flex flex-col justify-between gap-4 border-t border-brand-800 pt-8 md:flex-row"
>
<div class="mb-6">
<h3 class="mb-4 text-lg font-semibold text-white">
Payment Methods
</h3>
<PaymentCardImages />
</div>
{#if SPECIAL_PARTNERS.length > 0}
<div class="mb-8">
<h3 class="mb-4 text-lg font-semibold text-white">
Affiliated Partners
</h3>
<div class="flex flex-wrap items-center gap-6">
{#each SPECIAL_PARTNERS as partner}
<img
src={partner.link}
alt={partner.alt}
class="h-8 w-auto"
/>
{/each}
</div>
</div>
{/if}
</div>
<div class="mt-8 border-t border-brand-800 pt-8">
<div
class="flex flex-col items-center justify-between gap-4 md:flex-row"
>
<p class="text-sm text-brand-300">
© {new Date().getFullYear()}
{PROJECT_NAME}. All rights reserved.
</p>
<div class="flex flex-wrap justify-center gap-6">
{#each LEGAL_LINKS as link}
<a
href={link.url}
class="text-sm text-brand-300 hover:text-white"
>{link.name}</a
>
{/each}
</div>
<CurrencySelect invert={false} />
</div>
</div>
</MaxWidthWrapper>
</footer>

View File

@@ -1,54 +0,0 @@
<script lang="ts">
import { Sheet, SheetContent, SheetTrigger } from "$lib/components/ui/sheet";
import { buttonVariants } from "$lib/components/ui/button";
import IconMenu from "~icons/solar/hamburger-menu-broken";
import IconClose from "~icons/mingcute/close-line";
import { NAV_LINKS, TRANSITION_COLORS } from "$lib/core/constants";
import { cn } from "$lib/utils";
import Icon from "$lib/components/atoms/icon.svelte";
import Logo from "$lib/components/atoms/logo.svelte";
let { invert }: { invert: boolean } = $props();
let open = $state(false);
</script>
<Sheet {open} onOpenChange={(to) => (open = to)}>
<SheetTrigger
class={cn(
"block lg:hidden",
buttonVariants({
variant: invert ? "glassWhite" : "ghost",
size: "icon",
}),
)}
onclick={() => (open = true)}
>
<Icon icon={IconMenu} cls={"h-6 w-auto"} />
</SheetTrigger>
<SheetContent side="bottom" class="z-[101]">
<button
onclick={() => (open = false)}
class="absolute right-4 top-4 grid place-items-center rounded-md border border-neutral-400 p-1 text-neutral-500"
>
<IconClose class="h-5 w-5" />
</button>
<Logo />
<div class="mt-8 flex flex-col gap-2 overflow-y-auto">
{#each NAV_LINKS as link}
<a
href={link.link}
onclick={() => (open = false)}
class={cn(
"text-sgreen flex items-center gap-2 rounded-lg border-2 p-3 px-4",
TRANSITION_COLORS,
"border-transparent hover:border-brand-300 hover:bg-brand-100",
)}
>
<Icon icon={link.icon} cls="h-5 w-5" />
<span>{link.name}</span>
</a>
{/each}
</div>
</SheetContent>
</Sheet>

View File

@@ -1,64 +0,0 @@
<script lang="ts">
import { NAV_LINKS, TRANSITION_COLORS } from "$lib/core/constants";
import Logo from "$lib/components/atoms/logo.svelte";
import Button from "$lib/components/ui/button/button.svelte";
import { cn } from "$lib/utils";
import MobileNavSheet from "./mobile-nav-sheet.svelte";
import Icon from "$lib/components/atoms/icon.svelte";
import BookWithBookmarkIcon from "~icons/solar/book-bookmark-linear";
import MaxWidthWrapper from "../max-width-wrapper.svelte";
import { SearchIcon } from "@lucide/svelte";
let { invert }: { invert: boolean } = $props();
</script>
<div class="h-24"></div>
<nav
class={cn(
"fixed left-0 top-0 z-50 grid w-screen place-items-center bg-white/80 drop-shadow-md backdrop-blur-md",
)}
>
<MaxWidthWrapper
cls="flex w-full items-center justify-between gap-4 p-4 bg-transparent"
>
<div class="flex items-center gap-4">
<a href="/">
<Logo cls="w-auto h-8 md:h-10" />
</a>
<div
class="hidden w-full items-center justify-center gap-2 lg:flex lg:gap-4"
>
{#each NAV_LINKS as link}
<a
href={link.link}
class={cn(
"w-28 rounded-lg px-4 py-2 text-center",
"bg-transparent hover:bg-primary/10 hover:text-brand-700",
TRANSITION_COLORS,
)}
>
{link.name}
</a>
{/each}
</div>
</div>
<div class="hidden items-center gap-2 lg:flex">
<a href={"/track"}>
<Button variant="ghost">
<Icon icon={BookWithBookmarkIcon} cls="w-auto h-5" />
<span>Track</span>
</Button>
</a>
<a href={"/search"}>
<Button>
<Icon icon={SearchIcon} cls="w-auto h-5" />
<span>Search Flights</span>
</Button>
</a>
</div>
<MobileNavSheet {invert} />
</MaxWidthWrapper>
</nav>

View File

@@ -1,77 +0,0 @@
<script lang="ts">
import { ArrowRight, Plane } from "@lucide/svelte";
import Card from "$lib/components/ui/card/card.svelte";
import CardContent from "$lib/components/ui/card/card-content.svelte";
let { deals } = $props();
</script>
<div class="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-4">
{#each deals as deal}
<Card class="card-hover overflow-hidden">
<CardContent class="p-5">
{#if deal.tag}
<div class="mb-3">
<span
class="rounded-full bg-brand-500 px-3 py-1 text-xs font-medium text-white"
>
{deal.tag}
</span>
</div>
{/if}
<div class="mb-3 flex items-center justify-between">
<div class="flex items-center">
<Plane class="mr-2 h-5 w-5 text-brand-500" />
<span class="font-medium">{deal.airline}</span>
</div>
{#if deal.discount}
<span
class="rounded bg-brand-100 px-2 py-1 text-xs font-medium text-brand-600"
>
{deal.discount}% OFF
</span>
{/if}
</div>
<div class="mb-4 flex items-center justify-between">
<div>
<p class="text-xl font-bold">{deal.from}</p>
<p class="text-sm text-muted-foreground">Departure</p>
</div>
<div class="flex-1 px-4">
<div class="relative">
<div
class="absolute left-0 right-0 top-1/2 -translate-y-1/2 transform border-t border-dashed border-gray-300"
></div>
<div
class="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 transform bg-white p-1"
>
<ArrowRight class="h-4 w-4 text-brand-500" />
</div>
</div>
</div>
<div class="text-right">
<p class="text-xl font-bold">{deal.to}</p>
<p class="text-sm text-muted-foreground">Arrival</p>
</div>
</div>
<div class="flex items-end justify-between">
<div>
<p class="text-sm text-muted-foreground">{deal.date}</p>
</div>
<div>
<p class="text-sm">from</p>
<p class="text-2xl font-bold text-brand-600">
${deal.price}
</p>
</div>
</div>
</CardContent>
</Card>
{/each}
</div>

View File

@@ -1,132 +0,0 @@
<script lang="ts">
import Card from "$lib/components/ui/card/card.svelte";
import CardContent from "$lib/components/ui/card/card-content.svelte";
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselPrevious,
CarouselNext,
} from "$lib/components/ui/carousel";
import { StarIcon } from "@lucide/svelte";
import Icon from "../atoms/icon.svelte";
import { cn } from "$lib/utils";
import MaxWidthWrapper from "../molecules/max-width-wrapper.svelte";
type Testimonial = {
id: number;
name: string;
role: string;
message: string;
avatar: string;
rating: number;
};
const testimonials: Testimonial[] = [
{
id: 1,
name: "Sarah Johnson",
role: "Business Traveler",
message:
"FlyTicketTravel made my business trips so much easier. Their intuitive booking platform and exceptional customer service saved me hours of planning time.",
avatar: "https://images.unsplash.com/photo-1494790108377-be9c29b29330?auto=format&fit=crop&q=80&w=2487&ixlib=rb-4.0.3",
rating: 5,
},
{
id: 2,
name: "Michael Chen",
role: "Adventure Seeker",
message:
"I've used many flight booking platforms, but FlyTicketTravel stands out with their competitive prices and flexible cancellation policies. My go-to for all my adventures!",
avatar: "https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?auto=format&fit=crop&q=80&w=2187&ixlib=rb-4.0.3",
rating: 4,
},
{
id: 3,
name: "Emma Rodriguez",
role: "Family Traveler",
message:
"Booking flights for the whole family used to be stressful until I discovered FlyTicketTravel. Their group booking feature and 24/7 support made our vacation planning seamless.",
avatar: "https://images.unsplash.com/photo-1534528741775-53994a69daeb?auto=format&fit=crop&q=80&w=2564&ixlib=rb-4.0.3",
rating: 5,
},
{
id: 4,
name: "James Wilson",
role: "Digital Nomad",
message:
"As someone who travels constantly, I appreciate FlyTicketTravel's rewards program and their mobile app that lets me book flights from anywhere in the world.",
avatar: "https://images.unsplash.com/photo-1539571696357-5a69c17a67c6?auto=format&fit=crop&q=80&w=2662&ixlib=rb-4.0.3",
rating: 5,
},
];
</script>
<MaxWidthWrapper cls="py-24 px-4">
<div class="mx-auto mb-16 max-w-3xl text-center">
<h2 class="mb-4 text-3xl font-bold md:text-4xl">
What Our Customers Say
</h2>
<p class="text-lg text-muted-foreground">
Don't just take our word for it hear what our satisfied customers
have to say about their FlyTicketTravel experience.
</p>
</div>
<Carousel
opts={{
align: "start",
loop: true,
}}
class="w-full"
>
<CarouselContent>
{#each testimonials as testimonial}
<CarouselItem class="p-2 px-4 md:basis-1/2 lg:basis-1/3">
<Card class="h-full">
<CardContent class="flex h-full flex-col p-6">
<div class="mb-4 flex items-center">
{#each Array(5) as _, i}
<Icon
icon={StarIcon}
cls={cn(
"h-5 w-5",
i < testimonial.rating
? "text-brand-500"
: "text-muted-foreground",
)}
/>
{/each}
</div>
<p class="mb-6 flex-grow italic">
{testimonial.message}
</p>
<div class="flex items-center">
<img
src={testimonial.avatar}
alt={testimonial.name}
class="mr-4 h-12 w-12 rounded-full object-cover"
/>
<div>
<h4 class="font-semibold">
{testimonial.name}
</h4>
<p class="text-sm text-muted-foreground">
{testimonial.role}
</p>
</div>
</div>
</CardContent>
</Card>
</CarouselItem>
{/each}
</CarouselContent>
<div class="mt-8 flex justify-center gap-0">
<CarouselPrevious class="relative" />
<CarouselNext class="relative" />
</div>
</Carousel>
</MaxWidthWrapper>

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { Command as CommandPrimitive } from "bits-ui";
import { Command as CommandPrimitive, useId } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
@@ -7,6 +7,7 @@
class: className,
children,
heading,
value,
...restProps
}: CommandPrimitive.GroupProps & {
heading?: string;
@@ -16,6 +17,7 @@
<CommandPrimitive.Group
class={cn("text-foreground overflow-hidden p-1", className)}
bind:ref
value={value ?? heading ?? `----${useId()}`}
{...restProps}
>
{#if heading}

View File

@@ -15,8 +15,8 @@
<Search class="mr-2 size-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
class={cn(
"flex h-11 w-full rounded-md border-transparent bg-transparent py-3 text-base outline-none ring-0 placeholder:text-muted-foreground focus:border-transparent focus:outline-none focus:ring-0 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className,
"placeholder:text-muted-foreground border-none focus:border-none focus:outline-none focus:ring-0 flex h-11 w-full rounded-md bg-transparent py-3 text-base outline-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
bind:ref
{...restProps}

View File

@@ -11,7 +11,7 @@
<CommandPrimitive.Item
class={cn(
"aria-selected:bg-accent aria-selected:text-accent-foreground relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
"relative flex cursor-pointer select-none items-center gap-2 rounded-lg px-3 py-2.5 text-sm outline-none transition-colors hover:bg-primary/10 aria-selected:bg-primary/10 aria-selected:text-primary data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
className
)}
bind:ref

View File

@@ -11,7 +11,7 @@
<CommandPrimitive.LinkItem
class={cn(
"aria-selected:bg-accent aria-selected:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
"aria-selected:bg-accent aria-selected:text-accent-foreground relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
className
)}
bind:ref

View File

@@ -1,13 +1,9 @@
<script lang="ts">
import {
Dialog as DialogPrimitive,
type WithoutChildrenOrChild,
} from "bits-ui";
import CloseIcon from "~icons/lucide/x";
import { Dialog as DialogPrimitive, type WithoutChildrenOrChild } from "bits-ui";
import X from "@lucide/svelte/icons/x";
import type { Snippet } from "svelte";
import * as Dialog from "./index.js";
import { cn } from "$lib/utils.js";
import Icon from "$lib/components/atoms/icon.svelte";
let {
ref = $bindable(null),
@@ -26,16 +22,16 @@
<DialogPrimitive.Content
bind:ref
class={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className,
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] bg-background fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border p-6 shadow-lg duration-200 sm:rounded-lg",
className
)}
{...restProps}
>
{@render children?.()}
<DialogPrimitive.Close
class="absolute right-4 top-4 grid h-10 w-10 place-items-center rounded-sm opacity-70 ring-offset-background transition-opacity hover:bg-gray-200 hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 active:bg-gray-200 disabled:pointer-events-none"
class="ring-offset-background focus:ring-ring absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none"
>
<Icon icon={CloseIcon} cls="w-auto h-5" />
<X class="size-4" />
<span class="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>

View File

@@ -61,15 +61,15 @@
>
<Icon icon={CloseIcon} cls="h-5 w-auto" />
</button>
<div class="mt-8 flex flex-col gap-2 overflow-y-auto">
<div class="mt-8 flex flex-col gap-3 overflow-y-auto">
{#each checkoutSteps as step, index}
<button
class={cn(
"flex items-center gap-3 rounded-lg border-2 p-3 text-left outline-none transition",
"flex items-center gap-3 rounded-lg border-2 p-4 text-left outline-none transition-all",
index <= activeStepIndex
? "border-brand-200 bg-primary/10 hover:bg-primary/20"
? "border-primary/20 bg-primary/5 hover:bg-primary/10"
: "border-transparent bg-gray-100 opacity-50",
index === activeStepIndex && "border-brand-500",
index === activeStepIndex && "border-primary bg-primary/10 shadow-sm",
)}
disabled={index > activeStepIndex}
onclick={() => {
@@ -79,15 +79,23 @@
>
<div
class={cn(
"flex h-8 w-8 shrink-0 items-center justify-center rounded-full",
index <= activeStepIndex
? "bg-primary text-white"
: "bg-gray-200 text-gray-600",
"flex h-10 w-10 shrink-0 items-center justify-center rounded-full font-medium transition-all",
index < activeStepIndex
? "bg-green-500 text-white"
: index === activeStepIndex
? "bg-primary text-white"
: "bg-gray-200 text-gray-600",
)}
>
{index + 1}
{#if index < activeStepIndex}
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7" />
</svg>
{:else}
{index + 1}
{/if}
</div>
<span class="font-medium">
<span class="font-medium text-gray-900">
{step.label}
</span>
</button>

View File

@@ -20,33 +20,8 @@ class CheckoutViewModel {
checkoutSubmitted = $state(false);
livenessPinger: NodeJS.Timer | undefined = $state(undefined);
reset() {
this.checkoutStep = CheckoutStep.Initial;
this.resetPinger();
}
setupPinger() {
this.resetPinger();
this.livenessPinger = setInterval(() => {
this.ping();
}, 5_000);
}
resetPinger() {
if (this.livenessPinger) {
clearInterval(this.livenessPinger);
}
}
private async ping() {
const api = get(trpcApiStore);
if (!api) {
return false;
}
// TODO: no need to ping now REMOVE THIS PINGING LOGIC
}
async checkout() {
@@ -88,6 +63,7 @@ class CheckoutViewModel {
}
const pInfoParsed = paymentInfoPayloadModel.safeParse({
orderId: -1,
method: PaymentMethod.Card,
cardDetails: paymentInfoVM.cardDetails,
productId: get(productStore)?.id,
@@ -134,7 +110,7 @@ class CheckoutViewModel {
description: "Redirecting, please wait...",
});
setTimeout(() => {
window.location.href = `/checkout/success?oid=${out.data}`;
window.location.href = `/checkout/success?uid=${out.data}`;
}, 500);
return true;
} catch (e) {

View File

@@ -6,16 +6,12 @@
import { checkoutVM } from "$lib/domains/checkout/checkout.vm.svelte";
import { ckFlowVM } from "$lib/domains/ckflow/view/ckflow.vm.svelte";
import { CheckoutStep } from "$lib/domains/order/data/entities";
import { cn } from "$lib/utils";
import { onMount } from "svelte";
import { toast } from "svelte-sonner";
import RightArrowIcon from "~icons/solar/arrow-right-broken";
import CustomerPiiForm from "../customerinfo/view/customer-pii-form.svelte";
import { customerInfoVM } from "../customerinfo/view/customerinfo.vm.svelte";
const cardStyle =
"flex w-full flex-col gap-4 rounded-lg bg-white p-4 shadow-lg md:p-8";
$effect(() => {
const personalInfo = customerInfoVM.customerInfo;
@@ -79,13 +75,13 @@
</script>
{#if customerInfoVM.customerInfo}
<div class={cn(cardStyle, "border-2 border-gray-200")}>
<Title size="h5">Personal Info</Title>
<div class="flex w-full flex-col gap-6">
<Title size="h4" weight="medium">Personal Info</Title>
<CustomerPiiForm bind:info={customerInfoVM.customerInfo} />
</div>
{/if}
<div class="flex flex-col items-center justify-between gap-4 md:flex-row">
<div class="mt-8 flex flex-col items-center justify-between gap-4 border-t border-gray-200 pt-6 md:flex-row">
<div></div>
<Button

View File

@@ -16,9 +16,6 @@
import PaymentForm from "./payment-form.svelte";
import { paymentInfoVM } from "./payment.info.vm.svelte";
const cardStyle =
"flex w-full flex-col gap-4 rounded-lg bg-white p-4 shadow-lg md:p-8";
async function goBack() {
if ((await ckFlowVM.onBackToPIIBtnClick()) !== true) {
return;
@@ -76,23 +73,23 @@
});
</script>
<div class="flex flex-col gap-6">
<div class={cardStyle}>
<Title size="h4">Order Summary</Title>
<div class="flex flex-col gap-8">
<div class="flex w-full flex-col gap-6">
<Title size="h4" weight="medium">Order Summary</Title>
<OrderSummary />
</div>
<div class={cardStyle}>
<Title size="h4">Billing Details</Title>
<div class="flex w-full flex-col gap-6 border-t border-gray-200 pt-8">
<Title size="h4" weight="medium">Billing Details</Title>
<BillingDetailsForm info={billingDetailsVM.billingDetails} />
</div>
<div class={cardStyle}>
<Title size="h4">Payment Details</Title>
<div class="flex w-full flex-col gap-6 border-t border-gray-200 pt-8">
<Title size="h4" weight="medium">Payment Details</Title>
<PaymentForm />
</div>
<div class="flex flex-col items-center justify-between gap-4 md:flex-row">
<div class="flex flex-col items-center justify-between gap-4 border-t border-gray-200 pt-6 md:flex-row">
<Button variant="secondary" onclick={goBack} class="w-full md:w-max">
<Icon icon={RightArrowIcon} cls="w-auto h-6 rotate-180" />
Back

View File

@@ -74,7 +74,7 @@ class ActionRunner {
return;
}
toast.success("Your booking has been confirmed", {
toast.success("Checkout completed successfully", {
description: "Redirecting, please wait...",
});
@@ -160,9 +160,9 @@ class ActionRunner {
await ckFlowVM.cleanupFlowInfo();
ckFlowVM.reset();
checkoutVM.reset();
const tid = page.params.tid as any as string;
const plid = page.params.plid as any as string;
const sid = page.params.sid as any as string;
window.location.replace(`/checkout/terminated?sid=${sid}&tid=${tid}`);
window.location.replace(`/checkout/terminated?sid=${sid}&plid=${plid}`);
}
}
@@ -174,9 +174,8 @@ export class CKFlowViewModel {
otpCode: string | undefined = $state(undefined);
poller: NodeJS.Timer | undefined = undefined;
pinger: NodeJS.Timer | undefined = undefined;
priceFetcher: NodeJS.Timer | undefined = undefined;
_flowPoller: NodeJS.Timeout | undefined = undefined;
_flowPinger: NodeJS.Timeout | undefined = undefined;
// Data synchronization control
private personalInfoDebounceTimer: NodeJS.Timeout | null = null;
@@ -334,27 +333,27 @@ export class CKFlowViewModel {
}
private clearPoller() {
if (this.poller) {
clearInterval(this.poller);
if (this._flowPoller) {
clearInterval(this._flowPoller);
}
}
private clearPinger() {
if (this.pinger) {
clearInterval(this.pinger);
if (this._flowPinger) {
clearInterval(this._flowPinger);
}
}
private async startPolling() {
this.clearPoller();
this.poller = setInterval(() => {
this._flowPoller = setInterval(() => {
this.refreshFlowInfo();
}, 2000);
}
private async startPinging() {
this.clearPinger();
this.pinger = setInterval(() => {
this._flowPinger = setInterval(() => {
this.pingFlow();
}, 30000); // Every 30 seconds
}

View File

@@ -3,14 +3,24 @@
import Title from "$lib/components/atoms/title.svelte";
import Input from "$lib/components/ui/input/input.svelte";
import * as Select from "$lib/components/ui/select";
import * as Popover from "$lib/components/ui/popover";
import * as Command from "$lib/components/ui/command";
import { Button } from "$lib/components/ui/button";
import { COUNTRIES_SELECT } from "$lib/core/countries";
import { capitalize } from "$lib/core/string.utils";
import { PHONE_COUNTRY_CODES } from "@pkg/logic/core/data/phonecc";
import { cn } from "$lib/utils";
import { tick } from "svelte";
import ChevronsUpDownIcon from "~icons/lucide/chevrons-up-down";
import CheckIcon from "~icons/lucide/check";
import type { CustomerInfoModel } from "../data";
import { customerInfoVM } from "./customerinfo.vm.svelte";
let { info = $bindable() }: { info: CustomerInfoModel } = $props();
let phoneCodeOpen = $state(false);
let phoneCodeTriggerRef = $state<HTMLButtonElement>(null!);
function onSubmit(e: SubmitEvent) {
e.preventDefault();
customerInfoVM.validateCustomerInfo(info);
@@ -19,6 +29,13 @@
function debounceValidate() {
customerInfoVM.debounceValidate(info);
}
function closePhoneCodeAndFocus() {
phoneCodeOpen = false;
tick().then(() => {
phoneCodeTriggerRef?.focus();
});
}
</script>
<form action="#" class="flex w-full flex-col gap-4" onsubmit={onSubmit}>
@@ -74,32 +91,54 @@
<!-- Phone Number Field -->
<LabelWrapper label="Phone Number" error={customerInfoVM.errors.phoneNumber}>
<div class="flex gap-2">
<Select.Root
type="single"
required
onValueChange={(code) => {
info.phoneCountryCode = code;
debounceValidate();
}}
name="phoneCode"
>
<Select.Trigger class="w-28">
{#if info.phoneCountryCode}
{info.phoneCountryCode}
{:else}
Select
{/if}
</Select.Trigger>
<Select.Content>
{#each PHONE_COUNTRY_CODES as { country, phoneCode }}
<Select.Item value={phoneCode}>
<span class="flex items-center gap-2">
{phoneCode} ({country})
</span>
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
<Popover.Root bind:open={phoneCodeOpen}>
<Popover.Trigger bind:ref={phoneCodeTriggerRef}>
{#snippet child({ props })}
<Button
{...props}
variant="outline"
class="w-32 justify-between"
role="combobox"
aria-expanded={phoneCodeOpen}
>
{info.phoneCountryCode || "Select"}
<ChevronsUpDownIcon class="h-4 w-4 opacity-50" />
</Button>
{/snippet}
</Popover.Trigger>
<Popover.Content class="w-[300px] p-0">
<Command.Root>
<Command.Input placeholder="Search country..." class="h-9" />
<Command.List class="command-scrollbar max-h-[300px]">
<Command.Empty>No country found.</Command.Empty>
<Command.Group>
{#each PHONE_COUNTRY_CODES as { country, phoneCode } (country)}
<Command.Item
value={`${phoneCode} ${country}`}
onSelect={() => {
info.phoneCountryCode = phoneCode;
debounceValidate();
closePhoneCodeAndFocus();
}}
>
<CheckIcon
class={cn(
"h-4 w-4 flex-shrink-0",
info.phoneCountryCode !== phoneCode &&
"text-transparent",
)}
/>
<span class="flex flex-1 items-center justify-between gap-3">
<span class="font-semibold text-gray-900">{phoneCode}</span>
<span class="text-sm text-gray-600">{country}</span>
</span>
</Command.Item>
{/each}
</Command.Group>
</Command.List>
</Command.Root>
</Popover.Content>
</Popover.Root>
<Input
placeholder="Phone Number"

View File

@@ -90,7 +90,7 @@ export class OrderRepository {
error: getError(
{
code: ERROR_CODES.INTERNAL_SERVER_ERROR,
message: "An error occured while finding booking",
message: "An error occured while finding order",
userHint: "Please try again later",
detail: "An error occured while parsing order",
},

View File

@@ -1,187 +0,0 @@
import { billingDetailsVM } from "$lib/domains/checkout/payment-info-section/billing.details.vm.svelte";
import { calculateFinalPrices } from "$lib/domains/checkout/utils";
import { ckFlowVM } from "$lib/domains/ckflow/view/ckflow.vm.svelte";
import { currencyStore } from "$lib/domains/currency/view/currency.vm.svelte";
import { customerInfoVM } from "$lib/domains/customerinfo/view/customerinfo.vm.svelte";
import {
createOrderPayloadModel,
OrderCreationStep,
} from "$lib/domains/order/data/entities";
import { productStore } from "$lib/domains/product/store";
import { trpcApiStore } from "$lib/stores/api";
import { toast } from "svelte-sonner";
import { get } from "svelte/store";
/**
* CreateOrderViewModel manages the order creation flow for product checkout.
* Handles step progression, validation, and order submission.
*/
export class CreateOrderViewModel {
// Current step in the order creation flow
orderStep = $state(OrderCreationStep.CUSTOMER_INFO);
// Loading state
loading = $state(false);
/**
* Sets the current order creation step
* @param step - The step to navigate to
*/
setStep(step: OrderCreationStep) {
this.orderStep = step;
}
/**
* Advances to the next step in the order creation flow
*/
setNextStep() {
if (this.orderStep === OrderCreationStep.CUSTOMER_INFO) {
// Validate customer info before proceeding
if (!this.isCustomerInfoValid()) {
toast.error("Please complete customer information");
return;
}
this.orderStep = OrderCreationStep.PAYMENT;
} else if (this.orderStep === OrderCreationStep.PAYMENT) {
this.orderStep = OrderCreationStep.SUMMARY;
}
}
/**
* Goes back to the previous step
*/
setPrevStep() {
if (this.orderStep === OrderCreationStep.SUMMARY) {
this.orderStep = OrderCreationStep.PAYMENT;
} else if (this.orderStep === OrderCreationStep.PAYMENT) {
this.orderStep = OrderCreationStep.CUSTOMER_INFO;
}
}
/**
* Validates if customer information is complete
* @returns true if customer info is valid, false otherwise
*/
isCustomerInfoValid(): boolean {
if (!customerInfoVM.customerInfo) {
return false;
}
return customerInfoVM.isValid();
}
/**
* Validates if product is selected
* @returns true if product exists, false otherwise
*/
isProductValid(): boolean {
const product = get(productStore);
return product !== null && product.id !== undefined;
}
/**
* Checks if order can be submitted (all validations pass)
* @returns true if order is ready to submit, false otherwise
*/
canSubmitOrder(): boolean {
return this.isProductValid() && this.isCustomerInfoValid();
}
/**
* Creates and submits the order
* @returns true if successful, false otherwise
*/
async createOrder(): Promise<boolean> {
const api = get(trpcApiStore);
if (!api) {
toast.error("API client not initialized");
return false;
}
const product = get(productStore);
if (!product || !customerInfoVM.customerInfo) {
toast.error("Missing required information", {
description: "Product or customer information is incomplete",
});
return false;
}
// Calculate price details from product
const priceDetails = calculateFinalPrices(
product,
customerInfoVM.customerInfo,
);
// Build the order payload
const parsed = createOrderPayloadModel.safeParse({
product: product,
productId: product.id,
customerInfo: customerInfoVM.customerInfo,
paymentInfo: billingDetailsVM.billingDetails,
orderModel: {
...priceDetails,
productId: product.id,
currency: get(currencyStore).code,
},
flowId: ckFlowVM.flowId,
});
if (parsed.error) {
console.error("Order payload validation error:", parsed.error.errors);
const err = parsed.error.errors[0];
toast.error("Invalid order data", {
description: `${err.path.join(".")}: ${err.message}`,
});
return false;
}
this.loading = true;
try {
const out = await api.order.createOrder.mutate(parsed.data);
if (out.error) {
toast.error(out.error.message, {
description: out.error.userHint,
});
return false;
}
if (!out.data) {
toast.error("Order creation failed", {
description:
"Please try again, or contact support if the issue persists",
});
return false;
}
toast.success("Order created successfully", {
description: "Please wait, redirecting...",
});
// Redirect to success page after a short delay
setTimeout(() => {
window.location.href = `/order/success?uid=${out.data}`;
}, 1000);
return true;
} catch (e) {
console.error("Order creation error:", e);
toast.error("An unexpected error occurred", {
description: "Please try again later",
});
return false;
} finally {
this.loading = false;
}
}
/**
* Resets the view model state
*/
reset() {
this.orderStep = OrderCreationStep.CUSTOMER_INFO;
this.loading = false;
}
}
export const createOrderVM = new CreateOrderViewModel();

View File

@@ -1,217 +0,0 @@
<script lang="ts">
import Title from "$lib/components/atoms/title.svelte";
import Button from "$lib/components/ui/button/button.svelte";
import Icon from "$lib/components/atoms/icon.svelte";
import EmailIcon from "~icons/solar/letter-broken";
import TicketIcon from "~icons/solar/ticket-broken";
import UsersIcon from "~icons/solar/users-group-rounded-broken";
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 CreditCardIcon from "~icons/solar/card-broken";
import ButtonLoadableText from "$lib/components/atoms/button-loadable-text.svelte";
import TicketLegsOverview from "$lib/domains/ticket/view/ticket/ticket-legs-overview.svelte";
import Badge from "$lib/components/ui/badge/badge.svelte";
import { createOrderVM } from "./create.order.vm.svelte";
import { capitalize, snakeToSpacedPascal } from "$lib/core/string.utils";
import { passengerInfoVM } from "$lib/domains/passengerinfo/view/passenger.info.vm.svelte";
let isCreatingOrder = $state(false);
async function handleCreateOrder() {
isCreatingOrder = true;
await createOrderVM.createOrder();
isCreatingOrder = false;
}
const cardStyle =
"flex flex-col gap-4 rounded-lg border border-gray-200 bg-white p-4 shadow-md lg:p-8";
</script>
<div class={cardStyle}>
<div class="mb-4 flex items-center gap-2">
<Icon icon={EmailIcon} cls="w-5 h-5" />
<Title size="h5" color="black">Account Information</Title>
</div>
<p class="text-gray-800">{createOrderVM.accountInfo.email}</p>
</div>
{#if createOrderVM.ticketInfo}
<div class={cardStyle}>
<div class="flex flex-col items-center justify-between gap-2 md:flex-row">
<div class="mb-4 flex items-center gap-2">
<Icon icon={TicketIcon} cls="w-5 h-5" />
<Title size="h5" color="black">Flight Details</Title>
</div>
<div class="flex gap-2">
<Badge variant="outline">
{snakeToSpacedPascal(
createOrderVM.ticketInfo.flightType.toLowerCase(),
)}
</Badge>
<Badge variant="secondary">
{snakeToSpacedPascal(
createOrderVM.ticketInfo.cabinClass.toLowerCase(),
)}
</Badge>
</div>
</div>
{#if createOrderVM.ticketInfo}
<TicketLegsOverview data={createOrderVM.ticketInfo} />
{/if}
</div>
<div class={cardStyle}>
<div class="mb-4 flex items-center gap-2">
<Icon icon={UsersIcon} cls="w-5 h-5" />
<Title size="h5" color="black">Passengers</Title>
</div>
{#each passengerInfoVM.passengerInfo 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>
</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>
<span class="text-gray-500">Name</span>
<p class="font-medium">
{passenger.passengerPii.firstName}
{passenger.passengerPii.middleName}
{passenger.passengerPii.lastName}
</p>
</div>
<div>
<span class="text-gray-500">Nationality</span>
<p class="font-medium">
{capitalize(passenger.passengerPii.nationality)}
</p>
</div>
<div>
<span class="text-gray-500">Date of Birth</span>
<p class="font-medium">
{passenger.passengerPii.dob}
</p>
</div>
</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>
{passenger.bagSelection.personalBags}x Personal
Item
</span>
</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.seatNumber}
<div class="flex items-center gap-2 text-sm">
<Icon icon={SeatIcon} cls="h-5 w-5 text-gray-600" />
<span>Seat {passenger.seatSelection.seatNumber}</span>
</div>
{/if}
</div>
{#if index < passengerInfoVM.passengerInfos.length - 1}
<div class="border-b border-dashed"></div>
{/if}
{/each}
</div>
{/if}
{#if createOrderVM.ticketInfo}
<div class={cardStyle}>
<div class="mb-4 flex items-center gap-2">
<Icon icon={CreditCardIcon} cls="w-5 h-5" />
<Title size="h5" color="black">Price Summary</Title>
</div>
<div class="flex flex-col gap-2">
<div class="flex items-center justify-between">
<span>Ticket Price</span>
<span>
${createOrderVM.ticketInfo.priceDetails.basePrice.toFixed(2)}
</span>
</div>
{#if createOrderVM.ticketInfo.priceDetails.discountAmount > 0}
<div class="flex items-center justify-between">
<span>Discount</span>
<span class="text-green-600">
-${createOrderVM.ticketInfo.priceDetails.discountAmount.toFixed(
2,
)}
</span>
</div>
{/if}
<div class="mt-2 flex items-center justify-between border-t pt-2">
<span class="font-medium">Total Price</span>
<span class="font-medium">
${createOrderVM.ticketInfo.priceDetails.displayPrice.toFixed(
2,
)}
</span>
</div>
</div>
</div>
{/if}
<div class="flex flex-col items-center justify-between gap-4 lg:flex-row">
<Button
class="w-full lg:max-w-max"
variant="white"
onclick={() => {
createOrderVM.setPrevStep();
}}
>
Go Back
</Button>
<Button
class="w-full lg:max-w-max"
disabled={isCreatingOrder}
onclick={handleCreateOrder}
>
<ButtonLoadableText
loading={isCreatingOrder}
loadingText="Creating Order"
text="Confirm & Create Order"
/>
</Button>
</div>

View File

@@ -32,7 +32,6 @@
}
productStore.set(pageData.data);
checkoutVM.loading = false;
checkoutVM.setupPinger();
setTimeout(async () => {
await ckFlowVM.initFlow();
@@ -48,7 +47,9 @@
{#if !pageData.data || !!pageData.error}
<!-- Error State -->
<div class="flex min-h-screen w-full items-center justify-center p-4">
<div class="animate-fade-in w-full max-w-md rounded-2xl bg-white p-8 shadow-xl">
<div
class="animate-fade-in w-full max-w-md rounded-2xl bg-white p-8 shadow-xl"
>
<div class="mb-6 flex justify-center">
<div class="rounded-full bg-red-100 p-3">
<Icon icon={SearchIcon} cls="h-12 w-12 text-red-600" />
@@ -56,10 +57,11 @@
</div>
<Title size="h4" weight="medium" center>Product Not Found</Title>
<p class="mb-6 mt-3 text-center text-gray-600">
Something went wrong. Please try again or contact support for assistance.
Something went wrong. Please try again or contact support for
assistance.
</p>
<div class="flex justify-center">
<a
<a
href="/"
class="inline-flex items-center justify-center rounded-lg bg-gray-900 px-6 py-3 text-sm font-medium text-white transition-colors hover:bg-gray-800"
>
@@ -87,7 +89,9 @@
<div class="flex flex-col gap-6 lg:flex-row lg:gap-8">
<!-- Left Column: Forms -->
<div class="flex-1">
<div class="rounded-2xl bg-white p-6 shadow-lg md:p-8 lg:p-10">
<div
class="rounded-2xl bg-white p-6 shadow-lg md:p-8 lg:p-10"
>
{#if checkoutVM.loading}
<CheckoutLoadingSection />
{:else if checkoutVM.checkoutStep === CheckoutStep.Initial}

View File

@@ -107,12 +107,12 @@
"devDependencies": {
"@iconify/json": "^2.2.275",
"@internationalized/date": "^3.6.0",
"@lucide/svelte": "^0.503.0",
"@lucide/svelte": "^0.482.0",
"@sveltejs/kit": "^2.0.0",
"@sveltejs/vite-plugin-svelte": "^4.0.0",
"@types/node": "^22.9.3",
"autoprefixer": "^10.4.20",
"bits-ui": "^1.4.1",
"bits-ui": "^1.4.7",
"clsx": "^2.1.1",
"embla-carousel-svelte": "^8.6.0",
"formsnap": "^2.0.0",
@@ -1636,6 +1636,8 @@
"@better-auth/core/zod": ["zod@4.1.12", "", {}, "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ=="],
"@domain-wall/frontend/@lucide/svelte": ["@lucide/svelte@0.482.0", "", { "peerDependencies": { "svelte": "^5" } }, "sha512-n2ycHU9cNcleRDwwpEHBJ6pYzVhHIaL3a+9dQa8kns9hB2g05bY+v2p2KP8v0pZwtNhYTHk/F2o2uZ1bVtQGhw=="],
"@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="],
"@finom/zod-to-json-schema/zod": ["zod@4.1.12", "", {}, "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ=="],

View File

@@ -924,11 +924,6 @@ export const PHONE_COUNTRY_CODES = [
country: "Saint Martin",
phoneCode: "+590",
},
{
countryCode: "sx",
country: "Saint Martin",
phoneCode: "+1721",
},
{
countryCode: "pm",
country: "Saint Pierre and Miquelon",