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 {
:root {
--background: 24 33.3333% 97.0588%;
--foreground: 0 0% 10.1961%;
--card: 24 33.3333% 97.0588%;
--card-foreground: 0 0% 10.1961%;
--popover: 24 33.3333% 97.0588%;
--popover-foreground: 0 0% 10.1961%;
--primary: 0 55.7789% 39.0196%;
/* Backgrounds - Clean neutral grays */
--background: 0 0% 98%;
--foreground: 222 47% 11%;
--card: 0 0% 100%;
--card-foreground: 222 47% 11%;
--popover: 0 0% 100%;
--popover-foreground: 222 47% 11%;
/* Primary - Modern slate/blue (Stripe-inspired) */
--primary: 221 83% 53%;
--primary-foreground: 0 0% 100%;
--secondary: 43.0769 90.6977% 91.5686%;
--secondary-foreground: 39.8438 100% 25.098%;
--muted: 22.5 21.0526% 92.549%;
--muted-foreground: 33.3333 5.4545% 32.3529%;
--accent: 48 96.4912% 88.8235%;
--accent-foreground: 0 62.8205% 30.5882%;
--destructive: 0 70% 35.2941%;
/* Secondary - Soft gray-blue */
--secondary: 214 32% 91%;
--secondary-foreground: 222 47% 11%;
/* Muted - Light grays */
--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%;
--border: 37.7143 63.6364% 89.2157%;
--input: 37.7143 63.6364% 89.2157%;
--ring: 0 55.7789% 39.0196%;
--chart-1: 0 73.7089% 41.7647%;
--chart-2: 0 55.7789% 39.0196%;
--chart-3: 0 62.8205% 30.5882%;
--chart-4: 25.9649 90.4762% 37.0588%;
--chart-5: 22.7273 82.5% 31.3725%;
--sidebar: 22.5 21.0526% 92.549%;
--sidebar-foreground: 0 0% 10.1961%;
--sidebar-primary: 0 55.7789% 39.0196%;
/* Borders & Inputs - Subtle gray */
--border: 214 32% 91%;
--input: 214 32% 91%;
--ring: 221 83% 53%;
/* Charts - Modern palette */
--chart-1: 221 83% 53%;
--chart-2: 142 76% 36%;
--chart-3: 262 83% 58%;
--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-accent: 48 96.4912% 88.8235%;
--sidebar-accent-foreground: 0 62.8205% 30.5882%;
--sidebar-border: 37.7143 63.6364% 89.2157%;
--sidebar-ring: 0 55.7789% 39.0196%;
--font-sans: Poppins, sans-serif;
--font-serif: Libre Baskerville, serif;
--font-mono: IBM Plex Mono, monospace;
--radius: 0.375rem;
--shadow-x: 1px;
--shadow-y: 1px;
--shadow-blur: 16px;
--shadow-spread: -2px;
--shadow-opacity: 0.12;
--shadow-color: hsl(0 63% 18%);
--shadow-2xs: 1px 1px 16px -2px hsl(0 63% 18% / 0.06);
--shadow-xs: 1px 1px 16px -2px hsl(0 63% 18% / 0.06);
--shadow-sm:
1px 1px 16px -2px hsl(0 63% 18% / 0.12),
1px 1px 2px -3px hsl(0 63% 18% / 0.12);
--shadow:
1px 1px 16px -2px hsl(0 63% 18% / 0.12),
1px 1px 2px -3px hsl(0 63% 18% / 0.12);
--shadow-md:
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;
--sidebar-accent: 214 32% 91%;
--sidebar-accent-foreground: 222 47% 11%;
--sidebar-border: 214 32% 91%;
--sidebar-ring: 221 83% 53%;
/* Typography */
--font-sans: Fredoka, sans-serif;
--font-serif: Georgia, serif;
--font-mono: ui-monospace, monospace;
/* Design tokens */
--radius: 0.5rem;
/* Shadows - Neutral */
--shadow-color: 220 9% 46%;
--shadow-2xs: 0 1px 2px 0 hsl(220 9% 46% / 0.05);
--shadow-xs: 0 1px 3px 0 hsl(220 9% 46% / 0.1);
--shadow-sm: 0 2px 4px 0 hsl(220 9% 46% / 0.1);
--shadow: 0 4px 6px -1px hsl(220 9% 46% / 0.1);
--shadow-md: 0 6px 12px -2px hsl(220 9% 46% / 0.15);
--shadow-lg: 0 10px 20px -5px hsl(220 9% 46% / 0.2);
--shadow-xl: 0 20px 25px -5px hsl(220 9% 46% / 0.25);
--shadow-2xl: 0 25px 50px -12px hsl(220 9% 46% / 0.3);
}
.dark {
--background: 24 9.8039% 10%;
--foreground: 60 4.7619% 95.8824%;
--card: 12 6.4935% 15.098%;
--card-foreground: 60 4.7619% 95.8824%;
--popover: 12 6.4935% 15.098%;
--popover-foreground: 60 4.7619% 95.8824%;
--primary: 0 73.7089% 41.7647%;
--primary-foreground: 24 33.3333% 97.0588%;
--secondary: 22.7273 82.5% 31.3725%;
--secondary-foreground: 48 96.4912% 88.8235%;
--muted: 24 8.7719% 11.1765%;
--muted-foreground: 24 5.7471% 82.9412%;
--accent: 25.9649 90.4762% 37.0588%;
--accent-foreground: 48 96.4912% 88.8235%;
--destructive: 0 84.2365% 60.1961%;
--destructive-foreground: 0 0% 100%;
--border: 30 6.25% 25.098%;
--input: 30 6.25% 25.098%;
--ring: 0 73.7089% 41.7647%;
--chart-1: 0 90.604% 70.7843%;
--chart-2: 0 84.2365% 60.1961%;
--chart-3: 0 72.2222% 50.5882%;
--chart-4: 43.2558 96.4126% 56.2745%;
--chart-5: 37.6923 92.126% 50.1961%;
--sidebar: 24 9.8039% 10%;
--sidebar-foreground: 60 4.7619% 95.8824%;
--sidebar-primary: 0 73.7089% 41.7647%;
--sidebar-primary-foreground: 24 33.3333% 97.0588%;
--sidebar-accent: 25.9649 90.4762% 37.0588%;
--sidebar-accent-foreground: 48 96.4912% 88.8235%;
--sidebar-border: 30 6.25% 25.098%;
--sidebar-ring: 0 73.7089% 41.7647%;
--font-sans: Poppins, sans-serif;
--font-serif: Libre Baskerville, serif;
--font-mono: IBM Plex Mono, monospace;
--radius: 0.375rem;
--shadow-x: 1px;
--shadow-y: 1px;
--shadow-blur: 16px;
--shadow-spread: -2px;
--shadow-opacity: 0.12;
--shadow-color: hsl(0 63% 18%);
--shadow-2xs: 1px 1px 16px -2px hsl(0 63% 18% / 0.06);
--shadow-xs: 1px 1px 16px -2px hsl(0 63% 18% / 0.06);
--shadow-sm:
1px 1px 16px -2px hsl(0 63% 18% / 0.12),
1px 1px 2px -3px hsl(0 63% 18% / 0.12);
--shadow:
1px 1px 16px -2px hsl(0 63% 18% / 0.12),
1px 1px 2px -3px hsl(0 63% 18% / 0.12);
--shadow-md:
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);
/* Backgrounds - Dark mode */
--background: 222 47% 11%;
--foreground: 210 40% 98%;
--card: 222 47% 15%;
--card-foreground: 210 40% 98%;
--popover: 222 47% 15%;
--popover-foreground: 210 40% 98%;
/* Primary - Brighter blue for dark mode */
--primary: 217 91% 60%;
--primary-foreground: 222 47% 11%;
/* Secondary */
--secondary: 217 33% 17%;
--secondary-foreground: 210 40% 98%;
/* Muted */
--muted: 223 47% 11%;
--muted-foreground: 215 20% 65%;
/* Accent */
--accent: 217 91% 60%;
--accent-foreground: 222 47% 11%;
/* Destructive */
--destructive: 0 63% 50%;
--destructive-foreground: 210 40% 98%;
/* Borders & Inputs */
--border: 217 33% 17%;
--input: 217 33% 17%;
--ring: 217 91% 60%;
/* Charts */
--chart-1: 217 91% 60%;
--chart-2: 142 76% 36%;
--chart-3: 262 83% 58%;
--chart-4: 41 96% 50%;
--chart-5: 0 84% 60%;
/* Sidebar */
--sidebar: 222 47% 11%;
--sidebar-foreground: 210 40% 98%;
--sidebar-primary: 217 91% 60%;
--sidebar-primary-foreground: 222 47% 11%;
--sidebar-accent: 217 33% 17%;
--sidebar-accent-foreground: 210 40% 98%;
--sidebar-border: 217 33% 17%;
--sidebar-ring: 217 91% 60%;
/* Typography */
--font-sans: Fredoka, sans-serif;
--font-serif: Georgia, serif;
--font-mono: ui-monospace, monospace;
/* Design tokens */
--radius: 0.5rem;
/* Shadows - Dark mode */
--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;
font-family:
"Fredoka",
system-ui,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
Roboto,
"Helvetica Neue",
Arial,
"Noto Sans",
sans-serif,
"Apple Color Emoji",
"Segoe UI Emoji",
"Segoe UI Symbol",
"Noto Color Emoji";
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
h1, h2, h3, h4, h5, h6 {
font-weight: 400;
letter-spacing: -0.02em;
}
}
@@ -174,8 +189,49 @@
-ms-overflow-style: none; /* IE and Edge */
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: 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
bind:this={ref}
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],
TRANSITION_COLORS,
className,
)}
bind:value

