Init
135
src/app.css
Normal file
@@ -0,0 +1,135 @@
|
||||
@import url('$lib/assets/font/stylesheet.css');
|
||||
@import 'tailwindcss';
|
||||
|
||||
@import 'tw-animate-css';
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
:root {
|
||||
--radius: 0.625rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.145 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.145 0 0);
|
||||
--primary: oklch(49.872% 0.17259 23.047);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.97 0 0);
|
||||
--secondary-foreground: oklch(0.205 0 0);
|
||||
--muted: oklch(0.97 0 0);
|
||||
--muted-foreground: oklch(0.556 0 0);
|
||||
--accent: oklch(0.97 0 0);
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--ring: oklch(0.708 0 0);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.145 0 0);
|
||||
--sidebar-primary: oklch(0.205 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.97 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||
--sidebar-border: oklch(0.922 0 0);
|
||||
--sidebar-ring: oklch(0.708 0 0);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.145 0 0);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.205 0 0);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.205 0 0);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.922 0 0);
|
||||
--primary-foreground: oklch(0.205 0 0);
|
||||
--secondary: oklch(0.269 0 0);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.269 0 0);
|
||||
--muted-foreground: oklch(0.708 0 0);
|
||||
--accent: oklch(0.269 0 0);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.556 0 0);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.205 0 0);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.269 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.556 0 0);
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
font-family:
|
||||
'Euclid Circular B',
|
||||
system-ui,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
'Segoe UI',
|
||||
Roboto,
|
||||
Oxygen,
|
||||
Ubuntu,
|
||||
Cantarell,
|
||||
'Open Sans',
|
||||
'Helvetica Neue',
|
||||
sans-serif;
|
||||
}
|
||||
}
|
||||
20
src/app.d.ts
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
// See https://svelte.dev/docs/kit/types#app.d.ts
|
||||
// for information about these interfaces
|
||||
declare global {
|
||||
namespace App {
|
||||
// interface Error {}
|
||||
interface Locals {
|
||||
authenticated: boolean;
|
||||
user: {
|
||||
name: string;
|
||||
role: string;
|
||||
avatar: string;
|
||||
} | null;
|
||||
}
|
||||
// interface PageData {}
|
||||
// interface PageState {}
|
||||
// interface Platform {}
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
40
src/app.html
Normal file
@@ -0,0 +1,40 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<link rel="icon" type="image/png" href="%sveltekit.assets%/favicon-96x96.png" sizes="96x96" />
|
||||
<link rel="icon" type="image/svg+xml" href="%sveltekit.assets%/favicon.svg" />
|
||||
<link rel="shortcut icon" href="%sveltekit.assets%/favicon.ico" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="%sveltekit.assets%/apple-touch-icon.png" />
|
||||
<meta name="apple-mobile-web-app-title" content="Korean Skin Care" />
|
||||
<link rel="manifest" href="%sveltekit.assets%/site.webmanifest" />
|
||||
|
||||
%sveltekit.head%
|
||||
|
||||
<script>
|
||||
(function (w, d, s, l, i) {
|
||||
w[l] = w[l] || [];
|
||||
w[l].push({ 'gtm.start': new Date().getTime(), event: 'gtm.js' });
|
||||
var f = d.getElementsByTagName(s)[0],
|
||||
j = d.createElement(s),
|
||||
dl = l != 'dataLayer' ? '&l=' + l : '';
|
||||
j.async = true;
|
||||
j.src = 'https://www.googletagmanager.com/gtm.js?id=' + i + dl;
|
||||
f.parentNode.insertBefore(j, f);
|
||||
})(window, document, 'script', 'dataLayer', 'GTM-NV8J8K4N');
|
||||
</script>
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<noscript
|
||||
><iframe
|
||||
src="https://www.googletagmanager.com/ns.html?id=GTM-NV8J8K4N"
|
||||
height="0"
|
||||
width="0"
|
||||
style="display: none; visibility: hidden"
|
||||
></iframe
|
||||
></noscript>
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
166
src/hooks.server.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
import { VITE_BACKEND_URL } from '$env/static/private';
|
||||
import { endpoints } from '$lib/utils/api';
|
||||
import { redirect, type Handle, type HandleFetch } from '@sveltejs/kit';
|
||||
import { jwtDecode } from 'jwt-decode';
|
||||
|
||||
export const handleFetch: HandleFetch = async ({ request, fetch, event }) => {
|
||||
let newRequest = request;
|
||||
|
||||
const accessToken = event.cookies.get('accessToken');
|
||||
if (accessToken) {
|
||||
newRequest = new Request(request, {
|
||||
headers: {
|
||||
...request.headers,
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let response = await fetch(newRequest);
|
||||
|
||||
// Only attempt token refresh for 401 responses when we have an accessToken
|
||||
if (response.status === 401 && accessToken) {
|
||||
try {
|
||||
const refreshResponse = await fetch(`${VITE_BACKEND_URL}${endpoints.auth.rotateToken}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
});
|
||||
|
||||
if (refreshResponse.ok) {
|
||||
const { accessToken: newAccessToken } = await refreshResponse.json();
|
||||
event.cookies.set('accessToken', newAccessToken, {
|
||||
path: '/',
|
||||
httpOnly: true,
|
||||
sameSite: 'lax',
|
||||
secure: false,
|
||||
maxAge: 300
|
||||
});
|
||||
|
||||
newRequest = new Request(request, {
|
||||
headers: {
|
||||
...request.headers,
|
||||
Authorization: `Bearer ${newAccessToken}`
|
||||
}
|
||||
});
|
||||
response = await fetch(newRequest);
|
||||
} else {
|
||||
console.error('Refresh token failed:', await refreshResponse.text());
|
||||
event.cookies.delete('accessToken', { path: '/' });
|
||||
event.cookies.delete('u', { path: '/' });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Token refresh error:', error);
|
||||
event.cookies.delete('accessToken', { path: '/' });
|
||||
event.cookies.delete('u', { path: '/' });
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
};
|
||||
|
||||
export const handle: Handle = async ({ event, resolve }) => {
|
||||
const accessToken = event.cookies.get('accessToken');
|
||||
const userCookie = event.cookies.get('u');
|
||||
|
||||
// Initialize authentication state
|
||||
event.locals.authenticated = false;
|
||||
event.locals.user = null;
|
||||
|
||||
const refreshTokens = async () => {
|
||||
if (!accessToken) return null;
|
||||
|
||||
const response = await fetch(`${VITE_BACKEND_URL}${endpoints.auth.rotateToken}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to refresh tokens');
|
||||
}
|
||||
|
||||
const { accessToken: newAccessToken, user } = await response.json();
|
||||
|
||||
event.cookies.set('accessToken', newAccessToken, {
|
||||
path: '/',
|
||||
httpOnly: true,
|
||||
sameSite: 'lax',
|
||||
secure: false
|
||||
});
|
||||
|
||||
if (user) {
|
||||
event.cookies.set('u', JSON.stringify(user), {
|
||||
path: '/',
|
||||
httpOnly: true,
|
||||
sameSite: 'lax',
|
||||
secure: false
|
||||
});
|
||||
return user;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
try {
|
||||
if (accessToken) {
|
||||
// Check token expiration
|
||||
const decodedToken: { exp: number } = jwtDecode(accessToken);
|
||||
const currentTime = Date.now() / 1000;
|
||||
|
||||
// Refresh token if it's about to expire (within 2 minutes)
|
||||
if (decodedToken.exp - currentTime < 120) {
|
||||
const user = await refreshTokens();
|
||||
if (user) {
|
||||
event.locals.user = user;
|
||||
}
|
||||
}
|
||||
|
||||
// Set user from cookie if available
|
||||
if (userCookie) {
|
||||
try {
|
||||
event.locals.user = JSON.parse(userCookie);
|
||||
} catch (error) {
|
||||
console.error('Invalid user cookie:', error);
|
||||
event.cookies.delete('u', { path: '/' });
|
||||
}
|
||||
}
|
||||
|
||||
event.locals.authenticated = true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Authentication error:', error);
|
||||
event.cookies.delete('accessToken', { path: '/' });
|
||||
event.cookies.delete('u', { path: '/' });
|
||||
}
|
||||
|
||||
// Handle route protection
|
||||
const routeId = event.route.id;
|
||||
|
||||
// Only check authentication for routes under /(auth)
|
||||
if (routeId?.startsWith('/(auth)')) {
|
||||
if (!event.locals.authenticated) {
|
||||
throw redirect(302, '/signin');
|
||||
}
|
||||
}
|
||||
|
||||
// Redirect authenticated users away from auth pages
|
||||
if (
|
||||
event.locals.authenticated &&
|
||||
(event.url.pathname === '/signin' || event.url.pathname === '/register')
|
||||
) {
|
||||
throw redirect(303, '/');
|
||||
}
|
||||
|
||||
const response = await resolve(event);
|
||||
|
||||
if (routeId?.startsWith('/(auth)')) {
|
||||
response.headers.set('X-Auth-Check', 'required');
|
||||
}
|
||||
|
||||
return response;
|
||||
};
|
||||
27
src/lib/actions/swiper.svelte.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import Swiper from 'swiper';
|
||||
import { Autoplay, Thumbs } from 'swiper/modules';
|
||||
import 'swiper/swiper-bundle.css';
|
||||
import type { CSSSelector, SwiperOptions } from 'swiper/types';
|
||||
|
||||
export function swiper(node: CSSSelector | HTMLElement, options = {}) {
|
||||
let swiperInstance: Swiper;
|
||||
$effect(() => {
|
||||
swiperInstance = new Swiper(node, {
|
||||
modules: [Autoplay, Thumbs],
|
||||
...options
|
||||
});
|
||||
|
||||
return () => {
|
||||
swiperInstance.destroy();
|
||||
};
|
||||
});
|
||||
return {
|
||||
update(newOptions: SwiperOptions | undefined) {
|
||||
swiperInstance.destroy();
|
||||
swiperInstance = new Swiper(node, {
|
||||
modules: [Autoplay, Thumbs],
|
||||
...newOptions
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
1
src/lib/assets/favicon.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="107" height="128" viewBox="0 0 107 128"><title>svelte-logo</title><path d="M94.157 22.819c-10.4-14.885-30.94-19.297-45.792-9.835L22.282 29.608A29.92 29.92 0 0 0 8.764 49.65a31.5 31.5 0 0 0 3.108 20.231 30 30 0 0 0-4.477 11.183 31.9 31.9 0 0 0 5.448 24.116c10.402 14.887 30.942 19.297 45.791 9.835l26.083-16.624A29.92 29.92 0 0 0 98.235 78.35a31.53 31.53 0 0 0-3.105-20.232 30 30 0 0 0 4.474-11.182 31.88 31.88 0 0 0-5.447-24.116" style="fill:#ff3e00"/><path d="M45.817 106.582a20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.503 18 18 0 0 1 .624-2.435l.49-1.498 1.337.981a33.6 33.6 0 0 0 10.203 5.098l.97.294-.09.968a5.85 5.85 0 0 0 1.052 3.878 6.24 6.24 0 0 0 6.695 2.485 5.8 5.8 0 0 0 1.603-.704L69.27 76.28a5.43 5.43 0 0 0 2.45-3.631 5.8 5.8 0 0 0-.987-4.371 6.24 6.24 0 0 0-6.698-2.487 5.7 5.7 0 0 0-1.6.704l-9.953 6.345a19 19 0 0 1-5.296 2.326 20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.502 17.99 17.99 0 0 1 8.13-12.052l26.081-16.623a19 19 0 0 1 5.3-2.329 20.72 20.72 0 0 1 22.237 8.243 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-.624 2.435l-.49 1.498-1.337-.98a33.6 33.6 0 0 0-10.203-5.1l-.97-.294.09-.968a5.86 5.86 0 0 0-1.052-3.878 6.24 6.24 0 0 0-6.696-2.485 5.8 5.8 0 0 0-1.602.704L37.73 51.72a5.42 5.42 0 0 0-2.449 3.63 5.79 5.79 0 0 0 .986 4.372 6.24 6.24 0 0 0 6.698 2.486 5.8 5.8 0 0 0 1.602-.704l9.952-6.342a19 19 0 0 1 5.295-2.328 20.72 20.72 0 0 1 22.237 8.242 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-8.13 12.053l-26.081 16.622a19 19 0 0 1-5.3 2.328" style="fill:#fff"/></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
BIN
src/lib/assets/font/EuclidCircularB-Bold.eot
Normal file
BIN
src/lib/assets/font/EuclidCircularB-Bold.ttf
Normal file
BIN
src/lib/assets/font/EuclidCircularB-Bold.woff
Normal file
BIN
src/lib/assets/font/EuclidCircularB-Bold.woff2
Normal file
BIN
src/lib/assets/font/EuclidCircularB-BoldItalic.eot
Normal file
BIN
src/lib/assets/font/EuclidCircularB-BoldItalic.ttf
Normal file
BIN
src/lib/assets/font/EuclidCircularB-BoldItalic.woff
Normal file
BIN
src/lib/assets/font/EuclidCircularB-BoldItalic.woff2
Normal file
BIN
src/lib/assets/font/EuclidCircularB-Italic.eot
Normal file
BIN
src/lib/assets/font/EuclidCircularB-Italic.ttf
Normal file
BIN
src/lib/assets/font/EuclidCircularB-Italic.woff
Normal file
BIN
src/lib/assets/font/EuclidCircularB-Italic.woff2
Normal file
BIN
src/lib/assets/font/EuclidCircularB-Light.eot
Normal file
BIN
src/lib/assets/font/EuclidCircularB-Light.ttf
Normal file
BIN
src/lib/assets/font/EuclidCircularB-Light.woff
Normal file
BIN
src/lib/assets/font/EuclidCircularB-Light.woff2
Normal file
BIN
src/lib/assets/font/EuclidCircularB-LightItalic.eot
Normal file
BIN
src/lib/assets/font/EuclidCircularB-LightItalic.ttf
Normal file
BIN
src/lib/assets/font/EuclidCircularB-LightItalic.woff
Normal file
BIN
src/lib/assets/font/EuclidCircularB-LightItalic.woff2
Normal file
BIN
src/lib/assets/font/EuclidCircularB-Medium.eot
Normal file
BIN
src/lib/assets/font/EuclidCircularB-Medium.ttf
Normal file
BIN
src/lib/assets/font/EuclidCircularB-Medium.woff
Normal file
BIN
src/lib/assets/font/EuclidCircularB-Medium.woff2
Normal file
BIN
src/lib/assets/font/EuclidCircularB-MediumItalic.eot
Normal file
BIN
src/lib/assets/font/EuclidCircularB-MediumItalic.ttf
Normal file
BIN
src/lib/assets/font/EuclidCircularB-MediumItalic.woff
Normal file
BIN
src/lib/assets/font/EuclidCircularB-MediumItalic.woff2
Normal file
BIN
src/lib/assets/font/EuclidCircularB-Regular.eot
Normal file
BIN
src/lib/assets/font/EuclidCircularB-Regular.ttf
Normal file
BIN
src/lib/assets/font/EuclidCircularB-Regular.woff
Normal file
BIN
src/lib/assets/font/EuclidCircularB-Regular.woff2
Normal file
BIN
src/lib/assets/font/EuclidCircularB-SemiBold.eot
Normal file
BIN
src/lib/assets/font/EuclidCircularB-SemiBold.ttf
Normal file
BIN
src/lib/assets/font/EuclidCircularB-SemiBold.woff
Normal file
BIN
src/lib/assets/font/EuclidCircularB-SemiBold.woff2
Normal file
BIN
src/lib/assets/font/EuclidCircularB-SemiBoldItalic.eot
Normal file
BIN
src/lib/assets/font/EuclidCircularB-SemiBoldItalic.ttf
Normal file
BIN
src/lib/assets/font/EuclidCircularB-SemiBoldItalic.woff
Normal file
BIN
src/lib/assets/font/EuclidCircularB-SemiBoldItalic.woff2
Normal file
120
src/lib/assets/font/stylesheet.css
Normal file
@@ -0,0 +1,120 @@
|
||||
@font-face {
|
||||
font-family: 'Euclid Circular B';
|
||||
src: url('EuclidCircularB-Bold.eot');
|
||||
src: local('Euclid Circular B Bold'), local('EuclidCircularB-Bold'),
|
||||
url('EuclidCircularB-Bold.eot?#iefix') format('embedded-opentype'),
|
||||
url('EuclidCircularB-Bold.woff2') format('woff2'),
|
||||
url('EuclidCircularB-Bold.woff') format('woff'),
|
||||
url('EuclidCircularB-Bold.ttf') format('truetype');
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Euclid Circular B';
|
||||
src: url('EuclidCircularB-Italic.eot');
|
||||
src: local('Euclid Circular B Italic'), local('EuclidCircularB-Italic'),
|
||||
url('EuclidCircularB-Italic.eot?#iefix') format('embedded-opentype'),
|
||||
url('EuclidCircularB-Italic.woff2') format('woff2'),
|
||||
url('EuclidCircularB-Italic.woff') format('woff'),
|
||||
url('EuclidCircularB-Italic.ttf') format('truetype');
|
||||
font-weight: normal;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Euclid Circular B';
|
||||
src: url('EuclidCircularB-Medium.eot');
|
||||
src: local('Euclid Circular B Medium'), local('EuclidCircularB-Medium'),
|
||||
url('EuclidCircularB-Medium.eot?#iefix') format('embedded-opentype'),
|
||||
url('EuclidCircularB-Medium.woff2') format('woff2'),
|
||||
url('EuclidCircularB-Medium.woff') format('woff'),
|
||||
url('EuclidCircularB-Medium.ttf') format('truetype');
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Euclid Circular B';
|
||||
src: url('EuclidCircularB-SemiBoldItalic.eot');
|
||||
src: local('Euclid Circular B SemiBold Italic'), local('EuclidCircularB-SemiBoldItalic'),
|
||||
url('EuclidCircularB-SemiBoldItalic.eot?#iefix') format('embedded-opentype'),
|
||||
url('EuclidCircularB-SemiBoldItalic.woff2') format('woff2'),
|
||||
url('EuclidCircularB-SemiBoldItalic.woff') format('woff'),
|
||||
url('EuclidCircularB-SemiBoldItalic.ttf') format('truetype');
|
||||
font-weight: 600;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Euclid Circular B';
|
||||
src: url('EuclidCircularB-BoldItalic.eot');
|
||||
src: local('Euclid Circular B Bold Italic'), local('EuclidCircularB-BoldItalic'),
|
||||
url('EuclidCircularB-BoldItalic.eot?#iefix') format('embedded-opentype'),
|
||||
url('EuclidCircularB-BoldItalic.woff2') format('woff2'),
|
||||
url('EuclidCircularB-BoldItalic.woff') format('woff'),
|
||||
url('EuclidCircularB-BoldItalic.ttf') format('truetype');
|
||||
font-weight: bold;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Euclid Circular B';
|
||||
src: url('EuclidCircularB-MediumItalic.eot');
|
||||
src: local('Euclid Circular B Medium Italic'), local('EuclidCircularB-MediumItalic'),
|
||||
url('EuclidCircularB-MediumItalic.eot?#iefix') format('embedded-opentype'),
|
||||
url('EuclidCircularB-MediumItalic.woff2') format('woff2'),
|
||||
url('EuclidCircularB-MediumItalic.woff') format('woff'),
|
||||
url('EuclidCircularB-MediumItalic.ttf') format('truetype');
|
||||
font-weight: 500;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Euclid Circular B';
|
||||
src: url('EuclidCircularB-SemiBold.eot');
|
||||
src: local('Euclid Circular B SemiBold'), local('EuclidCircularB-SemiBold'),
|
||||
url('EuclidCircularB-SemiBold.eot?#iefix') format('embedded-opentype'),
|
||||
url('EuclidCircularB-SemiBold.woff2') format('woff2'),
|
||||
url('EuclidCircularB-SemiBold.woff') format('woff'),
|
||||
url('EuclidCircularB-SemiBold.ttf') format('truetype');
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Euclid Circular B';
|
||||
src: url('EuclidCircularB-Light.eot');
|
||||
src: local('Euclid Circular B Light'), local('EuclidCircularB-Light'),
|
||||
url('EuclidCircularB-Light.eot?#iefix') format('embedded-opentype'),
|
||||
url('EuclidCircularB-Light.woff2') format('woff2'),
|
||||
url('EuclidCircularB-Light.woff') format('woff'),
|
||||
url('EuclidCircularB-Light.ttf') format('truetype');
|
||||
font-weight: 300;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Euclid Circular B';
|
||||
src: url('EuclidCircularB-Regular.eot');
|
||||
src: local('Euclid Circular B Regular'), local('EuclidCircularB-Regular'),
|
||||
url('EuclidCircularB-Regular.eot?#iefix') format('embedded-opentype'),
|
||||
url('EuclidCircularB-Regular.woff2') format('woff2'),
|
||||
url('EuclidCircularB-Regular.woff') format('woff'),
|
||||
url('EuclidCircularB-Regular.ttf') format('truetype');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Euclid Circular B';
|
||||
src: url('EuclidCircularB-LightItalic.eot');
|
||||
src: local('Euclid Circular B Light Italic'), local('EuclidCircularB-LightItalic'),
|
||||
url('EuclidCircularB-LightItalic.eot?#iefix') format('embedded-opentype'),
|
||||
url('EuclidCircularB-LightItalic.woff2') format('woff2'),
|
||||
url('EuclidCircularB-LightItalic.woff') format('woff'),
|
||||
url('EuclidCircularB-LightItalic.ttf') format('truetype');
|
||||
font-weight: 300;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
104
src/lib/assets/img/Google_Favicon_2025.svg
Normal file
@@ -0,0 +1,104 @@
|
||||
<svg version="1.1" viewBox="0 0 268.1522 273.8827" overflow="hidden" xml:space="preserve" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="a">
|
||||
<stop offset="0" stop-color="#0fbc5c"/>
|
||||
<stop offset="1" stop-color="#0cba65"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="g">
|
||||
<stop offset=".2312727" stop-color="#0fbc5f"/>
|
||||
<stop offset=".3115468" stop-color="#0fbc5f"/>
|
||||
<stop offset=".3660131" stop-color="#0fbc5e"/>
|
||||
<stop offset=".4575163" stop-color="#0fbc5d"/>
|
||||
<stop offset=".540305" stop-color="#12bc58"/>
|
||||
<stop offset=".6993464" stop-color="#28bf3c"/>
|
||||
<stop offset=".7712418" stop-color="#38c02b"/>
|
||||
<stop offset=".8605665" stop-color="#52c218"/>
|
||||
<stop offset=".9150327" stop-color="#67c30f"/>
|
||||
<stop offset="1" stop-color="#86c504"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="h">
|
||||
<stop offset=".1416122" stop-color="#1abd4d"/>
|
||||
<stop offset=".2475151" stop-color="#6ec30d"/>
|
||||
<stop offset=".3115468" stop-color="#8ac502"/>
|
||||
<stop offset=".3660131" stop-color="#a2c600"/>
|
||||
<stop offset=".4456735" stop-color="#c8c903"/>
|
||||
<stop offset=".540305" stop-color="#ebcb03"/>
|
||||
<stop offset=".6156363" stop-color="#f7cd07"/>
|
||||
<stop offset=".6993454" stop-color="#fdcd04"/>
|
||||
<stop offset=".7712418" stop-color="#fdce05"/>
|
||||
<stop offset=".8605661" stop-color="#ffce0a"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="f">
|
||||
<stop offset=".3159041" stop-color="#ff4c3c"/>
|
||||
<stop offset=".6038179" stop-color="#ff692c"/>
|
||||
<stop offset=".7268366" stop-color="#ff7825"/>
|
||||
<stop offset=".884534" stop-color="#ff8d1b"/>
|
||||
<stop offset="1" stop-color="#ff9f13"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="b">
|
||||
<stop offset=".2312727" stop-color="#ff4541"/>
|
||||
<stop offset=".3115468" stop-color="#ff4540"/>
|
||||
<stop offset=".4575163" stop-color="#ff4640"/>
|
||||
<stop offset=".540305" stop-color="#ff473f"/>
|
||||
<stop offset=".6993464" stop-color="#ff5138"/>
|
||||
<stop offset=".7712418" stop-color="#ff5b33"/>
|
||||
<stop offset=".8605665" stop-color="#ff6c29"/>
|
||||
<stop offset="1" stop-color="#ff8c18"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="d">
|
||||
<stop offset=".4084578" stop-color="#fb4e5a"/>
|
||||
<stop offset="1" stop-color="#ff4540"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="c">
|
||||
<stop offset=".1315461" stop-color="#0cba65"/>
|
||||
<stop offset=".2097843" stop-color="#0bb86d"/>
|
||||
<stop offset=".2972969" stop-color="#09b479"/>
|
||||
<stop offset=".3962575" stop-color="#08ad93"/>
|
||||
<stop offset=".4771242" stop-color="#0aa6a9"/>
|
||||
<stop offset=".5684245" stop-color="#0d9cc6"/>
|
||||
<stop offset=".667385" stop-color="#1893dd"/>
|
||||
<stop offset=".7687273" stop-color="#258bf1"/>
|
||||
<stop offset=".8585063" stop-color="#3086ff"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="e">
|
||||
<stop offset=".3660131" stop-color="#ff4e3a"/>
|
||||
<stop offset=".4575163" stop-color="#ff8a1b"/>
|
||||
<stop offset=".540305" stop-color="#ffa312"/>
|
||||
<stop offset=".6156363" stop-color="#ffb60c"/>
|
||||
<stop offset=".7712418" stop-color="#ffcd0a"/>
|
||||
<stop offset=".8605665" stop-color="#fecf0a"/>
|
||||
<stop offset=".9150327" stop-color="#fecf08"/>
|
||||
<stop offset="1" stop-color="#fdcd01"/>
|
||||
</linearGradient>
|
||||
<linearGradient xlink:href="#a" id="s" x1="219.6997" y1="329.5351" x2="254.4673" y2="329.5351" gradientUnits="userSpaceOnUse"/>
|
||||
<radialGradient xlink:href="#b" id="m" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-1.936885,1.043001,1.455731,2.555422,290.5254,-400.6338)" cx="109.6267" cy="135.8619" fx="109.6267" fy="135.8619" r="71.46001"/>
|
||||
<radialGradient xlink:href="#c" id="n" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-3.512595,-4.45809,-1.692547,1.260616,870.8006,191.554)" cx="45.25866" cy="279.2738" fx="45.25866" fy="279.2738" r="71.46001"/>
|
||||
<radialGradient xlink:href="#d" id="l" cx="304.0166" cy="118.0089" fx="304.0166" fy="118.0089" r="47.85445" gradientTransform="matrix(2.064353,-4.926832e-6,-2.901531e-6,2.592041,-297.6788,-151.7469)" gradientUnits="userSpaceOnUse"/>
|
||||
<radialGradient xlink:href="#e" id="o" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-0.2485783,2.083138,2.962486,0.3341668,-255.1463,-331.1636)" cx="181.001" cy="177.2013" fx="181.001" fy="177.2013" r="71.46001"/>
|
||||
<radialGradient xlink:href="#f" id="p" cx="207.6733" cy="108.0972" fx="207.6733" fy="108.0972" r="41.1025" gradientTransform="matrix(-1.249206,1.343263,-3.896837,-3.425693,880.5011,194.9051)" gradientUnits="userSpaceOnUse"/>
|
||||
<radialGradient xlink:href="#g" id="r" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-1.936885,-1.043001,1.455731,-2.555422,290.5254,838.6834)" cx="109.6267" cy="135.8619" fx="109.6267" fy="135.8619" r="71.46001"/>
|
||||
<radialGradient xlink:href="#h" id="j" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-0.081402,-1.93722,2.926737,-0.1162508,-215.1345,632.8606)" cx="154.8697" cy="145.9691" fx="154.8697" fy="145.9691" r="71.46001"/>
|
||||
<filter id="q" x="-.04842873" y="-.0582241" width="1.096857" height="1.116448" color-interpolation-filters="sRGB">
|
||||
<feGaussianBlur stdDeviation="1.700914"/>
|
||||
</filter>
|
||||
<filter id="k" x="-.01670084" y="-.01009856" width="1.033402" height="1.020197" color-interpolation-filters="sRGB">
|
||||
<feGaussianBlur stdDeviation=".2419367"/>
|
||||
</filter>
|
||||
<clipPath clipPathUnits="userSpaceOnUse" id="i">
|
||||
<path d="M371.3784 193.2406H237.0825v53.4375h77.167c-1.2405 7.5627-4.0259 15.0024-8.1049 21.7862-4.6734 7.7723-10.4511 13.6895-16.373 18.1957-17.7389 13.4983-38.42 16.2584-52.7828 16.2584-36.2824 0-67.2833-23.2865-79.2844-54.9287-.4843-1.1482-.8059-2.3344-1.1975-3.5068-2.652-8.0533-4.101-16.5825-4.101-25.4474 0-9.226 1.5691-18.0575 4.4301-26.3985 11.2851-32.8967 42.9849-57.4674 80.1789-57.4674 7.4811 0 14.6854.8843 21.5173 2.6481 15.6135 4.0309 26.6578 11.9698 33.4252 18.2494l40.834-39.7111c-24.839-22.616-57.2194-36.3201-95.8444-36.3201-30.8782-.00066-59.3863 9.55308-82.7477 25.6992-18.9454 13.0941-34.4833 30.6254-44.9695 50.9861-9.75366 18.8785-15.09441 39.7994-15.09441 62.2934 0 22.495 5.34891 43.6334 15.10261 62.3374v.126c10.3023 19.8567 25.3678 36.9537 43.6783 49.9878 15.9962 11.3866 44.6789 26.5516 84.0307 26.5516 22.6301 0 42.6867-4.0517 60.3748-11.6447 12.76-5.4775 24.0655-12.6217 34.3012-21.8036 13.5247-12.1323 24.1168-27.1388 31.3465-44.4041 7.2297-17.2654 11.097-36.7895 11.097-57.957 0-9.858-.9971-19.8694-2.6881-28.9684Z" fill="#000"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<g transform="matrix(0.957922,0,0,0.985255,-90.17436,-78.85577)">
|
||||
<g clip-path="url(#i)">
|
||||
<path d="M92.07563 219.9585c.14844 22.14 6.5014 44.983 16.11767 63.4234v.1269c6.9482 13.3919 16.4444 23.9704 27.2604 34.4518l65.326-23.67c-12.3593-6.2344-14.2452-10.0546-23.1048-17.0253-9.0537-9.0658-15.8015-19.4735-20.0038-31.677h-.1693l.1693-.1269c-2.7646-8.0587-3.0373-16.6129-3.1393-25.5029Z" fill="url(#j)" filter="url(#k)"/>
|
||||
<path d="M237.0835 79.02491c-6.4568 22.52569-3.988 44.42139 0 57.16129 7.4561.0055 14.6388.8881 21.4494 2.6464 15.6135 4.0309 26.6566 11.97 33.424 18.2496l41.8794-40.7256c-24.8094-22.58904-54.6663-37.2961-96.7528-37.33169Z" fill="url(#l)" filter="url(#k)"/>
|
||||
<path d="M236.9434 78.84678c-31.6709-.00068-60.9107 9.79833-84.8718 26.35902-8.8968 6.149-17.0612 13.2521-24.3311 21.1509-1.9045 17.7429 14.2569 39.5507 46.2615 39.3702 15.5284-17.9373 38.4946-29.5427 64.0561-29.5427.0233 0 .046.0019.0693.002l-1.0439-57.33536c-.0472-.00003-.0929-.00406-.1401-.00406Z" fill="url(#m)" filter="url(#k)"/>
|
||||
<path d="m341.4751 226.3788-28.2685 19.2848c-1.2405 7.5627-4.0278 15.0023-8.1068 21.7861-4.6734 7.7723-10.4506 13.6898-16.3725 18.196-17.7022 13.4704-38.3286 16.2439-52.6877 16.2553-14.8415 25.1018-17.4435 37.6749 1.0439 57.9342 22.8762-.0167 43.157-4.1174 61.0458-11.7965 12.9312-5.551 24.3879-12.7913 34.7609-22.0964 13.7061-12.295 24.4421-27.5034 31.7688-45.0003 7.3267-17.497 11.2446-37.2822 11.2446-58.7336Z" fill="url(#n)" filter="url(#k)"/>
|
||||
<path d="M234.9956 191.2104v57.4981h136.0062c1.1962-7.8745 5.1523-18.0644 5.1523-26.5001 0-9.858-.9963-21.899-2.6873-30.998Z" fill="#3086ff" filter="url(#k)"/>
|
||||
<path d="M128.3894 124.3268c-8.393 9.1191-15.5632 19.326-21.2483 30.3646-9.75351 18.8785-15.09402 41.8295-15.09402 64.3235 0 .317.02642.6271.02855.9436 4.31953 8.2244 59.66647 6.6495 62.45617 0-.0035-.3103-.0387-.6128-.0387-.9238 0-9.226 1.5696-16.0262 4.4306-24.3672 3.5294-10.2885 9.0557-19.7628 16.1223-27.9257 1.6019-2.0309 5.8748-6.3969 7.1214-9.0157.4749-.9975-.8621-1.5574-.9369-1.9085-.0836-.3927-1.8762-.0769-2.2778-.3694-1.2751-.9288-3.8001-1.4138-5.3334-1.8449-3.2772-.9215-8.7085-2.9536-11.7252-5.0601-9.5357-6.6586-24.417-14.6122-33.5047-24.2164Z" fill="url(#o)" filter="url(#k)"/>
|
||||
<path d="M162.0989 155.8569c22.1123 13.3013 28.4714-6.7139 43.173-12.9771L179.698 90.21568c-9.4075 3.92642-18.2957 8.80465-26.5426 14.50442-12.316 8.5122-23.192 18.8995-32.1763 30.7204Z" fill="url(#p)" filter="url(#q)"/>
|
||||
<path d="M171.0987 290.222c-29.6829 10.6413-34.3299 11.023-37.0622 29.2903 5.2213 5.0597 10.8312 9.74 16.7926 13.9835 15.9962 11.3867 46.766 26.5517 86.1178 26.5517.0462 0 .0904-.004.1366-.004v-59.1574c-.0298.0001-.064.002-.0938.002-14.7359 0-26.5113-3.8435-38.5848-10.5273-2.9768-1.6479-8.3775 2.7772-11.1229.799-3.7865-2.7284-12.8991 2.3508-16.1833-.9378Z" fill="url(#r)" filter="url(#k)"/>
|
||||
<path d="M219.6997 299.0227v59.9959c5.506.6402 11.2361 1.0289 17.2472 1.0289 6.0259 0 11.8556-.3073 17.5204-.8723v-59.7481c-6.3482 1.0777-12.3272 1.461-17.4776 1.461-5.9318 0-11.7005-.6858-17.29-1.8654Z" opacity=".5" fill="url(#s)" filter="url(#k)"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 9.6 KiB |
BIN
src/lib/assets/img/actress/malika.png
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
BIN
src/lib/assets/img/logo/full_logo.png
Normal file
|
After Width: | Height: | Size: 213 KiB |
BIN
src/lib/assets/img/logo/full_logo_white.png
Normal file
|
After Width: | Height: | Size: 99 KiB |
BIN
src/lib/assets/img/logo/logo_mark._whitepng.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
src/lib/assets/img/logo/logo_mark.png
Normal file
|
After Width: | Height: | Size: 94 KiB |
BIN
src/lib/assets/img/meta.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
src/lib/assets/img/slider/image.png
Normal file
|
After Width: | Height: | Size: 3.9 MiB |
34
src/lib/components/Extra/QuantityCounter.svelte
Normal file
@@ -0,0 +1,34 @@
|
||||
<script lang="ts">
|
||||
// let quantity = $state(0);
|
||||
let { quantity = $bindable(1) } = $props();
|
||||
|
||||
$inspect(quantity);
|
||||
|
||||
$effect(() => {
|
||||
if (!quantity || quantity < 1) {
|
||||
quantity = 1;
|
||||
}
|
||||
});
|
||||
|
||||
function increment() {
|
||||
quantity++;
|
||||
}
|
||||
|
||||
function decrement() {
|
||||
if (quantity > 1) {
|
||||
quantity -= 1;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex w-min items-center gap-1">
|
||||
<button class="rounded-sm bg-primary px-2.5 text-lg text-white" onclick={decrement}>-</button>
|
||||
<!-- bind:value={quantity} -->
|
||||
<input
|
||||
type="text"
|
||||
value={quantity}
|
||||
name="quantity"
|
||||
class="h-7 w-7 rounded-md border border-gray-300 bg-white text-center"
|
||||
/>
|
||||
<button class="rounded-sm bg-primary px-2.5 text-lg text-white" onclick={increment}>+</button>
|
||||
</div>
|
||||
101
src/lib/components/layouts/Footer.svelte
Normal file
@@ -0,0 +1,101 @@
|
||||
<script>
|
||||
import fullLogo from '$lib/assets/img/logo/full_logo.png';
|
||||
import { Phone, MapPin, Mail } from '@lucide/svelte';
|
||||
import { SIIcon } from '@willingtonortiz/svelte-simple-icons';
|
||||
import { siFacebook, siInstagram, siTiktok } from 'simple-icons';
|
||||
</script>
|
||||
|
||||
<foote class="mx-auto w-full bg-primary/10 px-12 pt-10 pb-4 max-md:px-4">
|
||||
<div
|
||||
class="grid w-full grid-cols-4 items-start justify-between gap-12 max-md:flex max-md:flex-col max-md:gap-4"
|
||||
>
|
||||
<div>
|
||||
<div class="flex flex-col gap-4">
|
||||
<img src={fullLogo} alt="Korean Skin Care Logo" class="w-48" />
|
||||
<p class="text-sm">
|
||||
Korean Skin Care & Beauty Clinic in Kathmandu, Nepal offers advanced facials, acne scar
|
||||
removal, skin whitening, and anti-aging treatments. Our licensed dermatologist and expert
|
||||
team combine Korean skincare science with personalized care to deliver safe, clinically
|
||||
proven results. Trusted by Nepal’s top celebrities, we help clients rejuvenate, restore,
|
||||
and achieve glowing, youthful skin with world-class skincare solutions.
|
||||
</p>
|
||||
<div class="flex flex-col gap-2">
|
||||
<a href="tel:9849365761" class="flex items-center gap-2">
|
||||
<Phone size={20} strokeWidth={1.5} color="#AF272F" />
|
||||
<p class="text-sm">(+977) 984-936-5761 / 01-4011568</p>
|
||||
</a>
|
||||
<a href="https://maps.app.goo.gl/Gx2N5XvhZMqwpmFa8" class="flex items-center gap-2">
|
||||
<MapPin size={20} strokeWidth={1.5} color="#AF272F" />
|
||||
<p class="text-sm">City Center, First Floor, Kathmandu 44600, Nepal</p>
|
||||
</a>
|
||||
<a href="mailto:kscclinicnepal@gmail.com" class="flex items-center gap-2">
|
||||
<Mail size={20} strokeWidth={1.5} color="#AF272F" />
|
||||
<p class="text-sm">kscclinicnepal@gmail.com</p>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<div class="flex flex-col gap-2">
|
||||
<p class="text-lg font-semibold tracking-tight">About KSCBC</p>
|
||||
<ul class="flex flex-col gap-2">
|
||||
<li>
|
||||
<a href="/about-us" class="text-sm">About us</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/about-us" class="text-sm">My Account</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/about-us" class="text-sm">Privacy & Policy</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/contact-us" class="text-sm">Contact Us</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<div class="flex flex-col gap-2">
|
||||
<p class="text-lg font-semibold tracking-tight">Customer Support</p>
|
||||
<ul class="flex flex-col gap-2">
|
||||
<li>
|
||||
<a href="/faq" class="text-sm">F.A.Q</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/exchange-policy" class="text-sm">Refund and Exchange Policy</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/payment" class="text-sm">Payment Information</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/shipping" class="text-sm">Shipping and Delivery</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div></div>
|
||||
</div>
|
||||
<hr class="mt-6 border border-primary/10" />
|
||||
<div
|
||||
class="mt-4 flex items-center justify-between max-md:flex-col max-md:items-start max-md:gap-4"
|
||||
>
|
||||
<p class="text-sm">© {new Date().getFullYear()} Korean Skin Care & Beauty Clinic</p>
|
||||
<ul class="flex items-center gap-4">
|
||||
<li>
|
||||
<a href="https://www.facebook.com/koreanskincareclinic/" target="_blank">
|
||||
<SIIcon icon={siFacebook} size={16} color={'#0c0a09'} />
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://www.instagram.com/korean_skincare_nepal_/" target="_blank">
|
||||
<SIIcon icon={siInstagram} size={16} color={'#0c0a09'} />
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://www.tiktok.com/@koreanskincareclinic_np" target="_blank">
|
||||
<SIIcon icon={siTiktok} size={16} color={'#0c0a09'} />
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</foote>
|
||||
236
src/lib/components/layouts/Navbar.svelte
Normal file
@@ -0,0 +1,236 @@
|
||||
<script lang="ts">
|
||||
import { tick } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { enhance } from '$app/forms';
|
||||
|
||||
import {
|
||||
ShoppingBag,
|
||||
CircleUserRound,
|
||||
Search,
|
||||
SlidersHorizontal,
|
||||
LogOut,
|
||||
UserRound
|
||||
} from '@lucide/svelte';
|
||||
import logo from '$lib/assets/img/logo/full_logo.png?enhanced';
|
||||
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as Sheet from '$lib/components/ui/sheet/index.js';
|
||||
import { ScrollArea } from '$lib/components/ui/scroll-area/index.js';
|
||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
|
||||
import * as NavigationMenu from '$lib/components/ui/navigation-menu/index.js';
|
||||
|
||||
const languages = [
|
||||
{
|
||||
value: 'English',
|
||||
label: 'English',
|
||||
code: 'Us'
|
||||
},
|
||||
{
|
||||
value: 'Nepali',
|
||||
label: 'Nepali',
|
||||
code: 'Np'
|
||||
}
|
||||
];
|
||||
|
||||
let { user, category } = $props();
|
||||
let open = $state(false);
|
||||
let value = $state(languages[0].value);
|
||||
let triggerRef = $state<HTMLButtonElement>(null!);
|
||||
let searchInputText = $state('');
|
||||
|
||||
const selectedValue = $derived(languages.find((f) => f.value === value)?.label);
|
||||
function closeAndFocusTrigger() {
|
||||
open = false;
|
||||
tick().then(() => {
|
||||
triggerRef.focus();
|
||||
});
|
||||
}
|
||||
|
||||
function searchInput(e: Event) {
|
||||
e.preventDefault();
|
||||
goto(`/category/search/${searchInputText}?q=${searchInputText}`, {
|
||||
invalidateAll: true,
|
||||
keepFocus: false,
|
||||
replaceState: true
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<header class="sticky top-0 z-50 border-y border-gray-200 bg-white">
|
||||
<div
|
||||
class="max-xs:gap-0 mx-auto flex max-w-screen-2xl items-center justify-between gap-10 px-10 py-4 max-lg:flex-wrap max-lg:gap-4 max-md:px-6 max-sm:border-y-0 max-sm:border-b max-sm:px-4"
|
||||
>
|
||||
<div class="max-xs:w-32 w-40 max-sm:w-32">
|
||||
<a href="/" aria-label="Logo">
|
||||
<enhanced:img src={logo} alt="Logo" class="w-64" />
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex w-full items-center gap-2 max-lg:order-3 max-lg:w-full">
|
||||
<Sheet.Root>
|
||||
<Sheet.Trigger>
|
||||
<!-- <SlidersHorizontal class="md:hidden" strokeWidth={1.8} size={20} /> -->
|
||||
<p class="text-sm font-medium md:hidden">Categories</p>
|
||||
</Sheet.Trigger>
|
||||
<Sheet.Content side="left" class="flex w-full flex-col gap-8 sm:w-full">
|
||||
<Sheet.Header class="text-start">
|
||||
<Sheet.Title>Categories</Sheet.Title>
|
||||
</Sheet.Header>
|
||||
<ScrollArea
|
||||
class="w-full [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
|
||||
scrollbarXClasses="border-none"
|
||||
>
|
||||
<div class="flex flex-col gap-4">
|
||||
<!-- {#await category then categoryItem} -->
|
||||
{#each category?.categories as item}
|
||||
{#if item?.status !== 'draft'}
|
||||
<a href="/category/{item?.id}/{item?.slug}" class="text-nowrap">{item?.title}</a>
|
||||
{/if}
|
||||
{/each}
|
||||
<!-- {/await} -->
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</Sheet.Content>
|
||||
</Sheet.Root>
|
||||
|
||||
<NavigationMenu.Root>
|
||||
<NavigationMenu.List class="gap-0">
|
||||
<NavigationMenu.Item class="max-md:hidden">
|
||||
<NavigationMenu.Trigger>Categories</NavigationMenu.Trigger>
|
||||
<NavigationMenu.Content>
|
||||
<ul class="grid w-[400px] gap-1 p-2 md:w-[500px] md:grid-cols-2 lg:w-[600px]">
|
||||
<!-- {#await category then categoryItem} -->
|
||||
{#each category?.categories as item}
|
||||
{#if item?.status !== 'draft'}
|
||||
<li>
|
||||
<NavigationMenu.Link
|
||||
href="/category/{item?.id}/{item?.slug}"
|
||||
class="text-nowrap"
|
||||
>
|
||||
{item?.title}
|
||||
</NavigationMenu.Link>
|
||||
</li>
|
||||
{/if}
|
||||
{/each}
|
||||
<!-- {/await} -->
|
||||
</ul>
|
||||
</NavigationMenu.Content>
|
||||
</NavigationMenu.Item>
|
||||
<NavigationMenu.Item>
|
||||
<NavigationMenu.Link href="/brand" class="text-sm font-medium text-nowrap">
|
||||
Brands
|
||||
</NavigationMenu.Link>
|
||||
</NavigationMenu.Item>
|
||||
</NavigationMenu.List>
|
||||
</NavigationMenu.Root>
|
||||
|
||||
<form class="max-xs:mt-2 w-full max-lg:w-full max-md:hidden" onsubmit={searchInput}>
|
||||
<div
|
||||
class="flex w-full items-center gap-1 rounded-lg border-[1.6px] border-gray-200 p-1 pl-4 focus-within:border-primary/40 max-sm:gap-2 max-sm:pl-3"
|
||||
>
|
||||
<Search strokeWidth={1.5} class="text-primary/45 max-sm:w-5" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search for products..."
|
||||
class="max-xs:text-xs w-full text-sm tracking-tight focus-visible:ring-0 focus-visible:outline-none max-sm:py-2"
|
||||
bind:value={searchInputText}
|
||||
/>
|
||||
<Button
|
||||
class="max-xs:text-xs rounded-md border border-primary px-4 py-2 text-sm font-semibold tracking-tight text-primary max-sm:hidden"
|
||||
type="submit"
|
||||
variant="outline">Search</Button
|
||||
>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="flex items-center gap-5 max-sm:gap-3">
|
||||
<Sheet.Root>
|
||||
<Sheet.Trigger class="flex flex-col items-center justify-center gap-2 md:hidden">
|
||||
<Search class="md:hidden" strokeWidth={1.8} size={20} />
|
||||
<p class="text-xs font-medium text-nowrap">Search</p>
|
||||
</Sheet.Trigger>
|
||||
<Sheet.Content side="right" class="flex w-full flex-col gap-0 sm:w-full">
|
||||
<Sheet.Header class="text-start">
|
||||
<Sheet.Title>Search</Sheet.Title>
|
||||
</Sheet.Header>
|
||||
<form class="w-full px-2 max-lg:w-full md:hidden" onsubmit={searchInput}>
|
||||
<div
|
||||
class="flex w-full items-center gap-1 rounded-lg border-[1.6px] border-gray-200 p-1 pl-4 focus-within:border-primary/40 max-sm:gap-2 max-sm:pl-3"
|
||||
>
|
||||
<Search strokeWidth={1.5} class="text-primary/45 max-sm:w-5" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search for products..."
|
||||
class="max-xs:text-xs w-full text-sm tracking-tight focus-visible:ring-0 focus-visible:outline-none max-sm:py-2"
|
||||
bind:value={searchInputText}
|
||||
/>
|
||||
<Button
|
||||
class="max-xs:text-xs rounded-md border border-primary px-4 py-2 text-sm font-semibold tracking-tight text-primary max-sm:hidden"
|
||||
type="submit"
|
||||
variant="outline">Search</Button
|
||||
>
|
||||
</div>
|
||||
</form>
|
||||
</Sheet.Content>
|
||||
</Sheet.Root>
|
||||
<div class="max-xs:gap-4 flex items-center gap-4 max-sm:gap-3">
|
||||
<a
|
||||
href={user?.authenticated ? '/cart' : '/login'}
|
||||
class="max-xs:gap-0 flex flex-col items-center gap-1"
|
||||
>
|
||||
<ShoppingBag strokeWidth={1.2} class="max-xs:w-5" />
|
||||
<p class="text-xs font-medium text-nowrap">Cart</p>
|
||||
</a>
|
||||
{#if user?.authenticated}
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger class="cursor-pointer">
|
||||
<div class="flex flex-col items-center">
|
||||
{#if user?.user?.profileImage}
|
||||
<img
|
||||
src={user?.user?.profileImage}
|
||||
alt="{user?.user?.name} avatar"
|
||||
class="aspect-square h-7 w-7 rounded-full object-cover object-center"
|
||||
/>
|
||||
{:else}
|
||||
<p class="flex h-7 w-7 items-center justify-center rounded-full bg-slate-200">
|
||||
{user?.user?.name.slice(0, 1)}
|
||||
</p>
|
||||
{/if}
|
||||
<p class="text-xs font-medium">{user?.user?.name.split(' ')[0]}</p>
|
||||
</div>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content align="end" class="min-w-40">
|
||||
<DropdownMenu.Group>
|
||||
<DropdownMenu.GroupHeading>My Account</DropdownMenu.GroupHeading>
|
||||
<DropdownMenu.Separator class="bg-primary/15 " />
|
||||
<a href="/profile">
|
||||
<DropdownMenu.Item class="cursor-pointer">
|
||||
<UserRound size={20} strokeWidth={1.5} />
|
||||
<span> Profile </span>
|
||||
</DropdownMenu.Item>
|
||||
</a>
|
||||
<DropdownMenu.Item class="flex cursor-pointer items-center text-primary">
|
||||
<form method="POST" action="/logout" use:enhance>
|
||||
<button type="submit" class="flex items-center gap-2">
|
||||
<LogOut size={20} strokeWidth={1.5} />
|
||||
<p>Logout</p>
|
||||
</button>
|
||||
</form>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Group>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
{:else}
|
||||
<a href="/login" class="max-xs:gap-0 flex flex-col flex-nowrap items-center gap-1">
|
||||
<CircleUserRound strokeWidth={1.2} class="max-xs:w-5" />
|
||||
<p class="text-xs font-medium text-nowrap">Log in</p>
|
||||
</a>
|
||||
{/if}
|
||||
</div>
|
||||
<!-- <a
|
||||
href="/booking"
|
||||
class="text-nowrap rounded-md bg-primary px-5 py-3 text-sm font-semibold tracking-tight text-white max-md:px-3 max-md:py-3 max-md:text-xs max-xs:hidden"
|
||||
>Book Appointment</a
|
||||
> -->
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
10
src/lib/components/pages/Home/Brands.svelte
Normal file
@@ -0,0 +1,10 @@
|
||||
<script lang="ts"></script>
|
||||
|
||||
<section class="mx-auto mt-6 max-w-screen-2xl px-12">
|
||||
<h4 class="text-lg font-semibold">Premium Brands</h4>
|
||||
<div>
|
||||
<div>
|
||||
<img src="" alt="" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
35
src/lib/components/pages/Home/Celebrity.svelte
Normal file
@@ -0,0 +1,35 @@
|
||||
<script>
|
||||
import { Skeleton } from '$lib/components/ui/skeleton';
|
||||
|
||||
let { data } = $props();
|
||||
</script>
|
||||
|
||||
<section class="max-w-screen-2xl px-12 py-8 max-sm:px-4 max-sm:py-4">
|
||||
<div class="flex items-center gap-4 max-sm:flex-col max-sm:items-start">
|
||||
<h2 class="text-lg font-semibold tracking-tight text-nowrap">Recommended by:</h2>
|
||||
<div class="grid grid-cols-5 items-center gap-4 max-md:grid-cols-3 max-sm:grid-cols-2">
|
||||
{#await data}
|
||||
<Skeleton />
|
||||
{:then users}
|
||||
{#if users?.users.length > 0}
|
||||
{#each users?.users.slice(0, 5) as user, index}
|
||||
<div class="flex items-center gap-3 rounded-md border border-gray-200 p-2 pr-4">
|
||||
{#if user.image}
|
||||
<img src={user.image} alt={user.name} />
|
||||
{:else}
|
||||
<p class="flex h-12 w-12 items-center justify-center rounded-lg bg-gray-200">
|
||||
{user.name.slice(0, 1)}
|
||||
</p>
|
||||
{/if}
|
||||
<div>
|
||||
<p>{user.name}</p>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{:else}
|
||||
<p>No celebrity found</p>
|
||||
{/if}
|
||||
{/await}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
29
src/lib/components/pages/Home/FlashSale.svelte
Normal file
@@ -0,0 +1,29 @@
|
||||
<script>
|
||||
import { ProductCard } from '$lib/components/ui/product-card';
|
||||
|
||||
let { sales } = $props();
|
||||
</script>
|
||||
|
||||
<div class="mx-auto flex max-w-screen-2xl flex-col gap-6 bg-primary/10 px-10 py-10">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold tracking-tight">Flash Sale:</h2>
|
||||
<div></div>
|
||||
</div>
|
||||
<div class="grid grid-cols-5 justify-between gap-6">
|
||||
{#await sales}
|
||||
<div></div>
|
||||
{:then sale}
|
||||
{#each sale?.data as saleItem}
|
||||
<ProductCard
|
||||
imgUrl={saleItem?.product?.image}
|
||||
productRedirectLink="/product/{saleItem?.product?.id}/{saleItem?.product?.slug}"
|
||||
productPrice={saleItem?.product?.price}
|
||||
productTitle={saleItem?.product?.name}
|
||||
rating={saleItem?.product?.averageReview}
|
||||
discount={saleItem?.discount?.discount}
|
||||
flashSale
|
||||
/>
|
||||
{/each}
|
||||
{/await}
|
||||
</div>
|
||||
</div>
|
||||
66
src/lib/components/pages/Home/HeroSlider.svelte
Normal file
@@ -0,0 +1,66 @@
|
||||
<script lang="ts">
|
||||
import { swiper } from '$lib/actions/swiper.svelte';
|
||||
import { Skeleton } from '$lib/components/ui/skeleton';
|
||||
|
||||
let { bannerImage } = $props();
|
||||
</script>
|
||||
|
||||
<section>
|
||||
<div
|
||||
class="swiper !mx-auto overflow-clip"
|
||||
use:swiper={{
|
||||
slidesPerView: 1,
|
||||
spaceBetween: 20,
|
||||
effect: 'fade',
|
||||
autoplay: {
|
||||
delay: 4500,
|
||||
disableOnInteraction: false
|
||||
},
|
||||
loop: true
|
||||
}}
|
||||
>
|
||||
<div class="swiper-wrapper w-full">
|
||||
{#await bannerImage}
|
||||
<Skeleton class="aspect-[16/6] h-full w-full bg-gray-200" />
|
||||
{:then banners}
|
||||
{#if banners?.banners?.length > 0}
|
||||
{#each banners?.banners as banner (banner.id)}
|
||||
{#if banner?.media_type === 'image'}
|
||||
<div class="swiper-slide !w-full">
|
||||
<a href="/category/{banner?.category?.id}/{banner?.category?.slug}">
|
||||
<img
|
||||
src={banner?.image}
|
||||
alt="Slider"
|
||||
class="mx-auto aspect-[16/6] h-full !w-full max-w-screen-2xl object-cover object-center max-sm:aspect-video"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
{:else if banner?.media_type === 'video'}
|
||||
<div class="swiper-slide !w-full">
|
||||
<a href="/category/{banner?.category?.id}/{banner?.category?.slug}">
|
||||
<!-- svelte-ignore a11y_media_has_caption -->
|
||||
<video
|
||||
class="aspect-[16/6] h-full w-full max-w-screen-2xl object-cover object-center"
|
||||
autoplay
|
||||
muted
|
||||
>
|
||||
<source src={banner?.image} type="video/mp4" />
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
|
||||
<!-- <img
|
||||
src={banner?.image}
|
||||
alt="Slider"
|
||||
class="mx-auto aspect-[16/6] h-full !w-full max-w-screen-2xl object-cover object-center max-sm:aspect-video"
|
||||
/> -->
|
||||
</a>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
{:else}
|
||||
<Skeleton class="aspect-[16/6] h-full w-full rounded-xl bg-gray-200" />
|
||||
{/if}
|
||||
{/await}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
50
src/lib/components/pages/Home/NewArrival.svelte
Normal file
@@ -0,0 +1,50 @@
|
||||
<script>
|
||||
import { ProductCard } from '$lib/components/ui/product-card';
|
||||
import { Skeleton } from '$lib/components/ui/skeleton';
|
||||
// const celebrity = [
|
||||
// {
|
||||
// name: 'Malika Mahat',
|
||||
// img: malika
|
||||
// },
|
||||
// {
|
||||
// name: 'Malika Mahat',
|
||||
// img: malika
|
||||
// }
|
||||
// ];
|
||||
|
||||
let { products } = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="mx-auto flex max-w-screen-2xl flex-col gap-6 bg-primary/10 px-10 py-10 max-sm:gap-2 max-sm:px-4"
|
||||
>
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold tracking-tight max-sm:text-xl">New Arrivals</h2>
|
||||
</div>
|
||||
<div class="grid grid-cols-5 justify-between gap-6 max-sm:grid-cols-2 max-sm:gap-2">
|
||||
{#await products}
|
||||
{#each Array.from({ length: 10 })}
|
||||
<Skeleton class="aspect-[4/4.5] h-full w-full bg-gray-50" />
|
||||
{/each}
|
||||
{:then product}
|
||||
{#if product?.newArrival?.product?.length > 0}
|
||||
{#each product?.newArrival?.product as newArrival}
|
||||
<ProductCard
|
||||
imgUrl={newArrival?.image}
|
||||
productPrice={newArrival?.price}
|
||||
productTitle={newArrival?.name}
|
||||
rating={newArrival?.averageReview}
|
||||
discount={newArrival?.discount}
|
||||
productRedirectLink={`/product/${newArrival?.id}/${newArrival?.slug}`}
|
||||
totalReviewCount={newArrival?.totalReviewCount}
|
||||
celebrityPick={newArrival?.pickedByCelebrities}
|
||||
/>
|
||||
{/each}
|
||||
{:else}
|
||||
{#each Array.from({ length: 10 })}
|
||||
<Skeleton class="aspect-[4/4.5] h-full w-full bg-gray-50" />
|
||||
{/each}
|
||||
{/if}
|
||||
{/await}
|
||||
</div>
|
||||
</div>
|
||||
11
src/lib/components/pages/Home/Testimonial.svelte
Normal file
@@ -0,0 +1,11 @@
|
||||
<script lang="ts">
|
||||
</script>
|
||||
|
||||
<section class="mt-12">
|
||||
<h5 class="text-center text-4xl font-bold capitalize">Trust that speak for itself</h5>
|
||||
<!-- <div>
|
||||
<a href="#">
|
||||
<video src="#"></video>
|
||||
</a>
|
||||
</div> -->
|
||||
</section>
|
||||
48
src/lib/components/pages/Home/Trending.svelte
Normal file
@@ -0,0 +1,48 @@
|
||||
<script>
|
||||
import malika from '$lib/assets/img/actress/malika.png';
|
||||
import { ProductCard } from '$lib/components/ui/product-card';
|
||||
import { Skeleton } from '$lib/components/ui/skeleton';
|
||||
const celebrity = [
|
||||
{
|
||||
name: 'Malika Mahat',
|
||||
img: malika
|
||||
},
|
||||
{
|
||||
name: 'Malika Mahat',
|
||||
img: malika
|
||||
}
|
||||
];
|
||||
|
||||
let { products } = $props();
|
||||
</script>
|
||||
|
||||
<div class="mx-auto flex max-w-screen-2xl flex-col gap-6 px-10 py-10">
|
||||
<div>
|
||||
<h3 class="text-2xl font-bold tracking-tight">Trending Now</h3>
|
||||
</div>
|
||||
<div class="grid grid-cols-5 justify-between gap-6">
|
||||
{#await products}
|
||||
{#each Array.from({ length: 10 })}
|
||||
<Skeleton class="aspect-[4/5] h-full w-full bg-gray-50" />
|
||||
{/each}
|
||||
{:then product}
|
||||
{#if product?.data.length > 0}
|
||||
{#each product?.data.slice(0, 10) as trending}
|
||||
<ProductCard
|
||||
imgUrl={trending?.imageUrl}
|
||||
productPrice={trending?.price}
|
||||
productTitle={trending?.name}
|
||||
rating={trending?.averageReview}
|
||||
discount={trending?.discount}
|
||||
celebrityPick={celebrity}
|
||||
productRedirectLink={`/product/${trending?.id}/${trending?.slug}`}
|
||||
/>
|
||||
{/each}
|
||||
{:else}
|
||||
{#each Array.from({ length: 10 })}
|
||||
<Skeleton class="aspect-[4/5] h-full w-full bg-gray-50" />
|
||||
{/each}
|
||||
{/if}
|
||||
{/await}
|
||||
</div>
|
||||
</div>
|
||||
7
src/lib/components/pages/Home/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export { default as HeroSlider } from './HeroSlider.svelte';
|
||||
export { default as Celebrity } from './Celebrity.svelte';
|
||||
export { default as FlashSale } from './FlashSale.svelte';
|
||||
export { default as NewArrival } from './NewArrival.svelte';
|
||||
export { default as Trending } from './Trending.svelte';
|
||||
export { default as Testimonial } from './Testimonial.svelte';
|
||||
export { default as Brands } from './Brands.svelte';
|
||||
22
src/lib/components/ui/accordion/accordion-content.svelte
Normal file
@@ -0,0 +1,22 @@
|
||||
<script lang="ts">
|
||||
import { Accordion as AccordionPrimitive } from "bits-ui";
|
||||
import { cn, type WithoutChild } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithoutChild<AccordionPrimitive.ContentProps> = $props();
|
||||
</script>
|
||||
|
||||
<AccordionPrimitive.Content
|
||||
bind:ref
|
||||
data-slot="accordion-content"
|
||||
class="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
|
||||
{...restProps}
|
||||
>
|
||||
<div class={cn("pb-4 pt-0", className)}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
</AccordionPrimitive.Content>
|
||||
17
src/lib/components/ui/accordion/accordion-item.svelte
Normal file
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { Accordion as AccordionPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: AccordionPrimitive.ItemProps = $props();
|
||||
</script>
|
||||
|
||||
<AccordionPrimitive.Item
|
||||
bind:ref
|
||||
data-slot="accordion-item"
|
||||
class={cn("border-b last:border-b-0", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
32
src/lib/components/ui/accordion/accordion-trigger.svelte
Normal file
@@ -0,0 +1,32 @@
|
||||
<script lang="ts">
|
||||
import { Accordion as AccordionPrimitive } from "bits-ui";
|
||||
import ChevronDownIcon from "@lucide/svelte/icons/chevron-down";
|
||||
import { cn, type WithoutChild } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
level = 3,
|
||||
children,
|
||||
...restProps
|
||||
}: WithoutChild<AccordionPrimitive.TriggerProps> & {
|
||||
level?: AccordionPrimitive.HeaderProps["level"];
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<AccordionPrimitive.Header {level} class="flex">
|
||||
<AccordionPrimitive.Trigger
|
||||
data-slot="accordion-trigger"
|
||||
bind:ref
|
||||
class={cn(
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium outline-none transition-all hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
<ChevronDownIcon
|
||||
class="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200"
|
||||
/>
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
16
src/lib/components/ui/accordion/accordion.svelte
Normal file
@@ -0,0 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { Accordion as AccordionPrimitive } from "bits-ui";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
value = $bindable(),
|
||||
...restProps
|
||||
}: AccordionPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<AccordionPrimitive.Root
|
||||
bind:ref
|
||||
bind:value={value as never}
|
||||
data-slot="accordion"
|
||||
{...restProps}
|
||||
/>
|
||||
16
src/lib/components/ui/accordion/index.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import Root from "./accordion.svelte";
|
||||
import Content from "./accordion-content.svelte";
|
||||
import Item from "./accordion-item.svelte";
|
||||
import Trigger from "./accordion-trigger.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
Content,
|
||||
Item,
|
||||
Trigger,
|
||||
//
|
||||
Root as Accordion,
|
||||
Content as AccordionContent,
|
||||
Item as AccordionItem,
|
||||
Trigger as AccordionTrigger,
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
<script lang="ts">
|
||||
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
|
||||
import { buttonVariants } from "$lib/components/ui/button/index.js";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: AlertDialogPrimitive.ActionProps = $props();
|
||||
</script>
|
||||
|
||||
<AlertDialogPrimitive.Action
|
||||
bind:ref
|
||||
data-slot="alert-dialog-action"
|
||||
class={cn(buttonVariants(), className)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,18 @@
|
||||
<script lang="ts">
|
||||
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
|
||||
import { buttonVariants } from "$lib/components/ui/button/index.js";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: AlertDialogPrimitive.CancelProps = $props();
|
||||
</script>
|
||||
|
||||
<AlertDialogPrimitive.Cancel
|
||||
bind:ref
|
||||
data-slot="alert-dialog-cancel"
|
||||
class={cn(buttonVariants({ variant: "outline" }), className)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,27 @@
|
||||
<script lang="ts">
|
||||
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
|
||||
import AlertDialogOverlay from "./alert-dialog-overlay.svelte";
|
||||
import { cn, type WithoutChild, type WithoutChildrenOrChild } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
portalProps,
|
||||
...restProps
|
||||
}: WithoutChild<AlertDialogPrimitive.ContentProps> & {
|
||||
portalProps?: WithoutChildrenOrChild<AlertDialogPrimitive.PortalProps>;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<AlertDialogPrimitive.Portal {...portalProps}>
|
||||
<AlertDialogOverlay />
|
||||
<AlertDialogPrimitive.Content
|
||||
bind:ref
|
||||
data-slot="alert-dialog-content"
|
||||
class={cn(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed left-[50%] top-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
</AlertDialogPrimitive.Portal>
|
||||
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: AlertDialogPrimitive.DescriptionProps = $props();
|
||||
</script>
|
||||
|
||||
<AlertDialogPrimitive.Description
|
||||
bind:ref
|
||||
data-slot="alert-dialog-description"
|
||||
class={cn("text-muted-foreground text-sm", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="alert-dialog-footer"
|
||||
class={cn("flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="alert-dialog-header"
|
||||
class={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: AlertDialogPrimitive.OverlayProps = $props();
|
||||
</script>
|
||||
|
||||
<AlertDialogPrimitive.Overlay
|
||||
bind:ref
|
||||
data-slot="alert-dialog-overlay"
|
||||
class={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
17
src/lib/components/ui/alert-dialog/alert-dialog-title.svelte
Normal file
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: AlertDialogPrimitive.TitleProps = $props();
|
||||
</script>
|
||||
|
||||
<AlertDialogPrimitive.Title
|
||||
bind:ref
|
||||
data-slot="alert-dialog-title"
|
||||
class={cn("text-lg font-semibold", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
|
||||
|
||||
let { ref = $bindable(null), ...restProps }: AlertDialogPrimitive.TriggerProps = $props();
|
||||
</script>
|
||||
|
||||
<AlertDialogPrimitive.Trigger bind:ref data-slot="alert-dialog-trigger" {...restProps} />
|
||||
39
src/lib/components/ui/alert-dialog/index.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
|
||||
import Trigger from "./alert-dialog-trigger.svelte";
|
||||
import Title from "./alert-dialog-title.svelte";
|
||||
import Action from "./alert-dialog-action.svelte";
|
||||
import Cancel from "./alert-dialog-cancel.svelte";
|
||||
import Footer from "./alert-dialog-footer.svelte";
|
||||
import Header from "./alert-dialog-header.svelte";
|
||||
import Overlay from "./alert-dialog-overlay.svelte";
|
||||
import Content from "./alert-dialog-content.svelte";
|
||||
import Description from "./alert-dialog-description.svelte";
|
||||
|
||||
const Root = AlertDialogPrimitive.Root;
|
||||
const Portal = AlertDialogPrimitive.Portal;
|
||||
|
||||
export {
|
||||
Root,
|
||||
Title,
|
||||
Action,
|
||||
Cancel,
|
||||
Portal,
|
||||
Footer,
|
||||
Header,
|
||||
Trigger,
|
||||
Overlay,
|
||||
Content,
|
||||
Description,
|
||||
//
|
||||
Root as AlertDialog,
|
||||
Title as AlertDialogTitle,
|
||||
Action as AlertDialogAction,
|
||||
Cancel as AlertDialogCancel,
|
||||
Portal as AlertDialogPortal,
|
||||
Footer as AlertDialogFooter,
|
||||
Header as AlertDialogHeader,
|
||||
Trigger as AlertDialogTrigger,
|
||||
Overlay as AlertDialogOverlay,
|
||||
Content as AlertDialogContent,
|
||||
Description as AlertDialogDescription,
|
||||
};
|
||||
80
src/lib/components/ui/button/button.svelte
Normal file
@@ -0,0 +1,80 @@
|
||||
<script lang="ts" module>
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from "svelte/elements";
|
||||
import { type VariantProps, tv } from "tailwind-variants";
|
||||
|
||||
export const buttonVariants = tv({
|
||||
base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium outline-none transition-all focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60 text-white",
|
||||
outline:
|
||||
"bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 border",
|
||||
secondary: "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||
sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5",
|
||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||
icon: "size-9",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
});
|
||||
|
||||
export type ButtonVariant = VariantProps<typeof buttonVariants>["variant"];
|
||||
export type ButtonSize = VariantProps<typeof buttonVariants>["size"];
|
||||
|
||||
export type ButtonProps = WithElementRef<HTMLButtonAttributes> &
|
||||
WithElementRef<HTMLAnchorAttributes> & {
|
||||
variant?: ButtonVariant;
|
||||
size?: ButtonSize;
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
let {
|
||||
class: className,
|
||||
variant = "default",
|
||||
size = "default",
|
||||
ref = $bindable(null),
|
||||
href = undefined,
|
||||
type = "button",
|
||||
disabled,
|
||||
children,
|
||||
...restProps
|
||||
}: ButtonProps = $props();
|
||||
</script>
|
||||
|
||||
{#if href}
|
||||
<a
|
||||
bind:this={ref}
|
||||
data-slot="button"
|
||||
class={cn(buttonVariants({ variant, size }), className)}
|
||||
href={disabled ? undefined : href}
|
||||
aria-disabled={disabled}
|
||||
role={disabled ? "link" : undefined}
|
||||
tabindex={disabled ? -1 : undefined}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</a>
|
||||
{:else}
|
||||
<button
|
||||
bind:this={ref}
|
||||
data-slot="button"
|
||||
class={cn(buttonVariants({ variant, size }), className)}
|
||||
{type}
|
||||
{disabled}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</button>
|
||||
{/if}
|
||||
17
src/lib/components/ui/button/index.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import Root, {
|
||||
type ButtonProps,
|
||||
type ButtonSize,
|
||||
type ButtonVariant,
|
||||
buttonVariants,
|
||||
} from "./button.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
type ButtonProps as Props,
|
||||
//
|
||||
Root as Button,
|
||||
buttonVariants,
|
||||
type ButtonProps,
|
||||
type ButtonSize,
|
||||
type ButtonVariant,
|
||||
};
|
||||
76
src/lib/components/ui/calendar/calendar-caption.svelte
Normal file
@@ -0,0 +1,76 @@
|
||||
<script lang="ts">
|
||||
import type { ComponentProps } from "svelte";
|
||||
import type Calendar from "./calendar.svelte";
|
||||
import CalendarMonthSelect from "./calendar-month-select.svelte";
|
||||
import CalendarYearSelect from "./calendar-year-select.svelte";
|
||||
import { DateFormatter, getLocalTimeZone, type DateValue } from "@internationalized/date";
|
||||
|
||||
let {
|
||||
captionLayout,
|
||||
months,
|
||||
monthFormat,
|
||||
years,
|
||||
yearFormat,
|
||||
month,
|
||||
locale,
|
||||
placeholder = $bindable(),
|
||||
monthIndex = 0,
|
||||
}: {
|
||||
captionLayout: ComponentProps<typeof Calendar>["captionLayout"];
|
||||
months: ComponentProps<typeof CalendarMonthSelect>["months"];
|
||||
monthFormat: ComponentProps<typeof CalendarMonthSelect>["monthFormat"];
|
||||
years: ComponentProps<typeof CalendarYearSelect>["years"];
|
||||
yearFormat: ComponentProps<typeof CalendarYearSelect>["yearFormat"];
|
||||
month: DateValue;
|
||||
placeholder: DateValue | undefined;
|
||||
locale: string;
|
||||
monthIndex: number;
|
||||
} = $props();
|
||||
|
||||
function formatYear(date: DateValue) {
|
||||
const dateObj = date.toDate(getLocalTimeZone());
|
||||
if (typeof yearFormat === "function") return yearFormat(dateObj.getFullYear());
|
||||
return new DateFormatter(locale, { year: yearFormat }).format(dateObj);
|
||||
}
|
||||
|
||||
function formatMonth(date: DateValue) {
|
||||
const dateObj = date.toDate(getLocalTimeZone());
|
||||
if (typeof monthFormat === "function") return monthFormat(dateObj.getMonth() + 1);
|
||||
return new DateFormatter(locale, { month: monthFormat }).format(dateObj);
|
||||
}
|
||||
</script>
|
||||
|
||||
{#snippet MonthSelect()}
|
||||
<CalendarMonthSelect
|
||||
{months}
|
||||
{monthFormat}
|
||||
value={month.month}
|
||||
onchange={(e) => {
|
||||
if (!placeholder) return;
|
||||
const v = Number.parseInt(e.currentTarget.value);
|
||||
const newPlaceholder = placeholder.set({ month: v });
|
||||
placeholder = newPlaceholder.subtract({ months: monthIndex });
|
||||
}}
|
||||
/>
|
||||
{/snippet}
|
||||
|
||||
{#snippet YearSelect()}
|
||||
<CalendarYearSelect {years} {yearFormat} value={month.year} />
|
||||
{/snippet}
|
||||
|
||||
{#if captionLayout === "dropdown"}
|
||||
{@render MonthSelect()}
|
||||
{@render YearSelect()}
|
||||
{:else if captionLayout === "dropdown-months"}
|
||||
{@render MonthSelect()}
|
||||
{#if placeholder}
|
||||
{formatYear(placeholder)}
|
||||
{/if}
|
||||
{:else if captionLayout === "dropdown-years"}
|
||||
{#if placeholder}
|
||||
{formatMonth(placeholder)}
|
||||
{/if}
|
||||
{@render YearSelect()}
|
||||
{:else}
|
||||
{formatMonth(month)} {formatYear(month)}
|
||||
{/if}
|
||||
19
src/lib/components/ui/calendar/calendar-cell.svelte
Normal file
@@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
import { Calendar as CalendarPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: CalendarPrimitive.CellProps = $props();
|
||||
</script>
|
||||
|
||||
<CalendarPrimitive.Cell
|
||||
bind:ref
|
||||
class={cn(
|
||||
"size-(--cell-size) relative p-0 text-center text-sm focus-within:z-20 [&:first-child[data-selected]_[data-bits-day]]:rounded-l-md [&:last-child[data-selected]_[data-bits-day]]:rounded-r-md",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
35
src/lib/components/ui/calendar/calendar-day.svelte
Normal file
@@ -0,0 +1,35 @@
|
||||
<script lang="ts">
|
||||
import { buttonVariants } from "$lib/components/ui/button/index.js";
|
||||
import { cn } from "$lib/utils.js";
|
||||
import { Calendar as CalendarPrimitive } from "bits-ui";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: CalendarPrimitive.DayProps = $props();
|
||||
</script>
|
||||
|
||||
<CalendarPrimitive.Day
|
||||
bind:ref
|
||||
class={cn(
|
||||
buttonVariants({ variant: "ghost" }),
|
||||
"size-(--cell-size) flex select-none flex-col items-center justify-center gap-1 whitespace-nowrap p-0 font-normal leading-none",
|
||||
"[&[data-today]:not([data-selected])]:bg-accent [&[data-today]:not([data-selected])]:text-accent-foreground [&[data-today][data-disabled]]:text-muted-foreground",
|
||||
"data-[selected]:bg-primary dark:data-[selected]:hover:bg-accent/50 data-[selected]:text-primary-foreground",
|
||||
// Outside months
|
||||
"[&[data-outside-month]:not([data-selected])]:text-muted-foreground [&[data-outside-month]:not([data-selected])]:hover:text-accent-foreground",
|
||||
// Disabled
|
||||
"data-[disabled]:text-muted-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
// Unavailable
|
||||
"data-[unavailable]:text-muted-foreground data-[unavailable]:line-through",
|
||||
// hover
|
||||
"dark:hover:text-accent-foreground",
|
||||
// focus
|
||||
"focus:border-ring focus:ring-ring/50 focus:relative",
|
||||
// inner spans
|
||||
"[&>span]:text-xs [&>span]:opacity-70",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
12
src/lib/components/ui/calendar/calendar-grid-body.svelte
Normal file
@@ -0,0 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { Calendar as CalendarPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: CalendarPrimitive.GridBodyProps = $props();
|
||||
</script>
|
||||
|
||||
<CalendarPrimitive.GridBody bind:ref class={cn(className)} {...restProps} />
|
||||
12
src/lib/components/ui/calendar/calendar-grid-head.svelte
Normal file
@@ -0,0 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { Calendar as CalendarPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: CalendarPrimitive.GridHeadProps = $props();
|
||||
</script>
|
||||
|
||||
<CalendarPrimitive.GridHead bind:ref class={cn(className)} {...restProps} />
|
||||
12
src/lib/components/ui/calendar/calendar-grid-row.svelte
Normal file
@@ -0,0 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { Calendar as CalendarPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: CalendarPrimitive.GridRowProps = $props();
|
||||
</script>
|
||||
|
||||
<CalendarPrimitive.GridRow bind:ref class={cn("flex", className)} {...restProps} />
|
||||
16
src/lib/components/ui/calendar/calendar-grid.svelte
Normal file
@@ -0,0 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { Calendar as CalendarPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: CalendarPrimitive.GridProps = $props();
|
||||
</script>
|
||||
|
||||
<CalendarPrimitive.Grid
|
||||
bind:ref
|
||||
class={cn("mt-4 flex w-full border-collapse flex-col gap-1", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
19
src/lib/components/ui/calendar/calendar-head-cell.svelte
Normal file
@@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
import { Calendar as CalendarPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: CalendarPrimitive.HeadCellProps = $props();
|
||||
</script>
|
||||
|
||||
<CalendarPrimitive.HeadCell
|
||||
bind:ref
|
||||
class={cn(
|
||||
"text-muted-foreground w-(--cell-size) rounded-md text-[0.8rem] font-normal",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
19
src/lib/components/ui/calendar/calendar-header.svelte
Normal file
@@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
import { Calendar as CalendarPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: CalendarPrimitive.HeaderProps = $props();
|
||||
</script>
|
||||
|
||||
<CalendarPrimitive.Header
|
||||
bind:ref
|
||||
class={cn(
|
||||
"h-(--cell-size) flex w-full items-center justify-center gap-1.5 text-sm font-medium",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
16
src/lib/components/ui/calendar/calendar-heading.svelte
Normal file
@@ -0,0 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { Calendar as CalendarPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: CalendarPrimitive.HeadingProps = $props();
|
||||
</script>
|
||||
|
||||
<CalendarPrimitive.Heading
|
||||
bind:ref
|
||||
class={cn("px-(--cell-size) text-sm font-medium", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
44
src/lib/components/ui/calendar/calendar-month-select.svelte
Normal file
@@ -0,0 +1,44 @@
|
||||
<script lang="ts">
|
||||
import { Calendar as CalendarPrimitive } from "bits-ui";
|
||||
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
||||
import ChevronDownIcon from "@lucide/svelte/icons/chevron-down";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
value,
|
||||
onchange,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<CalendarPrimitive.MonthSelectProps> = $props();
|
||||
</script>
|
||||
|
||||
<span
|
||||
class={cn(
|
||||
"has-focus:border-ring border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] relative flex rounded-md border",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<CalendarPrimitive.MonthSelect bind:ref class="absolute inset-0 opacity-0" {...restProps}>
|
||||
{#snippet child({ props, monthItems, selectedMonthItem })}
|
||||
<select {...props} {value} {onchange}>
|
||||
{#each monthItems as monthItem (monthItem.value)}
|
||||
<option
|
||||
value={monthItem.value}
|
||||
selected={value !== undefined
|
||||
? monthItem.value === value
|
||||
: monthItem.value === selectedMonthItem.value}
|
||||
>
|
||||
{monthItem.label}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
<span
|
||||
class="[&>svg]:text-muted-foreground flex h-8 select-none items-center gap-1 rounded-md pl-2 pr-1 text-sm font-medium [&>svg]:size-3.5"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{monthItems.find((item) => item.value === value)?.label || selectedMonthItem.label}
|
||||
<ChevronDownIcon class="size-4" />
|
||||
</span>
|
||||
{/snippet}
|
||||
</CalendarPrimitive.MonthSelect>
|
||||
</span>
|
||||
15
src/lib/components/ui/calendar/calendar-month.svelte
Normal file
@@ -0,0 +1,15 @@
|
||||
<script lang="ts">
|
||||
import { type WithElementRef, cn } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div {...restProps} bind:this={ref} class={cn("flex flex-col", className)}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
19
src/lib/components/ui/calendar/calendar-months.svelte
Normal file
@@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
class={cn("relative flex flex-col gap-4 md:flex-row", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
19
src/lib/components/ui/calendar/calendar-nav.svelte
Normal file
@@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLElement>> = $props();
|
||||
</script>
|
||||
|
||||
<nav
|
||||
{...restProps}
|
||||
bind:this={ref}
|
||||
class={cn("absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1", className)}
|
||||
>
|
||||
{@render children?.()}
|
||||
</nav>
|
||||
31
src/lib/components/ui/calendar/calendar-next-button.svelte
Normal file
@@ -0,0 +1,31 @@
|
||||
<script lang="ts">
|
||||
import { Calendar as CalendarPrimitive } from "bits-ui";
|
||||
import ChevronRightIcon from "@lucide/svelte/icons/chevron-right";
|
||||
import { buttonVariants, type ButtonVariant } from "$lib/components/ui/button/index.js";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
variant = "ghost",
|
||||
...restProps
|
||||
}: CalendarPrimitive.NextButtonProps & {
|
||||
variant?: ButtonVariant;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
{#snippet Fallback()}
|
||||
<ChevronRightIcon class="size-4" />
|
||||
{/snippet}
|
||||
|
||||
<CalendarPrimitive.NextButton
|
||||
bind:ref
|
||||
class={cn(
|
||||
buttonVariants({ variant }),
|
||||
"size-(--cell-size) select-none bg-transparent p-0 disabled:opacity-50 rtl:rotate-180",
|
||||
className
|
||||
)}
|
||||
children={children || Fallback}
|
||||
{...restProps}
|
||||
/>
|
||||
31
src/lib/components/ui/calendar/calendar-prev-button.svelte
Normal file
@@ -0,0 +1,31 @@
|
||||
<script lang="ts">
|
||||
import { Calendar as CalendarPrimitive } from "bits-ui";
|
||||
import ChevronLeftIcon from "@lucide/svelte/icons/chevron-left";
|
||||
import { buttonVariants, type ButtonVariant } from "$lib/components/ui/button/index.js";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
variant = "ghost",
|
||||
...restProps
|
||||
}: CalendarPrimitive.PrevButtonProps & {
|
||||
variant?: ButtonVariant;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
{#snippet Fallback()}
|
||||
<ChevronLeftIcon class="size-4" />
|
||||
{/snippet}
|
||||
|
||||
<CalendarPrimitive.PrevButton
|
||||
bind:ref
|
||||
class={cn(
|
||||
buttonVariants({ variant }),
|
||||
"size-(--cell-size) select-none bg-transparent p-0 disabled:opacity-50 rtl:rotate-180",
|
||||
className
|
||||
)}
|
||||
children={children || Fallback}
|
||||
{...restProps}
|
||||
/>
|
||||
43
src/lib/components/ui/calendar/calendar-year-select.svelte
Normal file
@@ -0,0 +1,43 @@
|
||||
<script lang="ts">
|
||||
import { Calendar as CalendarPrimitive } from "bits-ui";
|
||||
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
||||
import ChevronDownIcon from "@lucide/svelte/icons/chevron-down";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
value,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<CalendarPrimitive.YearSelectProps> = $props();
|
||||
</script>
|
||||
|
||||
<span
|
||||
class={cn(
|
||||
"has-focus:border-ring border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] relative flex rounded-md border",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<CalendarPrimitive.YearSelect bind:ref class="absolute inset-0 opacity-0" {...restProps}>
|
||||
{#snippet child({ props, yearItems, selectedYearItem })}
|
||||
<select {...props} {value}>
|
||||
{#each yearItems as yearItem (yearItem.value)}
|
||||
<option
|
||||
value={yearItem.value}
|
||||
selected={value !== undefined
|
||||
? yearItem.value === value
|
||||
: yearItem.value === selectedYearItem.value}
|
||||
>
|
||||
{yearItem.label}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
<span
|
||||
class="[&>svg]:text-muted-foreground flex h-8 select-none items-center gap-1 rounded-md pl-2 pr-1 text-sm font-medium [&>svg]:size-3.5"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{yearItems.find((item) => item.value === value)?.label || selectedYearItem.label}
|
||||
<ChevronDownIcon class="size-4" />
|
||||
</span>
|
||||
{/snippet}
|
||||
</CalendarPrimitive.YearSelect>
|
||||
</span>
|
||||