From 5502ee7b3bf030d67f5c7d7bc95cacb872362b48 Mon Sep 17 00:00:00 2001 From: MOHAN Date: Tue, 14 Apr 2026 01:44:37 +0530 Subject: [PATCH] new changes in ui --- .gitignore | 16 + .react-router/types/+future.ts | 9 + .react-router/types/+routes.ts | 88 +++ .react-router/types/+server-build.d.ts | 18 + .react-router/types/app/+types/root.ts | 68 ++ .../types/app/routes/+types/app._index.ts | 74 ++ .../types/app/routes/+types/app.additional.ts | 74 ++ .react-router/types/app/routes/+types/app.ts | 71 ++ .../types/app/routes/+types/auth.$.ts | 71 ++ .../+types/webhooks.app.scopes_update.ts | 71 ++ .../routes/+types/webhooks.app.uninstalled.ts | 71 ++ .../types/app/routes/_index/+types/route.ts | 71 ++ .../app/routes/auth.login/+types/route.ts | 71 ++ app/routes/_index/route.jsx | 20 +- app/routes/app._index.jsx | 691 ++++++++++-------- app/routes/app.additional.jsx | 40 +- app/routes/app.jsx | 4 +- app/styles/app-dashboard.module.css | 283 +++++++ prisma/dev.sqlite | Bin 0 -> 20480 bytes shopify.app.toml | 7 +- 20 files changed, 1484 insertions(+), 334 deletions(-) create mode 100644 .gitignore create mode 100644 .react-router/types/+future.ts create mode 100644 .react-router/types/+routes.ts create mode 100644 .react-router/types/+server-build.d.ts create mode 100644 .react-router/types/app/+types/root.ts create mode 100644 .react-router/types/app/routes/+types/app._index.ts create mode 100644 .react-router/types/app/routes/+types/app.additional.ts create mode 100644 .react-router/types/app/routes/+types/app.ts create mode 100644 .react-router/types/app/routes/+types/auth.$.ts create mode 100644 .react-router/types/app/routes/+types/webhooks.app.scopes_update.ts create mode 100644 .react-router/types/app/routes/+types/webhooks.app.uninstalled.ts create mode 100644 .react-router/types/app/routes/_index/+types/route.ts create mode 100644 .react-router/types/app/routes/auth.login/+types/route.ts create mode 100644 app/styles/app-dashboard.module.css create mode 100644 prisma/dev.sqlite diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..477123c --- /dev/null +++ b/.gitignore @@ -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 \ No newline at end of file diff --git a/.react-router/types/+future.ts b/.react-router/types/+future.ts new file mode 100644 index 0000000..7f4533c --- /dev/null +++ b/.react-router/types/+future.ts @@ -0,0 +1,9 @@ +// Generated by React Router + +import "react-router"; + +declare module "react-router" { + interface Future { + v8_middleware: false + } +} \ No newline at end of file diff --git a/.react-router/types/+routes.ts b/.react-router/types/+routes.ts new file mode 100644 index 0000000..47b26a5 --- /dev/null +++ b/.react-router/types/+routes.ts @@ -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"); +}; \ No newline at end of file diff --git a/.react-router/types/+server-build.d.ts b/.react-router/types/+server-build.d.ts new file mode 100644 index 0000000..13792c1 --- /dev/null +++ b/.react-router/types/+server-build.d.ts @@ -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"]; +} \ No newline at end of file diff --git a/.react-router/types/app/+types/root.ts b/.react-router/types/app/+types/root.ts new file mode 100644 index 0000000..f79b35c --- /dev/null +++ b/.react-router/types/app/+types/root.ts @@ -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; + +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"]; +} \ No newline at end of file diff --git a/.react-router/types/app/routes/+types/app._index.ts b/.react-router/types/app/routes/+types/app._index.ts new file mode 100644 index 0000000..b003d13 --- /dev/null +++ b/.react-router/types/app/routes/+types/app._index.ts @@ -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; + +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"]; +} \ No newline at end of file diff --git a/.react-router/types/app/routes/+types/app.additional.ts b/.react-router/types/app/routes/+types/app.additional.ts new file mode 100644 index 0000000..dd51f3d --- /dev/null +++ b/.react-router/types/app/routes/+types/app.additional.ts @@ -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; + +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"]; +} \ No newline at end of file diff --git a/.react-router/types/app/routes/+types/app.ts b/.react-router/types/app/routes/+types/app.ts new file mode 100644 index 0000000..dc4f323 --- /dev/null +++ b/.react-router/types/app/routes/+types/app.ts @@ -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; + +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"]; +} \ No newline at end of file diff --git a/.react-router/types/app/routes/+types/auth.$.ts b/.react-router/types/app/routes/+types/auth.$.ts new file mode 100644 index 0000000..37d1e0a --- /dev/null +++ b/.react-router/types/app/routes/+types/auth.$.ts @@ -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; + +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"]; +} \ No newline at end of file diff --git a/.react-router/types/app/routes/+types/webhooks.app.scopes_update.ts b/.react-router/types/app/routes/+types/webhooks.app.scopes_update.ts new file mode 100644 index 0000000..454da51 --- /dev/null +++ b/.react-router/types/app/routes/+types/webhooks.app.scopes_update.ts @@ -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; + +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"]; +} \ No newline at end of file diff --git a/.react-router/types/app/routes/+types/webhooks.app.uninstalled.ts b/.react-router/types/app/routes/+types/webhooks.app.uninstalled.ts new file mode 100644 index 0000000..892713b --- /dev/null +++ b/.react-router/types/app/routes/+types/webhooks.app.uninstalled.ts @@ -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; + +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"]; +} \ No newline at end of file diff --git a/.react-router/types/app/routes/_index/+types/route.ts b/.react-router/types/app/routes/_index/+types/route.ts new file mode 100644 index 0000000..72808cd --- /dev/null +++ b/.react-router/types/app/routes/_index/+types/route.ts @@ -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; + +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"]; +} \ No newline at end of file diff --git a/.react-router/types/app/routes/auth.login/+types/route.ts b/.react-router/types/app/routes/auth.login/+types/route.ts new file mode 100644 index 0000000..2ecf821 --- /dev/null +++ b/.react-router/types/app/routes/auth.login/+types/route.ts @@ -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; + +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"]; +} \ No newline at end of file diff --git a/app/routes/_index/route.jsx b/app/routes/_index/route.jsx index 7efaa16..cc8af64 100644 --- a/app/routes/_index/route.jsx +++ b/app/routes/_index/route.jsx @@ -18,9 +18,11 @@ export default function App() { return (
-

