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 (
<div className={styles.index}>
<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}>
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>
{showForm && (
<Form className={styles.form} method="post" action="/auth/login">
@ -36,16 +38,18 @@ export default function App() {
)}
<ul className={styles.list}>
<li>
<strong>Product feature</strong>. Some detail about your feature and
its benefit to your customer.
<strong>Shopify-authenticated workflow</strong>. Connect the app to
a store and keep Shopify session/auth behavior inside the embedded
app.
</li>
<li>
<strong>Product feature</strong>. Some detail about your feature and
its benefit to your customer.
<strong>KYT pipeline controls</strong>. Launch scrape, image,
watermark, upload, conversion, and product upsert jobs from one
dashboard.
</li>
<li>
<strong>Product feature</strong>. Some detail about your feature and
its benefit to your customer.
<strong>Live status tracking</strong>. Follow job progress and see
step-by-step status while the backend runs.
</li>
</ul>
</div>

View File

@ -1,327 +1,422 @@
import { useEffect } from "react";
import { useFetcher } from "react-router";
import { useEffect, useMemo, useState } from "react";
import { useFetcher, useLoaderData } 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 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 }) => {
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 }) => {
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 {
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",
},
],
},
},
},
try {
const statusResponse = await fetch(
`${backendApiUrl}/pipeline/status/${encodeURIComponent(shop)}`,
);
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
if (statusResponse.ok) {
currentJob = await readJsonSafe(statusResponse);
}
} catch {
currentJob = null;
}
}`,
{
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 {
product: responseJson.data.productCreate.product,
variant: variantResponseJson.data.productVariantsBulkUpdate.productVariants,
metaobject: metaobjectResponseJson.data.metaobjectUpsert.metaobject,
shop,
backendApiUrl,
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() {
const { shop, backendApiUrl, connection, currentJob } = useLoaderData();
const fetcher = useFetcher();
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) &&
fetcher.formMethod === "POST";
useEffect(() => {
if (fetcher.data?.product?.id) {
shopify.toast.show("Product created");
const connectionState = useMemo(() => {
if (connection?.status === 1) {
return "Ready";
}
}, [fetcher.data?.product?.id, shopify]);
const generateProduct = () => fetcher.submit({}, { method: "POST" });
return "Setup needed";
}, [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 (
<s-page heading="Shopify app template">
<s-button slot="primary-action" onClick={generateProduct}>
Generate a product
</s-button>
<s-page heading="Race Nation Imports">
<div className={styles.hero}>
<div className={styles.heroCopy}>
<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 🎉">
<s-paragraph>
This embedded app template uses{" "}
<s-link
href="https://shopify.dev/docs/apps/tools/app-bridge"
target="_blank"
<div className={styles.grid}>
<div className={styles.primaryColumn}>
<s-section heading="Store Setup">
<div className={styles.panelBody}>
<div className={styles.inlineRow}>
<span className={styles.statLabel}>Store status</span>
<span
className={`${styles.statusPill} ${
connection?.status === 1 ? styles.success : styles.warning
}`}
>
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
{connectionState}
</span>
</div>
<div className={styles.detailCard}>
<span className={styles.statLabel}>Message</span>
<p>{connectionMessage}</p>
</div>
{connection?.status !== 1 ? (
<div className={`${styles.detailCard} ${styles.warningCard}`}>
<span className={styles.statLabel}>What to do</span>
<p>Connect your store first, then start the import.</p>
<div className={styles.actionRow}>
<s-button variant="primary" onClick={openSetup}>
Connect store
</s-button>
</div>
</div>
) : (
<div className={`${styles.detailCard} ${styles.successCard}`}>
<span className={styles.statLabel}>Store connection</span>
<p>Your store is ready. You can start importing products now.</p>
</div>
)}
</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>
)}
</div>
</s-section>
<s-section slot="aside" heading="App template specs">
<s-paragraph>
<s-text>Framework: </s-text>
<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"
<s-section
heading="Start Product Import"
description="Choose how much you want to import, then start the process."
>
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"
<fetcher.Form method="post">
<div className={styles.panelBody}>
<s-text-field
label="Product limit"
name="limit"
type="number"
min="1"
details="Leave this empty to import all products, or enter a smaller number for a partial import."
/>
<div className={styles.actionRow}>
<s-button
type="submit"
variant="primary"
{...(isSubmitting ? { loading: true } : {})}
>
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 &amp; 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>
Start import
</s-button>
</div>
</div>
</fetcher.Form>
</s-section>
</div>
<div className={styles.secondaryColumn}>
<s-section heading="Import Progress">
<JobSummary job={job} />
</s-section>
<s-section slot="aside" heading="Next steps">
<s-unordered-list>
<s-list-item>
Build an{" "}
<s-link
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&apos;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-section heading="What Happens Next">
<div className={styles.stepList}>
<div className={styles.stepItem}>1. Product information is collected.</div>
<div className={styles.stepItem}>2. Product images are prepared.</div>
<div className={styles.stepItem}>3. Images are uploaded to Shopify.</div>
<div className={styles.stepItem}>4. Products are created or updated in your store.</div>
</div>
</s-section>
</div>
</div>
</s-page>
);
}

View File

@ -1,34 +1,30 @@
export default function AdditionalPage() {
return (
<s-page heading="Additional page">
<s-section heading="Multiple pages">
<s-page heading="Race Nation App Guide">
<s-section heading="How This App Is Set Up">
<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>
.
This Shopify embedded app is the frontend for the Race Nation import
workflow. The dashboard starts the KYT import pipeline from the custom
backend and shows the current job status while the backend works
through each pipeline step.
</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>&lt;ui-nav-menu&gt;</code> component found in{" "}
<code>app/routes/app.jsx</code>.
The backend handles Shopify OAuth, webhook validation, token storage,
fulfillment setup, and the full KYT scrape-to-Shopify pipeline. This
frontend is where merchants will launch and monitor those imports from
inside Shopify Admin.
</s-paragraph>
</s-section>
<s-section slot="aside" heading="Resources">
<s-section slot="aside" heading="Current Focus">
<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>
Connect the dashboard to the Race Nation backend.
</s-list-item>
<s-list-item>
Start small test imports with a limit before running a full sync.
</s-list-item>
<s-list-item>
Replace the remaining template screens with store-specific tools.
</s-list-item>
</s-unordered-list>
</s-section>

View File

@ -16,8 +16,8 @@ export default function App() {
return (
<AppProvider embedded apiKey={apiKey}>
<s-app-nav>
<s-link href="/app">Home</s-link>
<s-link href="/app/additional">Additional page</s-link>
<s-link href="/app">Import Dashboard</s-link>
<s-link href="/app/additional">App Guide</s-link>
</s-app-nav>
<Outlet />
</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"
name = "Race-Nation-Import"
application_url = "https://example.com"
application_url = "https://racenation.thedomainnest.com"
embedded = true
[build]
automatically_update_urls_on_dev = true
include_config_on_deploy = true
[webhooks]
api_version = "2026-07"
@ -22,10 +21,10 @@ api_version = "2026-07"
[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]
redirect_urls = [ "https://example.com/api/auth" ]
redirect_urls = [ "https://racenationapi.thedomainnest.com/auth/callback" ]
[product.metafields.app.demo_info]
type = "single_line_text_field"