@thorprovider/blocks 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +24 -0
- package/.turbo/turbo-test.log +10 -0
- package/.turbo/turbo-type-check.log +0 -0
- package/AGENTS.md +1 -0
- package/README.md +161 -0
- package/components.json +33 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/contracts.d.mts +563 -0
- package/dist/contracts.d.ts +563 -0
- package/dist/contracts.js +207 -0
- package/dist/contracts.js.map +1 -0
- package/dist/contracts.mjs +168 -0
- package/dist/contracts.mjs.map +1 -0
- package/dist/index.d.mts +2408 -0
- package/dist/index.d.ts +2408 -0
- package/dist/index.js +16422 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +16352 -0
- package/dist/index.mjs.map +1 -0
- package/docs/standards/data-contracts.md +70 -0
- package/docs/standards/theming.md +85 -0
- package/package.json +62 -0
- package/src/blocks/application/app-shells/app-shell-1-sidebar.tsx +198 -0
- package/src/blocks/application/app-shells/app-shell-1.tsx +196 -0
- package/src/blocks/application/login-forms/login-page-1.tsx +246 -0
- package/src/blocks/application/login-forms/login-page-2.tsx +207 -0
- package/src/blocks/application/password-recovery/forgot-password-page-1-brand-mark.tsx +38 -0
- package/src/blocks/application/password-recovery/forgot-password-page-1.tsx +320 -0
- package/src/blocks/application/signup-forms/signup-page-1.tsx +231 -0
- package/src/blocks/application/verification/verify-1.tsx +307 -0
- package/src/blocks/application/verification/verify-confetti.tsx +192 -0
- package/src/blocks/e-commerce/category-filters/category-filter-1.tsx +260 -0
- package/src/blocks/e-commerce/category-filters/category-filter-2-filter-panel.tsx +179 -0
- package/src/blocks/e-commerce/category-filters/category-filter-2.tsx +341 -0
- package/src/blocks/e-commerce/category-filters/category-filter-4-chip-rail.tsx +57 -0
- package/src/blocks/e-commerce/category-filters/category-filter-4-drawer.tsx +124 -0
- package/src/blocks/e-commerce/category-filters/category-filter-4-fields.tsx +170 -0
- package/src/blocks/e-commerce/category-filters/category-filter-4-sidebar.tsx +52 -0
- package/src/blocks/e-commerce/category-filters/category-filter-4-sort-sheet.tsx +57 -0
- package/src/blocks/e-commerce/category-filters/category-filter-4-state.ts +68 -0
- package/src/blocks/e-commerce/category-filters/category-filter-4.tsx +298 -0
- package/src/blocks/e-commerce/checkout-forms/card-formatting.test.ts +47 -0
- package/src/blocks/e-commerce/checkout-forms/card-formatting.ts +9 -0
- package/src/blocks/e-commerce/checkout-forms/checkbox-check.tsx +74 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-confirmation-fulfilment-progress.tsx +189 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-confirmation.tsx +283 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-field-feedback.tsx +29 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-contact-step.tsx +96 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-country-field.tsx +51 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-order-summary.tsx +139 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-payment-step.tsx +184 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-progress.tsx +36 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-shipping-step.tsx +107 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1.tsx +235 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-2-order-summary.tsx +130 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-2-payment-card.tsx +175 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-2-shipping-card.tsx +190 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-2.tsx +312 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-information-step.tsx +91 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-order-summary.tsx +110 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-payment-step.tsx +137 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-progress.tsx +68 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-review-step.tsx +79 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-shipping-step.tsx +172 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3.tsx +392 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-types.ts +7 -0
- package/src/blocks/e-commerce/navigation/category-icon.test.tsx +54 -0
- package/src/blocks/e-commerce/navigation/category-icon.tsx +94 -0
- package/src/blocks/e-commerce/navigation/mega-menu.test.tsx +139 -0
- package/src/blocks/e-commerce/navigation/mega-menu.tsx +271 -0
- package/src/blocks/e-commerce/navigation/mobile-category-drawer.test.tsx +152 -0
- package/src/blocks/e-commerce/navigation/mobile-category-drawer.tsx +276 -0
- package/src/blocks/e-commerce/order-history/order-history-1.tsx +159 -0
- package/src/blocks/e-commerce/order-history/order-history-2.tsx +222 -0
- package/src/blocks/e-commerce/product-categories/avatar-group.tsx +86 -0
- package/src/blocks/e-commerce/product-categories/product-category-1.tsx +126 -0
- package/src/blocks/e-commerce/product-categories/product-category-2.tsx +100 -0
- package/src/blocks/e-commerce/product-categories/product-category-4.tsx +129 -0
- package/src/blocks/e-commerce/product-categories/product-category-5.tsx +218 -0
- package/src/blocks/e-commerce/product-list/product-card-1.tsx +131 -0
- package/src/blocks/e-commerce/product-list/product-card-2.tsx +71 -0
- package/src/blocks/e-commerce/product-list/product-card-3-reaction-toggle.tsx +166 -0
- package/src/blocks/e-commerce/product-list/product-card-3.tsx +215 -0
- package/src/blocks/e-commerce/product-list/product-card-5-checkbox-check.tsx +98 -0
- package/src/blocks/e-commerce/product-list/product-card-5-reaction-toggle.tsx +171 -0
- package/src/blocks/e-commerce/product-list/product-card-5.tsx +367 -0
- package/src/blocks/e-commerce/product-overview/product-overview-1.tsx +201 -0
- package/src/blocks/e-commerce/product-overview/product-overview-2.tsx +272 -0
- package/src/blocks/e-commerce/product-overview/product-overview-5-gallery.tsx +59 -0
- package/src/blocks/e-commerce/product-overview/product-overview-5-lightbox-tab.tsx +104 -0
- package/src/blocks/e-commerce/product-overview/product-overview-5-reviews-tab.tsx +82 -0
- package/src/blocks/e-commerce/product-overview/product-overview-5-tabs.tsx +69 -0
- package/src/blocks/e-commerce/product-overview/product-overview-5.tsx +274 -0
- package/src/blocks/e-commerce/product-overview/product-quick-view-card.tsx +135 -0
- package/src/blocks/e-commerce/product-overview/product-quick-view-dialog.tsx +393 -0
- package/src/blocks/e-commerce/product-overview/product-quick-view.tsx +136 -0
- package/src/blocks/e-commerce/reviews/review-rating-1-review-form.tsx +133 -0
- package/src/blocks/e-commerce/reviews/review-rating-1.tsx +178 -0
- package/src/blocks/e-commerce/reviews/review-rating-2.tsx +246 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-1-spinning-counter.tsx +142 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-1.tsx +291 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-item-row.tsx +110 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-recommendations.tsx +51 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-spinning-counter.tsx +146 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-2.tsx +295 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-3-spinning-counter.tsx +142 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-3.tsx +342 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-4-spinning-counter.tsx +146 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-4.tsx +391 -0
- package/src/blocks/e-commerce/store-containers/store-container-1.tsx +29 -0
- package/src/blocks/e-commerce/storefront-hero/storefront-hero-1.tsx +244 -0
- package/src/blocks/e-commerce/storefront-hero/storefront-hero-2.tsx +283 -0
- package/src/blocks/e-commerce/storefront-hero/storefront-hero-5.tsx +318 -0
- package/src/blocks/e-commerce/storefront-hero/storefront-hero-6.tsx +205 -0
- package/src/blocks/marketing/banners/avatar-group.tsx +69 -0
- package/src/blocks/marketing/banners/banner-1.tsx +188 -0
- package/src/blocks/marketing/cta/cta-section-1.tsx +112 -0
- package/src/blocks/marketing/faq/faq-block-1.tsx +45 -0
- package/src/blocks/marketing/features/feature-1.tsx +97 -0
- package/src/blocks/marketing/features/feature-2.tsx +82 -0
- package/src/blocks/marketing/footers/footer-section-1.tsx +141 -0
- package/src/blocks/marketing/footers/footer-section-3.tsx +192 -0
- package/src/blocks/marketing/footers/footer-section-4.tsx +355 -0
- package/src/blocks/marketing/hero-sections/avatar-group.tsx +86 -0
- package/src/blocks/marketing/hero-sections/hero-section-1.tsx +135 -0
- package/src/blocks/marketing/logo-clouds/logo-cloud-1-logo-icon.tsx +52 -0
- package/src/blocks/marketing/logo-clouds/logo-cloud-1.tsx +56 -0
- package/src/blocks/marketing/navbars/navbar-1.tsx +267 -0
- package/src/blocks/marketing/navbars/navbar-2.tsx +351 -0
- package/src/blocks/marketing/navbars/navbar-3.tsx +303 -0
- package/src/blocks/marketing/navbars/navbar-4-account-menu.tsx +118 -0
- package/src/blocks/marketing/navbars/navbar-4-cart-sheet.tsx +248 -0
- package/src/blocks/marketing/navbars/navbar-4-like-button.tsx +177 -0
- package/src/blocks/marketing/navbars/navbar-4-mobile-nav.tsx +104 -0
- package/src/blocks/marketing/navbars/navbar-4-product-grid.tsx +219 -0
- package/src/blocks/marketing/navbars/navbar-4-search-bar.tsx +52 -0
- package/src/blocks/marketing/navbars/navbar-4.tsx +411 -0
- package/src/blocks/marketing/pricing/pricing-section-1.tsx +94 -0
- package/src/blocks/marketing/statistics/statistic-1.tsx +93 -0
- package/src/blocks/marketing/testimonials/testimonial-1.tsx +141 -0
- package/src/blocks/marketing/testimonials/testimonial-2.tsx +108 -0
- package/src/blocks/shared/reaction-toggle.tsx +145 -0
- package/src/hooks/use-mobile.ts +19 -0
- package/src/index.ts +200 -0
- package/src/lib/contracts.test.ts +32 -0
- package/src/lib/contracts.ts +235 -0
- package/src/lib/types/menu.ts +43 -0
- package/src/lib/types.ts +4 -0
- package/src/lib/utils.ts +10 -0
- package/src/ui/accordion.tsx +65 -0
- package/src/ui/avatar.tsx +108 -0
- package/src/ui/badge.tsx +47 -0
- package/src/ui/breadcrumb.tsx +108 -0
- package/src/ui/button-group.tsx +83 -0
- package/src/ui/button.tsx +63 -0
- package/src/ui/card.tsx +91 -0
- package/src/ui/carousel.tsx +241 -0
- package/src/ui/dialog.tsx +158 -0
- package/src/ui/drawer.tsx +134 -0
- package/src/ui/dropdown-menu.tsx +256 -0
- package/src/ui/empty.tsx +103 -0
- package/src/ui/field.tsx +248 -0
- package/src/ui/input-group.tsx +170 -0
- package/src/ui/input-otp.tsx +76 -0
- package/src/ui/input.tsx +20 -0
- package/src/ui/label.tsx +23 -0
- package/src/ui/navigation-menu.tsx +167 -0
- package/src/ui/pagination.tsx +127 -0
- package/src/ui/progress.tsx +30 -0
- package/src/ui/radio-group.tsx +44 -0
- package/src/ui/select.tsx +189 -0
- package/src/ui/separator.tsx +27 -0
- package/src/ui/sheet.tsx +142 -0
- package/src/ui/sidebar.tsx +726 -0
- package/src/ui/skeleton.tsx +13 -0
- package/src/ui/table.tsx +115 -0
- package/src/ui/tabs.tsx +90 -0
- package/src/ui/textarea.tsx +17 -0
- package/src/ui/toggle-group.tsx +83 -0
- package/src/ui/toggle.tsx +46 -0
- package/src/ui/tooltip.tsx +56 -0
- package/tsconfig.json +20 -0
- package/tsup.config.ts +16 -0
- package/vitest.config.ts +9 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useRef, useState } from 'react'
|
|
4
|
+
import { useReducedMotion } from 'motion/react'
|
|
5
|
+
|
|
6
|
+
import { Button } from '../../../ui/button'
|
|
7
|
+
import { cn } from '../../../lib/utils'
|
|
8
|
+
|
|
9
|
+
const PARTICLE_COUNT = 8
|
|
10
|
+
const PARTICLE_DIST = 20
|
|
11
|
+
|
|
12
|
+
export interface Navbar4LikeButtonProps {
|
|
13
|
+
liked?: boolean
|
|
14
|
+
onLikedChange?: (liked: boolean) => void
|
|
15
|
+
className?: string
|
|
16
|
+
'aria-label'?: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Heart like control — Transitions.dev `like-button`:
|
|
21
|
+
* fill + spring pop on an HTML wrapper (never the SVG) + 8-dot particle burst.
|
|
22
|
+
*/
|
|
23
|
+
export function Navbar4LikeButton({
|
|
24
|
+
liked: likedProp,
|
|
25
|
+
onLikedChange,
|
|
26
|
+
className,
|
|
27
|
+
'aria-label': ariaLabel,
|
|
28
|
+
}: Navbar4LikeButtonProps) {
|
|
29
|
+
const [uncontrolledLiked, setUncontrolledLiked] = useState(false)
|
|
30
|
+
const [bursting, setBursting] = useState(false)
|
|
31
|
+
const particlesRef = useRef<HTMLSpanElement>(null)
|
|
32
|
+
const reduceMotion = useReducedMotion()
|
|
33
|
+
|
|
34
|
+
const liked = likedProp ?? uncontrolledLiked
|
|
35
|
+
|
|
36
|
+
const setLiked = (next: boolean) => {
|
|
37
|
+
onLikedChange?.(next)
|
|
38
|
+
if (likedProp === undefined) {
|
|
39
|
+
setUncontrolledLiked(next)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const seedParticles = () => {
|
|
44
|
+
const dots = particlesRef.current?.querySelectorAll<HTMLElement>('i')
|
|
45
|
+
if (!dots) {
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
dots.forEach((dot, index) => {
|
|
50
|
+
const angle = (360 / PARTICLE_COUNT) * index + (Math.random() * 2 - 1) * 16
|
|
51
|
+
const mag = PARTICLE_DIST * (0.68 + Math.random() * 0.5)
|
|
52
|
+
const rad = (angle * Math.PI) / 180
|
|
53
|
+
dot.style.setProperty('--px', `${(Math.cos(rad) * mag).toFixed(2)}px`)
|
|
54
|
+
dot.style.setProperty('--py', `${(Math.sin(rad) * mag).toFixed(2)}px`)
|
|
55
|
+
dot.style.setProperty('--pdur', `${Math.round(600 * (0.78 + Math.random() * 0.44))}ms`)
|
|
56
|
+
dot.style.setProperty('--pdelay', `${Math.round(Math.random() * 70)}ms`)
|
|
57
|
+
dot.style.setProperty('--p-end-scale', (0.35 + Math.random() * 0.4).toFixed(2))
|
|
58
|
+
dot.style.setProperty('--psize', (0.6 + Math.random() * 0.8).toFixed(2))
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const toggle = () => {
|
|
63
|
+
if (liked) {
|
|
64
|
+
setLiked(false)
|
|
65
|
+
setBursting(false)
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
setLiked(true)
|
|
70
|
+
if (reduceMotion) {
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
setBursting(false)
|
|
75
|
+
seedParticles()
|
|
76
|
+
requestAnimationFrame(() => setBursting(true))
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<Button
|
|
81
|
+
type='button'
|
|
82
|
+
variant='ghost'
|
|
83
|
+
size='icon'
|
|
84
|
+
className={cn(
|
|
85
|
+
'navbar-4-like bg-background/80 relative size-8 cursor-pointer overflow-visible rounded-full p-0 backdrop-blur-xs',
|
|
86
|
+
className,
|
|
87
|
+
)}
|
|
88
|
+
data-liked={liked ? 'true' : 'false'}
|
|
89
|
+
data-bursting={bursting ? 'true' : 'false'}
|
|
90
|
+
aria-pressed={liked}
|
|
91
|
+
aria-label={ariaLabel ?? (liked ? 'Remove from wishlist' : 'Add to wishlist')}
|
|
92
|
+
onClick={toggle}
|
|
93
|
+
>
|
|
94
|
+
<span
|
|
95
|
+
className={cn(
|
|
96
|
+
'navbar-4-like-icon inline-flex size-4 items-center justify-center',
|
|
97
|
+
liked && !reduceMotion && 'navbar-4-like-icon--pop',
|
|
98
|
+
)}
|
|
99
|
+
>
|
|
100
|
+
<svg width='16' height='16' viewBox='0 0 16 16' fill='none' aria-hidden='true'>
|
|
101
|
+
<path
|
|
102
|
+
d='M7.99511 3.42388C6.66221 1.8656 4.4395 1.44643 2.76947 2.87334C1.09944 4.30026 0.86432 6.68598 2.17581 8.3736C3.26622 9.77674 6.56619 12.7361 7.64774 13.6939C7.76874 13.801 7.82925 13.8546 7.89982 13.8757C7.96141 13.8941 8.02881 13.8941 8.0904 13.8757C8.16097 13.8546 8.22147 13.801 8.34248 13.6939C9.42403 12.7361 12.724 9.77674 13.8144 8.3736C15.1259 6.68598 14.9195 4.28525 13.2207 2.87334C11.522 1.46144 9.32801 1.8656 7.99511 3.42388Z'
|
|
103
|
+
className='navbar-4-like-path'
|
|
104
|
+
stroke='currentColor'
|
|
105
|
+
strokeWidth='1.5'
|
|
106
|
+
strokeLinecap='round'
|
|
107
|
+
strokeLinejoin='round'
|
|
108
|
+
/>
|
|
109
|
+
</svg>
|
|
110
|
+
</span>
|
|
111
|
+
<span ref={particlesRef} className='navbar-4-like-particles' aria-hidden='true'>
|
|
112
|
+
{Array.from({ length: PARTICLE_COUNT }, (_, index) => (
|
|
113
|
+
<i key={index} />
|
|
114
|
+
))}
|
|
115
|
+
</span>
|
|
116
|
+
|
|
117
|
+
<style>{`
|
|
118
|
+
.navbar-4-like {
|
|
119
|
+
--like-color: oklch(0.6 0.24 15);
|
|
120
|
+
color: var(--foreground);
|
|
121
|
+
}
|
|
122
|
+
.navbar-4-like[data-liked='true'] {
|
|
123
|
+
color: var(--like-color);
|
|
124
|
+
}
|
|
125
|
+
.navbar-4-like-path {
|
|
126
|
+
fill: transparent;
|
|
127
|
+
stroke: currentColor;
|
|
128
|
+
transition: fill 150ms cubic-bezier(0.22, 1, 0.36, 1), stroke 150ms cubic-bezier(0.22, 1, 0.36, 1);
|
|
129
|
+
}
|
|
130
|
+
.navbar-4-like[data-liked='true'] .navbar-4-like-path {
|
|
131
|
+
fill: currentColor;
|
|
132
|
+
}
|
|
133
|
+
.navbar-4-like-icon--pop {
|
|
134
|
+
animation: navbar-4-like-pop 350ms cubic-bezier(0.34, 1.96, 0.64, 1);
|
|
135
|
+
}
|
|
136
|
+
@keyframes navbar-4-like-pop {
|
|
137
|
+
0% { transform: scale(1); }
|
|
138
|
+
30% { transform: scale(0.82); }
|
|
139
|
+
100% { transform: scale(1); }
|
|
140
|
+
}
|
|
141
|
+
.navbar-4-like-particles {
|
|
142
|
+
position: absolute;
|
|
143
|
+
left: 50%;
|
|
144
|
+
top: 50%;
|
|
145
|
+
width: 0;
|
|
146
|
+
height: 0;
|
|
147
|
+
pointer-events: none;
|
|
148
|
+
color: var(--like-color);
|
|
149
|
+
}
|
|
150
|
+
.navbar-4-like-particles i {
|
|
151
|
+
position: absolute;
|
|
152
|
+
left: calc(2.5px * var(--psize, 1) / -2);
|
|
153
|
+
top: calc(2.5px * var(--psize, 1) / -2);
|
|
154
|
+
width: calc(2.5px * var(--psize, 1));
|
|
155
|
+
height: calc(2.5px * var(--psize, 1));
|
|
156
|
+
border-radius: 50%;
|
|
157
|
+
background: currentColor;
|
|
158
|
+
opacity: 0;
|
|
159
|
+
}
|
|
160
|
+
@keyframes navbar-4-like-burst {
|
|
161
|
+
0% { opacity: 0; transform: translate(0, 0) scale(0.4); }
|
|
162
|
+
20% { opacity: 1; transform: translate(calc(var(--px) * 0.25), calc(var(--py) * 0.25)) scale(1); }
|
|
163
|
+
100% { opacity: 0; transform: translate(var(--px), var(--py)) scale(var(--p-end-scale, 0.6)); }
|
|
164
|
+
}
|
|
165
|
+
.navbar-4-like[data-bursting='true'] .navbar-4-like-particles i {
|
|
166
|
+
animation: navbar-4-like-burst var(--pdur, 600ms) ease-out var(--pdelay, 0ms) forwards;
|
|
167
|
+
}
|
|
168
|
+
@media (prefers-reduced-motion: reduce) {
|
|
169
|
+
.navbar-4-like-icon--pop,
|
|
170
|
+
.navbar-4-like-particles i {
|
|
171
|
+
animation: none !important;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
`}</style>
|
|
175
|
+
</Button>
|
|
176
|
+
)
|
|
177
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '../../../ui/accordion'
|
|
2
|
+
import { Separator } from '../../../ui/separator'
|
|
3
|
+
import { SheetClose } from '../../../ui/sheet'
|
|
4
|
+
import { cn } from '../../../lib/utils'
|
|
5
|
+
|
|
6
|
+
export interface StoreCategory {
|
|
7
|
+
label: string
|
|
8
|
+
href: string
|
|
9
|
+
/** Rendered as an accordion inside the mobile sheet; omit for a flat link. */
|
|
10
|
+
children?: { label: string; href: string }[]
|
|
11
|
+
/** Styles the link with `text-destructive` — reserve it for one entry. */
|
|
12
|
+
emphasis?: boolean
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface UtilityLink {
|
|
16
|
+
label: string
|
|
17
|
+
href: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface Navbar4MobileNavProps {
|
|
21
|
+
storeCategories: StoreCategory[]
|
|
22
|
+
utilityLinks: UtilityLink[]
|
|
23
|
+
/** When provided, an extra "Account" link is appended after the utility links. */
|
|
24
|
+
accountHref?: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function Navbar4MobileNav({
|
|
28
|
+
storeCategories,
|
|
29
|
+
utilityLinks,
|
|
30
|
+
accountHref,
|
|
31
|
+
}: Navbar4MobileNavProps) {
|
|
32
|
+
return (
|
|
33
|
+
<nav aria-label='Store' className='px-2 py-2'>
|
|
34
|
+
<Accordion type='multiple'>
|
|
35
|
+
{storeCategories.map(category =>
|
|
36
|
+
category.children ? (
|
|
37
|
+
<AccordionItem key={category.href} value={category.href} className='border-none'>
|
|
38
|
+
<AccordionTrigger className='px-3 py-3 text-base font-medium hover:no-underline'>
|
|
39
|
+
{category.label}
|
|
40
|
+
</AccordionTrigger>
|
|
41
|
+
<AccordionContent className='pb-1'>
|
|
42
|
+
<ul className='flex flex-col'>
|
|
43
|
+
{category.children.map(child => (
|
|
44
|
+
<li key={child.href}>
|
|
45
|
+
<SheetClose asChild>
|
|
46
|
+
<a
|
|
47
|
+
href={child.href}
|
|
48
|
+
className='text-muted-foreground hover:bg-accent hover:text-accent-foreground block rounded-md px-3 py-3 text-sm transition-colors duration-150'
|
|
49
|
+
>
|
|
50
|
+
{child.label}
|
|
51
|
+
</a>
|
|
52
|
+
</SheetClose>
|
|
53
|
+
</li>
|
|
54
|
+
))}
|
|
55
|
+
</ul>
|
|
56
|
+
</AccordionContent>
|
|
57
|
+
</AccordionItem>
|
|
58
|
+
) : (
|
|
59
|
+
<SheetClose key={category.href} asChild>
|
|
60
|
+
<a
|
|
61
|
+
href={category.href}
|
|
62
|
+
className={cn(
|
|
63
|
+
'hover:bg-accent block rounded-md px-3 py-3 text-base font-medium transition-colors duration-150',
|
|
64
|
+
category.emphasis && 'text-destructive',
|
|
65
|
+
)}
|
|
66
|
+
>
|
|
67
|
+
{category.label}
|
|
68
|
+
</a>
|
|
69
|
+
</SheetClose>
|
|
70
|
+
),
|
|
71
|
+
)}
|
|
72
|
+
</Accordion>
|
|
73
|
+
|
|
74
|
+
<Separator className='my-2' />
|
|
75
|
+
|
|
76
|
+
<ul className='flex flex-col'>
|
|
77
|
+
{utilityLinks.map(link => (
|
|
78
|
+
<li key={link.href}>
|
|
79
|
+
<SheetClose asChild>
|
|
80
|
+
<a
|
|
81
|
+
href={link.href}
|
|
82
|
+
className='text-muted-foreground hover:bg-accent hover:text-accent-foreground block rounded-md px-3 py-3 text-sm transition-colors duration-150'
|
|
83
|
+
>
|
|
84
|
+
{link.label}
|
|
85
|
+
</a>
|
|
86
|
+
</SheetClose>
|
|
87
|
+
</li>
|
|
88
|
+
))}
|
|
89
|
+
{accountHref ? (
|
|
90
|
+
<li>
|
|
91
|
+
<SheetClose asChild>
|
|
92
|
+
<a
|
|
93
|
+
href={accountHref}
|
|
94
|
+
className='text-muted-foreground hover:bg-accent hover:text-accent-foreground block rounded-md px-3 py-3 text-sm transition-colors duration-150'
|
|
95
|
+
>
|
|
96
|
+
Account
|
|
97
|
+
</a>
|
|
98
|
+
</SheetClose>
|
|
99
|
+
</li>
|
|
100
|
+
) : null}
|
|
101
|
+
</ul>
|
|
102
|
+
</nav>
|
|
103
|
+
)
|
|
104
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import type { JSX } from 'react'
|
|
4
|
+
import { AnimatePresence, motion, useReducedMotion } from 'motion/react'
|
|
5
|
+
import { Flame, ShoppingBag, Sparkles, Star, Trophy } from 'lucide-react'
|
|
6
|
+
|
|
7
|
+
import { Badge } from '../../../ui/badge'
|
|
8
|
+
import { Button } from '../../../ui/button'
|
|
9
|
+
import { Card, CardContent } from '../../../ui/card'
|
|
10
|
+
import { cn } from '../../../lib/utils'
|
|
11
|
+
|
|
12
|
+
import { Navbar4LikeButton } from './navbar-4-like-button'
|
|
13
|
+
|
|
14
|
+
export interface CatalogProduct {
|
|
15
|
+
id: string
|
|
16
|
+
title: string
|
|
17
|
+
subtitle: string
|
|
18
|
+
image: string
|
|
19
|
+
price: number
|
|
20
|
+
originalPrice: number
|
|
21
|
+
rating: number
|
|
22
|
+
reviewCount: number
|
|
23
|
+
discountPercentage: number
|
|
24
|
+
isLowStock?: boolean
|
|
25
|
+
badge?: {
|
|
26
|
+
text: string
|
|
27
|
+
variant: 'trending' | 'bestseller' | 'new'
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface Navbar4ProductGridProps {
|
|
32
|
+
products: CatalogProduct[]
|
|
33
|
+
searchQuery: string
|
|
34
|
+
/** When omitted, the section heading is hidden. */
|
|
35
|
+
heading?: string
|
|
36
|
+
currencySymbol?: string
|
|
37
|
+
onAddToCart?: (product: CatalogProduct) => void
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const filterEase = [0.22, 1, 0.36, 1] as const
|
|
41
|
+
|
|
42
|
+
interface BadgeStyle {
|
|
43
|
+
bg: string
|
|
44
|
+
icon: JSX.Element | null
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const badgeStyles: Record<string, BadgeStyle> = {
|
|
48
|
+
trending: {
|
|
49
|
+
bg: 'bg-black',
|
|
50
|
+
icon: <Flame className='size-3' />,
|
|
51
|
+
},
|
|
52
|
+
bestseller: {
|
|
53
|
+
bg: 'bg-black',
|
|
54
|
+
icon: <Trophy className='size-3' />,
|
|
55
|
+
},
|
|
56
|
+
new: {
|
|
57
|
+
bg: 'bg-black',
|
|
58
|
+
icon: <Sparkles className='size-3' />,
|
|
59
|
+
},
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface Navbar4ProductCardProps {
|
|
63
|
+
product: CatalogProduct
|
|
64
|
+
currencySymbol?: string
|
|
65
|
+
onAddToCart?: (product: CatalogProduct) => void
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function Navbar4ProductCard({
|
|
69
|
+
product,
|
|
70
|
+
currencySymbol = '$',
|
|
71
|
+
onAddToCart,
|
|
72
|
+
}: Navbar4ProductCardProps) {
|
|
73
|
+
const badgeStyle = product.badge ? badgeStyles[product.badge.variant] || badgeStyles.new : null
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<Card className='relative overflow-hidden rounded-md py-0 shadow-none transition-shadow hover:shadow-md'>
|
|
77
|
+
<CardContent className='px-0'>
|
|
78
|
+
<div className='relative aspect-square w-full overflow-hidden'>
|
|
79
|
+
<img src={product.image || undefined} alt={product.title} className='size-full object-cover' loading='lazy' />
|
|
80
|
+
|
|
81
|
+
{product.badge && badgeStyle ? (
|
|
82
|
+
<div className='absolute start-2 top-2 z-20'>
|
|
83
|
+
<Badge
|
|
84
|
+
className={cn(
|
|
85
|
+
'flex h-auto items-center gap-1 px-2 py-1 font-semibold text-white backdrop-blur-xs',
|
|
86
|
+
badgeStyle.bg,
|
|
87
|
+
)}
|
|
88
|
+
>
|
|
89
|
+
{badgeStyle.icon}
|
|
90
|
+
<span className='text-xs font-medium'>{product.badge.text}</span>
|
|
91
|
+
</Badge>
|
|
92
|
+
</div>
|
|
93
|
+
) : null}
|
|
94
|
+
|
|
95
|
+
{product.rating > 0 ? (
|
|
96
|
+
<div className='bg-background/80 absolute start-2 bottom-2 z-10 flex items-center gap-1 rounded-md border px-2 py-1 text-sm backdrop-blur-xs'>
|
|
97
|
+
<Star className='fill-foreground text-foreground size-4' />
|
|
98
|
+
<span>{product.rating}</span>
|
|
99
|
+
{product.reviewCount > 0 ? (
|
|
100
|
+
<span className='text-muted-foreground'>
|
|
101
|
+
|{' '}
|
|
102
|
+
{product.reviewCount >= 1000
|
|
103
|
+
? `${(product.reviewCount / 1000).toFixed(1)}k`
|
|
104
|
+
: product.reviewCount}
|
|
105
|
+
</span>
|
|
106
|
+
) : null}
|
|
107
|
+
</div>
|
|
108
|
+
) : null}
|
|
109
|
+
|
|
110
|
+
<Navbar4LikeButton className='absolute end-2 top-2 z-20' />
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
<div className='flex flex-col gap-3 p-4'>
|
|
114
|
+
<div className='flex flex-col gap-1.5'>
|
|
115
|
+
<h3 className='font-medium text-balance'>{product.title}</h3>
|
|
116
|
+
<p className='text-muted-foreground text-sm text-balance'>{product.subtitle}</p>
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
<div className='flex items-center gap-2'>
|
|
120
|
+
<span className='font-semibold'>
|
|
121
|
+
{currencySymbol}
|
|
122
|
+
{product.price}
|
|
123
|
+
</span>
|
|
124
|
+
<span className='text-muted-foreground text-sm line-through'>
|
|
125
|
+
{currencySymbol}
|
|
126
|
+
{product.originalPrice}
|
|
127
|
+
</span>
|
|
128
|
+
<span className='text-sm'>({product.discountPercentage}% OFF)</span>
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
{product.isLowStock ? <p className='text-destructive text-sm'>Only few items left!</p> : null}
|
|
132
|
+
|
|
133
|
+
<Button className='w-full' onClick={onAddToCart ? () => onAddToCart(product) : undefined}>
|
|
134
|
+
<ShoppingBag data-icon='inline-start' />
|
|
135
|
+
Add to cart
|
|
136
|
+
</Button>
|
|
137
|
+
</div>
|
|
138
|
+
</CardContent>
|
|
139
|
+
</Card>
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function Navbar4ProductGrid({
|
|
144
|
+
products,
|
|
145
|
+
searchQuery,
|
|
146
|
+
heading,
|
|
147
|
+
currencySymbol,
|
|
148
|
+
onAddToCart,
|
|
149
|
+
}: Navbar4ProductGridProps) {
|
|
150
|
+
const reduceMotion = useReducedMotion()
|
|
151
|
+
const query = searchQuery.trim().toLowerCase()
|
|
152
|
+
const filtered = query
|
|
153
|
+
? products.filter(
|
|
154
|
+
product =>
|
|
155
|
+
product.title.toLowerCase().includes(query) || product.subtitle.toLowerCase().includes(query),
|
|
156
|
+
)
|
|
157
|
+
: products
|
|
158
|
+
|
|
159
|
+
return (
|
|
160
|
+
<section aria-label='Featured products' className='mx-auto w-full max-w-7xl px-4 py-10 sm:px-6 lg:px-8'>
|
|
161
|
+
<div className='mb-6 flex items-end justify-between gap-4'>
|
|
162
|
+
<div>
|
|
163
|
+
{heading ? (
|
|
164
|
+
<h2 className='text-lg font-semibold tracking-tight'>{heading}</h2>
|
|
165
|
+
) : null}
|
|
166
|
+
<p className='text-muted-foreground text-sm'>
|
|
167
|
+
{query
|
|
168
|
+
? `${filtered.length} ${filtered.length === 1 ? 'result' : 'results'} for “${searchQuery.trim()}”`
|
|
169
|
+
: 'Shop the latest pieces — add anything to your bag.'}
|
|
170
|
+
</p>
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
|
|
174
|
+
{filtered.length === 0 ? (
|
|
175
|
+
<div className='border-border bg-muted/30 flex min-h-64 items-center justify-center rounded-xl border border-dashed px-6 text-center'>
|
|
176
|
+
<div>
|
|
177
|
+
<p className='font-medium'>No products match your search</p>
|
|
178
|
+
<p className='text-muted-foreground mt-1 text-sm'>Try a different keyword or clear the search.</p>
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
) : (
|
|
182
|
+
<motion.ul
|
|
183
|
+
layout
|
|
184
|
+
className='grid grid-cols-1 gap-6 sm:grid-cols-2 md:grid-cols-3'
|
|
185
|
+
transition={{ duration: 0.28, ease: filterEase }}
|
|
186
|
+
>
|
|
187
|
+
<AnimatePresence mode='popLayout'>
|
|
188
|
+
{filtered.map(product => (
|
|
189
|
+
<motion.li
|
|
190
|
+
key={product.id}
|
|
191
|
+
layout={!reduceMotion}
|
|
192
|
+
initial={reduceMotion ? false : { opacity: 0, y: 12, scale: 0.97 }}
|
|
193
|
+
animate={{ opacity: 1, y: 0, scale: 1 }}
|
|
194
|
+
exit={
|
|
195
|
+
reduceMotion
|
|
196
|
+
? undefined
|
|
197
|
+
: {
|
|
198
|
+
opacity: 0,
|
|
199
|
+
y: -8,
|
|
200
|
+
scale: 0.98,
|
|
201
|
+
filter: 'blur(4px)',
|
|
202
|
+
transition: { duration: 0.2, ease: 'easeIn' },
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
transition={{ duration: 0.28, ease: filterEase }}
|
|
206
|
+
>
|
|
207
|
+
<Navbar4ProductCard
|
|
208
|
+
product={product}
|
|
209
|
+
currencySymbol={currencySymbol}
|
|
210
|
+
onAddToCart={onAddToCart}
|
|
211
|
+
/>
|
|
212
|
+
</motion.li>
|
|
213
|
+
))}
|
|
214
|
+
</AnimatePresence>
|
|
215
|
+
</motion.ul>
|
|
216
|
+
)}
|
|
217
|
+
</section>
|
|
218
|
+
)
|
|
219
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Search } from 'lucide-react'
|
|
2
|
+
|
|
3
|
+
import { InputGroup, InputGroupAddon, InputGroupInput } from '../../../ui/input-group'
|
|
4
|
+
|
|
5
|
+
export interface Navbar4SearchBarProps {
|
|
6
|
+
value: string
|
|
7
|
+
onValueChange: (value: string) => void
|
|
8
|
+
onDismiss?: () => void
|
|
9
|
+
id?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function Navbar4SearchBar({
|
|
13
|
+
value,
|
|
14
|
+
onValueChange,
|
|
15
|
+
onDismiss,
|
|
16
|
+
id = 'store-search',
|
|
17
|
+
}: Navbar4SearchBarProps) {
|
|
18
|
+
return (
|
|
19
|
+
<form
|
|
20
|
+
role='search'
|
|
21
|
+
onSubmit={event => event.preventDefault()}
|
|
22
|
+
className='w-full'
|
|
23
|
+
onKeyDown={event => {
|
|
24
|
+
if (event.key === 'Escape') {
|
|
25
|
+
if (value) {
|
|
26
|
+
onValueChange('')
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
onDismiss?.()
|
|
30
|
+
}
|
|
31
|
+
}}
|
|
32
|
+
>
|
|
33
|
+
<label htmlFor={id} className='sr-only'>
|
|
34
|
+
Search products
|
|
35
|
+
</label>
|
|
36
|
+
<InputGroup className='bg-muted/40 h-9 w-full rounded-full border-transparent shadow-none has-[[data-slot=input-group-control]:focus-visible]:bg-background'>
|
|
37
|
+
<InputGroupAddon>
|
|
38
|
+
<Search aria-hidden='true' className='text-muted-foreground size-4' />
|
|
39
|
+
</InputGroupAddon>
|
|
40
|
+
<InputGroupInput
|
|
41
|
+
id={id}
|
|
42
|
+
type='search'
|
|
43
|
+
autoComplete='off'
|
|
44
|
+
placeholder='Search products'
|
|
45
|
+
value={value}
|
|
46
|
+
onChange={event => onValueChange(event.target.value)}
|
|
47
|
+
className='rounded-full'
|
|
48
|
+
/>
|
|
49
|
+
</InputGroup>
|
|
50
|
+
</form>
|
|
51
|
+
)
|
|
52
|
+
}
|