new changes in ui

This commit is contained in:
MOHAN 2026-04-14 01:44:37 +05:30
parent 0f9bd58a49
commit 5502ee7b3b
20 changed files with 1484 additions and 334 deletions

16
.gitignore vendored Normal file
View File

@ -0,0 +1,16 @@
@"
node_modules/
.env
.env.*
dist/
build/
.next/
coverage/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.DS_Store
Thumbs.db
.vscode/
.idea/
"@ | Out-File -Encoding utf8 .gitignore

View File

@ -0,0 +1,9 @@
// Generated by React Router
import "react-router";
declare module "react-router" {
interface Future {
v8_middleware: false
}
}

View File

@ -0,0 +1,88 @@
// Generated by React Router
import "react-router"
declare module "react-router" {
interface Register {
pages: Pages
routeFiles: RouteFiles
routeModules: RouteModules
}
}
type Pages = {
"/": {
params: {};
};
"/webhooks/app/scopes_update": {
params: {};
};
"/webhooks/app/uninstalled": {
params: {};
};
"/auth/login": {
params: {};
};
"/auth/*": {
params: {
"*": string;
};
};
"/app": {
params: {};
};
"/app/additional": {
params: {};
};
};
type RouteFiles = {
"root.jsx": {
id: "root";
page: "/" | "/webhooks/app/scopes_update" | "/webhooks/app/uninstalled" | "/auth/login" | "/auth/*" | "/app" | "/app/additional";
};
"routes/webhooks.app.scopes_update.jsx": {
id: "routes/webhooks.app.scopes_update";
page: "/webhooks/app/scopes_update";
};
"routes/webhooks.app.uninstalled.jsx": {
id: "routes/webhooks.app.uninstalled";
page: "/webhooks/app/uninstalled";
};
"routes/auth.login/route.jsx": {
id: "routes/auth.login";
page: "/auth/login";
};
"routes/auth.$.jsx": {
id: "routes/auth.$";
page: "/auth/*";
};
"routes/_index/route.jsx": {
id: "routes/_index";
page: "/";
};
"routes/app.jsx": {
id: "routes/app";
page: "/app" | "/app/additional";
};
"routes/app.additional.jsx": {
id: "routes/app.additional";
page: "/app/additional";
};
"routes/app._index.jsx": {
id: "routes/app._index";
page: "/app";
};
};
type RouteModules = {
"root": typeof import("./app/root.jsx");
"routes/webhooks.app.scopes_update": typeof import("./app/routes/webhooks.app.scopes_update.jsx");
"routes/webhooks.app.uninstalled": typeof import("./app/routes/webhooks.app.uninstalled.jsx");
"routes/auth.login": typeof import("./app/routes/auth.login/route.jsx");
"routes/auth.$": typeof import("./app/routes/auth.$.jsx");
"routes/_index": typeof import("./app/routes/_index/route.jsx");
"routes/app": typeof import("./app/routes/app.jsx");
"routes/app.additional": typeof import("./app/routes/app.additional.jsx");
"routes/app._index": typeof import("./app/routes/app._index.jsx");
};

18
.react-router/types/+server-build.d.ts vendored Normal file
View File

@ -0,0 +1,18 @@
// Generated by React Router
declare module "virtual:react-router/server-build" {
import { ServerBuild } from "react-router";
export const assets: ServerBuild["assets"];
export const assetsBuildDirectory: ServerBuild["assetsBuildDirectory"];
export const basename: ServerBuild["basename"];
export const entry: ServerBuild["entry"];
export const future: ServerBuild["future"];
export const isSpaMode: ServerBuild["isSpaMode"];
export const prerender: ServerBuild["prerender"];
export const publicPath: ServerBuild["publicPath"];
export const routeDiscovery: ServerBuild["routeDiscovery"];
export const routes: ServerBuild["routes"];
export const ssr: ServerBuild["ssr"];
export const allowedActionOrigins: ServerBuild["allowedActionOrigins"];
export const unstable_getCriticalCss: ServerBuild["unstable_getCriticalCss"];
}

View File

@ -0,0 +1,68 @@
// Generated by React Router
import type { GetInfo, GetAnnotations } from "react-router/internal";
type Module = typeof import("../root.js")
type Info = GetInfo<{
file: "root.jsx",
module: Module
}>
type Matches = [{
id: "root";
module: typeof import("../root.js");
}];
type Annotations = GetAnnotations<Info & { module: Module, matches: Matches }>;
export namespace Route {
// links
export type LinkDescriptors = Annotations["LinkDescriptors"];
export type LinksFunction = Annotations["LinksFunction"];
// meta
export type MetaArgs = Annotations["MetaArgs"];
export type MetaDescriptors = Annotations["MetaDescriptors"];
export type MetaFunction = Annotations["MetaFunction"];
// headers
export type HeadersArgs = Annotations["HeadersArgs"];
export type HeadersFunction = Annotations["HeadersFunction"];
// middleware
export type MiddlewareFunction = Annotations["MiddlewareFunction"];
// clientMiddleware
export type ClientMiddlewareFunction = Annotations["ClientMiddlewareFunction"];
// loader
export type LoaderArgs = Annotations["LoaderArgs"];
// clientLoader
export type ClientLoaderArgs = Annotations["ClientLoaderArgs"];
// action
export type ActionArgs = Annotations["ActionArgs"];
// clientAction
export type ClientActionArgs = Annotations["ClientActionArgs"];
// HydrateFallback
export type HydrateFallbackProps = Annotations["HydrateFallbackProps"];
// ServerHydrateFallback
export type ServerHydrateFallbackProps = Annotations["ServerHydrateFallbackProps"];
// Component
export type ComponentProps = Annotations["ComponentProps"];
// ServerComponent
export type ServerComponentProps = Annotations["ServerComponentProps"];
// ErrorBoundary
export type ErrorBoundaryProps = Annotations["ErrorBoundaryProps"];
// ServerErrorBoundary
export type ServerErrorBoundaryProps = Annotations["ServerErrorBoundaryProps"];
}

