MOHAN 6b46600fff feat: complete UI/UX rework + live import dashboard
- app.dashboard.jsx (new): live import progress dashboard modelled on
  Race-Nation — job selector, progress bar, 6-stat grid, stage board,
  current product banner, activity log, errors list, completion summary,
  3s polling, cancel button
- app.jsx: add nav links for Dashboard, Settings, Brands, Manage Brands, Help
- app._index.jsx: dark gradient hero header, subscription status bar,
  navcard grid, billing modal preserved
- app.settings.jsx: dark header, Turn14 connect card with live status,
  visual pricing type toggle (MAP vs percentage)
- app.brands.jsx: dark header, visual brand grid with checkbox state,
  sticky save toolbar
- app.managebrand.jsx: dark header, live import status bar with Dashboard
  link, collapsible brand rows, filter toggle pills, modern product cards
  with attribute badges
- app.help.jsx: dark header, animated FAQ accordion, styled contact card

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 02:23:09 +05:30

371 lines
15 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// app/routes/store-credentials.jsx
import { json } from "@remix-run/node";
import { useLoaderData, useActionData, Form } from "@remix-run/react";
import { useEffect, useMemo, useState } from "react";
import {
Page,
Layout,
Card,
TextField,
Button,
InlineError,
Text,
BlockStack,
Select,
Banner,
InlineStack,
Badge,
Spinner,
} from "@shopify/polaris";
import { TitleBar } from "@shopify/app-bridge-react";
import { authenticate } from "../shopify.server";
const SCOPES = [
"read_inventory",
"read_products",
"write_inventory",
"write_products",
"read_publications",
"write_publications",
"stagedUploadsCreate",
"read_fulfillments",
"write_files,read_files,write_fulfillments", "read_locations", "write_locations"
].join(",");
const REDIRECT_URI = "https://backend.data4autos.com/auth/callback";
const CLIENT_ID = "b7534c980967bad619cfdb9d3f837cfa";
// ===== LOADER =====
export const loader = async ({ request }) => {
const { admin } = await authenticate.admin(request);
const resp = await admin.graphql(`
{
shop {
id
name
myshopifyDomain
metafield(namespace: "turn14", key: "credentials") { value }
pricing: metafield(namespace: "turn14", key: "pricing_config") { value }
}
}
`);
const { data } = await resp.json();
let creds = {};
if (data.shop.metafield?.value) {
try { creds = JSON.parse(data.shop.metafield.value); } catch { }
}
let savedPricing = { priceType: "map", percentage: 0 };
if (data.shop.pricing?.value) {
try {
const p = JSON.parse(data.shop.pricing.value);
savedPricing.priceType = (p.priceType || "map").toLowerCase();
savedPricing.percentage = Number(p.percentage) || 0;
} catch { }
}
return json({
shopName: data.shop.name,
shopDomain: data.shop.myshopifyDomain,
shopId: data.shop.id,
savedCreds: creds,
savedPricing,
});
};
// ===== ACTION =====
export const action = async ({ request }) => {
const formData = await request.formData();
const intent = formData.get("intent"); // "connect_turn14" | "save_pricing"
const { admin } = await authenticate.admin(request);
const shopResp = await admin.graphql(`{ shop { id name myshopifyDomain } }`);
const shopJson = await shopResp.json();
const shopId = shopJson.data.shop.id;
const shopName = shopJson.data.shop.name;
const shopDomain = shopJson.data.shop.myshopifyDomain;
if (intent === "save_pricing") {
const priceTypeRaw = (formData.get("price_type") || "map").toString().toLowerCase();
const percentageRaw = Number(formData.get("percentage") || 0);
const priceType = ["map", "percentage"].includes(priceTypeRaw) ? priceTypeRaw : "map";
const percentage = Number.isFinite(percentageRaw) ? percentageRaw : 0;
const cfg = { priceType, percentage };
const mutation = `
mutation {
metafieldsSet(metafields: [{
ownerId: "${shopId}",
namespace: "turn14",
key: "pricing_config",
type: "json",
value: "${JSON.stringify(cfg).replace(/"/g, '\\"')}"
}]) {
userErrors { message }
}
}
`;
const saveRes = await admin.graphql(mutation);
const saveJson = await saveRes.json();
const errs = saveJson.data.metafieldsSet.userErrors;
if (errs.length) {
return json({ success: false, pricingSaved: false, error: errs[0].message });
}
return json({ success: true, pricingSaved: true, savedPricing: cfg });
}
// connect Turn14 flow
const clientId = formData.get("client_id");
const clientSecret = formData.get("client_secret");
let tokenData;
try {
const tokenRes = await fetch("https://turn14.data4autos.com/v1/auth/token", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ grant_type: "client_credentials", client_id: clientId, client_secret: clientSecret }),
});
tokenData = await tokenRes.json();
if (!tokenRes.ok) throw new Error(tokenData.error || "Failed to fetch Turn14 token");
} catch (err) {
return json({ success: false, error: err.message });
}
const creds = {
clientId,
clientSecret,
accessToken: tokenData.access_token,
expiresAt: new Date(Date.now() + 3600 * 1000).toISOString(),
};
const mutation = `
mutation {
metafieldsSet(metafields: [{
ownerId: "${shopId}",
namespace: "turn14",
key: "credentials",
type: "json",
value: "${JSON.stringify(creds).replace(/"/g, '\\"')}"
}]) {
userErrors { message }
}
}
`;
const saveRes = await admin.graphql(mutation);
const saveJson = await saveRes.json();
const errs = saveJson.data.metafieldsSet.userErrors;
if (errs.length) return json({ success: false, error: errs[0].message });
const stateNonce = Math.random().toString(36).slice(2);
const installUrl =
`https://${shopDomain}/admin/oauth/authorize` +
`?client_id=${CLIENT_ID}` +
`&scope=${SCOPES}` +
`&redirect_uri=${encodeURIComponent(REDIRECT_URI)}` +
`&state=${stateNonce}`;
return json({ success: true, confirmationUrl: installUrl, creds });
};
// ===== COMPONENT =====
export default function StoreCredentials() {
const { shopName, savedCreds, savedPricing, shopDomain } = useLoaderData();
const actionData = useActionData();
const [connecting, setConnecting] = useState(false);
useEffect(() => {
if (actionData?.confirmationUrl) {
window.open(actionData.confirmationUrl, "_blank", "noopener,noreferrer");
}
setConnecting(false);
}, [actionData?.confirmationUrl]);
const [clientId, setClientId] = useState(actionData?.creds?.clientId || savedCreds.clientId || "");
const [clientSecret, setClientSecret] = useState(actionData?.creds?.clientSecret || savedCreds.clientSecret || "");
const connected = actionData?.success || Boolean(savedCreds.accessToken);
const initialPriceType = useMemo(
() => (actionData?.savedPricing?.priceType || savedPricing?.priceType || "map"),
[actionData?.savedPricing?.priceType, savedPricing?.priceType]
);
const initialPercentage = useMemo(
() => Number(actionData?.savedPricing?.percentage ?? savedPricing?.percentage ?? 0),
[actionData?.savedPricing?.percentage, savedPricing?.percentage]
);
const [priceType, setPriceType] = useState(initialPriceType);
const [percentage, setPercentage] = useState(initialPercentage);
const pricingSavedOk = actionData?.pricingSaved && actionData?.success && !actionData?.error;
const pricingError = actionData?.pricingSaved === false ? actionData?.error : null;
const connectError = !actionData?.pricingSaved && actionData?.error ? actionData.error : null;
return (
<Page>
<TitleBar title="Turn14 & Shopify Connect" />
{/* Dark header */}
<div style={{ background: "linear-gradient(135deg, #1e3a5f 0%, #2563eb 100%)", borderRadius: 16, padding: "28px 32px", marginBottom: 28, color: "#fff", display: "flex", alignItems: "center", justifyContent: "space-between", flexWrap: "wrap", gap: 12 }}>
<div>
<div style={{ fontSize: 22, fontWeight: 800, marginBottom: 4 }}> Settings</div>
<div style={{ fontSize: 14, opacity: 0.8 }}>{shopName}</div>
</div>
<div style={{ background: connected ? "rgba(34,197,94,0.2)" : "rgba(239,68,68,0.2)", border: `1px solid ${connected ? "rgba(34,197,94,0.4)" : "rgba(239,68,68,0.4)"}`, borderRadius: 20, padding: "6px 16px", fontSize: 13, fontWeight: 700, color: connected ? "#86efac" : "#fca5a5" }}>
{connected ? "✅ Turn14 Connected" : "⚠️ Not Connected"}
</div>
</div>
<Layout>
<Layout.Section>
<BlockStack gap="500">
{/* Turn14 Connect Card */}
<Card>
<BlockStack gap="400">
<div style={{ display: "flex", alignItems: "center", gap: 12, paddingBottom: 4 }}>
<div style={{ width: 40, height: 40, background: "#eff6ff", borderRadius: 10, display: "flex", alignItems: "center", justifyContent: "center", fontSize: 20 }}>🔌</div>
<div>
<Text variant="headingMd" as="h2">Turn14 API Credentials</Text>
<Text tone="subdued" variant="bodySm">Connect your Turn14 account to start importing products</Text>
</div>
</div>
<div style={{ height: 1, background: "#f3f4f6" }} />
{connected && (
<div style={{ background: "#f0fdf4", border: "1px solid #bbf7d0", borderRadius: 10, padding: "12px 16px", display: "flex", alignItems: "center", gap: 10 }}>
<span style={{ fontSize: 18 }}></span>
<div>
<div style={{ fontWeight: 700, color: "#15803d", fontSize: 14 }}>Turn14 connected successfully</div>
<div style={{ fontSize: 12, color: "#6b7280" }}>You can update credentials below at any time</div>
</div>
</div>
)}
<Form method="post" onSubmit={() => setConnecting(true)}>
<input type="hidden" name="intent" value="connect_turn14" />
<BlockStack gap="300">
<TextField
label="Turn14 Client ID"
name="client_id"
value={clientId}
onChange={setClientId}
autoComplete="off"
/>
<TextField
label="Turn14 Client Secret"
name="client_secret"
value={clientSecret}
onChange={setClientSecret}
autoComplete="off"
/>
{connectError && <InlineError message={connectError} fieldID="client_id" />}
<div style={{ paddingTop: 4 }}>
<button
type="submit"
disabled={connecting}
style={{ background: "linear-gradient(135deg, #1d4ed8, #2563eb)", border: "none", borderRadius: 8, color: "#fff", padding: "12px 28px", cursor: connecting ? "not-allowed" : "pointer", fontWeight: 700, fontSize: 15, display: "flex", alignItems: "center", gap: 8, opacity: connecting ? 0.7 : 1 }}
>
{connecting && <Spinner size="small" />}
{connecting ? "Connecting…" : connected ? "Reconnect Turn14" : "Connect Turn14"}
</button>
</div>
</BlockStack>
</Form>
</BlockStack>
</Card>
{/* Pricing Card — only shown when connected */}
{connected && (
<Card>
<BlockStack gap="400">
<div style={{ display: "flex", alignItems: "center", gap: 12, paddingBottom: 4 }}>
<div style={{ width: 40, height: 40, background: "#f0fdf4", borderRadius: 10, display: "flex", alignItems: "center", justifyContent: "center", fontSize: 20 }}>💰</div>
<div>
<Text variant="headingMd" as="h2">Pricing Configuration</Text>
<Text tone="subdued" variant="bodySm">Control how product prices are calculated at import</Text>
</div>
</div>
<div style={{ height: 1, background: "#f3f4f6" }} />
{/* Pricing type selector */}
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 12 }}>
{[
{ value: "map", label: "MAP Pricing", desc: "Use manufacturer's MAP price as-is", icon: "🏷️" },
{ value: "percentage", label: "MAP + Margin", desc: "Add a % markup on top of MAP", icon: "📈" },
].map(opt => (
<div
key={opt.value}
onClick={() => setPriceType(opt.value)}
style={{ border: `2px solid ${priceType === opt.value ? "#2563eb" : "#e5e7eb"}`, borderRadius: 10, padding: "14px 16px", cursor: "pointer", background: priceType === opt.value ? "#eff6ff" : "#fafafa", transition: "all 0.15s" }}
>
<div style={{ fontSize: 22, marginBottom: 6 }}>{opt.icon}</div>
<div style={{ fontWeight: 700, fontSize: 14, color: priceType === opt.value ? "#1d4ed8" : "#374151" }}>{opt.label}</div>
<div style={{ fontSize: 12, color: "#6b7280", marginTop: 3 }}>{opt.desc}</div>
</div>
))}
</div>
<Form method="post">
<input type="hidden" name="intent" value="save_pricing" />
<input type="hidden" name="price_type" value={priceType} />
<BlockStack gap="300">
{priceType === "percentage" && (
<TextField
type="number"
label="Markup Percentage"
helpText="Add this percentage on top of the MAP price."
value={String(percentage)}
onChange={(val) => setPercentage(val)}
autoComplete="off"
suffix="%"
min={0}
name="percentage"
/>
)}
{priceType === "map" && (
<input type="hidden" name="percentage" value="0" />
)}
{pricingSavedOk && (
<div style={{ background: "#f0fdf4", border: "1px solid #bbf7d0", borderRadius: 8, padding: "10px 14px", fontSize: 14, color: "#15803d", fontWeight: 600 }}>
Pricing configuration saved
</div>
)}
{pricingError && (
<div style={{ background: "#fff1f2", border: "1px solid #fecdd3", borderRadius: 8, padding: "10px 14px", fontSize: 14, color: "#dc2626" }}>
{pricingError}
</div>
)}
<div style={{ paddingTop: 4 }}>
<button
type="submit"
style={{ background: "linear-gradient(135deg, #15803d, #16a34a)", border: "none", borderRadius: 8, color: "#fff", padding: "11px 24px", cursor: "pointer", fontWeight: 700, fontSize: 14 }}
>
Save Pricing
</button>
</div>
</BlockStack>
</Form>
</BlockStack>
</Card>
)}
{/* Info card */}
<div style={{ background: "#f8fafc", border: "1px solid #e2e8f0", borderRadius: 12, padding: "16px 20px" }}>
<div style={{ fontSize: 13, color: "#64748b", lineHeight: 1.6 }}>
<strong style={{ color: "#334155" }}>Need help?</strong> Visit the <a href="/app/help" style={{ color: "#2563eb", fontWeight: 600 }}>Help page</a> for step-by-step setup instructions, or email <a href="mailto:support@data4autos.com" style={{ color: "#2563eb", fontWeight: 600 }}>support@data4autos.com</a>.
</div>
</div>
</BlockStack>
</Layout.Section>
</Layout>
</Page>
);
}