fulldev-ui 0.6.0-beta.0 → 0.6.0-beta.2

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.
Files changed (175) hide show
  1. package/astro.config.ts +10 -2
  2. package/example.svelte +0 -0
  3. package/package.json +12 -7
  4. package/parent.svelte +0 -0
  5. package/pnpm-lock.yaml +301 -35
  6. package/src/App.svelte +0 -0
  7. package/src/blocks/Products.astro +53 -72
  8. package/src/components/Card.astro +2 -2
  9. package/src/components/Cart.astro +12 -3
  10. package/src/components/CartAdd.svelte +76 -0
  11. package/src/components/CartCheckout.svelte +58 -0
  12. package/src/components/CartForm.astro +87 -88
  13. package/src/components/CartForm.svelte +130 -0
  14. package/src/components/CartList.astro +84 -79
  15. package/src/components/CartList.svelte +88 -0
  16. package/src/components/CartQuantity.svelte +81 -0
  17. package/src/components/CartSummary.astro +8 -94
  18. package/src/components/CartTotal.svelte +15 -0
  19. package/src/components/Dialog.astro +1 -0
  20. package/src/components/Drawer.astro +1 -0
  21. package/src/components/Gallery.astro +3 -1
  22. package/src/components/Header.astro +2 -0
  23. package/src/components/Image.astro +2 -2
  24. package/src/components/Input.astro +17 -9
  25. package/src/components/ProductCard.astro +52 -0
  26. package/src/components/Prose.astro +1 -1
  27. package/src/components/Search.astro +106 -43
  28. package/src/components/Section.astro +11 -269
  29. package/src/components/Select.astro +1 -0
  30. package/src/components/_CartList backup.astro +125 -0
  31. package/src/components/_CartList copy.astro +213 -0
  32. package/src/components/_CartListSimple.astro +148 -0
  33. package/src/content/categories/mugs.md +7 -0
  34. package/src/content/config.ts +24 -4
  35. package/src/content/{pages/docs → docs}/_internationalization.mdx +0 -1
  36. package/src/content/{pages/docs → docs}/_using-blocks.mdx +0 -1
  37. package/src/content/{pages/docs → docs}/_using-presets.mdx +0 -1
  38. package/src/content/{pages/docs → docs}/blocks.mdx +0 -1
  39. package/src/content/{pages/docs → docs}/collections/images.mdx +3 -2
  40. package/src/content/{pages/docs → docs}/collections/pages.mdx +0 -1
  41. package/src/content/{pages/docs → docs}/collections/presets.mdx +0 -1
  42. package/src/content/{pages/docs → docs}/collections.mdx +0 -1
  43. package/src/content/{pages/docs → docs}/components/_card.mdx +0 -2
  44. package/src/content/{pages/docs → docs}/components/_dialog.mdx +0 -2
  45. package/src/content/{pages/docs → docs}/components/_drawer.mdx +0 -2
  46. package/src/content/{pages/docs → docs}/components/_localeswitcher.mdx +0 -2
  47. package/src/content/{pages/docs → docs}/components/_switch.mdx +0 -2
  48. package/src/content/{pages/docs → docs}/components/_tagline.mdx +0 -2
  49. package/src/content/{pages/docs → docs}/components/_toggle.mdx +0 -2
  50. package/src/content/{pages/docs → docs}/components/avatar.mdx +0 -2
  51. package/src/content/{pages/docs → docs}/components/badge.mdx +0 -2
  52. package/src/content/{pages/docs → docs}/components/badges.mdx +0 -2
  53. package/src/content/{pages/docs → docs}/components/button.mdx +0 -2
  54. package/src/content/{pages/docs → docs}/components/buttons.mdx +0 -2
  55. package/src/content/{pages/docs → docs}/components/channel.mdx +0 -2
  56. package/src/content/{pages/docs → docs}/components/channels.mdx +0 -2
  57. package/src/content/{pages/docs → docs}/components/checkbox.mdx +0 -2
  58. package/src/content/{pages/docs → docs}/components/code.mdx +0 -2
  59. package/src/content/{pages/docs → docs}/components/heading.mdx +0 -2
  60. package/src/content/{pages/docs → docs}/components/icon.mdx +0 -2
  61. package/src/content/{pages/docs → docs}/components/image.mdx +0 -2
  62. package/src/content/{pages/docs → docs}/components/input.mdx +0 -2
  63. package/src/content/{pages/docs → docs}/components/label.mdx +0 -2
  64. package/src/content/{pages/docs → docs}/components/lead.mdx +0 -2
  65. package/src/content/{pages/docs → docs}/components/link.mdx +0 -2
  66. package/src/content/{pages/docs → docs}/components/links.mdx +0 -2
  67. package/src/content/{pages/docs → docs}/components/list.mdx +0 -2
  68. package/src/content/{pages/docs → docs}/components/logo.mdx +0 -2
  69. package/src/content/{pages/docs → docs}/components/menu.mdx +0 -2
  70. package/src/content/{pages/docs → docs}/components/paragraph.mdx +0 -2
  71. package/src/content/{pages/docs → docs}/components/prose.mdx +0 -2
  72. package/src/content/{pages/docs → docs}/components/rating.mdx +0 -2
  73. package/src/content/{pages/docs → docs}/components/section.mdx +0 -2
  74. package/src/content/{pages/docs → docs}/components/select.mdx +0 -2
  75. package/src/content/{pages/docs → docs}/components/social.mdx +0 -2
  76. package/src/content/{pages/docs → docs}/components/socials.mdx +0 -2
  77. package/src/content/{pages/docs → docs}/components/switch.mdx +0 -2
  78. package/src/content/{pages/docs → docs}/components/table.mdx +0 -2
  79. package/src/content/{pages/docs → docs}/components/tagline.mdx +0 -2
  80. package/src/content/{pages/docs → docs}/components/textarea.mdx +0 -2
  81. package/src/content/{pages/docs → docs}/components/themer.mdx +0 -2
  82. package/src/content/{pages/docs → docs}/components.mdx +0 -1
  83. package/src/content/{pages/docs → docs}/customization.mdx +0 -1
  84. package/src/content/{pages/docs → docs}/frameworks.mdx +0 -1
  85. package/src/content/{pages/docs → docs}/generating-pages.mdx +0 -1
  86. package/src/content/{pages/docs → docs}/index.mdx +0 -1
  87. package/src/content/{pages/docs → docs}/installation.mdx +0 -1
  88. package/src/content/{pages/docs → docs}/layouts.mdx +0 -1
  89. package/src/content/layouts/base.yml +101 -0
  90. package/src/content/layouts/docs.yml +70 -0
  91. package/src/content/pages/blocks/_categories.mdx +0 -2
  92. package/src/content/pages/blocks/_footer.mdx +0 -2
  93. package/src/content/pages/blocks/_header.mdx +0 -2
  94. package/src/content/pages/blocks/_jobs.mdx +0 -2
  95. package/src/content/pages/blocks/_prices.mdx +0 -2
  96. package/src/content/pages/blocks/_product.mdx +0 -2
  97. package/src/content/pages/blocks/_products.mdx +0 -2
  98. package/src/content/pages/blocks/_stats.mdx +0 -2
  99. package/src/content/pages/blocks/banner.mdx +0 -2
  100. package/src/content/pages/blocks/colleagues.mdx +0 -2
  101. package/src/content/pages/blocks/contact.mdx +0 -2
  102. package/src/content/pages/blocks/content.mdx +0 -2
  103. package/src/content/pages/blocks/cta.mdx +0 -2
  104. package/src/content/pages/blocks/features.mdx +0 -2
  105. package/src/content/pages/blocks/hero.mdx +0 -2
  106. package/src/content/pages/blocks/intro.mdx +0 -2
  107. package/src/content/pages/blocks/posts.mdx +0 -2
  108. package/src/content/pages/blocks/reviews.mdx +0 -2
  109. package/src/content/pages/index.mdx +0 -2
  110. package/src/content/pages/showcase/anti-slip-profiles.md +2 -2
  111. package/src/content/pages/showcase/fulldev-ui.md +1 -1
  112. package/src/content/pages/showcase/lavis-detailing.md +1 -1
  113. package/src/content/pages/showcase/leren-trainingsacteren.md +1 -1
  114. package/src/content/pages/showcase/miele-witgoedcenter-amsterdam.md +1 -1
  115. package/src/content/pages/showcase/sloepverhuur-bolsward.md +1 -1
  116. package/src/content/pages/showcase/stark-vulcanising-products.md +1 -1
  117. package/src/content/pages/showcase/the-gym-haren.md +1 -1
  118. package/src/content/pages/showcase/van-dillen-antieke-bouwmaterialen.md +1 -1
  119. package/src/content/pages/showcase.mdx +0 -1
  120. package/src/content/products/product.md +20 -0
  121. package/src/env.d.ts +1 -0
  122. package/src/integration/index.ts +20 -8
  123. package/src/layouts/CategoryLayout.astro +12 -17
  124. package/src/layouts/DocLayout.astro +17 -0
  125. package/src/layouts/Layout.astro +11 -40
  126. package/src/layouts/PageLayout.astro +6 -12
  127. package/src/layouts/PostLayout.astro +6 -12
  128. package/src/layouts/ProductLayout.astro +28 -33
  129. package/src/pages/[...category].astro +17 -0
  130. package/src/pages/[...doc].astro +17 -0
  131. package/src/pages/[...page].astro +6 -25
  132. package/src/pages/[...post].astro +17 -0
  133. package/src/pages/[...product].astro +17 -0
  134. package/src/pages/test.astro +1 -16
  135. package/src/schemas/archive/_pageSchema.ts +94 -0
  136. package/src/schemas/categorySchema.ts +16 -0
  137. package/src/schemas/components/banner.ts +3 -0
  138. package/src/schemas/components/card.ts +1 -1
  139. package/src/schemas/components/footer.ts +1 -0
  140. package/src/schemas/components/header.ts +1 -0
  141. package/src/schemas/components/link.ts +21 -14
  142. package/src/schemas/components/section.ts +2 -0
  143. package/src/schemas/components/sections.ts +1 -1
  144. package/src/schemas/components/sidebar.ts +1 -0
  145. package/src/schemas/components/subheader.ts +1 -0
  146. package/src/schemas/components/writeup.ts +1 -1
  147. package/src/schemas/docSchema.ts +10 -0
  148. package/src/schemas/fields/categories.ts +3 -0
  149. package/src/schemas/fields/description.ts +3 -0
  150. package/src/schemas/fields/href.ts +3 -0
  151. package/src/schemas/fields/image.ts +24 -0
  152. package/src/schemas/fields/images.ts +4 -0
  153. package/src/schemas/fields/order.ts +3 -0
  154. package/src/schemas/fields/price.ts +3 -0
  155. package/src/schemas/fields/products.ts +5 -0
  156. package/src/schemas/fields/seo.ts +12 -0
  157. package/src/schemas/fields/soldout.ts +3 -0
  158. package/src/schemas/{components/price.ts → fields/title.ts} +1 -1
  159. package/src/schemas/fields/variants.ts +3 -0
  160. package/src/schemas/layoutSchema.ts +28 -0
  161. package/src/schemas/pageSchema.ts +12 -86
  162. package/src/schemas/postSchema.ts +16 -0
  163. package/src/schemas/productSchema.ts +22 -0
  164. package/src/stores/cart.svelte.ts +79 -0
  165. package/src/stores/cart.ts +59 -6
  166. package/src/utils/formatEntries.ts +8 -0
  167. package/src/utils/formatEntry.ts +29 -0
  168. package/src/utils/getHref.ts +22 -0
  169. package/svelte.config.js +5 -0
  170. package/src/content/presets/base.yml +0 -100
  171. package/src/content/presets/docs.yml +0 -6
  172. package/src/schemas/components/images.ts +0 -4
  173. package/src/schemas/presetSchema.ts +0 -3
  174. package/src/schemas/utils/categories.ts +0 -3
  175. package/src/schemas/utils/navigation.ts +0 -8