View File

@@ -2,7 +2,6 @@
import { Select as SelectPrimitive, type WithoutChild } from "bits-ui";
import ChevronDown from "@lucide/svelte/icons/chevron-down";
import { cn } from "$lib/utils.js";
import { TRANSITION_COLORS } from "$lib/core/constants";
const inputSizes = {
sm: "px-3 py-2 text-sm file:text-sm",
@@ -27,13 +26,9 @@
<SelectPrimitive.Trigger
bind:ref
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 &&
"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],
TRANSITION_COLORS,
className,
)}
{...restProps}

View File

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

View File

@@ -24,73 +24,77 @@
});
</script>
<div class="flex flex-col gap-4 rounded-lg bg-white p-4 drop-shadow-lg md:p-8">
<Title size="h4" weight="medium">Payment Summary</Title>
<div class="h-0.5 w-full border-t-2 border-gray-200"></div>
<div class="flex flex-col gap-6">
<Title size="h4" weight="medium">Order Summary</Title>
{#if !calculating}
<!-- Product Information -->
{#if $productStore}
<div class="flex flex-col gap-2">
<Title size="p" weight="medium">{$productStore.title}</Title>
<p class="text-sm text-gray-600">{$productStore.description}</p>
<div class="flex flex-col gap-3 border-b border-gray-200 pb-6">
<Title size="p" weight="medium">
{$productStore.title}
</Title>
<p class="text-sm leading-relaxed text-gray-600">
{$productStore.description}
</p>
</div>
{/if}
<!-- Price Breakdown -->
<div class="mt-2 flex flex-col gap-2 border-t pt-4">
<div class="flex justify-between text-sm">
<span>Base Price</span>
<span>{convertAndFormatCurrency(priceDetails.basePrice)}</span>
<div class="flex flex-col gap-3 border-b border-gray-200 pb-6">
<div class="flex items-center justify-between text-sm">
<span class="text-gray-600">Base Price</span>
<span class="font-medium text-gray-900">
{convertAndFormatCurrency(priceDetails.basePrice)}
</span>
</div>
{#if priceDetails.discountAmount > 0}
<div class="flex justify-between text-sm text-green-600">
<span>Discount</span>
<span
>-{convertAndFormatCurrency(
priceDetails.discountAmount,
)}</span
>
<div class="flex items-center justify-between text-sm">
<span class="text-gray-600">Discount</span>
<span class="font-medium text-green-600">
-{convertAndFormatCurrency(priceDetails.discountAmount)}
</span>
</div>
<div class="flex justify-between text-sm">
<span>Display Price</span>
<span
>{convertAndFormatCurrency(
priceDetails.displayPrice,
)}</span
>
<div class="flex items-center justify-between text-sm">
<span class="text-gray-600">Display Price</span>
<span class="font-medium text-gray-900">
{convertAndFormatCurrency(priceDetails.displayPrice)}
</span>
</div>
{/if}
</div>
<!-- Final Total -->
<div class="mt-4 flex flex-col gap-2 border-t pt-4">
<div class="flex justify-between font-medium">
<Title size="h5" weight="medium"
>Total ({$currencyStore.code})</Title
>
<span class="text-lg"
>{convertAndFormatCurrency(priceDetails.orderPrice)}</span
>
<div class="flex items-center justify-between">
<span class="text-base font-semibold text-gray-900">
Total ({$currencyStore.code})
</span>
<span class="text-2xl font-bold text-gray-900">
{convertAndFormatCurrency(priceDetails.orderPrice)}
</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>
{:else}
<div class="grid place-items-center p-8 text-center">
<span class="text-gray-600">Calculating...</span>
<div class="grid place-items-center py-12">
<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>
{/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>

View File

@@ -1,13 +1,9 @@
<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 { svTrpcApiStore, trpcApiStore } from "$lib/stores/api";
import { sessionInfo, sessionUserInfo } from "$lib/stores/session.info";
import { trpc, trpcRaw } from "$lib/trpc/trpc";
import { onMount } from "svelte";
import UpIcon from "~icons/material-symbols/keyboard-double-arrow-up-rounded";
import type { LayoutData } from "./$types";
let {
@@ -23,18 +19,6 @@
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(() => {
trpcApiStore.set(trpcRaw());
@@ -42,32 +26,11 @@
setTimeout(() => {
currencyVM.getCurrencies();
}, 500);
// Add scroll event listener
window.addEventListener("scroll", handleScroll);
return () => {
window.removeEventListener("scroll", handleScroll);
};
});
</script>
<Navbar invert={false} />
<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">
<div class="min-h-screen w-full">
<main class="flex w-full flex-col">
{@render children?.()}
</main>
</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";
export const load: PageServerLoad = async ({ params }) => {
const pid = params.pageid;
return await getProductUseCases().getProductByLinkId(pid);
const plid = params.plid;
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>
<div class="grid h-full w-full place-items-center">
<MaxWidthWrapper cls="p-4 md:p-8 lg:p-10 3xl:p-0">
{#if !pageData.data || !!pageData.error}
<div class="grid h-full min-h-screen w-full place-items-center">
<div class="flex flex-col items-center justify-center gap-4">
<Icon icon={SearchIcon} cls="w-12 h-12" />
<Title size="h4" color="black">Product not found</Title>
<p>Something went wrong, please try again or contact us</p>
<div class="min-h-screen w-full bg-gradient-to-br from-gray-50 to-gray-100">
{#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="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" />
</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>
{:else if checkoutVM.checkoutStep === CheckoutStep.Confirmation}
<div class="grid w-full place-items-center p-4 py-32">
</div>
{: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 />
</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>
{:else}
<CheckoutStepsIndicator />
<div class="flex w-full flex-col gap-8 lg:flex-row">
<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}
<CheckoutLoadingSection />
{:else if checkoutVM.checkoutStep === CheckoutStep.Initial}
@@ -74,16 +99,33 @@
{/if}
</div>
</div>
<div
class="grid w-full place-items-center lg:max-w-lg lg:place-items-start"
>
<div
class="flex w-full flex-col gap-8 pt-8 md:max-w-md lg:max-w-full lg:pt-0"
>
<PaymentSummary />
<!-- Right Column: Summary (Sticky on larger screens) -->
<div class="lg:w-[400px] xl:w-[440px]">
<div class="lg:sticky lg:top-8">
<div class="rounded-2xl bg-white p-6 shadow-lg md:p-8">
<PaymentSummary />
</div>
</div>
</div>
</div>
{/if}
</MaxWidthWrapper>
</div>
{/if}
</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">
import Icon from "$lib/components/atoms/icon.svelte";
import Title from "$lib/components/atoms/title.svelte";
import MaxWidthWrapper from "$lib/components/molecules/max-width-wrapper.svelte";
import CheckIcon from "~icons/ic/round-check";
import CheckIcon from "~icons/heroicons/check-circle-20-solid";
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
</script>
<div class="grid min-h-[80vh] w-full place-items-center px-4 sm:px-6">
<MaxWidthWrapper
cls="flex flex-col gap-6 sm:gap-8 items-center justify-center"
>
<div
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="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 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">
<div class="w-full max-w-lg">
<div class="animate-scale-in rounded-3xl bg-white p-8 shadow-2xl md:p-12">
<!-- Success Icon with Animation -->
<div class="mb-8 flex justify-center">
<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>
<Title size="h3" center weight="medium">Booking confirmed</Title>
<!-- Title -->
<Title size="h3" weight="medium" center>Order Confirmed!</Title>
<p
class="w-full max-w-prose text-center text-sm text-gray-600 sm:text-base"
>
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.
<!-- Description -->
<p class="mb-6 mt-4 text-center text-base text-gray-600">
Thank you for your order! Your payment has been processed successfully.
</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>
</MaxWidthWrapper>
</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 Icon from "$lib/components/atoms/icon.svelte";
import Title from "$lib/components/atoms/title.svelte";
import MaxWidthWrapper from "$lib/components/molecules/max-width-wrapper.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);
@@ -18,25 +19,90 @@
});
</script>
<div class="grid min-h-[80vh] w-full place-items-center">
<MaxWidthWrapper
cls="flex flex-col gap-8 items-center justify-center p-4 md:p-8"
>
<div
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="rounded-full bg-rose-100 p-2 text-rose-600 drop-shadow-lg"
>
<Icon icon={CloseIcon} cls="w-12 h-12" />
<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">
<div class="w-full max-w-lg">
<div class="animate-scale-in rounded-3xl bg-white p-8 shadow-2xl md:p-12">
<!-- Error Icon with Animation -->
<div class="mb-8 flex justify-center">
<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>
<Title size="h3" center weight="medium">Session Terminated</Title>
<p class="w-full max-w-prose text-center text-gray-600">
Unfortunately, your session has been terminated due to inactivity
or something went wrong, please contact us to walk through the
steps again.
<!-- Title -->
<Title size="h3" weight="medium" center>Session Terminated</Title>
<!-- Description -->
<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>
</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>
</MaxWidthWrapper>
</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>