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

390 lines
18 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.

import { json } from "@remix-run/node";
import { useLoaderData, Form, useActionData, useNavigate } from "@remix-run/react";
import {
Page,
Layout,
Card,
TextField,
Checkbox,
Button,
Spinner,
Toast,
Frame,
Text,
Banner,
InlineStack,
} from "@shopify/polaris";
import { useEffect, useMemo, useState } from "react";
import { TitleBar } from "@shopify/app-bridge-react";
import { getTurn14AccessTokenFromMetafield } from "../utils/turn14Token.server";
import { authenticate } from "../shopify.server";
const PLAN_NAME = "Starter Sync";
const ALLOWED_STATUSES = ["ACTIVE", "TRIAL"];
async function checkShopExists(shop) {
try {
const resp = await fetch(`https://backend.data4autos.com/checkisshopdataexists/${shop}`);
const data = await resp.json();
return data.status === 1;
} catch (err) {
console.error("Error checking shop:", err);
return false;
}
}
function getIntervalLabel(interval) {
if (interval === "ANNUAL") return "Every 12 months";
if (interval === "EVERY_30_DAYS") return "Every 30 days";
return interval || "N/A";
}
function formatMoney(amount, currencyCode = "USD") {
if (amount == null) return "N/A";
return `${currencyCode} ${Number(amount).toFixed(2)}`;
}
function formatDate(date) {
if (!date) return "N/A";
return new Date(date).toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric" });
}
async function getSubscriptionDetails(request) {
const { admin, session } = await authenticate.admin(request);
const shop = session.shop;
const resp = await admin.graphql(`
query CurrentSubscriptionDetails {
currentAppInstallation {
activeSubscriptions {
id name status test createdAt trialDays currentPeriodEnd
lineItems {
id
plan {
pricingDetails {
__typename
... on AppRecurringPricing {
interval
price { amount currencyCode }
}
}
}
}
}
}
}
`);
const result = await resp.json();
const subscriptions = result?.data?.currentAppInstallation?.activeSubscriptions || [];
const subscription = subscriptions.find((sub) => ALLOWED_STATUSES.includes(sub.status)) || subscriptions[0] || null;
const recurringPricing = subscription?.lineItems?.find((item) => item?.plan?.pricingDetails?.__typename === "AppRecurringPricing")?.plan?.pricingDetails || null;
const isSubscribed = !!subscription && ALLOWED_STATUSES.includes(subscription.status);
return {
shop,
isSubscribed,
subscription: subscription ? {
id: subscription.id, name: subscription.name || PLAN_NAME, status: subscription.status,
test: subscription.test ?? false, createdAt: subscription.createdAt,
trialDays: subscription.trialDays ?? 0, currentPeriodEnd: subscription.currentPeriodEnd,
interval: recurringPricing?.interval || null, priceAmount: recurringPricing?.price?.amount || null,
currencyCode: recurringPricing?.price?.currencyCode || "USD",
} : null,
};
}
export const loader = async ({ request }) => {
console.log("🚀 Loader started");
let admin, session, shop;
try {
const authResult = await authenticate.admin(request);
admin = authResult.admin;
session = authResult.session;
shop = session?.shop;
} catch (err) {
return json({ brands: [], collections: [], selectedBrandsFromShopify: [], shop: "", error: "Shopify authentication failed", isSubscribed: false, subscription: null });
}
const { isSubscribed, subscription } = await getSubscriptionDetails(request);
let accessToken = "";
try {
accessToken = await getTurn14AccessTokenFromMetafield(request);
} catch (err) {
return json({ brands: [], collections: [], selectedBrandsFromShopify: [], shop, error: "Failed to fetch Turn14 access token", isSubscribed, subscription });
}
let brandJson;
try {
const brandRes = await fetch("https://turn14.data4autos.com/v1/brands", { headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json" } });
brandJson = await brandRes.json();
if (!brandRes.ok) return json({ brands: [], collections: [], selectedBrandsFromShopify: [], shop, error: brandJson?.error || "Failed to fetch brands", isSubscribed, subscription });
} catch (err) {
return json({ brands: [], collections: [], selectedBrandsFromShopify: [], shop, error: "Turn14 brands fetch crashed", isSubscribed, subscription });
}
let collections = [];
try {
const gqlRaw = await admin.graphql(`{ collections(first: 100) { edges { node { id title } } } }`);
const gql = await gqlRaw.json();
collections = gql?.data?.collections?.edges?.map((e) => e.node) || [];
} catch (err) {}
let selectedBrands = [];
try {
const res = await admin.graphql(`{ shop { metafield(namespace: "turn14", key: "selected_brands") { value } } }`);
const data = await res.json();
const rawValue = data?.data?.shop?.metafield?.value;
if (rawValue) selectedBrands = JSON.parse(rawValue);
} catch (err) {}
return json({ brands: brandJson?.data || [], collections, selectedBrandsFromShopify: selectedBrands, shop, isSubscribed, subscription });
};
export const action = async ({ request }) => {
const { isSubscribed } = await getSubscriptionDetails(request);
if (!isSubscribed) return json({ error: "An active subscription or free trial is required to save brand collections." }, { status: 403 });
const formData = await request.formData();
const selectedBrands = JSON.parse(formData.get("selectedBrands") || "[]");
const selectedOldBrands = JSON.parse(formData.get("selectedOldBrands") || "[]");
const { session } = await authenticate.admin(request);
const shop = session.shop;
selectedBrands.forEach((brand) => { delete brand.pricegroups; });
selectedOldBrands.forEach((brand) => { delete brand.pricegroups; });
const resp = await fetch("https://backend.data4autos.com/managebrands", {
method: "POST",
headers: { "Content-Type": "application/json", "shop-domain": shop },
body: JSON.stringify({ shop, selectedBrands, selectedOldBrands }),
});
if (!resp.ok) {
const err = await resp.text();
return json({ error: err }, { status: resp.status });
}
const { processId, status } = await resp.json();
return json({ processId, status });
};
export default function BrandsPage() {
const navigate = useNavigate();
const { brands = [], selectedBrandsFromShopify = [], shop = "", error, isSubscribed = false, subscription = null } = useLoaderData() || {};
const actionData = useActionData() || {};
const [selectedIdsold, setSelectedIdsold] = useState([]);
const [selectedIds, setSelectedIds] = useState(() => (selectedBrandsFromShopify ?? []).map((b) => b.id));
const [search, setSearch] = useState("");
const [filteredBrands, setFilteredBrands] = useState(brands);
const [toastActive, setToastActive] = useState(false);
const [polling, setPolling] = useState(false);
const [status, setStatus] = useState(actionData.status || "");
const [Turn14Enabled, setTurn14Enabled] = useState(null);
const [saving, setSaving] = useState(false);
useEffect(() => {
if (!shop) return;
(async () => { const result = await checkShopExists(shop); setTurn14Enabled(result); })();
}, [shop]);
useEffect(() => { setSelectedIdsold(selectedIds); }, [toastActive]);
useEffect(() => {
const term = search.toLowerCase();
setFilteredBrands(brands.filter((b) => b.name.toLowerCase().includes(term)));
}, [search, brands]);
useEffect(() => {
if (actionData.status) { setStatus(actionData.status); setToastActive(true); setSaving(false); }
if (actionData.error) setSaving(false);
}, [actionData.status, actionData.error]);
const checkStatus = async () => {
if (!actionData.processId) return;
setPolling(true);
const resp = await fetch(`https://backend.data4autos.com/managebrands/status/${actionData.processId}`, { headers: { "shop-domain": window.shopify?.shop || "" } });
const jsonBody = await resp.json();
setStatus(jsonBody.status + (jsonBody.detail ? ` (${jsonBody.detail})` : ""));
setPolling(false);
};
const toggleSelect = (id) => {
if (!isSubscribed) return;
setSelectedIds((prev) => prev.includes(id) ? prev.filter((i) => i !== id) : [...prev, id]);
};
const allFilteredSelected = filteredBrands.length > 0 && filteredBrands.every((b) => selectedIds.includes(b.id));
const toggleSelectAll = () => {
if (!isSubscribed) return;
const ids = filteredBrands.map((b) => b.id);
if (allFilteredSelected) setSelectedIds((prev) => prev.filter((id) => !ids.includes(id)));
else setSelectedIds((prev) => Array.from(new Set([...prev, ...ids])));
};
const trialDaysLeft = useMemo(() => {
if (!subscription?.trialDays || !subscription?.createdAt || subscription.status !== "TRIAL") return null;
const created = new Date(subscription.createdAt);
const trialEnd = new Date(created);
trialEnd.setDate(trialEnd.getDate() + subscription.trialDays);
return Math.max(0, Math.ceil((trialEnd.getTime() - Date.now()) / (1000 * 60 * 60 * 24)));
}, [subscription]);
const selectedBrands = brands.filter((b) => selectedIds.includes(b.id));
const selectedOldBrands = brands.filter((b) => selectedIdsold.includes(b.id));
const shopDomain = (shop || "").split(".")[0];
if (Turn14Enabled === false) {
return (
<Frame>
<Page fullWidth>
<TitleBar title="Data4Autos Turn14 Integration" />
<div style={{ background: "linear-gradient(135deg, #1e3a5f 0%, #2563eb 100%)", borderRadius: 16, padding: "28px 32px", marginBottom: 24, color: "#fff" }}>
<div style={{ fontSize: 22, fontWeight: 800 }}>🏷 Brand Selection</div>
<div style={{ fontSize: 14, opacity: 0.8, marginTop: 4 }}>{shop}</div>
</div>
<Card>
<div style={{ padding: 48, textAlign: "center" }}>
<div style={{ fontSize: 48, marginBottom: 12 }}>🔌</div>
<Text as="h1" variant="headingLg">Turn14 not connected yet</Text>
<div style={{ marginTop: 8, color: "#6b7280", marginBottom: 24 }}>Complete the connection in Settings before browsing brands.</div>
<div style={{ display: "flex", gap: 12, justifyContent: "center" }}>
<Button variant="primary" onClick={() => navigate("/app/settings")}>Go to Settings</Button>
<Button onClick={() => navigate("/app/help")}>View Help</Button>
</div>
</div>
</Card>
</Page>
</Frame>
);
}
return (
<Frame>
<Page fullWidth>
<TitleBar title="Data4Autos Turn14 Integration" />
{/* Header */}
<div style={{ background: "linear-gradient(135deg, #1e3a5f 0%, #2563eb 100%)", borderRadius: 16, padding: "28px 32px", marginBottom: 24, color: "#fff", display: "flex", alignItems: "center", justifyContent: "space-between", flexWrap: "wrap", gap: 12 }}>
<div>
<div style={{ fontSize: 22, fontWeight: 800 }}>🏷 Brand Selection</div>
<div style={{ fontSize: 14, opacity: 0.8, marginTop: 4 }}>{filteredBrands.length} brands available · {selectedIds.length} selected</div>
</div>
{!isSubscribed && (
<div style={{ background: "rgba(251,191,36,0.2)", border: "1px solid rgba(251,191,36,0.4)", borderRadius: 8, padding: "8px 14px", fontSize: 13, color: "#fbbf24", fontWeight: 700 }}>
Subscription required
</div>
)}
</div>
{/* Subscription warning */}
{!isSubscribed && (
<div style={{ background: "#fffbeb", border: "1px solid #fde68a", borderRadius: 12, padding: "16px 20px", marginBottom: 20 }}>
<div style={{ fontWeight: 700, color: "#b45309", marginBottom: 6 }}> Active subscription required</div>
<div style={{ fontSize: 13, color: "#92400e", marginBottom: 12 }}>Brand selection is only available with an active subscription or free trial.</div>
<Button variant="primary" onClick={() => navigate("/app")}>Manage Subscription</Button>
</div>
)}
{error && (
<div style={{ background: "#fff1f2", border: "1px solid #fecdd3", borderRadius: 12, padding: "14px 18px", marginBottom: 20, color: "#dc2626" }}>
{error}
</div>
)}
{actionData?.error && (
<div style={{ background: "#fff1f2", border: "1px solid #fecdd3", borderRadius: 12, padding: "14px 18px", marginBottom: 20, color: "#dc2626" }}>
{actionData.error}
</div>
)}
{/* Sticky toolbar */}
<div style={{ position: "sticky", top: 0, zIndex: 10, background: "#fff", borderRadius: 12, boxShadow: "0 2px 12px rgba(0,0,0,0.08)", padding: "14px 20px", marginBottom: 20, display: "flex", justifyContent: "space-between", alignItems: "center", gap: 16, flexWrap: "wrap" }}>
<div style={{ display: "flex", gap: 14, alignItems: "center", flexWrap: "wrap" }}>
<div style={{ minWidth: 280 }}>
<TextField
labelHidden
label="Search brands"
value={search}
onChange={setSearch}
placeholder="🔍 Search brands…"
autoComplete="off"
disabled={!isSubscribed}
/>
</div>
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
<Checkbox label="" checked={allFilteredSelected} onChange={toggleSelectAll} disabled={!isSubscribed} />
<span style={{ fontSize: 13, fontWeight: 600, color: "#374151", cursor: "pointer" }} onClick={toggleSelectAll}>Select All</span>
</div>
<div style={{ fontSize: 13, color: "#6b7280" }}>
<span style={{ fontWeight: 700, color: "#1d4ed8" }}>{selectedIds.length}</span> selected
</div>
{(actionData?.processId) && (
<div style={{ display: "flex", gap: 8, alignItems: "center" }}>
<div style={{ fontSize: 13, color: "#374151" }}><strong>Status:</strong> {status || ""}</div>
<Button onClick={checkStatus} loading={polling} size="slim">Refresh</Button>
</div>
)}
</div>
<Form method="post" onSubmit={() => setSaving(true)}>
<input type="hidden" name="selectedBrands" value={JSON.stringify(selectedBrands)} />
<input type="hidden" name="selectedOldBrands" value={JSON.stringify(selectedOldBrands)} />
<button
type="submit"
disabled={saving || !isSubscribed}
style={{ background: saving || !isSubscribed ? "#9ca3af" : "linear-gradient(135deg, #1d4ed8, #2563eb)", border: "none", borderRadius: 8, color: "#fff", padding: "10px 22px", cursor: saving || !isSubscribed ? "not-allowed" : "pointer", fontWeight: 700, fontSize: 14, display: "flex", alignItems: "center", gap: 8 }}
>
{saving && <Spinner size="small" />}
{saving ? "Saving…" : `Save ${selectedIds.length} Collections`}
</button>
</Form>
</div>
{/* Brand grid */}
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(200px, 1fr))", gap: 14 }}>
{filteredBrands.map((brand) => {
const isSelected = selectedIds.includes(brand.id);
return (
<div
key={brand.id}
onClick={() => toggleSelect(brand.id)}
style={{ background: isSelected ? "#eff6ff" : "#fff", border: `2px solid ${isSelected ? "#2563eb" : "#e5e7eb"}`, borderRadius: 12, padding: "16px 14px", cursor: isSubscribed ? "pointer" : "default", position: "relative", transition: "all 0.15s", boxShadow: isSelected ? "0 0 0 3px rgba(37,99,235,0.12)" : "none" }}
>
{/* Checkbox badge */}
<div style={{ position: "absolute", top: 10, right: 10 }}>
<div style={{ width: 20, height: 20, borderRadius: "50%", background: isSelected ? "#2563eb" : "#e5e7eb", border: `2px solid ${isSelected ? "#2563eb" : "#d1d5db"}`, display: "flex", alignItems: "center", justifyContent: "center" }}>
{isSelected && <span style={{ color: "#fff", fontSize: 11 }}></span>}
</div>
</div>
<div style={{ display: "flex", justifyContent: "center", marginBottom: 12 }}>
<img
src={brand.logo || "https://cdn.shopify.com/s/files/1/0757/9955/files/no-image_280x@2x.png"}
alt={brand.name}
style={{ width: 72, height: 72, objectFit: "contain", borderRadius: 8 }}
/>
</div>
<div style={{ textAlign: "center", fontWeight: 700, fontSize: 14, color: isSelected ? "#1d4ed8" : "#374151", lineHeight: 1.3 }}>
{brand.name}
</div>
<div style={{ textAlign: "center", fontSize: 11, color: "#9ca3af", marginTop: 4 }}>ID: {brand.id}</div>
</div>
);
})}
</div>
{filteredBrands.length === 0 && (
<div style={{ textAlign: "center", padding: 48, color: "#9ca3af" }}>
<div style={{ fontSize: 36, marginBottom: 12 }}>🔍</div>
<div style={{ fontSize: 16, fontWeight: 600 }}>No brands found</div>
<div style={{ fontSize: 13, marginTop: 6 }}>Try a different search term</div>
</div>
)}
{toastActive && (
<Toast content="Collections saved successfully!" onDismiss={() => setToastActive(false)} />
)}
</Page>
</Frame>
);
}