ui refactor: yuhhhhhh 80% done

This commit is contained in:
user
2025-10-21 18:10:39 +03:00
parent 6a0571fcfa
commit 1a89236449
12 changed files with 606 additions and 359 deletions

View File

@@ -9,131 +9,145 @@
@layer base { @layer base {
:root { :root {
--background: 24 33.3333% 97.0588%; /* Backgrounds - Clean neutral grays */
--foreground: 0 0% 10.1961%; --background: 0 0% 98%;
--card: 24 33.3333% 97.0588%; --foreground: 222 47% 11%;
--card-foreground: 0 0% 10.1961%; --card: 0 0% 100%;
--popover: 24 33.3333% 97.0588%; --card-foreground: 222 47% 11%;
--popover-foreground: 0 0% 10.1961%; --popover: 0 0% 100%;
--primary: 0 55.7789% 39.0196%; --popover-foreground: 222 47% 11%;
/* Primary - Modern slate/blue (Stripe-inspired) */
--primary: 221 83% 53%;
--primary-foreground: 0 0% 100%; --primary-foreground: 0 0% 100%;
--secondary: 43.0769 90.6977% 91.5686%;
--secondary-foreground: 39.8438 100% 25.098%; /* Secondary - Soft gray-blue */
--muted: 22.5 21.0526% 92.549%; --secondary: 214 32% 91%;
--muted-foreground: 33.3333 5.4545% 32.3529%; --secondary-foreground: 222 47% 11%;
--accent: 48 96.4912% 88.8235%;
--accent-foreground: 0 62.8205% 30.5882%; /* Muted - Light grays */
--destructive: 0 70% 35.2941%; --muted: 210 40% 96%;
--muted-foreground: 215 16% 47%;
/* Accent - Vibrant blue */
--accent: 217 91% 60%;
--accent-foreground: 0 0% 100%;
/* Destructive - Red for errors */
--destructive: 0 84% 60%;
--destructive-foreground: 0 0% 100%; --destructive-foreground: 0 0% 100%;
--border: 37.7143 63.6364% 89.2157%;
--input: 37.7143 63.6364% 89.2157%; /* Borders & Inputs - Subtle gray */
--ring: 0 55.7789% 39.0196%; --border: 214 32% 91%;
--chart-1: 0 73.7089% 41.7647%; --input: 214 32% 91%;
--chart-2: 0 55.7789% 39.0196%; --ring: 221 83% 53%;
--chart-3: 0 62.8205% 30.5882%;
--chart-4: 25.9649 90.4762% 37.0588%; /* Charts - Modern palette */
--chart-5: 22.7273 82.5% 31.3725%; --chart-1: 221 83% 53%;
--sidebar: 22.5 21.0526% 92.549%; --chart-2: 142 76% 36%;
--sidebar-foreground: 0 0% 10.1961%; --chart-3: 262 83% 58%;
--sidebar-primary: 0 55.7789% 39.0196%; --chart-4: 41 96% 50%;
--chart-5: 0 84% 60%;
/* Sidebar */
--sidebar: 0 0% 98%;
--sidebar-foreground: 222 47% 11%;
--sidebar-primary: 221 83% 53%;
--sidebar-primary-foreground: 0 0% 100%; --sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 48 96.4912% 88.8235%; --sidebar-accent: 214 32% 91%;
--sidebar-accent-foreground: 0 62.8205% 30.5882%; --sidebar-accent-foreground: 222 47% 11%;
--sidebar-border: 37.7143 63.6364% 89.2157%; --sidebar-border: 214 32% 91%;
--sidebar-ring: 0 55.7789% 39.0196%; --sidebar-ring: 221 83% 53%;
--font-sans: Poppins, sans-serif;
--font-serif: Libre Baskerville, serif; /* Typography */
--font-mono: IBM Plex Mono, monospace; --font-sans: Fredoka, sans-serif;
--radius: 0.375rem; --font-serif: Georgia, serif;
--shadow-x: 1px; --font-mono: ui-monospace, monospace;
--shadow-y: 1px;
--shadow-blur: 16px; /* Design tokens */
--shadow-spread: -2px; --radius: 0.5rem;
--shadow-opacity: 0.12;
--shadow-color: hsl(0 63% 18%); /* Shadows - Neutral */
--shadow-2xs: 1px 1px 16px -2px hsl(0 63% 18% / 0.06); --shadow-color: 220 9% 46%;
--shadow-xs: 1px 1px 16px -2px hsl(0 63% 18% / 0.06); --shadow-2xs: 0 1px 2px 0 hsl(220 9% 46% / 0.05);
--shadow-sm: --shadow-xs: 0 1px 3px 0 hsl(220 9% 46% / 0.1);
1px 1px 16px -2px hsl(0 63% 18% / 0.12), --shadow-sm: 0 2px 4px 0 hsl(220 9% 46% / 0.1);
1px 1px 2px -3px hsl(0 63% 18% / 0.12); --shadow: 0 4px 6px -1px hsl(220 9% 46% / 0.1);
--shadow: --shadow-md: 0 6px 12px -2px hsl(220 9% 46% / 0.15);
1px 1px 16px -2px hsl(0 63% 18% / 0.12), --shadow-lg: 0 10px 20px -5px hsl(220 9% 46% / 0.2);
1px 1px 2px -3px hsl(0 63% 18% / 0.12); --shadow-xl: 0 20px 25px -5px hsl(220 9% 46% / 0.25);
--shadow-md: --shadow-2xl: 0 25px 50px -12px hsl(220 9% 46% / 0.3);
1px 1px 16px -2px hsl(0 63% 18% / 0.12),
1px 2px 4px -3px hsl(0 63% 18% / 0.12);
--shadow-lg:
1px 1px 16px -2px hsl(0 63% 18% / 0.12),
1px 4px 6px -3px hsl(0 63% 18% / 0.12);
--shadow-xl:
1px 1px 16px -2px hsl(0 63% 18% / 0.12),
1px 8px 10px -3px hsl(0 63% 18% / 0.12);
--shadow-2xl: 1px 1px 16px -2px hsl(0 63% 18% / 0.3);
--tracking-normal: 0em;
--spacing: 0.25rem;
} }
.dark { .dark {
--background: 24 9.8039% 10%; /* Backgrounds - Dark mode */
--foreground: 60 4.7619% 95.8824%; --background: 222 47% 11%;
--card: 12 6.4935% 15.098%; --foreground: 210 40% 98%;
--card-foreground: 60 4.7619% 95.8824%; --card: 222 47% 15%;
--popover: 12 6.4935% 15.098%; --card-foreground: 210 40% 98%;
--popover-foreground: 60 4.7619% 95.8824%; --popover: 222 47% 15%;
--primary: 0 73.7089% 41.7647%; --popover-foreground: 210 40% 98%;
--primary-foreground: 24 33.3333% 97.0588%;
--secondary: 22.7273 82.5% 31.3725%; /* Primary - Brighter blue for dark mode */
--secondary-foreground: 48 96.4912% 88.8235%; --primary: 217 91% 60%;
--muted: 24 8.7719% 11.1765%; --primary-foreground: 222 47% 11%;
--muted-foreground: 24 5.7471% 82.9412%;
--accent: 25.9649 90.4762% 37.0588%; /* Secondary */
--accent-foreground: 48 96.4912% 88.8235%; --secondary: 217 33% 17%;
--destructive: 0 84.2365% 60.1961%; --secondary-foreground: 210 40% 98%;
--destructive-foreground: 0 0% 100%;
--border: 30 6.25% 25.098%; /* Muted */
--input: 30 6.25% 25.098%; --muted: 223 47% 11%;
--ring: 0 73.7089% 41.7647%; --muted-foreground: 215 20% 65%;
--chart-1: 0 90.604% 70.7843%;
--chart-2: 0 84.2365% 60.1961%; /* Accent */
--chart-3: 0 72.2222% 50.5882%; --accent: 217 91% 60%;
--chart-4: 43.2558 96.4126% 56.2745%; --accent-foreground: 222 47% 11%;
--chart-5: 37.6923 92.126% 50.1961%;
--sidebar: 24 9.8039% 10%; /* Destructive */
--sidebar-foreground: 60 4.7619% 95.8824%; --destructive: 0 63% 50%;
--sidebar-primary: 0 73.7089% 41.7647%; --destructive-foreground: 210 40% 98%;
--sidebar-primary-foreground: 24 33.3333% 97.0588%;
--sidebar-accent: 25.9649 90.4762% 37.0588%; /* Borders & Inputs */
--sidebar-accent-foreground: 48 96.4912% 88.8235%; --border: 217 33% 17%;
--sidebar-border: 30 6.25% 25.098%; --input: 217 33% 17%;
--sidebar-ring: 0 73.7089% 41.7647%; --ring: 217 91% 60%;
--font-sans: Poppins, sans-serif;
--font-serif: Libre Baskerville, serif; /* Charts */
--font-mono: IBM Plex Mono, monospace; --chart-1: 217 91% 60%;
--radius: 0.375rem; --chart-2: 142 76% 36%;
--shadow-x: 1px; --chart-3: 262 83% 58%;
--shadow-y: 1px; --chart-4: 41 96% 50%;
--shadow-blur: 16px; --chart-5: 0 84% 60%;
--shadow-spread: -2px;
--shadow-opacity: 0.12; /* Sidebar */
--shadow-color: hsl(0 63% 18%); --sidebar: 222 47% 11%;
--shadow-2xs: 1px 1px 16px -2px hsl(0 63% 18% / 0.06); --sidebar-foreground: 210 40% 98%;
--shadow-xs: 1px 1px 16px -2px hsl(0 63% 18% / 0.06); --sidebar-primary: 217 91% 60%;
--shadow-sm: --sidebar-primary-foreground: 222 47% 11%;
1px 1px 16px -2px hsl(0 63% 18% / 0.12), --sidebar-accent: 217 33% 17%;
1px 1px 2px -3px hsl(0 63% 18% / 0.12); --sidebar-accent-foreground: 210 40% 98%;
--shadow: --sidebar-border: 217 33% 17%;
1px 1px 16px -2px hsl(0 63% 18% / 0.12), --sidebar-ring: 217 91% 60%;
1px 1px 2px -3px hsl(0 63% 18% / 0.12);
--shadow-md: /* Typography */
1px 1px 16px -2px hsl(0 63% 18% / 0.12), --font-sans: Fredoka, sans-serif;
1px 2px 4px -3px hsl(0 63% 18% / 0.12); --font-serif: Georgia, serif;
--shadow-lg: --font-mono: ui-monospace, monospace;
1px 1px 16px -2px hsl(0 63% 18% / 0.12),
1px 4px 6px -3px hsl(0 63% 18% / 0.12); /* Design tokens */
--shadow-xl: --radius: 0.5rem;
1px 1px 16px -2px hsl(0 63% 18% / 0.12),
1px 8px 10px -3px hsl(0 63% 18% / 0.12); /* Shadows - Dark mode */
--shadow-2xl: 1px 1px 16px -2px hsl(0 63% 18% / 0.3); --shadow-color: 0 0% 0%;
--shadow-2xs: 0 1px 2px 0 hsl(0 0% 0% / 0.3);
--shadow-xs: 0 1px 3px 0 hsl(0 0% 0% / 0.4);
--shadow-sm: 0 2px 4px 0 hsl(0 0% 0% / 0.4);
--shadow: 0 4px 6px -1px hsl(0 0% 0% / 0.4);
--shadow-md: 0 6px 12px -2px hsl(0 0% 0% / 0.5);
--shadow-lg: 0 10px 20px -5px hsl(0 0% 0% / 0.6);
--shadow-xl: 0 20px 25px -5px hsl(0 0% 0% / 0.7);
--shadow-2xl: 0 25px 50px -12px hsl(0 0% 0% / 0.75);
} }
} }
@@ -147,19 +161,20 @@
scroll-behavior: smooth; scroll-behavior: smooth;
font-family: font-family:
"Fredoka", "Fredoka",
system-ui,
-apple-system, -apple-system,
BlinkMacSystemFont, BlinkMacSystemFont,
"Segoe UI", "Segoe UI",
Roboto, Roboto,
"Helvetica Neue", "Helvetica Neue",
Arial, Arial,
"Noto Sans", sans-serif;
sans-serif, -webkit-font-smoothing: antialiased;
"Apple Color Emoji", -moz-osx-font-smoothing: grayscale;
"Segoe UI Emoji", }
"Segoe UI Symbol",
"Noto Color Emoji"; h1, h2, h3, h4, h5, h6 {
font-weight: 400;
letter-spacing: -0.02em;
} }
} }
@@ -174,8 +189,49 @@
-ms-overflow-style: none; /* IE and Edge */ -ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */ scrollbar-width: none; /* Firefox */
} }
/* Stripe-inspired gradients */
.gradient-mesh {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.gradient-success {
background: linear-gradient(135deg, #84fab0 0%, #8fd3f4 100%);
}
.gradient-error {
background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);
}
/* Smooth transitions */
.transition-smooth {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
/* Glass morphism effect */
.glass {
background: rgba(255, 255, 255, 0.7);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
} }
.all-reset { .all-reset {
all: unset; all: unset;
} }
/* Custom focus styles for better accessibility */
@layer base {
*:focus-visible {
@apply outline-none ring-2 ring-primary ring-offset-2;
}
}
/* Smooth page transitions */
@layer base {
@media (prefers-reduced-motion: no-preference) {
html {
scroll-behavior: smooth;
}
}
}

View File

@@ -25,9 +25,8 @@
<input <input
bind:this={ref} bind:this={ref}
class={cn( class={cn(
"flex w-full items-center rounded-md border border-input bg-background/50 px-3 py-2 text-sm shadow-[inset_0_3px_8px_0_rgba(0,0,0,0.15)] file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50", "flex w-full items-center rounded-lg border border-input bg-white px-3 py-2 text-sm shadow-sm transition-all file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:border-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/20 disabled:cursor-not-allowed disabled:opacity-50",
inputSizes[inputSize], inputSizes[inputSize],
TRANSITION_COLORS,
className, className,
)} )}
bind:value bind:value

View File

@@ -2,7 +2,6 @@
import { Select as SelectPrimitive, type WithoutChild } from "bits-ui"; import { Select as SelectPrimitive, type WithoutChild } from "bits-ui";
import ChevronDown from "@lucide/svelte/icons/chevron-down"; import ChevronDown from "@lucide/svelte/icons/chevron-down";
import { cn } from "$lib/utils.js"; import { cn } from "$lib/utils.js";
import { TRANSITION_COLORS } from "$lib/core/constants";
const inputSizes = { const inputSizes = {
sm: "px-3 py-2 text-sm file:text-sm", sm: "px-3 py-2 text-sm file:text-sm",
@@ -27,13 +26,9 @@
<SelectPrimitive.Trigger <SelectPrimitive.Trigger
bind:ref bind:ref
class={cn( class={cn(
// !overrideClasses &&
// "flex w-full items-center justify-between rounded-md border border-input bg-white ring-offset-background focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[placeholder]:text-muted-foreground [&>span]:line-clamp-1",
!overrideClasses && !overrideClasses &&
"flex w-full items-center justify-between rounded-md border border-input bg-background shadow-[inset_0_3px_8px_0_rgba(0,0,0,0.15)] file:border-0 file:bg-transparent file:font-medium placeholder:text-muted-foreground focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50", "flex w-full items-center justify-between rounded-lg border border-input bg-white shadow-sm transition-all file:border-0 file:bg-transparent file:font-medium placeholder:text-muted-foreground focus-visible:border-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/20 disabled:cursor-not-allowed disabled:opacity-50 data-[placeholder]:text-muted-foreground [&>span]:line-clamp-1",
inputSizes[inputSize], inputSizes[inputSize],
TRANSITION_COLORS,
className, className,
)} )}
{...restProps} {...restProps}

View File

@@ -97,17 +97,15 @@
</Sheet> </Sheet>
<div class="hidden w-full overflow-x-auto lg:block"> <div class="hidden w-full overflow-x-auto lg:block">
<div <div class="flex w-full items-center justify-between gap-2 overflow-x-auto">
class="flex w-full min-w-[30rem] items-center justify-between gap-2 overflow-x-auto py-8"
>
{#each checkoutSteps as step, index} {#each checkoutSteps as step, index}
<div class="flex flex-1 items-center gap-2"> <div class="flex flex-1 items-center gap-2">
<div <div
class={cn( class={cn(
"flex items-center justify-center", "flex items-center justify-center transition-all",
index <= activeStepIndex index <= activeStepIndex
? "cursor-pointer" ? "cursor-pointer"
: "cursor-not-allowed opacity-50", : "cursor-not-allowed opacity-40",
)} )}
onclick={() => handleStepClick(index, step.id)} onclick={() => handleStepClick(index, step.id)}
onkeydown={(e) => { onkeydown={(e) => {
@@ -120,23 +118,28 @@
> >
<div <div
class={cn( class={cn(
"flex h-10 min-h-10 w-10 min-w-10 items-center justify-center rounded-full border-2 transition-colors", "flex h-10 min-h-10 w-10 min-w-10 items-center justify-center rounded-full border-2 font-medium transition-all",
index <= activeStepIndex index < activeStepIndex
? "hover:bg-primary-600 border-brand-700 bg-primary text-white/60" ? "border-green-500 bg-green-500 text-white"
: "border-gray-400 bg-gray-100 text-gray-700", : index === activeStepIndex
index === activeStepIndex ? "border-primary bg-primary text-white shadow-md"
? "text-lg font-semibold text-white" : "border-gray-300 bg-white text-gray-400",
: "",
)} )}
> >
{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> </div>
<span <span
class={cn( class={cn(
"ml-2 hidden w-max text-sm md:block", "ml-3 hidden w-max text-sm transition-all md:block",
index <= activeStepIndex index <= activeStepIndex
? "font-semibold" ? "font-semibold text-gray-900"
: "text-gray-800", : "text-gray-500",
)} )}
> >
{step.label} {step.label}
@@ -145,10 +148,10 @@
{#if index !== checkoutSteps.length - 1} {#if index !== checkoutSteps.length - 1}
<div <div
class={cn( class={cn(
"h-0.5 w-full min-w-4 flex-1 border-t transition-colors", "h-0.5 w-full min-w-4 flex-1 transition-all",
index <= activeStepIndex index < activeStepIndex
? "border-primary" ? "bg-green-500"
: "border-gray-400", : "bg-gray-300",
)} )}
></div> ></div>
{/if} {/if}

View File

@@ -24,73 +24,77 @@
}); });
</script> </script>
<div class="flex flex-col gap-4 rounded-lg bg-white p-4 drop-shadow-lg md:p-8"> <div class="flex flex-col gap-6">
<Title size="h4" weight="medium">Payment Summary</Title> <Title size="h4" weight="medium">Order Summary</Title>
<div class="h-0.5 w-full border-t-2 border-gray-200"></div>
{#if !calculating} {#if !calculating}
<!-- Product Information --> <!-- Product Information -->
{#if $productStore} {#if $productStore}
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-3 border-b border-gray-200 pb-6">
<Title size="p" weight="medium">{$productStore.title}</Title> <Title size="p" weight="medium">
<p class="text-sm text-gray-600">{$productStore.description}</p> {$productStore.title}
</Title>
<p class="text-sm leading-relaxed text-gray-600">
{$productStore.description}
</p>
</div> </div>
{/if} {/if}
<!-- Price Breakdown --> <!-- Price Breakdown -->
<div class="mt-2 flex flex-col gap-2 border-t pt-4"> <div class="flex flex-col gap-3 border-b border-gray-200 pb-6">
<div class="flex justify-between text-sm"> <div class="flex items-center justify-between text-sm">
<span>Base Price</span> <span class="text-gray-600">Base Price</span>
<span>{convertAndFormatCurrency(priceDetails.basePrice)}</span> <span class="font-medium text-gray-900">
{convertAndFormatCurrency(priceDetails.basePrice)}
</span>
</div> </div>
{#if priceDetails.discountAmount > 0} {#if priceDetails.discountAmount > 0}
<div class="flex justify-between text-sm text-green-600"> <div class="flex items-center justify-between text-sm">
<span>Discount</span> <span class="text-gray-600">Discount</span>
<span <span class="font-medium text-green-600">
>-{convertAndFormatCurrency( -{convertAndFormatCurrency(priceDetails.discountAmount)}
priceDetails.discountAmount, </span>
)}</span
>
</div> </div>
<div class="flex justify-between text-sm"> <div class="flex items-center justify-between text-sm">
<span>Display Price</span> <span class="text-gray-600">Display Price</span>
<span <span class="font-medium text-gray-900">
>{convertAndFormatCurrency( {convertAndFormatCurrency(priceDetails.displayPrice)}
priceDetails.displayPrice, </span>
)}</span
>
</div> </div>
{/if} {/if}
</div> </div>
<!-- Final Total --> <!-- Final Total -->
<div class="mt-4 flex flex-col gap-2 border-t pt-4"> <div class="flex items-center justify-between">
<div class="flex justify-between font-medium"> <span class="text-base font-semibold text-gray-900">
<Title size="h5" weight="medium" Total ({$currencyStore.code})
>Total ({$currencyStore.code})</Title </span>
> <span class="text-2xl font-bold text-gray-900">
<span class="text-lg" {convertAndFormatCurrency(priceDetails.orderPrice)}
>{convertAndFormatCurrency(priceDetails.orderPrice)}</span </span>
> </div>
<!-- Security Badge -->
<div class="mt-4 rounded-lg bg-blue-50 p-4">
<div class="flex items-start gap-3">
<svg class="mt-0.5 h-5 w-5 flex-shrink-0 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
</svg>
<div class="flex-1">
<p class="text-xs font-medium text-blue-900">Secure Checkout</p>
<p class="mt-1 text-xs text-blue-700">
Your payment information is encrypted and secure
</p>
</div>
</div> </div>
</div> </div>
{:else} {:else}
<div class="grid place-items-center p-8 text-center"> <div class="grid place-items-center py-12">
<span class="text-gray-600">Calculating...</span> <div class="flex items-center gap-3">
<div class="h-5 w-5 animate-spin rounded-full border-2 border-gray-300 border-t-primary"></div>
<span class="text-sm text-gray-600">Calculating...</span>
</div>
</div> </div>
{/if} {/if}
<!-- Important Information -->
<div class="mt-4 rounded-lg bg-gray-50 p-4 text-xs text-gray-600">
<p class="mb-2 font-medium">Important Information:</p>
<ul class="list-disc space-y-1 pl-4">
<li>Price includes all applicable taxes and fees</li>
<li>
Cancellation and refund policies may apply as per our terms of
service
</li>
<li>Payment will be processed securely upon order confirmation</li>
</ul>
</div>
</div> </div>

View File

@@ -1,13 +1,9 @@
<script lang="ts"> <script lang="ts">
import Icon from "$lib/components/atoms/icon.svelte";
import Footer from "$lib/components/molecules/footer/footer.svelte";
import Navbar from "$lib/components/molecules/navbar/navbar.svelte";
import { currencyVM } from "$lib/domains/currency/view/currency.vm.svelte"; import { currencyVM } from "$lib/domains/currency/view/currency.vm.svelte";
import { svTrpcApiStore, trpcApiStore } from "$lib/stores/api"; import { svTrpcApiStore, trpcApiStore } from "$lib/stores/api";
import { sessionInfo, sessionUserInfo } from "$lib/stores/session.info"; import { sessionInfo, sessionUserInfo } from "$lib/stores/session.info";
import { trpc, trpcRaw } from "$lib/trpc/trpc"; import { trpc, trpcRaw } from "$lib/trpc/trpc";
import { onMount } from "svelte"; import { onMount } from "svelte";
import UpIcon from "~icons/material-symbols/keyboard-double-arrow-up-rounded";
import type { LayoutData } from "./$types"; import type { LayoutData } from "./$types";
let { let {
@@ -23,18 +19,6 @@
svTrpcApiStore.set(trpc()); svTrpcApiStore.set(trpc());
// let invert = $derived(page.url.pathname === "/");
let showScrollTop = $state(false);
function scrollToTop() {
window.scrollTo({ top: 0, behavior: "smooth" });
}
function handleScroll() {
showScrollTop = window.scrollY > 200; // Show button when scrolled down 200px
}
onMount(() => { onMount(() => {
trpcApiStore.set(trpcRaw()); trpcApiStore.set(trpcRaw());
@@ -42,32 +26,11 @@
setTimeout(() => { setTimeout(() => {
currencyVM.getCurrencies(); currencyVM.getCurrencies();
}, 500); }, 500);
// Add scroll event listener
window.addEventListener("scroll", handleScroll);
return () => {
window.removeEventListener("scroll", handleScroll);
};
}); });
</script> </script>
<Navbar invert={false} /> <div class="min-h-screen w-full">
<main class="flex w-full flex-col">
<div class="grid h-full w-full place-items-center overflow-x-hidden">
<main class="flex w-full flex-col gap-8 overflow-x-hidden">
{@render children?.()} {@render children?.()}
</main> </main>
</div> </div>
<Footer />
<!-- Scroll to top FAB -->
{#if showScrollTop}
<button
onclick={scrollToTop}
class="fixed bottom-4 left-4 z-50 flex h-12 w-12 items-center justify-center rounded-full border-2 border-white/60 bg-primary text-white shadow-lg transition-opacity hover:bg-primary/90 lg:bottom-8 lg:right-8"
aria-label="Scroll to top"
>
<Icon icon={UpIcon} cls="w-auto h-6 text-white" />
</button>
{/if}

View File

@@ -1,45 +0,0 @@
<script lang="ts">
import Title from "$lib/components/atoms/title.svelte";
import { checkoutSessionIdStore } from "$lib/domains/checkout/sid.store";
import { onMount } from "svelte";
import { toast } from "svelte-sonner";
import type { PageData } from "./$types";
let { data }: { data: PageData } = $props();
onMount(() => {
toast("Do the check here to then either redirect or show the error");
if (data.error) {
return;
}
setTimeout(() => {
if (!data.data) {
toast.error("An error occurred during checkout", {
description: "Please try again later or contact support",
});
return;
}
toast("Please hold...", {
description: "Preparing checkout session",
});
window.location.replace(
`/checkout/${$checkoutSessionIdStore}/${data.data.linkId}`,
);
}, 3000);
});
</script>
<div class="flex w-full flex-col items-center justify-center gap-8 p-20">
{#if data.data}
<Title size="h3" weight="medium">{data.data.title}</Title>
<p>{data.data.description}</p>
<span>
Either show the user the product as being valid and redirecting them
to the checkout
</span>
{:else}
<span
>Show the user an error around "page not found" or "expired link"</span
>
{/if}
</div>

View File

@@ -2,6 +2,6 @@ import { getProductUseCases } from "$lib/domains/product/usecases";
import type { PageServerLoad } from "./$types"; import type { PageServerLoad } from "./$types";
export const load: PageServerLoad = async ({ params }) => { export const load: PageServerLoad = async ({ params }) => {
const pid = params.pageid; const plid = params.plid;
return await getProductUseCases().getProductByLinkId(pid); return await getProductUseCases().getProductByLinkId(plid);
}; };

View File

@@ -0,0 +1,107 @@
<script lang="ts">
import Icon from "$lib/components/atoms/icon.svelte";
import Title from "$lib/components/atoms/title.svelte";
import { checkoutSessionIdStore } from "$lib/domains/checkout/sid.store";
import { onMount } from "svelte";
import { toast } from "svelte-sonner";
import CheckCircleIcon from "~icons/heroicons/check-circle-20-solid";
import ExclamationIcon from "~icons/heroicons/exclamation-triangle-20-solid";
import type { PageData } from "./$types";
let { data }: { data: PageData } = $props();
onMount(() => {
if (data.error) {
return;
}
setTimeout(() => {
if (!data.data) {
toast.error("An error occurred during checkout", {
description: "Please try again later or contact support",
});
return;
}
window.location.replace(
`/checkout/${$checkoutSessionIdStore}/${data.data.linkId}`,
);
}, 2000);
});
</script>
<div class="flex min-h-screen w-full items-center justify-center bg-gradient-to-br from-gray-50 to-gray-100 p-4">
<div class="w-full max-w-md">
{#if data.data}
<!-- Valid Product Card -->
<div class="animate-fade-in rounded-2xl bg-white p-8 shadow-xl">
<div class="mb-6 flex justify-center">
<div class="rounded-full bg-green-100 p-3">
<Icon icon={CheckCircleIcon} cls="h-12 w-12 text-green-600" />
</div>
</div>
<Title size="h4" weight="medium" center>
{data.data.title}
</Title>
{#if data.data.description}
<p class="mb-6 mt-3 text-center text-gray-600">
{data.data.description}
</p>
{/if}
<div class="mb-6 flex justify-center">
<div class="flex space-x-1">
<div class="h-2 w-2 animate-bounce rounded-full bg-primary [animation-delay:-0.3s]"></div>
<div class="h-2 w-2 animate-bounce rounded-full bg-primary [animation-delay:-0.15s]"></div>
<div class="h-2 w-2 animate-bounce rounded-full bg-primary"></div>
</div>
</div>
<p class="text-center text-sm text-gray-500">
Redirecting you to checkout...
</p>
</div>
{:else}
<!-- Error State -->
<div class="animate-fade-in 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={ExclamationIcon} cls="h-12 w-12 text-red-600" />
</div>
</div>
<Title size="h4" weight="medium" center>Page Not Found</Title>
<p class="mb-6 mt-3 text-center text-gray-600">
The link you followed may be expired or invalid. Please contact support for assistance.
</p>
<div class="flex justify-center">
<a
href="/"
class="inline-flex items-center justify-center rounded-lg bg-primary px-6 py-3 text-sm font-medium text-white transition-colors hover:bg-primary/90"
>
Return Home
</a>
</div>
</div>
{/if}
</div>
</div>
<style>
@keyframes fade-in {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-fade-in {
animation: fade-in 0.5s ease-out;
}
</style>

View File

@@ -44,25 +44,50 @@
}); });
</script> </script>
<div class="grid h-full w-full place-items-center"> <div class="min-h-screen w-full bg-gradient-to-br from-gray-50 to-gray-100">
<MaxWidthWrapper cls="p-4 md:p-8 lg:p-10 3xl:p-0"> {#if !pageData.data || !!pageData.error}
{#if !pageData.data || !!pageData.error} <!-- Error State -->
<div class="grid h-full min-h-screen w-full place-items-center"> <div class="flex min-h-screen w-full items-center justify-center p-4">
<div class="flex flex-col items-center justify-center gap-4"> <div class="animate-fade-in w-full max-w-md rounded-2xl bg-white p-8 shadow-xl">
<Icon icon={SearchIcon} cls="w-12 h-12" /> <div class="mb-6 flex justify-center">
<Title size="h4" color="black">Product not found</Title> <div class="rounded-full bg-red-100 p-3">
<p>Something went wrong, please try again or contact us</p> <Icon icon={SearchIcon} cls="h-12 w-12 text-red-600" />
</div>
</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.
</p>
<div class="flex justify-center">
<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"
>
Return Home
</a>
</div> </div>
</div> </div>
{:else if checkoutVM.checkoutStep === CheckoutStep.Confirmation} </div>
<div class="grid w-full place-items-center p-4 py-32"> {:else if checkoutVM.checkoutStep === CheckoutStep.Confirmation}
<!-- Confirmation State -->
<div class="flex min-h-screen w-full items-center justify-center p-4">
<MaxWidthWrapper cls="w-full">
<CheckoutConfirmationSection /> <CheckoutConfirmationSection />
</MaxWidthWrapper>
</div>
{:else}
<!-- Checkout Flow -->
<div class="mx-auto w-full max-w-7xl px-4 py-8 md:px-6 lg:px-8 lg:py-12">
<!-- Steps Indicator -->
<div class="mb-8 lg:mb-12">
<CheckoutStepsIndicator />
</div> </div>
{:else}
<CheckoutStepsIndicator /> <!-- Main Checkout Layout -->
<div class="flex w-full flex-col gap-8 lg:flex-row"> <div class="flex flex-col gap-6 lg:flex-row lg:gap-8">
<div class="flex w-full flex-col"> <!-- Left Column: Forms -->
<div class="flex w-full flex-col gap-12"> <div class="flex-1">
<div class="rounded-2xl bg-white p-6 shadow-lg md:p-8 lg:p-10">
{#if checkoutVM.loading} {#if checkoutVM.loading}
<CheckoutLoadingSection /> <CheckoutLoadingSection />
{:else if checkoutVM.checkoutStep === CheckoutStep.Initial} {:else if checkoutVM.checkoutStep === CheckoutStep.Initial}
@@ -74,16 +99,33 @@
{/if} {/if}
</div> </div>
</div> </div>
<div
class="grid w-full place-items-center lg:max-w-lg lg:place-items-start" <!-- Right Column: Summary (Sticky on larger screens) -->
> <div class="lg:w-[400px] xl:w-[440px]">
<div <div class="lg:sticky lg:top-8">
class="flex w-full flex-col gap-8 pt-8 md:max-w-md lg:max-w-full lg:pt-0" <div class="rounded-2xl bg-white p-6 shadow-lg md:p-8">
> <PaymentSummary />
<PaymentSummary /> </div>
</div> </div>
</div> </div>
</div> </div>
{/if} </div>
</MaxWidthWrapper> {/if}
</div> </div>
<style>
@keyframes fade-in {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-fade-in {
animation: fade-in 0.5s ease-out;
}
</style>

View File

@@ -1,41 +1,98 @@
<script lang="ts"> <script lang="ts">
import Icon from "$lib/components/atoms/icon.svelte"; import Icon from "$lib/components/atoms/icon.svelte";
import Title from "$lib/components/atoms/title.svelte"; import Title from "$lib/components/atoms/title.svelte";
import MaxWidthWrapper from "$lib/components/molecules/max-width-wrapper.svelte"; import CheckIcon from "~icons/heroicons/check-circle-20-solid";
import CheckIcon from "~icons/ic/round-check"; import ChatBubbleLeftRightIcon from "~icons/heroicons/chat-bubble-left-right-20-solid";
// Maybe todo? if the `uid` search param is present, do something?? figure out later // Maybe todo? if the `uid` search param is present, do something?? figure out later
</script> </script>
<div class="grid min-h-[80vh] w-full place-items-center px-4 sm:px-6"> <div class="flex min-h-screen w-full items-center justify-center bg-gradient-to-br from-green-50 via-blue-50 to-purple-50 p-4">
<MaxWidthWrapper <div class="w-full max-w-lg">
cls="flex flex-col gap-6 sm:gap-8 items-center justify-center" <div class="animate-scale-in rounded-3xl bg-white p-8 shadow-2xl md:p-12">
> <!-- Success Icon with Animation -->
<div <div class="mb-8 flex justify-center">
class="flex w-full max-w-md flex-col items-center justify-center gap-6 rounded-xl bg-white p-4 drop-shadow-lg sm:gap-8 sm:p-6 md:p-8 md:py-12 lg:max-w-xl" <div class="animate-check-bounce rounded-full bg-gradient-to-br from-green-400 to-green-600 p-4 shadow-lg">
> <Icon icon={CheckIcon} cls="h-16 w-16 text-white" />
<div </div>
class="rounded-full bg-emerald-100 p-1.5 text-emerald-600 drop-shadow-lg sm:p-2"
>
<Icon
icon={CheckIcon}
cls="w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12"
/>
</div> </div>
<Title size="h3" center weight="medium">Booking confirmed</Title> <!-- Title -->
<Title size="h3" weight="medium" center>Order Confirmed!</Title>
<p <!-- Description -->
class="w-full max-w-prose text-center text-sm text-gray-600 sm:text-base" <p class="mb-6 mt-4 text-center text-base text-gray-600">
> Thank you for your order! Your payment has been processed successfully.
Thank you for booking your flight! Your order has been placed
successfully. You will receive a confirmation email shortly.
</p>
<p
class="w-full max-w-prose text-center text-sm text-gray-600 sm:text-base"
>
In it you will not only find the booking details, but also a
tracking number for your flight.
</p> </p>
<!-- Confirmation Notification Card -->
<div class="mb-8 rounded-xl bg-blue-50 p-4 md:p-6">
<div class="flex items-start gap-3">
<div class="flex-shrink-0">
<div class="rounded-lg bg-blue-100 p-2">
<Icon icon={ChatBubbleLeftRightIcon} cls="h-5 w-5 text-blue-600" />
</div>
</div>
<div class="flex-1">
<p class="mb-1 text-sm font-semibold text-gray-900">
We'll Be In Touch
</p>
<p class="text-sm text-gray-600">
Our team will reach back to you soon with order confirmation and details.
</p>
</div>
</div>
</div>
<!-- Info Cards Grid -->
<div class="mb-8 grid gap-4 sm:grid-cols-2">
<div class="rounded-lg border border-gray-200 bg-gray-50 p-4">
<div class="mb-1 text-xs font-medium uppercase tracking-wide text-gray-500">
Status
</div>
<div class="text-sm font-semibold text-gray-900">
Processing
</div>
</div>
<div class="rounded-lg border border-gray-200 bg-gray-50 p-4">
<div class="mb-1 text-xs font-medium uppercase tracking-wide text-gray-500">
Confirmation
</div>
<div class="text-sm font-semibold text-gray-900">
Via Contact
</div>
</div>
</div>
</div> </div>
</MaxWidthWrapper> </div>
</div> </div>
<style>
@keyframes scale-in {
from {
opacity: 0;
transform: scale(0.9);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes check-bounce {
0%, 100% {
transform: scale(1);
}
50% {
transform: scale(1.1);
}
}
.animate-scale-in {
animation: scale-in 0.5s cubic-bezier(0.16, 1, 0.3, 1);
}
.animate-check-bounce {
animation: check-bounce 0.6s ease-in-out;
}
</style>

View File

@@ -2,9 +2,10 @@
import { page } from "$app/state"; import { page } from "$app/state";
import Icon from "$lib/components/atoms/icon.svelte"; import Icon from "$lib/components/atoms/icon.svelte";
import Title from "$lib/components/atoms/title.svelte"; import Title from "$lib/components/atoms/title.svelte";
import MaxWidthWrapper from "$lib/components/molecules/max-width-wrapper.svelte";
import { onMount } from "svelte"; import { onMount } from "svelte";
import CloseIcon from "~icons/mdi/window-close"; import ExclamationIcon from "~icons/heroicons/exclamation-triangle-20-solid";
import ClockIcon from "~icons/heroicons/clock-20-solid";
import SupportIcon from "~icons/heroicons/chat-bubble-left-right-20-solid";
let canRedirect = $state(false); let canRedirect = $state(false);
@@ -18,25 +19,90 @@
}); });
</script> </script>
<div class="grid min-h-[80vh] w-full place-items-center"> <div class="flex min-h-screen w-full items-center justify-center bg-gradient-to-br from-red-50 via-orange-50 to-yellow-50 p-4">
<MaxWidthWrapper <div class="w-full max-w-lg">
cls="flex flex-col gap-8 items-center justify-center p-4 md:p-8" <div class="animate-scale-in rounded-3xl bg-white p-8 shadow-2xl md:p-12">
> <!-- Error Icon with Animation -->
<div <div class="mb-8 flex justify-center">
class="flex w-full flex-col items-center justify-center gap-8 rounded-xl bg-white p-4 drop-shadow-lg md:p-8 md:py-12 lg:w-max" <div class="animate-pulse-slow rounded-full bg-gradient-to-br from-red-400 to-red-600 p-4 shadow-lg">
> <Icon icon={ExclamationIcon} cls="h-16 w-16 text-white" />
<div </div>
class="rounded-full bg-rose-100 p-2 text-rose-600 drop-shadow-lg"
>
<Icon icon={CloseIcon} cls="w-12 h-12" />
</div> </div>
<Title size="h3" center weight="medium">Session Terminated</Title> <!-- Title -->
<p class="w-full max-w-prose text-center text-gray-600"> <Title size="h3" weight="medium" center>Session Terminated</Title>
Unfortunately, your session has been terminated due to inactivity
or something went wrong, please contact us to walk through the <!-- Description -->
steps again. <p class="mb-8 mt-4 text-center text-base text-gray-600">
Your checkout session has been terminated. This may have occurred due to inactivity or a connection issue.
</p> </p>
<!-- Reason Cards -->
<div class="mb-8 space-y-3">
<div class="flex items-start gap-3 rounded-lg border border-gray-200 bg-gray-50 p-4">
<div class="flex-shrink-0">
<div class="rounded-lg bg-orange-100 p-2">
<Icon icon={ClockIcon} cls="h-5 w-5 text-orange-600" />
</div>
</div>
<div class="flex-1">
<p class="mb-1 text-sm font-semibold text-gray-900">
Session Timeout
</p>
<p class="text-sm text-gray-600">
Sessions expire after a period of inactivity for security.
</p>
</div>
</div>
<div class="flex items-start gap-3 rounded-lg border border-gray-200 bg-gray-50 p-4">
<div class="flex-shrink-0">
<div class="rounded-lg bg-blue-100 p-2">
<Icon icon={SupportIcon} cls="h-5 w-5 text-blue-600" />
</div>
</div>
<div class="flex-1">
<p class="mb-1 text-sm font-semibold text-gray-900">
Need Help?
</p>
<p class="text-sm text-gray-600">
Contact our support team to continue your order.
</p>
</div>
</div>
</div>
</div> </div>
</MaxWidthWrapper> </div>
</div> </div>
<style>
@keyframes scale-in {
from {
opacity: 0;
transform: scale(0.9);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes pulse-slow {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.8;
}
}
.animate-scale-in {
animation: scale-in 0.5s cubic-bezier(0.16, 1, 0.3, 1);
}
.animate-pulse-slow {
animation: pulse-slow 2s ease-in-out infinite;
}
</style>