@solumflow-app/product-configurator 0.4.0 → 0.5.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/README.md +98 -3
- package/dist/checkout-step-H45B2AXW.js +1 -0
- package/dist/chunk-5SFWVZIX.js +1 -0
- package/dist/chunk-6ON4U6IB.js +1 -0
- package/dist/chunk-GA7MFMMF.js +1 -0
- package/dist/{chunk-ATEH2U7F.js → chunk-UEIARFB4.js} +29 -29
- package/dist/chunk-VQ7GFV6B.js +1 -0
- package/dist/chunk-Y4YXTBBE.js +1 -0
- package/dist/chunk-YLNPPHX4.js +1 -0
- package/dist/configurator-scene-RAWYHWX5.js +1 -0
- package/dist/index.d.ts +115 -3
- package/dist/index.js +49 -2
- package/dist/model-part-NMRB6VKW.js +1 -0
- package/dist/request-step-AI77BVKM.js +1 -0
- package/dist/stripe-payment-MYZ6OYY4.js +1 -0
- package/package.json +13 -1
- package/dist/chunk-QLPSL2DY.js +0 -1
- package/dist/chunk-RNTBO6KY.js +0 -1
- package/dist/configurator-scene-3ABIR55G.js +0 -1
- package/dist/model-part-CGYO323N.js +0 -1
package/README.md
CHANGED
|
@@ -17,8 +17,13 @@ npm install @solumflow-app/product-configurator @solumflow-app/crm-client \
|
|
|
17
17
|
|
|
18
18
|
`react`, `react-dom`, `three`, `@react-three/fiber` and `@react-three/drei` are
|
|
19
19
|
peer dependencies: your site has one copy of each. three.js is only fetched when
|
|
20
|
-
the configurator scrolls into view
|
|
21
|
-
|
|
20
|
+
the configurator scrolls into view. The dimension sketch is a second view: under
|
|
21
|
+
the picture in the default layout, a tab beside the 3D view in `layout="fill"`,
|
|
22
|
+
and the only view where the browser has no WebGL.
|
|
23
|
+
|
|
24
|
+
`@stripe/stripe-js` and `@stripe/react-stripe-js` are optional peers, needed
|
|
25
|
+
only when the panel takes orders itself (`checkout`, below); they are fetched
|
|
26
|
+
when a visitor reaches the payment.
|
|
22
27
|
|
|
23
28
|
## Draw it
|
|
24
29
|
|
|
@@ -89,9 +94,99 @@ object builder, on the product page and on your site. The canvas background is
|
|
|
89
94
|
transparent; your page decides what is behind it. The reflections come from a
|
|
90
95
|
room three.js builds in the browser, so nothing is fetched from a CDN.
|
|
91
96
|
|
|
97
|
+
## Give it the whole screen
|
|
98
|
+
|
|
99
|
+
```tsx
|
|
100
|
+
<div style={{ height: '100dvh' }}>
|
|
101
|
+
<ProductConfigurator
|
|
102
|
+
product={product}
|
|
103
|
+
attribute="meubel"
|
|
104
|
+
layout="fill"
|
|
105
|
+
actions={({ payload, priced }) => (
|
|
106
|
+
<button disabled={!payload || !priced} onClick={() => payload && order(payload)}>
|
|
107
|
+
Bestellen
|
|
108
|
+
</button>
|
|
109
|
+
)}
|
|
110
|
+
/>
|
|
111
|
+
</div>
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
- `layout="fill"` takes the whole box you give it -- give that box a height,
|
|
115
|
+
or the picture is zero pixels tall (a development build says so in the
|
|
116
|
+
console). The choices stand to the right, from 900px wide; below that under
|
|
117
|
+
the picture, with the price and the buttons always in view.
|
|
118
|
+
- `views` says which views the picture offers: `['3d', 'sketch']` draws the tabs
|
|
119
|
+
"3D" and "Tekening" over its top right corner (the default in `fill`).
|
|
120
|
+
- `actions` puts your own buttons in the panel, under the price. A function is
|
|
121
|
+
given the `payload` to submit (null while the choice cannot be priced) and
|
|
122
|
+
whether everything is `priced`, which an order needs. Without `actions`,
|
|
123
|
+
`onSubmit` and `submitLabel` draw one button there.
|
|
124
|
+
- `wheelZoom`: `'modifier'` (the default in `fill`) zooms only with Ctrl or ⌘
|
|
125
|
+
held, so a page that scrolls past the configurator keeps scrolling; a pinch
|
|
126
|
+
zooms either way. `'always'` is the default layout's wheel.
|
|
127
|
+
|
|
128
|
+
## The room, the light and the fronts
|
|
129
|
+
|
|
130
|
+
What the configurator shows is set in the CRM, not on your site:
|
|
131
|
+
|
|
132
|
+
- **A room.** A structure with `scene.room` draws a wall and a floor around the
|
|
133
|
+
furniture, and the panel gets a tab "Ruimte" where the visitor picks their
|
|
134
|
+
colours. That choice is a view: it reaches no price, no request and no
|
|
135
|
+
order. `onChange` reports it as `view`, `initialView` sets it. A shape that
|
|
136
|
+
draws a room should not also draw a wall of its own.
|
|
137
|
+
- **The camera stays in front of the wall**: it turns at most 75° to either side
|
|
138
|
+
and never below the floor.
|
|
139
|
+
- **LED strips give light** on what is around them, in their colour
|
|
140
|
+
temperature; a button "Avondlicht" dims the room to show it. The light of a
|
|
141
|
+
strip passes through a shelf: area lights cast no shadow.
|
|
142
|
+
- **Fronts open.** A block can say how it opens (`motion`: a hinge or a slide)
|
|
143
|
+
and the structure what a choice opens and looks at (`focus`). Choosing a
|
|
144
|
+
drawer for compartment 2 then slides it out and turns the camera to it; a few
|
|
145
|
+
seconds later, or at the visitor's next move in the picture, it closes. With
|
|
146
|
+
`prefers-reduced-motion`, nothing moves: it simply stands open.
|
|
147
|
+
|
|
148
|
+
## Order and ask from the panel
|
|
149
|
+
|
|
150
|
+
The panel can take the order or the request itself: details, delivery, the
|
|
151
|
+
terms, Stripe's payment field and a confirmation, all beside the furniture. Add
|
|
152
|
+
one route to your site, which keeps the key on your server:
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
// app/api/solumflow/configurator/route.ts
|
|
156
|
+
import { createConfiguratorCheckoutHandler } from '@solumflow-app/crm-client/configurator-checkout';
|
|
157
|
+
|
|
158
|
+
export const POST = createConfiguratorCheckoutHandler({
|
|
159
|
+
baseUrl: process.env.CRM_URL!,
|
|
160
|
+
apiKey: process.env.CRM_API_KEY!, // crms_, with orders:write and requests:write
|
|
161
|
+
});
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
```tsx
|
|
165
|
+
<ProductConfigurator
|
|
166
|
+
product={product} // as getProduct returns it, with its id
|
|
167
|
+
attribute="meubel"
|
|
168
|
+
layout="fill"
|
|
169
|
+
checkout={{
|
|
170
|
+
endpoint: '/api/solumflow/configurator',
|
|
171
|
+
termsUrl: '/algemene-voorwaarden',
|
|
172
|
+
privacyUrl: '/privacy',
|
|
173
|
+
}}
|
|
174
|
+
/>
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
"Bestellen" appears while everything is priced, "Offerte aanvragen" always
|
|
178
|
+
(`allowOrder` and `allowRequest` switch either off). The order lands in the CRM
|
|
179
|
+
with the configuration, and with the fireplace a preset stands for as a line of
|
|
180
|
+
its own -- unless your order already names it. The route refuses a post from
|
|
181
|
+
another site and passes on only what the payment needs. A changed price or
|
|
182
|
+
structure is shown in the step, never as a broken page. A bank that needs its
|
|
183
|
+
own page sends the visitor back to `returnUrl` (this page by default).
|
|
184
|
+
|
|
92
185
|
### Content Security Policy
|
|
93
186
|
|
|
94
|
-
If your site sends a CSP, allow the CRM's storage origin in two directives
|
|
187
|
+
If your site sends a CSP, allow the CRM's storage origin in two directives
|
|
188
|
+
(and Stripe's, `js.stripe.com` in `script-src` and `frame-src`, when the panel
|
|
189
|
+
takes payments):
|
|
95
190
|
|
|
96
191
|
- `img-src`: textures and their maps are loaded as images;
|
|
97
192
|
- `connect-src`: 3D models (`.glb`) are fetched.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import {a as a$2}from'./chunk-5SFWVZIX.js';import {g,e,a as a$1,b,h,d,f,i}from'./chunk-Y4YXTBBE.js';import {a}from'./chunk-6ON4U6IB.js';import'./chunk-GA7MFMMF.js';import {lazy,useId,useRef,useState,Suspense}from'react';import {jsxs,jsx}from'react/jsx-runtime';var M=lazy(()=>import('./stripe-payment-MYZ6OYY4.js'));function _(l){let{checkout:a$3,labels:o,quote:b$1}=l,d$1=useId(),g$1=useRef(g()),[m,q]=useState(null),[r,p]=useState({busy:false,error:null,order:null,paid:false}),h$1=t=>a$2(t,l.locale,l.currency),C=e(a$3,l.selection,b$1),u=useRef({place:"",count:0}),D=async t=>{let i=`${t.countryCode}|${t.postalCode}`;if(!t.countryCode||!t.postalCode||i===u.current.place)return;u.current={place:i,count:u.current.count+1};let c=u.current.count,s=await f(a$3.endpoint,{action:"shipping",productId:a$3.productId,configuration:C,address:t});c===u.current.count&&q("action"in s&&s.action==="shipping"?s:null);},K=async t=>{t.preventDefault();let i$1=new FormData(t.currentTarget),c=f=>String(i$1.get(f)??"").trim();p(f=>({...f,busy:true,error:null}));let s=await f(a$3.endpoint,{action:"order",productId:a$3.productId,configuration:C,buyer:{email:c("email"),firstName:c("firstName"),lastName:c("lastName"),phone:c("phone")||void 0},address:{line1:`${c("street")} ${c("houseNumber")}`.trim(),postalCode:c("postalCode"),city:c("city"),countryCode:c("countryCode")},shippingMethodId:c("shippingMethodId")||void 0,idempotencyKey:g$1.current});if("action"in s&&s.action==="order"){p({busy:false,error:null,order:s,paid:false});return}let N="error"in s?s.error:"";N!=="unavailable"&&(g$1.current=g()),p({busy:false,error:i(N,o),order:null,paid:false});};return r.paid&&r.order?jsxs("div",{className:"pc-checkout",role:"status","data-test":"configurator-order-placed",children:[jsx("h3",{className:"pc-checkout-title",children:o.checkoutOrderPlaced}),jsxs("p",{children:[o.checkoutOrderNumber,":"," ",jsx("strong",{children:r.order.orderNumber})]}),jsx("p",{className:"pc-price-note",children:o.checkoutPaymentReceived})]}):jsxs("div",{className:"pc-checkout","data-test":"configurator-checkout",children:[jsxs("button",{type:"button",className:"pc-checkout-back",onClick:l.onBack,children:["\u2190 ",o.checkoutBack]}),jsxs("section",{className:"pc-field",children:[jsx("h3",{className:"pc-checkout-title",children:o.checkoutSummary}),jsx("ul",{className:"pc-checkout-lines",children:b$1.lines.map(t=>jsxs("li",{children:[jsx("span",{children:t.label}),jsx("span",{children:t.amountCents===null?"":h$1(t.amountCents)})]},t.key))})]}),r.order?jsxs("section",{className:"pc-field",children:[jsxs("p",{className:"pc-checkout-total",children:[jsx("span",{children:o.checkoutTotal}),jsx("strong",{children:h$1(r.order.totalCents)})]}),r.order.publishableKey?jsx(a,{fallback:jsx("p",{className:"pc-checkout-error",role:"alert",children:o.checkoutPaymentsUnavailable}),children:jsx(Suspense,{fallback:jsx("p",{className:"pc-scene-note",children:"\u2026"}),children:jsx(M,{publishableKey:r.order.publishableKey,stripeAccount:r.order.stripeAccount,clientSecret:r.order.clientSecret,returnUrl:a$3.returnUrl??(typeof window>"u"?"":window.location.href),payLabel:o.checkoutPay,onPaid:()=>p(t=>({...t,paid:true}))})})}):jsx("p",{className:"pc-checkout-error",role:"alert",children:o.checkoutPaymentsUnavailable})]}):jsxs("form",{className:"pc-checkout-form",onSubmit:K,children:[jsx("h3",{className:"pc-checkout-title",children:o.checkoutDetails}),jsx(a$1,{id:d$1,labels:o,nameRequired:true}),jsx(b,{id:d$1,labels:o,countries:a$3.countries??h,onPlaceChange:D}),jsxs("fieldset",{className:"pc-field",children:[jsx("legend",{className:"pc-label",children:o.checkoutDelivery}),m===null?jsx("p",{className:"pc-price-note",children:o.checkoutDeliveryAfterAddress}):m.requiresShipping?jsx("div",{className:"pc-choices",children:m.methods.map((t,i)=>jsxs("label",{className:"pc-choice",children:[jsx("input",{type:"radio",name:"shippingMethodId",value:t.id,defaultChecked:i===0,required:true}),jsxs("span",{children:[t.name,jsx("small",{children:t.amountCents>0?h$1(t.amountCents):""})]})]},t.id))}):null]}),jsx(d,{id:d$1,labels:o,termsUrl:a$3.termsUrl,privacyUrl:a$3.privacyUrl}),r.error?jsx("p",{className:"pc-checkout-error",role:"alert",children:r.error}):null,jsx("button",{type:"submit",className:"pc-checkout-primary",disabled:r.busy,"data-test":"configurator-to-payment",children:o.checkoutToPayment})]})]})}export{_ as CheckoutStep};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
function m(r,n,i){let t=r%100===0;return new Intl.NumberFormat(n,{style:"currency",currency:i,minimumFractionDigits:t?0:2,maximumFractionDigits:t?0:2}).format(r/100)}export{m as a};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import {Component}from'react';var e=class extends Component{constructor(){super(...arguments);this.state={failed:false};}static getDerivedStateFromError(){return {failed:true}}render(){return this.state.failed?this.props.fallback:this.props.children}};export{e as a};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var d=Object.defineProperty;var e=(c,a)=>{for(var b in a)d(c,b,{get:a[b],enumerable:true});};export{e as a};
|