new changes in ui
This commit is contained in:
parent
0f9bd58a49
commit
5502ee7b3b
16
.gitignore
vendored
Normal file
16
.gitignore
vendored
Normal 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
|
||||||
9
.react-router/types/+future.ts
Normal file
9
.react-router/types/+future.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
// Generated by React Router
|
||||||
|
|
||||||
|
import "react-router";
|
||||||
|
|
||||||
|
declare module "react-router" {
|
||||||
|
interface Future {
|
||||||
|
v8_middleware: false
|
||||||
|
}
|
||||||
|
}
|
||||||
88
.react-router/types/+routes.ts
Normal file
88
.react-router/types/+routes.ts
Normal 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
18
.react-router/types/+server-build.d.ts
vendored
Normal 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"];
|
||||||
|
}
|
||||||
68
.react-router/types/app/+types/root.ts
Normal file
68
.react-router/types/app/+types/root.ts
Normal 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"];
|
||||||
|
}
|
||||||
74
.react-router/types/app/routes/+types/app._index.ts
Normal file
74
.react-router/types/app/routes/+types/app._index.ts
Normal 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"];
|
||||||
|
}
|
||||||
74
.react-router/types/app/routes/+types/app.additional.ts
Normal file
74
.react-router/types/app/routes/+types/app.additional.ts
Normal 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"];
|
||||||
|
}
|
||||||
71
.react-router/types/app/routes/+types/app.ts
Normal file
71
.react-router/types/app/routes/+types/app.ts
Normal 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"];
|
||||||
|
}
|
||||||
71
.react-router/types/app/routes/+types/auth.$.ts
Normal file
71
.react-router/types/app/routes/+types/auth.$.ts
Normal 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"];
|
||||||
|
}
|
||||||
@ -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"];
|
||||||
|
}
|
||||||
@ -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"];
|
||||||
|
}
|
||||||
71
.react-router/types/app/routes/_index/+types/route.ts
Normal file
71
.react-router/types/app/routes/_index/+types/route.ts
Normal 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"];
|
||||||
|
}
|
||||||
71
.react-router/types/app/routes/auth.login/+types/route.ts
Normal file
71
.react-router/types/app/routes/auth.login/+types/route.ts
Normal 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"];
|
||||||
|
}
|
||||||
@ -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>
|
||||||
|
|||||||
@ -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 & 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'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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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><ui-nav-menu></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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
283
app/styles/app-dashboard.module.css
Normal file
283
app/styles/app-dashboard.module.css
Normal 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
BIN
prisma/dev.sqlite
Normal file
Binary file not shown.
@ -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"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user