ui refactor: yuhhhhhh 80% done
This commit is contained in:
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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",
|
||||||
: "",
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
{#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}
|
{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}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
|
||||||
|
|||||||
@@ -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>
|
|
||||||
@@ -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);
|
||||||
};
|
};
|
||||||
107
apps/frontend/src/routes/(main)/[plid]/+page.svelte
Normal file
107
apps/frontend/src/routes/(main)/[plid]/+page.svelte
Normal 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>
|
||||||
@@ -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}
|
||||||
<div class="grid h-full min-h-screen w-full place-items-center">
|
<!-- Error State -->
|
||||||
<div class="flex flex-col items-center justify-center gap-4">
|
<div class="flex min-h-screen w-full items-center justify-center p-4">
|
||||||
<Icon icon={SearchIcon} cls="w-12 h-12" />
|
<div class="animate-fade-in w-full max-w-md rounded-2xl bg-white p-8 shadow-xl">
|
||||||
<Title size="h4" color="black">Product not found</Title>
|
<div class="mb-6 flex justify-center">
|
||||||
<p>Something went wrong, please try again or contact us</p>
|
<div class="rounded-full bg-red-100 p-3">
|
||||||
|
<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>
|
</div>
|
||||||
{:else if checkoutVM.checkoutStep === CheckoutStep.Confirmation}
|
{:else if checkoutVM.checkoutStep === CheckoutStep.Confirmation}
|
||||||
<div class="grid w-full place-items-center p-4 py-32">
|
<!-- Confirmation State -->
|
||||||
|
<div class="flex min-h-screen w-full items-center justify-center p-4">
|
||||||
|
<MaxWidthWrapper cls="w-full">
|
||||||
<CheckoutConfirmationSection />
|
<CheckoutConfirmationSection />
|
||||||
|
</MaxWidthWrapper>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{: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 />
|
<CheckoutStepsIndicator />
|
||||||
<div class="flex w-full flex-col gap-8 lg:flex-row">
|
</div>
|
||||||
<div class="flex w-full flex-col">
|
|
||||||
<div class="flex w-full flex-col gap-12">
|
<!-- Main Checkout Layout -->
|
||||||
|
<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">
|
||||||
{#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>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</MaxWidthWrapper>
|
|
||||||
</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>
|
||||||
|
|||||||
@@ -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>
|
||||||
<p
|
|
||||||
class="w-full max-w-prose text-center text-sm text-gray-600 sm:text-base"
|
<!-- Confirmation Notification Card -->
|
||||||
>
|
<div class="mb-8 rounded-xl bg-blue-50 p-4 md:p-6">
|
||||||
In it you will not only find the booking details, but also a
|
<div class="flex items-start gap-3">
|
||||||
tracking number for your flight.
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</MaxWidthWrapper>
|
</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>
|
||||||
</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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
<!-- 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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</MaxWidthWrapper>
|
</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>
|
||||||
</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>
|
||||||
|
|||||||
Reference in New Issue
Block a user