style: cleanup admin dashboard and update landing page branding
This commit is contained in:
parent
c91cffab72
commit
f017bb8847
@ -18,34 +18,30 @@ export default function App() {
|
|||||||
return (
|
return (
|
||||||
<div className={styles.index}>
|
<div className={styles.index}>
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
<h1 className={styles.heading}>A short heading about [your app]</h1>
|
<h1 className={styles.heading}>Native Product Reviews</h1>
|
||||||
<p className={styles.text}>
|
<p className={styles.text}>
|
||||||
A tagline about [your app] that describes your value proposition.
|
Collect and display verified customer reviews without external databases.
|
||||||
</p>
|
</p>
|
||||||
{showForm && (
|
{showForm && (
|
||||||
<Form className={styles.form} method="post" action="/auth/login">
|
<Form className={styles.form} method="post" action="/auth/login">
|
||||||
<label className={styles.label}>
|
<label className={styles.label}>
|
||||||
<span>Shop domain</span>
|
<span>Shop domain</span>
|
||||||
<input className={styles.input} type="text" name="shop" />
|
<input className={styles.input} type="text" name="shop" placeholder="my-shop.myshopify.com" />
|
||||||
<span>e.g: my-shop-domain.myshopify.com</span>
|
|
||||||
</label>
|
</label>
|
||||||
<button className={styles.button} type="submit">
|
<button className={styles.button} type="submit">
|
||||||
Log in
|
Install App
|
||||||
</button>
|
</button>
|
||||||
</Form>
|
</Form>
|
||||||
)}
|
)}
|
||||||
<ul className={styles.list}>
|
<ul className={styles.list}>
|
||||||
<li>
|
<li>
|
||||||
<strong>Product feature</strong>. Some detail about your feature and
|
<strong>100% Native</strong>. All data is stored in Shopify Metaobjects for speed and security.
|
||||||
its benefit to your customer.
|
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<strong>Product feature</strong>. Some detail about your feature and
|
<strong>Verified Buyers</strong>. Automatically verify if a customer has purchased the product before they can review.
|
||||||
its benefit to your customer.
|
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<strong>Product feature</strong>. Some detail about your feature and
|
<strong>No Extra Databases</strong>. Clean setup with zero hosting or database costs.
|
||||||
its benefit to your customer.
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -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 { authenticate } from "../shopify.server";
|
||||||
|
import { useLoaderData } from "react-router";
|
||||||
|
|
||||||
export const loader = async ({ request }) => {
|
export const loader = async ({ request }) => {
|
||||||
await authenticate.admin(request);
|
const { admin, session } = await authenticate.admin(request);
|
||||||
|
|
||||||
return null;
|
// Fetch some reviews to show in the admin dashboard
|
||||||
};
|
const response = await admin.graphql(`#graphql
|
||||||
|
query {
|
||||||
export const action = async ({ request }) => {
|
metaobjects(first: 10, type: "product_review") {
|
||||||
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 {
|
|
||||||
id
|
|
||||||
title
|
|
||||||
handle
|
|
||||||
status
|
|
||||||
variants(first: 10) {
|
|
||||||
edges {
|
edges {
|
||||||
node {
|
node {
|
||||||
id
|
id
|
||||||
price
|
fields { key value }
|
||||||
barcode
|
|
||||||
createdAt
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
demoInfo: metafield(namespace: "$app", key: "demo_info") {
|
|
||||||
jsonValue
|
|
||||||
}
|
}
|
||||||
}
|
`);
|
||||||
}
|
const data = await response.json();
|
||||||
}`,
|
const reviews = (data.data?.metaobjects?.edges || []).map(edge => {
|
||||||
{
|
const fields = {};
|
||||||
variables: {
|
edge.node.fields.forEach(f => { fields[f.key] = f.value; });
|
||||||
product: {
|
return { id: edge.node.id, ...fields };
|
||||||
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();
|
|
||||||
|
|
||||||
return {
|
return { reviews };
|
||||||
product: responseJson.data.productCreate.product,
|
|
||||||
variant: variantResponseJson.data.productVariantsBulkUpdate.productVariants,
|
|
||||||
metaobject: metaobjectResponseJson.data.metaobjectUpsert.metaobject,
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Index() {
|
export default function Index() {
|
||||||
const fetcher = useFetcher();
|
const { reviews } = useLoaderData();
|
||||||
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" });
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<s-page heading="Shopify app template">
|
<s-page heading="Product Reviews Dashboard">
|
||||||
<s-button slot="primary-action" onClick={generateProduct}>
|
<s-section heading="Manage your reviews">
|
||||||
Generate a product
|
<s-paragraph>
|
||||||
</s-button>
|
All reviews are stored directly in Shopify Metaobjects. You can view, edit, or delete them here.
|
||||||
|
</s-paragraph>
|
||||||
|
|
||||||
<s-section heading="Congrats on creating a new Shopify app 🎉">
|
{reviews.length === 0 ? (
|
||||||
<s-paragraph>
|
<s-paragraph>No reviews found yet. Try adding one from your store!</s-paragraph>
|
||||||
This embedded app template uses{" "}
|
) : (
|
||||||
<s-link
|
|
||||||
href="https://shopify.dev/docs/apps/tools/app-bridge"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
App Bridge
|
|
||||||
</s-link>{" "}
|
|
||||||
interface examples like an{" "}
|
|
||||||
<s-link href="/app/additional">additional page in the app nav</s-link>
|
|
||||||
, as well as an{" "}
|
|
||||||
<s-link
|
|
||||||
href="https://shopify.dev/docs/api/admin-graphql"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
Admin GraphQL
|
|
||||||
</s-link>{" "}
|
|
||||||
mutation demo, to provide a starting point for app development.
|
|
||||||
</s-paragraph>
|
|
||||||
</s-section>
|
|
||||||
<s-section heading="Get started with products">
|
|
||||||
<s-paragraph>
|
|
||||||
Generate a product with GraphQL and get the JSON output for that
|
|
||||||
product. Learn more about the{" "}
|
|
||||||
<s-link
|
|
||||||
href="https://shopify.dev/docs/api/admin-graphql/latest/mutations/productCreate"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
productCreate
|
|
||||||
</s-link>{" "}
|
|
||||||
mutation in our API references. Includes a product{" "}
|
|
||||||
<s-link
|
|
||||||
href="https://shopify.dev/docs/apps/build/custom-data/metafields"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
metafield
|
|
||||||
</s-link>{" "}
|
|
||||||
and{" "}
|
|
||||||
<s-link
|
|
||||||
href="https://shopify.dev/docs/apps/build/custom-data/metaobjects"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
metaobject
|
|
||||||
</s-link>
|
|
||||||
.
|
|
||||||
</s-paragraph>
|
|
||||||
<s-stack direction="inline" gap="base">
|
|
||||||
<s-button
|
|
||||||
onClick={generateProduct}
|
|
||||||
{...(isLoading ? { loading: true } : {})}
|
|
||||||
>
|
|
||||||
Generate a product
|
|
||||||
</s-button>
|
|
||||||
{fetcher.data?.product && (
|
|
||||||
<s-button
|
|
||||||
onClick={() => {
|
|
||||||
shopify.intents.invoke?.("edit:shopify/Product", {
|
|
||||||
value: fetcher.data?.product?.id,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
target="_blank"
|
|
||||||
variant="tertiary"
|
|
||||||
>
|
|
||||||
Edit product
|
|
||||||
</s-button>
|
|
||||||
)}
|
|
||||||
</s-stack>
|
|
||||||
{fetcher.data?.product && (
|
|
||||||
<s-section heading="productCreate mutation">
|
|
||||||
<s-stack direction="block" gap="base">
|
<s-stack direction="block" gap="base">
|
||||||
<s-box
|
{reviews.map((r) => (
|
||||||
padding="base"
|
<s-box padding="base" borderWidth="base" borderRadius="base" background="subdued" key={r.id}>
|
||||||
borderWidth="base"
|
<s-stack direction="block" gap="small">
|
||||||
borderRadius="base"
|
<s-text style={{ fontWeight: 'bold' }}>{r.customer_name || 'Anonymous'}</s-text>
|
||||||
background="subdued"
|
<s-text>{r.content}</s-text>
|
||||||
>
|
<s-text style={{ color: '#666', fontSize: '0.8em' }}>Product ID: {r.product_id}</s-text>
|
||||||
<pre style={{ margin: 0 }}>
|
</s-stack>
|
||||||
<code>{JSON.stringify(fetcher.data.product, null, 2)}</code>
|
</s-box>
|
||||||
</pre>
|
))}
|
||||||
</s-box>
|
|
||||||
|
|
||||||
<s-heading>productVariantsBulkUpdate mutation</s-heading>
|
|
||||||
<s-box
|
|
||||||
padding="base"
|
|
||||||
borderWidth="base"
|
|
||||||
borderRadius="base"
|
|
||||||
background="subdued"
|
|
||||||
>
|
|
||||||
<pre style={{ margin: 0 }}>
|
|
||||||
<code>{JSON.stringify(fetcher.data.variant, null, 2)}</code>
|
|
||||||
</pre>
|
|
||||||
</s-box>
|
|
||||||
|
|
||||||
<s-heading>metaobjectUpsert mutation</s-heading>
|
|
||||||
<s-box
|
|
||||||
padding="base"
|
|
||||||
borderWidth="base"
|
|
||||||
borderRadius="base"
|
|
||||||
background="subdued"
|
|
||||||
>
|
|
||||||
<pre style={{ margin: 0 }}>
|
|
||||||
<code>
|
|
||||||
{JSON.stringify(fetcher.data.metaobject, null, 2)}
|
|
||||||
</code>
|
|
||||||
</pre>
|
|
||||||
</s-box>
|
|
||||||
</s-stack>
|
</s-stack>
|
||||||
</s-section>
|
|
||||||
)}
|
)}
|
||||||
</s-section>
|
</s-section>
|
||||||
|
|
||||||
<s-section slot="aside" heading="App template specs">
|
<s-section slot="aside" heading="Tech Stack">
|
||||||
<s-paragraph>
|
<s-paragraph>
|
||||||
<s-text>Framework: </s-text>
|
This app is 100% database-less.
|
||||||
<s-link href="https://reactrouter.com/" target="_blank">
|
|
||||||
React Router
|
|
||||||
</s-link>
|
|
||||||
</s-paragraph>
|
</s-paragraph>
|
||||||
<s-paragraph>
|
|
||||||
<s-text>Interface: </s-text>
|
|
||||||
<s-link
|
|
||||||
href="https://shopify.dev/docs/api/app-home/using-polaris-components"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
Polaris web components
|
|
||||||
</s-link>
|
|
||||||
</s-paragraph>
|
|
||||||
<s-paragraph>
|
|
||||||
<s-text>API: </s-text>
|
|
||||||
<s-link
|
|
||||||
href="https://shopify.dev/docs/api/admin-graphql"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
GraphQL
|
|
||||||
</s-link>
|
|
||||||
</s-paragraph>
|
|
||||||
<s-paragraph>
|
|
||||||
<s-text>Custom data: </s-text>
|
|
||||||
<s-link
|
|
||||||
href="https://shopify.dev/docs/apps/build/custom-data"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
Metafields & metaobjects
|
|
||||||
</s-link>
|
|
||||||
</s-paragraph>
|
|
||||||
<s-paragraph>
|
|
||||||
<s-text>Database: </s-text>
|
|
||||||
<s-link href="https://www.prisma.io/" target="_blank">
|
|
||||||
Prisma
|
|
||||||
</s-link>
|
|
||||||
</s-paragraph>
|
|
||||||
</s-section>
|
|
||||||
|
|
||||||
<s-section slot="aside" heading="Next steps">
|
|
||||||
<s-unordered-list>
|
<s-unordered-list>
|
||||||
<s-list-item>
|
<s-list-item>Storage: Shopify Metaobjects</s-list-item>
|
||||||
Build an{" "}
|
<s-list-item>Auth: App Proxy & Admin API</s-list-item>
|
||||||
<s-link
|
<s-list-item>Framework: React Router</s-list-item>
|
||||||
href="https://shopify.dev/docs/apps/getting-started/build-app-example"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
example app
|
|
||||||
</s-link>
|
|
||||||
</s-list-item>
|
|
||||||
<s-list-item>
|
|
||||||
Explore Shopify's API with{" "}
|
|
||||||
<s-link
|
|
||||||
href="https://shopify.dev/docs/apps/tools/graphiql-admin-api"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
GraphiQL
|
|
||||||
</s-link>
|
|
||||||
</s-list-item>
|
|
||||||
</s-unordered-list>
|
</s-unordered-list>
|
||||||
</s-section>
|
</s-section>
|
||||||
</s-page>
|
</s-page>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const headers = (headersArgs) => {
|
|
||||||
return boundary.headers(headersArgs);
|
|
||||||
};
|
|
||||||
|
|||||||
@ -1,37 +0,0 @@
|
|||||||
export default function AdditionalPage() {
|
|
||||||
return (
|
|
||||||
<s-page heading="Additional page">
|
|
||||||
<s-section heading="Multiple pages">
|
|
||||||
<s-paragraph>
|
|
||||||
The app template comes with an additional page which demonstrates how
|
|
||||||
to create multiple pages within app navigation using{" "}
|
|
||||||
<s-link
|
|
||||||
href="https://shopify.dev/docs/apps/tools/app-bridge"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
App Bridge
|
|
||||||
</s-link>
|
|
||||||
.
|
|
||||||
</s-paragraph>
|
|
||||||
<s-paragraph>
|
|
||||||
To create your own page and have it show up in the app navigation, add
|
|
||||||
a page inside <code>app/routes</code>, and a link to it in the{" "}
|
|
||||||
<code><ui-nav-menu></code> component found in{" "}
|
|
||||||
<code>app/routes/app.jsx</code>.
|
|
||||||
</s-paragraph>
|
|
||||||
</s-section>
|
|
||||||
<s-section slot="aside" heading="Resources">
|
|
||||||
<s-unordered-list>
|
|
||||||
<s-list-item>
|
|
||||||
<s-link
|
|
||||||
href="https://shopify.dev/docs/apps/design-guidelines/navigation#app-nav"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
App nav best practices
|
|
||||||
</s-link>
|
|
||||||
</s-list-item>
|
|
||||||
</s-unordered-list>
|
|
||||||
</s-section>
|
|
||||||
</s-page>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -5,8 +5,6 @@ import { authenticate } from "../shopify.server";
|
|||||||
|
|
||||||
export const loader = async ({ request }) => {
|
export const loader = async ({ request }) => {
|
||||||
await authenticate.admin(request);
|
await authenticate.admin(request);
|
||||||
|
|
||||||
// eslint-disable-next-line no-undef
|
|
||||||
return { apiKey: process.env.SHOPIFY_API_KEY || "" };
|
return { apiKey: process.env.SHOPIFY_API_KEY || "" };
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -16,15 +14,13 @@ export default function App() {
|
|||||||
return (
|
return (
|
||||||
<AppProvider embedded apiKey={apiKey}>
|
<AppProvider embedded apiKey={apiKey}>
|
||||||
<s-app-nav>
|
<s-app-nav>
|
||||||
<s-link href="/app">Home</s-link>
|
<s-link href="/app">Reviews Dashboard</s-link>
|
||||||
<s-link href="/app/additional">Additional page</s-link>
|
|
||||||
</s-app-nav>
|
</s-app-nav>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</AppProvider>
|
</AppProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shopify needs React Router to catch some thrown responses, so that their headers are included in the response.
|
|
||||||
export function ErrorBoundary() {
|
export function ErrorBoundary() {
|
||||||
return boundary.error(useRouteError());
|
return boundary.error(useRouteError());
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user