View File

@ -0,0 +1,74 @@
// Generated by React Router
import type { GetInfo, GetAnnotations } from "react-router/internal";
type Module = typeof import("../app._index.js")
type Info = GetInfo<{
file: "routes/app._index.jsx",
module: Module
}>
type Matches = [{
id: "root";
module: typeof import("../../root.js");
}, {
id: "routes/app";
module: typeof import("../app.js");
}, {
id: "routes/app._index";
module: typeof import("../app._index.js");
}];
type Annotations = GetAnnotations<Info & { module: Module, matches: Matches }>;
export namespace Route {
// links
export type LinkDescriptors = Annotations["LinkDescriptors"];
export type LinksFunction = Annotations["LinksFunction"];
// meta
export type MetaArgs = Annotations["MetaArgs"];
export type MetaDescriptors = Annotations["MetaDescriptors"];
export type MetaFunction = Annotations["MetaFunction"];
// headers
export type HeadersArgs = Annotations["HeadersArgs"];
export type HeadersFunction = Annotations["HeadersFunction"];
// middleware
export type MiddlewareFunction = Annotations["MiddlewareFunction"];
// clientMiddleware
export type ClientMiddlewareFunction = Annotations["ClientMiddlewareFunction"];
// loader
export type LoaderArgs = Annotations["LoaderArgs"];
// clientLoader
export type ClientLoaderArgs = Annotations["ClientLoaderArgs"];
// action
export type ActionArgs = Annotations["ActionArgs"];
// clientAction
export type ClientActionArgs = Annotations["ClientActionArgs"];
// HydrateFallback
export type HydrateFallbackProps = Annotations["HydrateFallbackProps"];
// ServerHydrateFallback
export type ServerHydrateFallbackProps = Annotations["ServerHydrateFallbackProps"];
// Component
export type ComponentProps = Annotations["ComponentProps"];
// ServerComponent
export type ServerComponentProps = Annotations["ServerComponentProps"];
// ErrorBoundary
export type ErrorBoundaryProps = Annotations["ErrorBoundaryProps"];
// ServerErrorBoundary
export type ServerErrorBoundaryProps = Annotations["ServerErrorBoundaryProps"];
}

View File

@ -0,0 +1,74 @@
// Generated by React Router
import type { GetInfo, GetAnnotations } from "react-router/internal";
type Module = typeof import("../app.additional.js")
type Info = GetInfo<{
file: "routes/app.additional.jsx",
module: Module
}>
type Matches = [{
id: "root";
module: typeof import("../../root.js");
}, {
id: "routes/app";
module: typeof import("../app.js");
}, {
id: "routes/app.additional";
module: typeof import("../app.additional.js");
}];
type Annotations = GetAnnotations<Info & { module: Module, matches: Matches }>;
export namespace Route {
// links
export type LinkDescriptors = Annotations["LinkDescriptors"];
export type LinksFunction = Annotations["LinksFunction"];
// meta
export type MetaArgs = Annotations["MetaArgs"];
export type MetaDescriptors = Annotations["MetaDescriptors"];
export type MetaFunction = Annotations["MetaFunction"];
// headers
export type HeadersArgs = Annotations["HeadersArgs"];
export type HeadersFunction = Annotations["HeadersFunction"];
// middleware
export type MiddlewareFunction = Annotations["MiddlewareFunction"];
// clientMiddleware
export type ClientMiddlewareFunction = Annotations["ClientMiddlewareFunction"];
// loader
export type LoaderArgs = Annotations["LoaderArgs"];
// clientLoader
export type ClientLoaderArgs = Annotations["ClientLoaderArgs"];
// action
export type ActionArgs = Annotations["ActionArgs"];
// clientAction
export type ClientActionArgs = Annotations["ClientActionArgs"];
// HydrateFallback
export type HydrateFallbackProps = Annotations["HydrateFallbackProps"];
// ServerHydrateFallback
export type ServerHydrateFallbackProps = Annotations["ServerHydrateFallbackProps"];
// Component
export type ComponentProps = Annotations["ComponentProps"];
// ServerComponent
export type ServerComponentProps = Annotations["ServerComponentProps"];
// ErrorBoundary
export type ErrorBoundaryProps = Annotations["ErrorBoundaryProps"];
// ServerErrorBoundary
export type ServerErrorBoundaryProps = Annotations["ServerErrorBoundaryProps"];
}

View File

@ -0,0 +1,71 @@
// Generated by React Router
import type { GetInfo, GetAnnotations } from "react-router/internal";
type Module = typeof import("../app.js")
type Info = GetInfo<{
file: "routes/app.jsx",
module: Module
}>
type Matches = [{
id: "root";
module: typeof import("../../root.js");
}, {
id: "routes/app";
module: typeof import("../app.js");
}];
type Annotations = GetAnnotations<Info & { module: Module, matches: Matches }>;
export namespace Route {
// links
export type LinkDescriptors = Annotations["LinkDescriptors"];
export type LinksFunction = Annotations["LinksFunction"];
// meta
export type MetaArgs = Annotations["MetaArgs"];
export type MetaDescriptors = Annotations["MetaDescriptors"];
export type MetaFunction = Annotations["MetaFunction"];
// headers
export type HeadersArgs = Annotations["HeadersArgs"];
export type HeadersFunction = Annotations["HeadersFunction"];
// middleware
export type MiddlewareFunction = Annotations["MiddlewareFunction"];
// clientMiddleware
export type ClientMiddlewareFunction = Annotations["ClientMiddlewareFunction"];
// loader
export type LoaderArgs = Annotations["LoaderArgs"];
// clientLoader
export type ClientLoaderArgs = Annotations["ClientLoaderArgs"];
// action
export type ActionArgs = Annotations["ActionArgs"];
// clientAction
export type ClientActionArgs = Annotations["ClientActionArgs"];
// HydrateFallback
export type HydrateFallbackProps = Annotations["HydrateFallbackProps"];
// ServerHydrateFallback
export type ServerHydrateFallbackProps = Annotations["ServerHydrateFallbackProps"];
// Component
export type ComponentProps = Annotations["ComponentProps"];
// ServerComponent
export type ServerComponentProps = Annotations["ServerComponentProps"];
// ErrorBoundary
export type ErrorBoundaryProps = Annotations["ErrorBoundaryProps"];
// ServerErrorBoundary
export type ServerErrorBoundaryProps = Annotations["ServerErrorBoundaryProps"];
}

