Test-app/app/routes/app.qrcodes.$id.jsx
Albez0-An7h 49d55588b0 feat: QR redirection app
- Scanning the QR code app redicts to page
- Option to chose if we want to redirect to page or checkout
2026-03-04 13:36:11 +05:30

338 lines
14 KiB
JavaScript

import { useState, useEffect } from "react";
import {
useActionData,
useLoaderData,
useSubmit,
useNavigation,
useNavigate,
useParams,
} from "react-router";
import { authenticate } from "../shopify.server";
import { boundary } from "@shopify/shopify-app-react-router/server";
import db from "../db.server";
import { getQRCode, validateQRCode } from "../models/QRCode.server";
export async function loader({ request, params }) {
const { admin } = await authenticate.admin(request);
if (params.id === "new") {
return {
destination: "product",
title: "",
};
}
return await getQRCode(Number(params.id), admin.graphql);
}
export async function action({ request, params }) {
const { session, redirect } = await authenticate.admin(request);
const { shop } = session;
/** @type {any} */
const data = {
...Object.fromEntries(await request.formData()),
shop,
};
if (data.action === "delete") {
await db.qRCode.delete({ where: { id: Number(params.id) } });
return redirect("/app");
}
const errors = validateQRCode(data);
if (errors) {
return new Response(JSON.stringify({ errors }), {
status: 422,
headers: {
"Content-Type": "application/json",
},
});
}
const qrCode =
params.id === "new"
? await db.qRCode.create({ data })
: await db.qRCode.update({ where: { id: Number(params.id) }, data });
return redirect(`/app/qrcodes/${qrCode.id}`);
}
export default function QRCodeForm() {
const navigate = useNavigate();
const { id } = useParams();
const qrCode = useLoaderData();
const [initialFormState, setInitialFormState] = useState(qrCode);
const [formState, setFormState] = useState(qrCode);
const errors = useActionData()?.errors || {};
const isSaving = useNavigation().state === "submitting";
const isDirty =
JSON.stringify(formState) !== JSON.stringify(initialFormState);
async function selectProduct() {
const products = await window.shopify.resourcePicker({
type: "product",
action: "select", // customized action verb, either 'select' or 'add',
});
if (products) {
const { images, id, variants, title, handle } = products[0];
setFormState({
...formState,
productId: id,
productVariantId: variants[0].id,
productTitle: title,
productHandle: handle,
productAlt: images[0]?.altText,
productImage: images[0]?.originalSrc,
});
}
}
function removeProduct() {
setFormState({
title: formState.title,
destination: formState.destination,
});
}
const productUrl = formState.productId
? `shopify://admin/products/${formState.productId.split("/").at(-1)}`
: "";
const submit = useSubmit();
function handleSave(e) {
e.preventDefault();
const data = {
title: formState.title,
productId: formState.productId || "",
productVariantId: formState.productVariantId || "",
productHandle: formState.productHandle || "",
destination: formState.destination,
};
submit(data, { method: "post" });
}
function handleDelete(e) {
e.preventDefault();
submit({ action: "delete" }, { method: "post" });
}
function handleReset() {
setFormState(initialFormState);
window.shopify.saveBar.hide("qr-code-form");
}
useEffect(() => {
if (isDirty) {
window.shopify.saveBar.show("qr-code-form");
} else {
window.shopify.saveBar.hide("qr-code-form");
}
return () => {
window.shopify.saveBar.hide("qr-code-form");
};
}, [isDirty]);
useEffect(() => {
setInitialFormState(qrCode);
setFormState(qrCode);
}, [id, qrCode]);
return (
<>
<form data-save-bar onSubmit={handleSave} onReset={handleReset}>
<s-page heading={initialFormState.title || "Create QR code"}>
<s-link
href="/app"
slot="breadcrumb-actions"
onClick={(e) => (isDirty ? e.preventDefault() : navigate("/app/"))}
>
QR Codes
</s-link>
{initialFormState.id &&
<s-button slot="secondary-actions" onClick={handleDelete}>Delete</s-button>}
<s-section heading="QR Code information">
<s-stack gap="base">
<s-text-field
label="Title"
details="Only store staff can see this title"
error={errors.title}
autoComplete="off"
name="title"
value={formState.title}
onInput={(e) =>
setFormState({ ...formState, title: e.target.value })
}
></s-text-field>
<s-stack gap="500" align="space-between" blockAlign="start">
<s-select
name="destination"
label="Scan destination"
value={formState.destination}
onChange={(e) =>
setFormState({ ...formState, destination: e.target.value })
}
>
<s-option
value="product"
selected={formState.destination === "product"}
>
Link to product page
</s-option>
<s-option
value="cart"
selected={formState.destination === "cart"}
>
Link to checkout page with product in the cart
</s-option>
</s-select>
{initialFormState.destinationUrl ? (
<s-link
variant="plain"
href={initialFormState.destinationUrl}
target="_blank"
>
Go to destination URL
</s-link>
) : null}
</s-stack>
<s-stack gap="small-400">
<s-stack direction="inline" gap="small-100" justifyContent="space-between">
<s-text color="subdued">Product</s-text>
{formState.productId ? (
<s-link
onClick={removeProduct}
accessibilityLabel="Remove the product from this QR Code"
variant="tertiary"
tone="neutral"
>
Clear
</s-link>
) : null}
</s-stack>
{formState.productId ? (
<s-stack
direction="inline"
justifyContent="space-between"
alignItems="center"
>
<s-stack
direction="inline"
gap="small-100"
alignItems="center"
>
<s-clickable
href={productUrl}
target="_blank"
accessibilityLabel={`Go to the product page for ${formState.productTitle}`}
borderRadius="base"
>
<s-box
padding="small-200"
border="base"
borderRadius="base"
background="subdued"
inlineSize="38px"
blockSize="38px"
>
{formState.productImage ? (
<s-image src={formState.productImage}></s-image>
) : (
<s-icon size="large" type="product" />
)}
</s-box>
</s-clickable>
<s-link href={productUrl} target="_blank">
{formState.productTitle}
</s-link>
</s-stack>
<s-stack direction="inline" gap="small">
<s-button
onClick={selectProduct}
accessibilityLabel="Change the product the QR code should be for"
>
Change
</s-button>
</s-stack>
</s-stack>
) : (
<s-button
onClick={selectProduct}
accessibilityLabel="Select the product the QR code should be for"
>
Select product
</s-button>
)}
</s-stack>
</s-stack>
</s-section>
<s-box slot="aside">
<s-section heading="Preview">
<s-stack gap="base">
<s-box
padding="base"
border="none"
borderRadius="base"
background="subdued"
>
{initialFormState.image ? (
<s-image
aspectRatio="1/0.8"
src={initialFormState.image}
alt="The QR Code for the current form"
/>
) : (
<s-stack
direction="inline"
alignItems="center"
justifyContent="center"
blockSize="198px"
>
<s-text color="subdued">
See a preview once you save
</s-text>
</s-stack>
)}
</s-box>
<s-stack
gap="small"
direction="inline"
alignItems="center"
justifyContent="space-between"
>
<s-button
disabled={!initialFormState.id}
href={`/qrcodes/${initialFormState.id}`}
target="_blank"
>
Go to public URL
</s-button>
<s-button
disabled={!initialFormState?.image}
href={initialFormState?.image}
download
variant="primary"
>
Download
</s-button>
</s-stack>
</s-stack>
</s-section>
</s-box>
</s-page>
</form>
</>
);
}
export const headers = (headersArgs) => {
return boundary.headers(headersArgs);
};