A short heading about [your app]

+

Race Nation Shopify Import

- 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.

{showForm && (
@@ -36,16 +38,18 @@ export default function App() { )}
  • - Product feature. Some detail about your feature and - its benefit to your customer. + Shopify-authenticated workflow. Connect the app to + a store and keep Shopify session/auth behavior inside the embedded + app.
  • - Product feature. Some detail about your feature and - its benefit to your customer. + KYT pipeline controls. Launch scrape, image, + watermark, upload, conversion, and product upsert jobs from one + dashboard.
  • - Product feature. Some detail about your feature and - its benefit to your customer. + Live status tracking. Follow job progress and see + step-by-step status while the backend runs.
diff --git a/app/routes/app._index.jsx b/app/routes/app._index.jsx index b9380fc..ee52a6c 100644 --- a/app/routes/app._index.jsx +++ b/app/routes/app._index.jsx @@ -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", - }, - ], - }, - }, - }, - ); - 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(); + try { + const statusResponse = await fetch( + `${backendApiUrl}/pipeline/status/${encodeURIComponent(shop)}`, + ); + if (statusResponse.ok) { + currentJob = await readJsonSafe(statusResponse); + } + } catch { + currentJob = null; + } 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 Your store is not ready yet.; + } + + return ( + + {Object.entries(fields).map(([key, value]) => ( + + {key} + + {value} + + + ))} + + ); +} + +function JobSummary({ job }) { + if (!job) { + return ( +
+
+

No import in progress

+

Start an import to see the live progress and current step here.

+
+ ); + } + + const progressPercent = Math.max( + 0, + Math.min( + 100, + Math.round(((job.stepIndex || 0) / (job.totalSteps || 6)) * 100), + ), + ); + + return ( +
+
+
+

Current import

+

{job.id}

+
+ + {job.status} + +
+ +
+ {job.step || "-"} + {progressPercent}% complete +
+
+
+
+ +
+
+ Step + + {job.stepIndex || 0}/{job.totalSteps || 6} + +
+
+ Started + {job.startedAt ? new Date(job.startedAt).toLocaleString() : "-"} +
+
+ Updated + {job.updatedAt ? new Date(job.updatedAt).toLocaleString() : "-"} +
+
+ +
+ Detail +

{job.detail || "-"}

+
+ + {job.error ? ( +
+ Error +

{job.error}

+
+ ) : null} + + {job.summary ? ( +
+ Summary +
+            {JSON.stringify(job.summary, null, 2)}
+          
+
+ ) : null} +
+ ); +} + 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 ( - - - Generate a product - + +
+
+

Import Center

+

Bring KYT products into your store from one simple dashboard.

+

+ Start an import, follow the progress, and keep track of what is + happening without leaving Shopify. +

+
+
+
+ Store + {shop} +
+
+ Status + {connectionState} +
+
+ Import + {job ? "In progress" : "Not started"} +
+
+
- - - This embedded app template uses{" "} - - App Bridge - {" "} - interface examples like an{" "} - additional page in the app nav - , as well as an{" "} - - Admin GraphQL - {" "} - mutation demo, to provide a starting point for app development. - - - - - Generate a product with GraphQL and get the JSON output for that - product. Learn more about the{" "} - - productCreate - {" "} - mutation in our API references. Includes a product{" "} - - metafield - {" "} - and{" "} - - metaobject - - . - - - - Generate a product - - {fetcher.data?.product && ( - { - shopify.intents.invoke?.("edit:shopify/Product", { - value: fetcher.data?.product?.id, - }); - }} - target="_blank" - variant="tertiary" - > - Edit product - - )} - - {fetcher.data?.product && ( - - - -
-                  {JSON.stringify(fetcher.data.product, null, 2)}
-                
-
- - productVariantsBulkUpdate mutation - -
-                  {JSON.stringify(fetcher.data.variant, null, 2)}
-                
-
- - metaobjectUpsert mutation - -
-                  
-                    {JSON.stringify(fetcher.data.metaobject, null, 2)}
-                  
-                
-
-
+
+
+ +
+
+ Store status + + {connectionState} + +
+
+ Message +

{connectionMessage}

+
+ {connection?.status !== 1 ? ( +
+ What to do +

Connect your store first, then start the import.

+
+ + Connect store + +
+
+ ) : ( +
+ Store connection +

Your store is ready. You can start importing products now.

+
+ )} +
- )} - - - - Framework: - - React Router - - - - Interface: - - Polaris web components - - - - API: - - GraphQL - - - - Custom data: - - Metafields & metaobjects - - - - Database: - - Prisma - - - + +
+ +
+ + Start import + +
+
+
+ +
- - - - Build an{" "} - - example app - - - - Explore Shopify's API with{" "} - - GraphiQL - - - - +
+ + + + + +
+
1. Product information is collected.
+
2. Product images are prepared.
+
3. Images are uploaded to Shopify.
+
4. Products are created or updated in your store.
+
+
+
+
); } diff --git a/app/routes/app.additional.jsx b/app/routes/app.additional.jsx index e7c8c72..e1f8993 100644 --- a/app/routes/app.additional.jsx +++ b/app/routes/app.additional.jsx @@ -1,34 +1,30 @@ export default function AdditionalPage() { return ( - - + + - The app template comes with an additional page which demonstrates how - to create multiple pages within app navigation using{" "} - - App Bridge - - . + 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. - To create your own page and have it show up in the app navigation, add - a page inside app/routes, and a link to it in the{" "} - <ui-nav-menu> component found in{" "} - app/routes/app.jsx. + 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. - + - - App nav best practices - + Connect the dashboard to the Race Nation backend. + + + Start small test imports with a limit before running a full sync. + + + Replace the remaining template screens with store-specific tools. diff --git a/app/routes/app.jsx b/app/routes/app.jsx index 95524f6..8bf73e8 100644 --- a/app/routes/app.jsx +++ b/app/routes/app.jsx @@ -16,8 +16,8 @@ export default function App() { return ( - Home - Additional page + Import Dashboard + App Guide diff --git a/app/styles/app-dashboard.module.css b/app/styles/app-dashboard.module.css new file mode 100644 index 0000000..e03679c --- /dev/null +++ b/app/styles/app-dashboard.module.css @@ -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; + } +} diff --git a/prisma/dev.sqlite b/prisma/dev.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..1dfdd163f100e3ed0f4c7af353ae5fbf7bc650f2 GIT binary patch literal 20480 zcmeI3&uimG6vw3`-Z);23t7k^6oe^s7vdmUKO)mhokR_$Ub{`~rdcTJXf(5)jeb>< zvq^7h%d&_53!#@n>A8PNFWvqrJ$7WvUdK+B(1k+a4H6#Bym@clM_&s(@*I8HW+{YA|qeN?XeBdk0`Liw-C9}iLa zx7&=N9S8scAOHk_01yBIKmZ8bdjdc0Ew65DZi=tJNs;4`Ba$R64Dyeay+f7^9JZb|4^O1et&^?XC>ey&oY}N1MJc(Z zk0sr}V*&5p?RMK%v-38{zV%tN-|k5ic?p^N!eNyAnlzE~h{YtSO8Y#CUhAnf=|C9s zJbU3O32uo#Oi0|~+3mItTAkix9A$Bmc91_8$3r)@5+5;d?0zzy#|?);+I<<2_^x?* zAx7R@Kpc7=a>5v;;f$Xfv5aQ%6^R*TB+Dmfd82JiD91IJ;WC-{Y>zchwOo2qTY0e2 z+!O^Cxa2(f&f~w?Y&1;6(Y2lH##Zu5;m2p<>V~3-uk4ZN>?mf5k8Gbk=P7adC5tUf z*QK~y^VQafcn z;e-7s4~L@2<9e}^lthU=5^Xj={#CFJtV5~aIcgnrto=oYa_Pz5(u0j-H3D3iQky3^o?BFnPl?6`(%$_~b= zsTw+_4MNqOdc#m%M3I3FqC315J38+XuU`HB^Hp80YqGA&bwxFG)y6U53xRerUVI!c z3XKeZDDk|+%HIM%pa20N00e*l5C8%|00;m9AOHk_01yBI_l&^D%l$hoH;mu^uM3s+ zdzK(f7YG0WAOHk_01yBIKmZ5;0U!VbfWZ3^xO^e5A0L#9m#c-7%u`V)7EcNxrP*#7 z`;2(udA9Y!4^wtVw*8B2{|%!T+c@;!+4(Zgc5P7Rkz{LJXTcfY91zAA`E?YB?hvPw z?r7+E439T#h*z&4{rPJ$h%mA>hbndCxF)4UHyl@1RMj+92bnH)h^BC>IJH%zIM`Gb z7u8k8r2Ovy#8D_UWxAs^REor3j-P$<(fIxU8=>;%{h+~UAOHk_01yBIKmZ5;0U!Vb WfB+Bx0zlw@C-8Xrc