@@ -1,119 +1,100 @@
1
1
  ---
2
- import type { ComponentProps, HTMLAttributes, HTMLTag } from 'astro/types'
2
+ import type { ComponentProps, HTMLAttributes } from 'astro/types'
3
+ import type { CollectionEntry } from 'astro:content'
3
4
  import Button from 'fulldev-ui/components/Button.astro'
4
5
  import Buttons from 'fulldev-ui/components/Buttons.astro'
5
- import Cards from 'fulldev-ui/components/Cards.astro'
6
6
  import Element from 'fulldev-ui/components/Element.astro'
7
7
  import Heading from 'fulldev-ui/components/Heading.astro'
8
- import Paragraph from 'fulldev-ui/components/Paragraph.astro'
9
- import Tagline from 'fulldev-ui/components/Tagline.astro'
8
+ import Image from 'fulldev-ui/components/Image.astro'
9
+ import Price from 'fulldev-ui/components/Price.astro'
10
10
 
11
11
  interface Props extends HTMLAttributes<'section'> {
12
- tagline?: ComponentProps<typeof Tagline>['text']
13
12
  heading?: ComponentProps<typeof Heading>['text']
14
- paragraph?: ComponentProps<typeof Paragraph>['text']
15
13
  button?: ComponentProps<typeof Buttons> | undefined
16
- cards?: ComponentProps<typeof Cards>['cards']
17
- level?: ComponentProps<typeof Heading>['level']
18
- as?: HTMLTag
19
- size?: 'sm' | 'md' | 'lg' | undefined
20
- variant?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | undefined
14
+ products?: CollectionEntry<'products'>[]
21
15
  }