View File

@ -0,0 +1,71 @@
// Generated by React Router
import type { GetInfo, GetAnnotations } from "react-router/internal";
type Module = typeof import("../auth.$.js")
type Info = GetInfo<{
file: "routes/auth.$.jsx",
module: Module
}>
type Matches = [{
id: "root";
module: typeof import("../../root.js");
}, {
id: "routes/auth.$";
module: typeof import("../auth.$.js");
}];
type Annotations = GetAnnotations<Info & { module: Module, matches: Matches }>;
export namespace Route {
// links
export type LinkDescriptors = Annotations["LinkDescriptors"];
export type LinksFunction = Annotations["LinksFunction"];
// meta
export type MetaArgs = Annotations["MetaArgs"];
export type MetaDescriptors = Annotations["MetaDescriptors"];
export type MetaFunction = Annotations["MetaFunction"];
// headers
export type HeadersArgs = Annotations["HeadersArgs"];
export type HeadersFunction = Annotations["HeadersFunction"];
// middleware
export type MiddlewareFunction = Annotations["MiddlewareFunction"];
// clientMiddleware
export type ClientMiddlewareFunction = Annotations["ClientMiddlewareFunction"];
// loader
export type LoaderArgs = Annotations["LoaderArgs"];
// clientLoader
export type ClientLoaderArgs = Annotations["ClientLoaderArgs"];
// action
export type ActionArgs = Annotations["ActionArgs"];
// clientAction
export type ClientActionArgs = Annotations["ClientActionArgs"];
// HydrateFallback
export type HydrateFallbackProps = Annotations["HydrateFallbackProps"];
// ServerHydrateFallback
export type ServerHydrateFallbackProps = Annotations["ServerHydrateFallbackProps"];
// Component
export type ComponentProps = Annotations["ComponentProps"];
// ServerComponent
export type ServerComponentProps = Annotations["ServerComponentProps"];
// ErrorBoundary
export type ErrorBoundaryProps = Annotations["ErrorBoundaryProps"];
// ServerErrorBoundary
export type ServerErrorBoundaryProps = Annotations["ServerErrorBoundaryProps"];
}

View File

@ -0,0 +1,71 @@
// Generated by React Router
import type { GetInfo, GetAnnotations } from "react-router/internal";
type Module = typeof import("../webhooks.app.scopes_update.js")
type Info = GetInfo<{
file: "routes/webhooks.app.scopes_update.jsx",
module: Module
}>
type Matches = [{
id: "root";
module: typeof import("../../root.js");
}, {
id: "routes/webhooks.app.scopes_update";
module: typeof import("../webhooks.app.scopes_update.js");
}];
type Annotations = GetAnnotations<Info & { module: Module, matches: Matches }>;
export namespace Route {
// links
export type LinkDescriptors = Annotations["LinkDescriptors"];
export type LinksFunction = Annotations["LinksFunction"];
// meta
export type MetaArgs = Annotations["MetaArgs"];
export type MetaDescriptors = Annotations["MetaDescriptors"];
export type MetaFunction = Annotations["MetaFunction"];
// headers
export type HeadersArgs = Annotations["HeadersArgs"];
export type HeadersFunction = Annotations["HeadersFunction"];
// middleware
export type MiddlewareFunction = Annotations["MiddlewareFunction"];
// clientMiddleware
export type ClientMiddlewareFunction = Annotations["ClientMiddlewareFunction"];
// loader
export type LoaderArgs = Annotations["LoaderArgs"];
// clientLoader
export type ClientLoaderArgs = Annotations["ClientLoaderArgs"];
// action
export type ActionArgs = Annotations["ActionArgs"];
// clientAction
export type ClientActionArgs = Annotations["ClientActionArgs"];
// HydrateFallback
export type HydrateFallbackProps = Annotations["HydrateFallbackProps"];
// ServerHydrateFallback
export type ServerHydrateFallbackProps = Annotations["ServerHydrateFallbackProps"];
// Component
export type ComponentProps = Annotations["ComponentProps"];
// ServerComponent
export type ServerComponentProps = Annotations["ServerComponentProps"];
// ErrorBoundary
export type ErrorBoundaryProps = Annotations["ErrorBoundaryProps"];
// ServerErrorBoundary
export type ServerErrorBoundaryProps = Annotations["ServerErrorBoundaryProps"];
}

View File

@ -0,0 +1,71 @@
// Generated by React Router
import type { GetInfo, GetAnnotations } from "react-router/internal";
type Module = typeof import("../webhooks.app.uninstalled.js")
type Info = GetInfo<{
file: "routes/webhooks.app.uninstalled.jsx",
module: Module
}>
type Matches = [{
id: "root";
module: typeof import("../../root.js");
}, {
id: "routes/webhooks.app.uninstalled";
module: typeof import("../webhooks.app.uninstalled.js");
}];
type Annotations = GetAnnotations<Info & { module: Module, matches: Matches }>;
export namespace Route {
// links
export type LinkDescriptors = Annotations["LinkDescriptors"];
export type LinksFunction = Annotations["LinksFunction"];
// meta
export type MetaArgs = Annotations["MetaArgs"];
export type MetaDescriptors = Annotations["MetaDescriptors"];
export type MetaFunction = Annotations["MetaFunction"];
// headers
export type HeadersArgs = Annotations["HeadersArgs"];
export type HeadersFunction = Annotations["HeadersFunction"];
// middleware
export type MiddlewareFunction = Annotations["MiddlewareFunction"];
// clientMiddleware
export type ClientMiddlewareFunction = Annotations["ClientMiddlewareFunction"];
// loader
export type LoaderArgs = Annotations["LoaderArgs"];
// clientLoader
export type ClientLoaderArgs = Annotations["ClientLoaderArgs"];
// action
export type ActionArgs = Annotations["ActionArgs"];
// clientAction
export type ClientActionArgs = Annotations["ClientActionArgs"];
// HydrateFallback
export type HydrateFallbackProps = Annotations["HydrateFallbackProps"];
// ServerHydrateFallback
export type ServerHydrateFallbackProps = Annotations["ServerHydrateFallbackProps"];
// Component
export type ComponentProps = Annotations["ComponentProps"];
// ServerComponent
export type ServerComponentProps = Annotations["ServerComponentProps"];
// ErrorBoundary
export type ErrorBoundaryProps = Annotations["ErrorBoundaryProps"];
// ServerErrorBoundary
export type ServerErrorBoundaryProps = Annotations["ServerErrorBoundaryProps"];
}

