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 }) => { const { session } = await authenticate.admin(request); const backendApiUrl = getBackendApiUrl(); const shop = session.shop; 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}`, }; } try { const statusResponse = await fetch( `${backendApiUrl}/pipeline/status/${encodeURIComponent(shop)}`, ); if (statusResponse.ok) { currentJob = await readJsonSafe(statusResponse); } } catch { currentJob = null; } return { 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 [job, setJob] = useState(currentJob); const setupUrl = `${backendApiUrl}/auth/login?shop=${encodeURIComponent(shop)}`; const isSubmitting = ["loading", "submitting"].includes(fetcher.state) && fetcher.formMethod === "POST"; const connectionState = useMemo(() => { if (connection?.status === 1) { return "Ready"; } 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 (

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

)}
Start import
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.
); } export const headers = (headersArgs) => { return boundary.headers(headersArgs); };