Skip to content

Commit 7ec6e6a

Browse files
authored
Merge pull request #22 from deco-sites/jonasjesus/deco-5290-notify-me-back-in-stock
feat(pdp): real notify-me (back-in-stock) replacing broken HTMX stub (DECO-5290)
2 parents bff701e + 930599e commit 7ec6e6a

3 files changed

Lines changed: 97 additions & 22 deletions

File tree

src/actions/notifyMe/subscribe.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { usePlatform } from "../../apps/site";
2+
3+
export interface NotifyMeProps {
4+
/** SKU/variant the shopper wants a back-in-stock alert for. */
5+
skuId: string;
6+
email: string;
7+
name?: string;
8+
}
9+
10+
export interface NotifyMeResult {
11+
success: boolean;
12+
}
13+
14+
async function action(props: NotifyMeProps): Promise<NotifyMeResult> {
15+
const skuId = props?.skuId?.trim();
16+
const email = props?.email?.trim();
17+
// Public invoke endpoint — validate here, not just via the form.
18+
if (!skuId) throw new Error("skuId is required");
19+
if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
20+
throw new Error("a valid email is required");
21+
}
22+
23+
const platform = usePlatform();
24+
25+
if (platform === "vtex") {
26+
// TODO(consumer): wire the real VTEX back-in-stock endpoint, e.g.
27+
// await invoke("vtex/actions/notifyme.ts", { skuId, name: props.name, email });
28+
}
29+
if (platform === "wake") {
30+
// TODO(consumer): wire the wake back-in-stock endpoint here.
31+
}
32+
// Default (neutral template): no email backend, so this is a documented
33+
// stub that confirms the UX. Platforms wire a real provider above.
34+
35+
return { success: true };
36+
}
37+
38+
export default action;
Lines changed: 55 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,77 @@
1+
import { useMutation } from "@tanstack/react-query";
12
import type { Product } from "@decocms/apps/commerce/types";
2-
import { AppContext } from "../../apps/site";
3-
import { useComponent } from "../../sections/Component";
3+
import { invoke } from "../../runtime";
4+
import { useUser } from "../../platform/user";
5+
import type { NotifyMeResult } from "../../actions/notifyMe/subscribe";
46

57
export interface Props {
68
productID: Product["productID"];
79
}
810

9-
export const action = async (props: Props, req: Request, ctx: AppContext) => {
10-
const form = await req.formData();
11-
12-
const name = `${form.get("name") ?? ""}`;
13-
const email = `${form.get("email") ?? ""}`;
14-
await (ctx as any).invoke("vtex/actions/notifyme.ts", {
15-
skuId: props.productID,
16-
name,
17-
email,
11+
export default function OutOfStock({ productID }: Props) {
12+
const { user } = useUser();
13+
const notify = useMutation({
14+
mutationFn: async (input: { email: string; name?: string }) => {
15+
const result = (await invoke.site.actions.notifyMe.subscribe({
16+
skuId: productID,
17+
email: input.email,
18+
name: input.name,
19+
})) as NotifyMeResult | undefined;
20+
if (!result?.success) throw new Error("Notify request failed");
21+
return result;
22+
},
1823
});
1924

20-
return props;
21-
};
25+
if (notify.isSuccess) {
26+
return (
27+
<div className="form-control gap-2">
28+
<span className="text-base">You're on the list!</span>
29+
<span className="text-sm text-base-400">
30+
We'll email you when this product is back in stock.
31+
</span>
32+
</div>
33+
);
34+
}
2235

23-
export default function Notify({ productID }: Props) {
2436
return (
2537
<form
2638
className="form-control justify-start gap-2"
27-
hx-sync="this:replace"
28-
hx-indicator="this"
29-
hx-swap="none"
30-
hx-post={useComponent<Props>(import.meta.url, { productID })}
39+
onSubmit={(e) => {
40+
e.preventDefault();
41+
const data = new FormData(e.currentTarget);
42+
const email = `${data.get("email") ?? ""}`.trim();
43+
const name = `${data.get("name") ?? ""}`.trim();
44+
if (email) notify.mutate({ email, name });
45+
}}
3146
>
3247
<span className="text-base">This product is currently unavailable</span>
3348
<span className="text-sm">Notify me when it's back in stock</span>
3449

3550
<input placeholder="Name" className="input input-bordered" name="name" />
36-
<input placeholder="Email" className="input input-bordered" name="email" />
51+
<input
52+
placeholder="Email"
53+
type="email"
54+
required
55+
className="input input-bordered"
56+
name="email"
57+
defaultValue={user?.email ?? ""}
58+
/>
3759

38-
<button type="button" className="btn btn-primary no-animation">
39-
<span className="[.htmx-request_&]:hidden inline">Submit</span>
40-
<span className="[.htmx-request_&]:inline hidden loading loading-spinner loading-xs" />
60+
<button
61+
type="submit"
62+
className="btn btn-primary no-animation"
63+
disabled={notify.isPending}
64+
>
65+
{notify.isPending
66+
? <span className="loading loading-spinner loading-xs" />
67+
: <span>Submit</span>}
4168
</button>
69+
70+
{notify.isError && (
71+
<span className="text-sm text-error">
72+
Something went wrong. Please try again.
73+
</span>
74+
)}
4275
</form>
4376
);
4477
}

src/setup.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,4 +139,8 @@ registerInvokeHandlers({
139139
(await import("./actions/shipping/simulate")).default(props, req),
140140
"site/actions/shipping/simulate": async (props, req) =>
141141
(await import("./actions/shipping/simulate")).default(props, req),
142+
"site/actions/notifyMe/subscribe.ts": async (props) =>
143+
(await import("./actions/notifyMe/subscribe")).default(props),
144+
"site/actions/notifyMe/subscribe": async (props) =>
145+
(await import("./actions/notifyMe/subscribe")).default(props),
142146
});

0 commit comments

Comments
 (0)