View File

@ -0,0 +1,71 @@
// Generated by React Router
import type { GetInfo, GetAnnotations } from "react-router/internal";
type Module = typeof import("../route.js")
type Info = GetInfo<{
file: "routes/_index/route.jsx",
module: Module
}>
type Matches = [{
id: "root";
module: typeof import("../../../root.js");
}, {
id: "routes/_index";
module: typeof import("../route.js");
}];
type Annotations = GetAnnotations<Info & { module: Module, matches: Matches }>;
export namespace Route {
// links
export type LinkDescriptors = Annotations["LinkDescriptors"];
export type LinksFunction = Annotations["LinksFunction"];
// meta
export type MetaArgs = Annotations["MetaArgs"];
export type MetaDescriptors = Annotations["MetaDescriptors"];
export type MetaFunction = Annotations["MetaFunction"];
// headers
export type HeadersArgs = Annotations["HeadersArgs"];
export type HeadersFunction = Annotations["HeadersFunction"];
// middleware
export type MiddlewareFunction = Annotations["MiddlewareFunction"];
// clientMiddleware
export type ClientMiddlewareFunction = Annotations["ClientMiddlewareFunction"];
// loader
export type LoaderArgs = Annotations["LoaderArgs"];
// clientLoader
export type ClientLoaderArgs = Annotations["ClientLoaderArgs"];
// action
export type ActionArgs = Annotations["ActionArgs"];
// clientAction
export type ClientActionArgs = Annotations["ClientActionArgs"];
// HydrateFallback
export type HydrateFallbackProps = Annotations["HydrateFallbackProps"];
// ServerHydrateFallback
export type ServerHydrateFallbackProps = Annotations["ServerHydrateFallbackProps"];
// Component
export type ComponentProps = Annotations["ComponentProps"];
// ServerComponent
export type ServerComponentProps = Annotations["ServerComponentProps"];
// ErrorBoundary
export type ErrorBoundaryProps = Annotations["ErrorBoundaryProps"];
// ServerErrorBoundary
export type ServerErrorBoundaryProps = Annotations["ServerErrorBoundaryProps"];
}

View File

@ -0,0 +1,71 @@
// Generated by React Router
import type { GetInfo, GetAnnotations } from "react-router/internal";
type Module = typeof import("../route.js")
type Info = GetInfo<{
file: "routes/auth.login/route.jsx",
module: Module
}>
type Matches = [{
id: "root";
module: typeof import("../../../root.js");
}, {
id: "routes/auth.login";
module: typeof import("../route.js");
}];
type Annotations = GetAnnotations<Info & { module: Module, matches: Matches }>;
export namespace Route {
// links
export type LinkDescriptors = Annotations["LinkDescriptors"];
export type LinksFunction = Annotations["LinksFunction"];
// meta
export type MetaArgs = Annotations["MetaArgs"];
export type MetaDescriptors = Annotations["MetaDescriptors"];
export type MetaFunction = Annotations["MetaFunction"];
// headers
export type HeadersArgs = Annotations["HeadersArgs"];
export type HeadersFunction = Annotations["HeadersFunction"];
// middleware
export type MiddlewareFunction = Annotations["MiddlewareFunction"];
// clientMiddleware
export type ClientMiddlewareFunction = Annotations["ClientMiddlewareFunction"];
// loader
export type LoaderArgs = Annotations["LoaderArgs"];
// clientLoader
export type ClientLoaderArgs = Annotations["ClientLoaderArgs"];
// action
export type ActionArgs = Annotations["ActionArgs"];
// clientAction
export type ClientActionArgs = Annotations["ClientActionArgs"];
// HydrateFallback
export type HydrateFallbackProps = Annotations["HydrateFallbackProps"];
// ServerHydrateFallback
export type ServerHydrateFallbackProps = Annotations["ServerHydrateFallbackProps"];
// Component
export type ComponentProps = Annotations["ComponentProps"];
// ServerComponent
export type ServerComponentProps = Annotations["ServerComponentProps"];
// ErrorBoundary
export type ErrorBoundaryProps = Annotations["ErrorBoundaryProps"];
// ServerErrorBoundary
export type ServerErrorBoundaryProps = Annotations["ServerErrorBoundaryProps"];
}

View File