22
16
 
23
- const {
24
- as = 'section',
25
- size = 'sm',
26
- variant = 1,
27
- level = 2,
28
- tagline,
29
- heading,
30
- paragraph,
31
- button,
32
- cards,
33
- ...rest
34
- } = Astro.props
17
+ const { heading, button, products, ...rest } = Astro.props
35
18
  ---
36
19
 
37
20
  <Element
38
21
  class:list="products"
39
- {as}
40
- {size}
41
- {variant}
22
+ if={products?.length}
23
+ as="section"
42
24
  {...rest}
43
25
  >
44
26
  <Element class:list="products-container">
45
27
  <Element class:list="products-segment">
46
- <Element class:list="products-content">
47
- <Tagline
48
- text={tagline}
49
- {size}
50
- />
51
- <Heading
52
- text={heading}
53
- {level}
54
- {size}
55
- />
56
- <Paragraph
57
- text={paragraph}
58
- {size}
59
- />
60
- <slot name="content" />
61
- </Element>
28
+ <Heading
29
+ level={2}
30
+ size="sm"
31
+ text={heading}
32
+ />
62
33
  <Button
63
34
  variant="ghost"
64
- {size}
35
+ size="sm"
65
36
  {...button}
66
37
  />
67
- <slot name="segment" />
68
38
  </Element>
