@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 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; without WebGL the visitor gets a dimension
21
- sketch, the choices and the price.
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};