@ -18,9 +18,11 @@ 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}>Race Nation Shopify Import</h1>
<p className={styles.text}> <p className={styles.text}>
A tagline about [your app] that describes your value proposition. Install the embedded app to connect your store, run the KYT import
pipeline, and monitor long-running product sync jobs from inside
Shopify Admin.
</p> </p>
{showForm && ( {showForm && (
<Form className={styles.form} method="post" action="/auth/login"> <Form className={styles.form} method="post" action="/auth/login">
@ -36,16 +38,18 @@ export default function App() {
)} )}
<ul className={styles.list}> <ul className={styles.list}>
<li> <li>
<strong>Product feature</strong>. Some detail about your feature and <strong>Shopify-authenticated workflow</strong>. Connect the app to
its benefit to your customer. a store and keep Shopify session/auth behavior inside the embedded
app.
</li> </li>
<li> <li>
<strong>Product feature</strong>. Some detail about your feature and <strong>KYT pipeline controls</strong>. Launch scrape, image,
its benefit to your customer. watermark, upload, conversion, and product upsert jobs from one
dashboard.
</li> </li>
<li> <li>
<strong>Product feature</strong>. Some detail about your feature and <strong>Live status tracking</strong>. Follow job progress and see
its benefit to your customer. step-by-step status while the backend runs.
</li> </li>
</ul> </ul>
</div> </div>

View File

@ -1,327 +1,422 @@
import { useEffect } from "react"; import { useEffect, useMemo, useState } from "react";
import { useFetcher } from "react-router"; import { useFetcher, useLoaderData } from "react-router";
import { useAppBridge } from "@shopify/app-bridge-react"; import { useAppBridge } from "@shopify/app-bridge-react";
import { boundary } from "@shopify/shopify-app-react-router/server"; import { boundary } from "@shopify/shopify-app-react-router/server";
import { authenticate } from "../shopify.server"; import { authenticate } from "../shopify.server";
import styles from "../styles/app-dashboard.module.css";
function getBackendApiUrl() {
return String(process.env.BACKEND_API_URL || "http://localhost:3002").replace(/\/+$/, "");
}
async function readJsonSafe(response) {
const text = await response.text();
try {
return text ? JSON.parse(text) : null;
} catch {
return { raw: text };
}
}
export const loader = async ({ request }) => { export const loader = async ({ request }) => {
await authenticate.admin(request); const { session } = await authenticate.admin(request);
const backendApiUrl = getBackendApiUrl();
const shop = session.shop;
return null; let connection = null;
}; let currentJob = null;
try {
const response = await fetch(
`${backendApiUrl}/shops/${encodeURIComponent(shop)}`,
);
connection = await readJsonSafe(response);
} catch (error) {
connection = {
status: 0,
message: `Backend unavailable: ${error.message}`,
};
}
export const action = async ({ request }) => { try {
const { admin } = await authenticate.admin(request); const statusResponse = await fetch(
const color = ["Red", "Orange", "Yellow", "Green"][ `${backendApiUrl}/pipeline/status/${encodeURIComponent(shop)}`,
Math.floor(Math.random() * 4) );
]; if (statusResponse.ok) {
const response = await admin.graphql( currentJob = await readJsonSafe(statusResponse);
`#graphql }
mutation populateProduct($product: ProductCreateInput!) { } catch {
productCreate(product: $product) { currentJob = null;
product { }
id
title
handle
status
variants(first: 10) {
edges {
node {
id
price
barcode
createdAt
}
}
}
demoInfo: metafield(namespace: "$app", key: "demo_info") {
jsonValue
}
}
}
}`,
{
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();
return { return {
product: responseJson.data.productCreate.product, shop,
variant: variantResponseJson.data.productVariantsBulkUpdate.productVariants, backendApiUrl,
metaobject: metaobjectResponseJson.data.metaobjectUpsert.metaobject, connection,
currentJob,
}; };
}; };
export const action = async ({ request }) => {
const { session } = await authenticate.admin(request);
const formData = await request.formData();
const backendApiUrl = getBackendApiUrl();
const shop = session.shop;
const limitValue = String(formData.get("limit") || "").trim();
const limit = limitValue ? Number(limitValue) : null;
try {
const response = await fetch(`${backendApiUrl}/pipeline/run`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
shop,
limit: Number.isFinite(limit) && limit > 0 ? limit : null,
}),
});
const payload = await readJsonSafe(response);
if (!response.ok) {
return {
ok: false,
error: payload?.error || "Failed to start import job.",
};
}
return {
ok: true,
jobId: payload.jobId,
shop,
limit: payload.limit,
backendApiUrl,
};
} catch (error) {
return {
ok: false,
error: error.message,
};
}
};
function FieldState({ fields }) {
if (!fields) {
return <s-text tone="subdued">Your store is not ready yet.</s-text>;
}
return (
<s-stack direction="block" gap="tight">
{Object.entries(fields).map(([key, value]) => (
<s-inline-stack key={key} gap="base" alignItems="center">
<s-text>{key}</s-text>
<s-badge tone={value === "present" ? "success" : "critical"}>
{value}
</s-badge>
</s-inline-stack>
))}
</s-stack>
);
}
function JobSummary({ job }) {
if (!job) {
return (
<div className={styles.emptyPanel}>
<div className={styles.emptyGlow}></div>
<h3>No import in progress</h3>
<p>Start an import to see the live progress and current step here.</p>
</div>
);
}
const progressPercent = Math.max(
0,
Math.min(
100,
Math.round(((job.stepIndex || 0) / (job.totalSteps || 6)) * 100),
),
);
return (
<div className={styles.jobPanel}>
<div className={styles.jobHeader}>
<div>
<p className={styles.eyebrow}>Current import</p>
<h3 className={styles.panelTitle}>{job.id}</h3>
</div>
<span
className={`${styles.statusPill} ${
job.status === "done"
? styles.success
: job.status === "error"
? styles.error
: styles.active
}`}
>
{job.status}
</span>
</div>
<div className={styles.progressMeta}>
<span>{job.step || "-"}</span>
<span>{progressPercent}% complete</span>
</div>
<div className={styles.progressTrack}>
<div
className={styles.progressFill}
style={{ width: `${progressPercent}%` }}
/>
</div>
<div className={styles.statGrid}>
<div className={styles.statCard}>
<span className={styles.statLabel}>Step</span>
<strong>
{job.stepIndex || 0}/{job.totalSteps || 6}
</strong>
</div>
<div className={styles.statCard}>
<span className={styles.statLabel}>Started</span>
<strong>{job.startedAt ? new Date(job.startedAt).toLocaleString() : "-"}</strong>
</div>
<div className={styles.statCard}>
<span className={styles.statLabel}>Updated</span>
<strong>{job.updatedAt ? new Date(job.updatedAt).toLocaleString() : "-"}</strong>
</div>
</div>
<div className={styles.detailCard}>
<span className={styles.statLabel}>Detail</span>
<p>{job.detail || "-"}</p>
</div>
{job.error ? (
<div className={`${styles.detailCard} ${styles.errorCard}`}>
<span className={styles.statLabel}>Error</span>
<p>{job.error}</p>
</div>
) : null}
{job.summary ? (
<div className={styles.jsonCard}>
<span className={styles.statLabel}>Summary</span>
<pre style={{ margin: 0, whiteSpace: "pre-wrap" }}>
<code>{JSON.stringify(job.summary, null, 2)}</code>
</pre>
</div>
) : null}
</div>
);
}
export default function Index() { export default function Index() {
const { shop, backendApiUrl, connection, currentJob } = useLoaderData();
const fetcher = useFetcher(); const fetcher = useFetcher();
const shopify = useAppBridge(); const shopify = useAppBridge();
const isLoading = const [job, setJob] = useState(currentJob);
const setupUrl = `${backendApiUrl}/auth/login?shop=${encodeURIComponent(shop)}`;
const isSubmitting =
["loading", "submitting"].includes(fetcher.state) && ["loading", "submitting"].includes(fetcher.state) &&
fetcher.formMethod === "POST"; fetcher.formMethod === "POST";
useEffect(() => { const connectionState = useMemo(() => {
if (fetcher.data?.product?.id) { if (connection?.status === 1) {
shopify.toast.show("Product created"); return "Ready";
} }
}, [fetcher.data?.product?.id, shopify]); return "Setup needed";
const generateProduct = () => fetcher.submit({}, { method: "POST" }); }, [connection]);
const connectionMessage = useMemo(() => {
if (connection?.status === 1) {
return "Your store is connected and ready for import.";
}
if (String(connection?.message || "").toLowerCase().includes("fetch failed")) {
return "We could not reach the import service right now.";
}
if (String(connection?.message || "").toLowerCase().includes("shop not found")) {
return "This store still needs to be connected before imports can run.";
}
return "This store is not ready yet.";
}, [connection]);
const openSetup = () => {
if (typeof window !== "undefined") {
window.top.location.href = setupUrl;
}
};
useEffect(() => {
if (!fetcher.data) {
return;
}
if (fetcher.data.ok && fetcher.data.jobId) {
setJob({
id: fetcher.data.jobId,
status: "queued",
step: "queued",
stepIndex: 0,
totalSteps: 6,
detail: "Job queued",
});
shopify.toast.show("KYT import job started");
return;
}
if (fetcher.data.error) {
shopify.toast.show(fetcher.data.error, { isError: true });
}
}, [fetcher.data, shopify]);
useEffect(() => {
if (!job?.id) {
return undefined;
}
let cancelled = false;
async function pollStatus() {
try {
const response = await fetch(
`${backendApiUrl}/pipeline/status/${encodeURIComponent(job.id)}`,
);
const payload = await readJsonSafe(response);
if (!cancelled && response.ok) {
setJob(payload);
}
} catch (error) {
if (!cancelled) {
setJob((current) =>
current
? {
...current,
detail: `Status polling failed: ${error.message}`,
}
: current,
);
}
}
}
pollStatus();
const timer = setInterval(pollStatus, 3000);
return () => {
cancelled = true;
clearInterval(timer);
};
}, [backendApiUrl, job?.id]);
return ( return (
<s-page heading="Shopify app template"> <s-page heading="Race Nation Imports">
<s-button slot="primary-action" onClick={generateProduct}> <div className={styles.hero}>
Generate a product <div className={styles.heroCopy}>
</s-button> <p className={styles.kicker}>Import Center</p>
<h2>Bring KYT products into your store from one simple dashboard.</h2>
<p className={styles.heroText}>
Start an import, follow the progress, and keep track of what is
happening without leaving Shopify.
</p>
</div>
<div className={styles.heroMetrics}>
<div className={styles.metricTile}>
<span className={styles.metricLabel}>Store</span>
<strong>{shop}</strong>
</div>
<div className={styles.metricTile}>
<span className={styles.metricLabel}>Status</span>
<strong>{connectionState}</strong>
</div>
<div className={styles.metricTile}>
<span className={styles.metricLabel}>Import</span>
<strong>{job ? "In progress" : "Not started"}</strong>
</div>
</div>
</div>
<s-section heading="Congrats on creating a new Shopify app 🎉"> <div className={styles.grid}>
<s-paragraph> <div className={styles.primaryColumn}>
This embedded app template uses{" "} <s-section heading="Store Setup">
<s-link <div className={styles.panelBody}>
href="https://shopify.dev/docs/apps/tools/app-bridge" <div className={styles.inlineRow}>
target="_blank" <span className={styles.statLabel}>Store status</span>
> <span
App Bridge className={`${styles.statusPill} ${
</s-link>{" "} connection?.status === 1 ? styles.success : styles.warning
interface examples like an{" "} }`}
<s-link href="/app/additional">additional page in the app nav</s-link> >
, as well as an{" "} {connectionState}
<s-link </span>
href="https://shopify.dev/docs/api/admin-graphql" </div>
target="_blank" <div className={styles.detailCard}>
> <span className={styles.statLabel}>Message</span>
Admin GraphQL <p>{connectionMessage}</p>
</s-link>{" "} </div>
mutation demo, to provide a starting point for app development. {connection?.status !== 1 ? (
</s-paragraph> <div className={`${styles.detailCard} ${styles.warningCard}`}>
</s-section> <span className={styles.statLabel}>What to do</span>
<s-section heading="Get started with products"> <p>Connect your store first, then start the import.</p>
<s-paragraph> <div className={styles.actionRow}>
Generate a product with GraphQL and get the JSON output for that <s-button variant="primary" onClick={openSetup}>
product. Learn more about the{" "} Connect store
<s-link </s-button>
href="https://shopify.dev/docs/api/admin-graphql/latest/mutations/productCreate" </div>
target="_blank" </div>
> ) : (
productCreate <div className={`${styles.detailCard} ${styles.successCard}`}>
</s-link>{" "} <span className={styles.statLabel}>Store connection</span>
mutation in our API references. Includes a product{" "} <p>Your store is ready. You can start importing products now.</p>
<s-link </div>
href="https://shopify.dev/docs/apps/build/custom-data/metafields" )}
target="_blank" </div>
>
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-box
padding="base"
borderWidth="base"
borderRadius="base"
background="subdued"
>
<pre style={{ margin: 0 }}>
<code>{JSON.stringify(fetcher.data.product, null, 2)}</code>
</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-section> </s-section>
)}
</s-section>
<s-section slot="aside" heading="App template specs"> <s-section
<s-paragraph> heading="Start Product Import"
<s-text>Framework: </s-text> description="Choose how much you want to import, then start the process."
<s-link href="https://reactrouter.com/" target="_blank">
React Router
</s-link>
</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 <fetcher.Form method="post">
</s-link> <div className={styles.panelBody}>
</s-paragraph> <s-text-field
<s-paragraph> label="Product limit"
<s-text>API: </s-text> name="limit"
<s-link type="number"
href="https://shopify.dev/docs/api/admin-graphql" min="1"
target="_blank" details="Leave this empty to import all products, or enter a smaller number for a partial import."
> />
GraphQL <div className={styles.actionRow}>
</s-link> <s-button
</s-paragraph> type="submit"
<s-paragraph> variant="primary"
<s-text>Custom data: </s-text> {...(isSubmitting ? { loading: true } : {})}
<s-link >
href="https://shopify.dev/docs/apps/build/custom-data" Start import
target="_blank" </s-button>
> </div>
Metafields &amp; metaobjects </div>
</s-link> </fetcher.Form>
</s-paragraph> </s-section>
<s-paragraph> </div>
<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"> <div className={styles.secondaryColumn}>
<s-unordered-list> <s-section heading="Import Progress">
<s-list-item> <JobSummary job={job} />
Build an{" "} </s-section>
<s-link
href="https://shopify.dev/docs/apps/getting-started/build-app-example" <s-section heading="What Happens Next">
target="_blank" <div className={styles.stepList}>
> <div className={styles.stepItem}>1. Product information is collected.</div>
example app <div className={styles.stepItem}>2. Product images are prepared.</div>
</s-link> <div className={styles.stepItem}>3. Images are uploaded to Shopify.</div>
</s-list-item> <div className={styles.stepItem}>4. Products are created or updated in your store.</div>
<s-list-item> </div>
Explore Shopify&apos;s API with{" "} </s-section>
<s-link </div>
href="https://shopify.dev/docs/apps/tools/graphiql-admin-api" </div>
target="_blank"
>
GraphiQL
</s-link>
</s-list-item>
</s-unordered-list>
</s-section>
</s-page> </s-page>
); );
} }