69
- <Cards
70
- {cards}
71
- {size}
72
- />
73
- <slot />
39
+ <Element class:list="products-cards">
40
+ {
41
+ products?.map(({ slug, data }) => (
42
+ <Element
43
+ class:list="products-card"
44
+ as="a"
45
+ href={`/producten/${slug}/`}
46
+ >
47
+ <Image
48
+ class:list="products-card-image"
49
+ {...data.images?.[0]}
50
+ />
51
+ <Element class:list="products-card-content">
52
+ <Heading
53
+ as="h3"
54
+ level={5}
55
+ size={'sm'}
56
+ text={data.title}
57
+ />
58
+ <Price value={data.price} />
59
+ </Element>
60
+ </Element>
61
+ ))
62
+ }
63
+ </Element>
74
64
  </Element>
75
65
  </Element>
76
66
 
77
67
  <style is:global>
78
68
  @layer fulldev {
79
69
  .products {
80
- @apply relative w-full;
70
+ @apply relative w-full py-16;
81
71
 
82
72
  .products-container {
83
- @apply flex h-full w-full flex-col;
73
+ @apply container flex flex-col gap-8;
84
74
  }
85
75
 
86
76
  .products-segment {
87
- @apply z-10 flex flex-col items-start gap-8;
77
+ @apply flex items-center justify-between gap-8;
88
78
  }
89
79
 
90
- .products-content {
91
- @apply flex max-w-screen-md flex-col gap-6;
80
+ .products-cards {
81
+ @apply grid grid-cols-2 items-start gap-4 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6;
92
82
  }
93
83
 
94
- &.dark {
95
- @apply bg-background;
96
- }
84
+ .products-card {
85
+ @apply relative flex flex-col gap-3;
97
86
 
98
- /* Column left aligned */
99
- &.variant-1 {
100
- .products-container {
101
- @apply container py-8;
87
+ &:hover > .image {
88
+ @apply opacity-75 transition-opacity;
102
89
  }
103
90
 
104
- .products-segment {
105
- @apply w-full flex-row flex-wrap justify-between gap-8 py-8;
91
+ .image {
92
+ @apply rounded-md border;
106
93
  }
94
+ }
107
95
 
108
- .cards {
109
- --column-width: 200px;
110
-
111
- @apply gap-2;
112
-
113
- .image {
114
- @apply p-4;
115
- }
116
- }
96
+ .products-card-content {
97
+ @apply flex h-full flex-col gap-2;
117
98
  }
118
99
  }
119
100
  }
@@ -97,8 +97,8 @@ const {
97
97
  .card {
98
98
  @apply relative flex flex-col items-start;
99
99
 
100
- &:is(a[href]) > .image {
101
- @apply transition hover:opacity-80;
100
+ &:is(a[href]):hover > .image {
101
+ @apply opacity-50 transition;
102
102
  }
103
103
 
104
104
  .card-content {
@@ -1,8 +1,12 @@
1
1
  ---
2
2
  import type { ComponentProps } from 'astro/types'
3
- import CartList from 'fulldev-ui/components/CartList.astro'
3
+ import CartList from 'fulldev-ui/components/CartList.svelte'
4
4
  import CartSummary from 'fulldev-ui/components/CartSummary.astro'
5
5
  import Drawer from 'fulldev-ui/components/Drawer.astro'
6
+ import Button from './Button.astro'
7
+ import Icon from './Icon.astro'
8
+ import Input from './Input.astro'
9
+ import Select from './Select.astro'
6
10
 
7
11
  interface Props extends ComponentProps<typeof Drawer> {}
8
12
 
@@ -15,6 +19,11 @@ const {
15
19
  } = Astro.props
16
20
  ---
17
21
 
22
+ <Select if={false} />
23
+ <Button if={false} />
24
+ <Input if={false} />
25
+ <Icon if={false} />
26
+
18
27
  <Drawer
19
28
  if={ifProp}
20
29
  class:list="cart"
@@ -23,11 +32,11 @@ const {
23
32
  {id}
24
33
  {...rest}
25
34
  >
26
- <CartList />
35
+ <CartList client:load />
27
36
  <CartSummary slot="footer" />
28
37
  </Drawer>
29
38
 
30
- <style is:global>
39
+ <style is:global>
31
40
  @layer fulldev {
32
41
  form {
33
42
  display: flex;
@@ -0,0 +1,76 @@
1
+ <script lang="ts">
2
+ import { updateQuantity } from 'fulldev-ui/stores/cart.svelte.ts'
3
+ import { tick } from 'svelte'
4
+
5
+ let { uid, quantity } = $props()
6
+ let inputElement: HTMLInputElement | null = $state(null)
7
+
8
+ async function onQuantityChange(e: Event) {
9
+ const target = e.target as HTMLInputElement
10
+ const value = Number(target.value)
11
+ updateQuantity(uid, value)
12
+
13
+ if (value === 11) {
14
+ await tick()
15
+ inputElement?.focus()
16
+ }
17
+ }
18
+ </script>
19
+
20
+ <label
21
+ class="quantity input select size-md"
22
+ for="quantity"
23
+ >
24
+ {#if quantity > 10}
25
+ <input
26
+ bind:this={inputElement}
27
+ class="input-input"
28
+ name="quantity"
29
+ type="number"
30
+ value={quantity}
31
+ min={0}
32
+ onchange={onQuantityChange}
33
+ />
34
+ {:else}
35
+ <svg
36
+ xmlns="http://www.w3.org/2000/svg"
37
+ viewBox="0 0 24 24"
38
+ class="icon size-md"
39
+ >
40
+ <path
41
+ fill="none"
42
+ stroke="currentColor"
43
+ stroke-linecap="round"
44
+ stroke-linejoin="round"
45
+ stroke-width="2"
46
+ d="m6 9l6 6l6-6"
47
+ ></path>
48
+ </svg>
49
+ <select
50
+ class="select-select"
51
+ value={quantity}
52
+ onchange={onQuantityChange}
53
+ >
54
+ {#each Array(10) as _, i}
55
+ <option
56
+ value={i + 1}
57
+ selected={quantity === i + 1}
58
+ >
59
+ {i + 1}
60
+ </option>
61
+ {/each}
62
+ <option
63
+ value="11"
64
+ selected={quantity > 10}
65
+ >
66
+ Meer
67
+ </option>
68
+ </select>
69
+ {/if}
70
+ </label>
71
+
72
+ <style lang="postcss">
73
+ .quantity {
74
+ @apply w-24;
75
+ }
76
+ </style>
@@ -0,0 +1,58 @@
1
+ <script lang="ts">
2
+ import { cart } from 'fulldev-ui/stores/cart.svelte.ts'
3
+ import Stripe from 'stripe'
4
+
5
+ const { STRIPE_RESTRICTED_KEY, URL } = $props()
6
+
7
+ async function handeClick() {
8
+ if (!STRIPE_RESTRICTED_KEY) {
9
+ console.error('Stripe key is missing')
10
+ return
11
+ }
12
+ const stripe = new Stripe(STRIPE_RESTRICTED_KEY)
13
+ const checkoutConfig = {
14
+ success_url: `${URL}/checkout`,
15
+ mode: 'payment',
16
+ customer_creation: 'always',
17
+ shipping_address_collection: {
18
+ allowed_countries: ['NL'],
19
+ },
20
+ phone_number_collection: {
21
+ enabled: true,
22
+ },
23
+ }
24
+
25
+ const lineItems = $cart.map((item) => ({
26
+ quantity: item.quantity,
27
+ adjustable_quantity: {
28
+ enabled: true,
29
+ maximum: 999999,
30
+ },
31
+ price_data: {
32
+ currency: 'eur',
33
+ product_data: {
34
+ name:
35
+ item.variants && Object.values(item.variants || {}).length
36
+ ? `${item.title} - ${Object.values(item.variants).join(' / ')}`
37
+ : item.title,
38
+ // images: [item.image ? URL + item.image : ''],
39
+ },
40
+ unit_amount: Math.round(item.price || 0 * 100),
41
+ },
42
+ }))
43
+
44
+ const checkoutSession = await stripe.checkout.sessions.create({
45
+ line_items: lineItems,
46
+ ...checkoutConfig,
47
+ })
48
+
49
+ checkoutSession.url && window.location.replace(checkoutSession.url)
50
+ }
51
+ </script>
52
+
53
+ <button
54
+ class="button variant-primary size-md"
55
+ onclick={handeClick}
56
+ >
57
+ Afrekenennn
58
+ </button>
@@ -1,109 +1,100 @@
1
1
  ---
2
- import type { ComponentProps } from 'astro/types'
2
+ import type { CollectionEntry } from 'astro:content'
3
3
  import Button from 'fulldev-ui/components/Button.astro'
4
4
  import Input from 'fulldev-ui/components/Input.astro'
5
5
  import Select from 'fulldev-ui/components/Select.astro'
6
6
  import { capitalize } from 'radash'
7
+ import { scope } from 'simple:scope'
8
+ import Element from './Element.astro'
7
9
  import Form from './Form.astro'
8
10
  import Price from './Price.astro'
9
11
 
10
- interface Props extends Omit<ComponentProps<typeof Form>, 'fields' | 'submit'> {
11
- title: string
12
- slug: string
13
- price: number
14
- image?: string | undefined
15
- soldout?: boolean
16
- variants?:
17
- | {
18
- [key: string]: string[]
19
- }
20
- | undefined
12
+ interface Props {
13
+ product: CollectionEntry<'products'>
21
14
  }
22
15
 
23
- const { title, slug, image, price, variants, soldout, ...rest } = Astro.props
16
+ const { slug, data } = Astro.props.product
17
+
18
+ const src = data.images?.[0]?.src || `/${data.images?.[0]?.id}`
24
19
  ---
25
20
 
26
- <Form
27
- class:list="cart-form"
28
- {...rest}
29
- >
30
- <Input
31
- type="hidden"
32
- name="title"
33
- value={title}
34
- />
35
- <Input
36
- type="hidden"
37
- name="slug"
38
- value={slug}
39
- />
40
- <Input
41
- type="hidden"
42
- name="url"
43
- value={Astro.site ? Astro.site.href + slug : undefined}
44
- />
45
- <Input
46
- type="hidden"
47
- name="image"
48
- value={image}
49
- />
50
- {
51
- Object.entries(variants ?? {}).map(([key, options]) => (
52
- <Select
53
- label={capitalize(key)}
54
- name={`variant-${key}`}
55
- options={options}
56
- disabled={soldout}
21
+ <RootElement data={{ variants: Object.keys(data.variants || {}) }}>
22
+ <Form
23
+ class:list="cart-form"
24
+ data-target={scope('form')}
25
+ >
26
+ <Input
27
+ name="slug"
28
+ type="hidden"
29
+ value={slug}
30
+ />
31
+ <Input
32
+ name="title"
33
+ type="hidden"
34
+ value={data.title}
35
+ />
36
+ <Input
37
+ name="price"
38
+ type="hidden"
39
+ value={data.price}
40
+ />
41
+ <Input
42
+ name="image"
43
+ type="hidden"
44
+ value={Astro.site && (Astro.site.href + src).replace('//', '/')}
45
+ />
46
+ {
47
+ Object.entries(data.variants ?? {}).map(([key, options]) => (
48
+ <Select
49
+ name={key}
50
+ label={capitalize(key)}
51
+ name={key}
52
+ options={options}
53
+ disabled={data.soldout}
54
+ />
55
+ ))
56
+ }
57
+ <Price value={data.price} />
58
+ <Element class:list="inputs">
59
+ <Input
60
+ class:list="quantity-input"
61
+ name="quantity"
62
+ type="number"
63
+ min={1}
64
+ value={1}
57
65
  />
58
- ))
59
- }
60
- <Price value={price} />
61
- <Input
62
- type="hidden"
63
- name="price"
64
- value={price}
65
- />
66
- <Button
67
- variant="primary"
68
- type="submit"
69
- class="cart-add"
70
- text={soldout ? 'Uitverkocht' : 'Toevoegen aan winkelwagen'}
71
- disabled={soldout}
72
- />
73
- </Form>
66
+ <Button
67
+ variant="primary"
68
+ type="submit"
69
+ class="cart-add"
70
+ text={data.soldout ? 'Uitverkocht' : 'Toevoegen aan winkelwagen'}
71
+ disabled={data.soldout}
72
+ />
73
+ </Element>
74
+ </Form>
75
+ </RootElement>
74
76
 
75
77
  <script>
76
- import { $cart } from 'fulldev-ui/stores/cart.ts'
77
- import { generateProductId, generateProductName } from 'fulldev-ui/utils/cart'
78
-
79
- document.addEventListener('astro:page-load', () => {
80
- const cartForm = document.querySelector('.cart-form')
78
+ import { addItem, openCart } from 'fulldev-ui/stores/cart.ts'
81
79
 
82
- cartForm?.addEventListener('submit', (e) => {
80
+ RootElement.ready(($, ctx) => {
81
+ $('form').addEventListener('submit', async (e) => {
82
+ if (!(e.target instanceof HTMLFormElement)) return
83
83
  e.preventDefault()
84
84
 
85
- if (e.target instanceof HTMLFormElement) {
86
- const formData = new FormData(e.target)
87
-
88
- const title = formData.get('title') as string
89
- const slug = formData.get('slug') as string
90
- const url = formData.get('url') as string
91
- const image = formData.get('image') as string
92
- const price = Number(formData.get('price')) as number
93
- const variants = Array.from(formData.entries())
94
- .filter(([key]) => key.startsWith('variant-'))
95
- .map(([_, value]) => value) as string[]
96
-
97
- const id = generateProductId(slug, variants)
98
- const name = generateProductName(title, variants)
99
-
100
- const currentCart = $cart.get()
101
- const newCart = [...currentCart, { id, name, price, image, url }]
102
- $cart.set(newCart)
103
-
104
- const cartDialog = document?.querySelector('dialog#cart')
105
- if (cartDialog instanceof HTMLDialogElement) cartDialog.showModal()
85
+ const formData = new FormData(e.target)
86
+ const object = Object.fromEntries(formData.entries())
87
+ const formatted = {
88
+ slug: String(object['slug']),
89
+ title: String(object['title']),
90
+ price: Number(object['price']),
91
+ quantity: Number(object['quantity']),
92
+ image: String(object['image']),
93
+ variants: ctx.data.variants.map((key: string) => object[key]),
106
94
  }
95
+
96
+ addItem(formatted)
97
+ openCart()
107
98
  })
108
99
  })
109
100
  </script>
@@ -113,7 +104,15 @@ const { title, slug, image, price, variants, soldout, ...rest } = Astro.props
113
104
  .cart-form {
114
105
  @apply flex flex-col gap-4;
115
106
 
116
- .input {
107
+ .inputs {
108
+ @apply flex gap-2;
109
+ }
110
+
111
+ .quantity-input {
112
+ @apply w-16;
113
+ }
114
+
115
+ .price-input {
117
116
  @apply hidden;
118
117
  }
119
118
  }
@@ -0,0 +1,130 @@
1
+ <script lang="ts">
2
+ import { cart, getUid, openCart } from 'fulldev-ui/stores/cart.svelte'
3
+
4
+ import { capitalize } from 'radash'
5
+ import CartQuantity from './CartQuantity.svelte'
6
+ import { formatPrice } from 'fulldev-ui/utils/cart'
7
+ import type { CollectionEntry } from 'astro:content'
8
+
9
+ interface Props {
10
+ product: CollectionEntry<'products'>
11
+ }
12
+
13
+ const { product }: Props = $props()
14
+ const { slug, data } = product
15
+
16
+ const form = $state({
17
+ slug: slug,
18
+ title: data.title,
19
+ price: data.price,
20
+ image: data.images?.[0]?.src || `/${data.images?.[0]?.id}`,
21
+ variants: Object.fromEntries(
22
+ Object.entries(data.variants ?? {}).map(([key, value]) => [key, value[0]])
23
+ ),
24
+ quantity: 1,
25
+ })
26
+
27
+ function handleSubmit(event: Event) {
28
+ event.preventDefault()
29
+
30
+ const variants = { ...form.variants }
31
+ const uid = getUid(slug, variants)
32
+ const item = { ...form, uid, variants }
33
+
34
+ const index = $cart.findIndex((i) => i.uid === item.uid)
35
+ if (index === -1) $cart = [...$cart, item]
36
+ else if ($cart[index]) $cart[index].quantity += form.quantity
37
+
38
+ openCart()
39
+ }
40
+ </script>
41
+
42
+ <form
43
+ class="cart-form"
44
+ onsubmit={handleSubmit}
45
+ >
46
+ <input
47
+ name="slug"
48
+ type="hidden"
49
+ bind:value={form.slug}
50
+ />
51
+ <input
52
+ name="title"
53
+ type="hidden"
54
+ bind:value={form.title}
55
+ />
56
+ <input
57
+ name="price"
58
+ type="hidden"
59
+ bind:value={form.price}
60
+ />
61
+ <input
62
+ name="image"
63
+ type="hidden"
64
+ bind:value={form.image}
65
+ />
66
+ {#if form.variants}
67
+ {#each Object.entries(data.variants ?? {}) as [key, options]}
68
+ <label
69
+ class="select size-md"
70
+ for={`variant-${key}`}
71
+ >
72
+ <p class="label">
73
+ {capitalize(key)}
74
+ </p>
75
+ <svg
76
+ xmlns="http://www.w3.org/2000/svg"
77
+ viewBox="0 0 24 24"
78
+ class="icon size-md"
79
+ >
80
+ <path
81
+ fill="none"
82
+ stroke="currentColor"
83
+ stroke-linecap="round"
84
+ stroke-linejoin="round"
85
+ stroke-width="2"
86
+ d="m6 9l6 6l6-6"
87
+ ></path>
88
+ </svg>
89
+ <select
90
+ class="select-select"
91
+ name={key}
92
+ disabled={data.soldout}
93
+ bind:value={form.variants[key]}
94
+ >
95
+ {#each options as option}
96
+ <option value={option}>{option}</option>
97
+ {/each}
98
+ </select>
99
+ </label>
100
+ {/each}
101
+ {/if}
102
+ {#if data.price}
103
+ <div class="price">
104
+ {formatPrice(data.price)}
105
+ </div>
106
+ {/if}
107
+
108
+ <div class="inputs">
109
+ <CartQuantity bind:value={form.quantity} />
110
+ <button
111
+ class="variant-primary button size-md cart-add"
112
+ type="submit"
113
+ disabled={data.soldout}
114
+ >
115
+ {data.soldout ? 'Uitverkocht' : 'Toevoegen aan winkelwagen'}
116
+ </button>
117
+ </div>
118
+ </form>
119
+
120
+ <style lang="postcss">
121
+ @layer fulldev {
122
+ .cart-form {
123
+ @apply flex flex-col gap-4;
124
+
125
+ .inputs {
126
+ @apply flex gap-2;
127
+ }
128
+ }
129
+ }
130
+ </style>