diff --git a/app/routes/_index/route.jsx b/app/routes/_index/route.jsx
index 7efaa16..8a38611 100644
--- a/app/routes/_index/route.jsx
+++ b/app/routes/_index/route.jsx
@@ -18,34 +18,30 @@ export default function App() {
return (
-
A short heading about [your app]
+
Native Product Reviews
- A tagline about [your app] that describes your value proposition.
+ Collect and display verified customer reviews without external databases.
{showForm && (
)}
-
- Product feature. Some detail about your feature and
- its benefit to your customer.
+ 100% Native. All data is stored in Shopify Metaobjects for speed and security.
-
- Product feature. Some detail about your feature and
- its benefit to your customer.
+ Verified Buyers. Automatically verify if a customer has purchased the product before they can review.
-
- Product feature. Some detail about your feature and
- its benefit to your customer.
+ No Extra Databases. Clean setup with zero hosting or database costs.
diff --git a/app/routes/app._index.jsx b/app/routes/app._index.jsx
index b9380fc..8db137f 100644
--- a/app/routes/app._index.jsx
+++ b/app/routes/app._index.jsx
@@ -1,331 +1,69 @@
-import { useEffect } from "react";
-import { useFetcher } from "react-router";
-import { useAppBridge } from "@shopify/app-bridge-react";
-import { boundary } from "@shopify/shopify-app-react-router/server";
import { authenticate } from "../shopify.server";
+import { useLoaderData } from "react-router";
export const loader = async ({ request }) => {
- await authenticate.admin(request);
+ const { admin, session } = await authenticate.admin(request);
- return null;
-};
-
-export const action = async ({ request }) => {
- const { admin } = await authenticate.admin(request);
- const color = ["Red", "Orange", "Yellow", "Green"][
- Math.floor(Math.random() * 4)
- ];
- const response = await admin.graphql(
- `#graphql
- mutation populateProduct($product: ProductCreateInput!) {
- productCreate(product: $product) {
- product {
+ // Fetch some reviews to show in the admin dashboard
+ const response = await admin.graphql(`#graphql
+ query {
+ metaobjects(first: 10, type: "product_review") {
+ edges {
+ node {
id
- title
- handle
- status
- variants(first: 10) {
- edges {
- node {
- id
- price
- barcode
- createdAt
- }
- }
- }
- demoInfo: metafield(namespace: "$app", key: "demo_info") {
- jsonValue
- }
+ fields { key value }
}
}
- }`,
- {
- variables: {
- product: {
- title: `${color} Snowboard`,
- metafields: [
- {
- namespace: "$app",
- key: "demo_info",
- value: "Created by React Router Template",
- },
- ],
- },
- },
- },
- );
- const responseJson = await response.json();
- const product = responseJson.data.productCreate.product;
- const variantId = product.variants.edges[0].node.id;
- const variantResponse = await admin.graphql(
- `#graphql
- mutation shopifyReactRouterTemplateUpdateVariant($productId: ID!, $variants: [ProductVariantsBulkInput!]!) {
- productVariantsBulkUpdate(productId: $productId, variants: $variants) {
- productVariants {
- id
- price
- barcode
- createdAt
- }
}
- }`,
- {
- variables: {
- productId: product.id,
- variants: [{ id: variantId, price: "100.00" }],
- },
- },
- );
- const variantResponseJson = await variantResponse.json();
- const metaobjectResponse = await admin.graphql(
- `#graphql
- mutation shopifyReactRouterTemplateUpsertMetaobject($handle: MetaobjectHandleInput!, $metaobject: MetaobjectUpsertInput!) {
- metaobjectUpsert(handle: $handle, metaobject: $metaobject) {
- metaobject {
- id
- handle
- title: field(key: "title") {
- jsonValue
- }
- description: field(key: "description") {
- jsonValue
- }
- }
- userErrors {
- field
- message
- }
- }
- }`,
- {
- variables: {
- handle: {
- type: "$app:example",
- handle: "demo-entry",
- },
- metaobject: {
- fields: [
- { key: "title", value: "Demo Entry" },
- {
- key: "description",
- value:
- "This metaobject was created by the Shopify app template to demonstrate the metaobject API.",
- },
- ],
- },
- },
- },
- );
- const metaobjectResponseJson = await metaobjectResponse.json();
+ }
+ `);
+ const data = await response.json();
+ const reviews = (data.data?.metaobjects?.edges || []).map(edge => {
+ const fields = {};
+ edge.node.fields.forEach(f => { fields[f.key] = f.value; });
+ return { id: edge.node.id, ...fields };
+ });
- return {
- product: responseJson.data.productCreate.product,
- variant: variantResponseJson.data.productVariantsBulkUpdate.productVariants,
- metaobject: metaobjectResponseJson.data.metaobjectUpsert.metaobject,
- };
+ return { reviews };
};
export default function Index() {
- const fetcher = useFetcher();
- const shopify = useAppBridge();
- const isLoading =
- ["loading", "submitting"].includes(fetcher.state) &&
- fetcher.formMethod === "POST";
-
- useEffect(() => {
- if (fetcher.data?.product?.id) {
- shopify.toast.show("Product created");
- }
- }, [fetcher.data?.product?.id, shopify]);
- const generateProduct = () => fetcher.submit({}, { method: "POST" });
+ const { reviews } = useLoaderData();
return (
-
-
- Generate a product
-
-
-
+
+
- This embedded app template uses{" "}
-
- App Bridge
- {" "}
- interface examples like an{" "}
- additional page in the app nav
- , as well as an{" "}
-
- Admin GraphQL
- {" "}
- mutation demo, to provide a starting point for app development.
+ All reviews are stored directly in Shopify Metaobjects. You can view, edit, or delete them here.
-
-
-
- Generate a product with GraphQL and get the JSON output for that
- product. Learn more about the{" "}
-
- productCreate
- {" "}
- mutation in our API references. Includes a product{" "}
-
- metafield
- {" "}
- and{" "}
-
- metaobject
-
- .
-
-
-
- Generate a product
-
- {fetcher.data?.product && (
- {
- shopify.intents.invoke?.("edit:shopify/Product", {
- value: fetcher.data?.product?.id,
- });
- }}
- target="_blank"
- variant="tertiary"
- >
- Edit product
-
- )}
-
- {fetcher.data?.product && (
-
-
-
-
- {JSON.stringify(fetcher.data.product, null, 2)}
-
-
- productVariantsBulkUpdate mutation
-
-
- {JSON.stringify(fetcher.data.variant, null, 2)}
-
+ {reviews.length === 0 ? (
+ No reviews found yet. Try adding one from your store!
+ ) : (
+
+ {reviews.map((r) => (
+
+
+ {r.customer_name || 'Anonymous'}
+ {r.content}
+ Product ID: {r.product_id}
+
-
- metaobjectUpsert mutation
-
-
-
- {JSON.stringify(fetcher.data.metaobject, null, 2)}
-
-
-
-
-
+ ))}
+
)}
-
+
- Framework:
-
- React Router
-
+ This app is 100% database-less.
-
- Interface:
-
- Polaris web components
-
-
-
- API:
-
- GraphQL
-
-
-
- Custom data:
-
- Metafields & metaobjects
-
-
-
- Database:
-
- Prisma
-
-
-
-
-
-
- Build an{" "}
-
- example app
-
-
-
- Explore Shopify's API with{" "}
-
- GraphiQL
-
-
+ Storage: Shopify Metaobjects
+ Auth: App Proxy & Admin API
+ Framework: React Router
);
}
-
-export const headers = (headersArgs) => {
- return boundary.headers(headersArgs);
-};
diff --git a/app/routes/app.additional.jsx b/app/routes/app.additional.jsx
deleted file mode 100644
index e7c8c72..0000000
--- a/app/routes/app.additional.jsx
+++ /dev/null
@@ -1,37 +0,0 @@
-export default function AdditionalPage() {
- return (
-
-
-
- The app template comes with an additional page which demonstrates how
- to create multiple pages within app navigation using{" "}
-
- App Bridge
-
- .
-
-
- To create your own page and have it show up in the app navigation, add
- a page inside app/routes, and a link to it in the{" "}
- <ui-nav-menu> component found in{" "}
- app/routes/app.jsx.
-
-
-
-
-
-
- App nav best practices
-
-
-
-
-
- );
-}
diff --git a/app/routes/app.jsx b/app/routes/app.jsx
index 95524f6..34c8be2 100644
--- a/app/routes/app.jsx
+++ b/app/routes/app.jsx
@@ -5,8 +5,6 @@ import { authenticate } from "../shopify.server";
export const loader = async ({ request }) => {
await authenticate.admin(request);
-
- // eslint-disable-next-line no-undef
return { apiKey: process.env.SHOPIFY_API_KEY || "" };
};
@@ -16,15 +14,13 @@ export default function App() {
return (
- Home
- Additional page
+ Reviews Dashboard
);
}
-// Shopify needs React Router to catch some thrown responses, so that their headers are included in the response.
export function ErrorBoundary() {
return boundary.error(useRouteError());
}