View File

@ -1,34 +1,30 @@
export default function AdditionalPage() { export default function AdditionalPage() {
return ( return (
<s-page heading="Additional page"> <s-page heading="Race Nation App Guide">
<s-section heading="Multiple pages"> <s-section heading="How This App Is Set Up">
<s-paragraph> <s-paragraph>
The app template comes with an additional page which demonstrates how This Shopify embedded app is the frontend for the Race Nation import
to create multiple pages within app navigation using{" "} workflow. The dashboard starts the KYT import pipeline from the custom
<s-link backend and shows the current job status while the backend works
href="https://shopify.dev/docs/apps/tools/app-bridge" through each pipeline step.
target="_blank"
>
App Bridge
</s-link>
.
</s-paragraph> </s-paragraph>
<s-paragraph> <s-paragraph>
To create your own page and have it show up in the app navigation, add The backend handles Shopify OAuth, webhook validation, token storage,
a page inside <code>app/routes</code>, and a link to it in the{" "} fulfillment setup, and the full KYT scrape-to-Shopify pipeline. This
<code>&lt;ui-nav-menu&gt;</code> component found in{" "} frontend is where merchants will launch and monitor those imports from
<code>app/routes/app.jsx</code>. inside Shopify Admin.
</s-paragraph> </s-paragraph>
</s-section> </s-section>
<s-section slot="aside" heading="Resources"> <s-section slot="aside" heading="Current Focus">
<s-unordered-list> <s-unordered-list>
<s-list-item> <s-list-item>
<s-link Connect the dashboard to the Race Nation backend.
href="https://shopify.dev/docs/apps/design-guidelines/navigation#app-nav" </s-list-item>
target="_blank" <s-list-item>
> Start small test imports with a limit before running a full sync.
App nav best practices </s-list-item>
</s-link> <s-list-item>
Replace the remaining template screens with store-specific tools.
</s-list-item> </s-list-item>
</s-unordered-list> </s-unordered-list>
</s-section> </s-section>

View File

@ -16,8 +16,8 @@ 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">Import Dashboard</s-link>
<s-link href="/app/additional">Additional page</s-link> <s-link href="/app/additional">App Guide</s-link>
</s-app-nav> </s-app-nav>
<Outlet /> <Outlet />
</AppProvider> </AppProvider>

View File

@ -0,0 +1,283 @@
.hero {
position: relative;
overflow: hidden;
display: grid;
grid-template-columns: 1.5fr 1fr;
gap: 1.5rem;
margin-bottom: 1.5rem;
padding: 1.5rem;
border-radius: 24px;
background:
radial-gradient(circle at top left, rgba(255, 135, 61, 0.24), transparent 34%),
linear-gradient(135deg, #0f172a, #162033 42%, #1f2f46 100%);
color: #f8fafc;
box-shadow: 0 24px 80px rgba(15, 23, 42, 0.24);
}
.hero::after {
content: "";
position: absolute;
inset: auto -10% -35% auto;
width: 260px;
height: 260px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.06);
filter: blur(8px);
}
.heroCopy {
position: relative;
z-index: 1;
}
.kicker,
.eyebrow,
.metricLabel,
.statLabel {
display: inline-block;
text-transform: uppercase;
letter-spacing: 0.12em;
font-size: 0.72rem;
opacity: 0.72;
}
.heroCopy h2 {
margin: 0.35rem 0 0.85rem;
font-size: 2rem;
line-height: 1.08;
max-width: 14ch;
}
.heroText {
max-width: 62ch;
margin: 0;
color: rgba(248, 250, 252, 0.8);
}
.heroMetrics {
position: relative;
z-index: 1;
display: grid;
gap: 0.9rem;
}
.metricTile,
.statCard,
.detailCard,
.jsonCard,
.emptyPanel,
.stepItem {
border: 1px solid rgba(148, 163, 184, 0.18);
border-radius: 20px;
background: rgba(255, 255, 255, 0.06);
backdrop-filter: blur(10px);
}
.metricTile {
padding: 1rem 1.1rem;
display: grid;
gap: 0.35rem;
}
.metricTile strong {
font-size: 1rem;
line-height: 1.35;
}
.grid {
display: grid;
grid-template-columns: 1.15fr 0.85fr;
gap: 1.5rem;
}
.primaryColumn,
.secondaryColumn {
display: grid;
gap: 1.5rem;
}
.panelBody {
display: grid;
gap: 1rem;
}
.inlineRow,
.actionRow,
.jobHeader,
.progressMeta {
display: flex;
justify-content: space-between;
align-items: center;
gap: 0.75rem;
flex-wrap: wrap;
}
.statusPill {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 2rem;
padding: 0.3rem 0.8rem;
border-radius: 999px;
font-size: 0.8rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
}
.success {
background: rgba(22, 163, 74, 0.14);
color: #166534;
}
.warning {
background: rgba(245, 158, 11, 0.16);
color: #92400e;
}
.active {
background: rgba(14, 165, 233, 0.14);
color: #075985;
}
.error {
background: rgba(239, 68, 68, 0.16);
color: #991b1b;
}
.jobPanel {
display: grid;
gap: 1rem;
}
.panelTitle {
margin: 0.15rem 0 0;
font-size: 1.15rem;
}
.progressTrack {
position: relative;
height: 0.8rem;
border-radius: 999px;
overflow: hidden;
background: linear-gradient(90deg, #e2e8f0, #f8fafc);
}
.progressFill {
height: 100%;
border-radius: inherit;
background: linear-gradient(90deg, #ff7a18, #ffb347 55%, #ffd166);
box-shadow: 0 0 24px rgba(255, 122, 24, 0.35);
animation: pulse 1.8s ease-in-out infinite;
}
.statGrid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 0.85rem;
}
.statCard,
.detailCard,
.jsonCard {
padding: 1rem;
}
.detailCard p {
margin: 0.35rem 0 0;
}
.warningCard {
background: linear-gradient(135deg, rgba(255, 237, 213, 0.9), rgba(255, 247, 237, 0.95));
}
.successCard {
background: linear-gradient(135deg, rgba(220, 252, 231, 0.92), rgba(240, 253, 244, 0.96));
}
.errorCard {
background: linear-gradient(135deg, rgba(254, 226, 226, 0.92), rgba(255, 241, 242, 0.96));
}
.jsonCard {
overflow: auto;
background: #0f172a;
color: #e2e8f0;
}
.emptyPanel {
position: relative;
overflow: hidden;
padding: 1.3rem;
background: linear-gradient(135deg, #fff8ed, #ffffff);
}
.emptyGlow {
position: absolute;
inset: auto -30px -40px auto;
width: 110px;
height: 110px;
border-radius: 50%;
background: rgba(255, 122, 24, 0.14);
filter: blur(6px);
}
.emptyPanel h3,
.emptyPanel p {
position: relative;
z-index: 1;
}
.emptyPanel h3 {
margin: 0 0 0.35rem;
}
.emptyPanel p {
margin: 0;
color: #475569;
}
.stepList {
display: grid;
gap: 0.75rem;
}
.stepItem {
padding: 0.95rem 1rem;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.98), rgba(248, 250, 252, 0.94));
}
@keyframes pulse {
0%,
100% {
filter: saturate(1);
}
50% {
filter: saturate(1.25);
}
}
@media (max-width: 960px) {
.hero,
.grid {
grid-template-columns: 1fr;
}
.heroCopy h2 {
max-width: none;
}
}
@media (max-width: 640px) {
.hero {
padding: 1.2rem;
}
.heroCopy h2 {
font-size: 1.55rem;
}
.statGrid {
grid-template-columns: 1fr;
}
}

BIN
prisma/dev.sqlite Normal file

Binary file not shown.

View File

@ -2,12 +2,11 @@
client_id = "4601ec30c0ded33750016848efad52fc" client_id = "4601ec30c0ded33750016848efad52fc"
name = "Race-Nation-Import" name = "Race-Nation-Import"
application_url = "https://example.com" application_url = "https://racenation.thedomainnest.com"
embedded = true embedded = true
[build] [build]
automatically_update_urls_on_dev = true automatically_update_urls_on_dev = true
include_config_on_deploy = true
[webhooks] [webhooks]
api_version = "2026-07" api_version = "2026-07"
@ -22,10 +21,10 @@ api_version = "2026-07"
[access_scopes] [access_scopes]
# Learn more at https://shopify.dev/docs/apps/tools/cli/configuration#access_scopes # Learn more at https://shopify.dev/docs/apps/tools/cli/configuration#access_scopes
scopes = "write_products,write_metaobjects,write_metaobject_definitions" scopes = "write_products,write_files,write_inventory,write_publications"
[auth] [auth]
redirect_urls = [ "https://example.com/api/auth" ] redirect_urls = [ "https://racenationapi.thedomainnest.com/auth/callback" ]
[product.metafields.app.demo_info] [product.metafields.app.demo_info]
type = "single_line_text_field" type = "single_line_text_field"