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

364 lines
20 KiB
JavaScript

import React, { useMemo, useState } from "react";
import { json } from "@remix-run/node";
import { useLoaderData, useActionData, useSubmit, Form, useNavigate } from "@remix-run/react";
import {
Page,
Modal,
TextField,
BlockStack,
InlineStack,
ChoiceList,
Banner,
Button,
Divider,
} from "@shopify/polaris";
import { TitleBar } from "@shopify/app-bridge-react";
import { authenticate } from "../shopify.server";
/* ─── pricing constants ────────────────────────────────────────────────────── */
const PLAN_NAME = "Starter Sync";
const MONTHLY_AMOUNT = 79;
const ANNUAL_AMOUNT = 790;
const TRIAL_DAYS = 14;
const ALLOWED_STATUSES = ["ACTIVE", "TRIAL"];
/* ─── helpers ──────────────────────────────────────────────────────────────── */
function formatMoney(amount, currencyCode = "USD") {
if (amount == null) return "N/A";
return `${currencyCode} ${Number(amount).toFixed(2)}`;
}
function getIntervalLabel(interval) {
if (interval === "ANNUAL") return "Every 12 months";
if (interval === "EVERY_30_DAYS") return "Every 30 days";
return interval || "N/A";
}
function getStatusTone(status) {
if (status === "ACTIVE") return { bg: "#f0fdf4", border: "#bbf7d0", color: "#15803d" };
if (status === "TRIAL") return { bg: "#eff6ff", border: "#bfdbfe", color: "#1d4ed8" };
if (["CANCELLED", "EXPIRED", "DECLINED"].includes(status)) return { bg: "#fff1f2", border: "#fecdd3", color: "#dc2626" };
return { bg: "#fefce8", border: "#fde68a", color: "#d97706" };
}
/* ─── loader ────────────────────────────────────────────────────────────────── */
export const loader = async ({ 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);
const subscriptionDetails = 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;
if (shop === "racewerksengg.myshopify.com") {
return json({ redirectToBilling: false, shop, isSubscribed: true, subscription: subscriptionDetails, allSubscriptions: subscriptions });
}
return json({ redirectToBilling: !isSubscribed, shop, isSubscribed, subscription: subscriptionDetails, allSubscriptions: subscriptions });
};
/* ─── action ────────────────────────────────────────────────────────────────── */
export const action = async ({ request }) => {
const { admin, session } = await authenticate.admin(request);
const form = await request.formData();
const rawCadence = form.get("cadence");
const cadence = rawCadence === "ANNUAL" ? "ANNUAL" : "MONTHLY";
const interval = cadence === "ANNUAL" ? "ANNUAL" : "EVERY_30_DAYS";
const amount = cadence === "ANNUAL" ? ANNUAL_AMOUNT : MONTHLY_AMOUNT;
const shop = session.shop;
const shopDomain = (shop || "").split(".")[0];
const returnUrl = `https://admin.shopify.com/store/${shopDomain}/apps/d4a-turn14/app`;
const createRes = await admin.graphql(`
mutation CreateSubscription {
appSubscriptionCreate(
name: "${PLAN_NAME} - ${cadence === "ANNUAL" ? "Annual" : "Monthly"}"
returnUrl: "${returnUrl}"
lineItems: [{ plan: { appRecurringPricingDetails: { price: { amount: ${amount}, currencyCode: USD } interval: ${interval} } } }]
trialDays: ${TRIAL_DAYS}
replacementBehavior: STANDARD
test: false
) {
confirmationUrl
appSubscription { id name status trialDays }
userErrors { field message }
}
}
`);
const data = await createRes.json();
const url = data?.data?.appSubscriptionCreate?.confirmationUrl || null;
const userErrors = data?.data?.appSubscriptionCreate?.userErrors || [];
const topLevelErrors = data?.errors || [];
if (!url || userErrors.length || topLevelErrors.length) {
return json({ errors: ["Failed to create subscription.", ...userErrors.map((e) => e.message), ...topLevelErrors.map((e) => e.message || String(e))] }, { status: 400 });
}
return json({ confirmationUrl: url });
};
/* ─── NavCard ────────────────────────────────────────────────────────────────── */
function NavCard({ icon, title, desc, link, accent }) {
const colors = {
blue: { bg: "#eff6ff", border: "#bfdbfe", icon: "#2563eb" },
green: { bg: "#f0fdf4", border: "#bbf7d0", icon: "#16a34a" },
purple: { bg: "#faf5ff", border: "#e9d5ff", icon: "#7c3aed" },
amber: { bg: "#fffbeb", border: "#fde68a", icon: "#b45309" },
};
const c = colors[accent] || colors.blue;
return (
<a href={link} style={{ textDecoration: "none", display: "block" }}>
<div style={{ background: c.bg, border: `1px solid ${c.border}`, borderRadius: 12, padding: "18px 20px", cursor: "pointer", transition: "transform 0.15s, box-shadow 0.15s", height: "100%", display: "flex", flexDirection: "column", gap: 10 }}
onMouseEnter={(e) => { e.currentTarget.style.transform = "translateY(-2px)"; e.currentTarget.style.boxShadow = "0 8px 20px rgba(0,0,0,0.08)"; }}
onMouseLeave={(e) => { e.currentTarget.style.transform = ""; e.currentTarget.style.boxShadow = ""; }}
>
<div style={{ fontSize: 28 }}>{icon}</div>
<div style={{ fontWeight: 700, fontSize: 15, color: c.icon }}>{title}</div>
<div style={{ fontSize: 13, color: "#6b7280", lineHeight: 1.5 }}>{desc}</div>
</div>
</a>
);
}
/* ─── Page ───────────────────────────────────────────────────────────────────── */
export default function Index() {
const actionData = useActionData();
const loaderData = useLoaderData();
const submit = useSubmit();
const navigate = useNavigate();
const [activeModal, setActiveModal] = useState(false);
const [cadence, setCadence] = useState("MONTHLY");
const subscription = loaderData?.subscription || null;
const shop = loaderData?.shop || "";
const isSubscribed = loaderData?.isSubscribed || false;
const hasConfirmationUrl = Boolean(actionData?.confirmationUrl);
const errors = actionData?.errors || [];
const shopDomain = (shop || "").split(".")[0];
const formatDate = (d) => d ? new Date(d).toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric" }) : "N/A";
const previewBase = subscription?.createdAt ? new Date(subscription.createdAt) : new Date();
const previewTrialEnd = new Date(previewBase);
previewTrialEnd.setDate(previewTrialEnd.getDate() + TRIAL_DAYS);
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);
const msLeft = trialEnd.getTime() - Date.now();
return Math.max(0, Math.ceil(msLeft / (1000 * 60 * 60 * 24)));
}, [subscription?.trialDays, subscription?.createdAt, subscription?.status]);
const isPreviewMode = !isSubscribed;
const displayPlan = isPreviewMode ? `${PLAN_NAME}${cadence === "ANNUAL" ? "Annual" : "Monthly"}` : subscription?.name || PLAN_NAME;
const displayInterval = isPreviewMode ? (cadence === "ANNUAL" ? "Every 12 months" : "Every 30 days") : getIntervalLabel(subscription?.interval);
const displayPrice = isPreviewMode ? (cadence === "ANNUAL" ? `$${ANNUAL_AMOUNT}/yr` : `$${MONTHLY_AMOUNT}/mo`) : (subscription?.priceAmount ? `${formatMoney(subscription.priceAmount, subscription.currencyCode)}${subscription?.interval === "ANNUAL" ? "/yr" : "/mo"}` : "N/A");
const displayNextRenewal = isPreviewMode ? `${formatDate(previewTrialEnd)} (after ${TRIAL_DAYS}-day trial)` : formatDate(subscription?.currentPeriodEnd);
const displayStatus = isPreviewMode ? "Not active" : subscription?.status || "N/A";
const statusStyle = getStatusTone(subscription?.status || "PENDING");
const navItems = [
{ icon: "⚙️", title: "Settings", desc: "Connect Turn14 API & configure pricing", link: `/app/settings`, accent: "purple" },
{ icon: "🏷️", title: "Browse Brands", desc: "Select brands to sync from Turn14", link: `https://admin.shopify.com/store/${shopDomain}/apps/d4a-turn14/app/brands`, accent: "blue" },
{ icon: "📦", title: "Manage Brands", desc: "Import products from selected brands", link: `https://admin.shopify.com/store/${shopDomain}/apps/d4a-turn14/app/managebrand`, accent: "green" },
{ icon: "📊", title: "Dashboard", desc: "Track live import progress & stats", link: `/app/dashboard`, accent: "amber" },
];
return (
<Page>
<TitleBar title="Data4Autos Turn14 Integration" />
{/* Hero header */}
<div style={{ background: "linear-gradient(135deg, #0f172a 0%, #1e3a5f 50%, #2563eb 100%)", borderRadius: 16, padding: "36px 40px", marginBottom: 28, color: "#fff", position: "relative", overflow: "hidden" }}>
<div style={{ position: "absolute", right: -40, top: -40, width: 220, height: 220, borderRadius: "50%", background: "rgba(255,255,255,0.04)" }} />
<div style={{ position: "absolute", right: 60, bottom: -60, width: 150, height: 150, borderRadius: "50%", background: "rgba(255,255,255,0.03)" }} />
<div style={{ position: "relative" }}>
<div style={{ fontSize: 13, fontWeight: 700, letterSpacing: "0.1em", opacity: 0.6, textTransform: "uppercase", marginBottom: 8 }}>Data4Autos</div>
<div style={{ fontSize: 26, fontWeight: 900, marginBottom: 8, lineHeight: 1.2 }}>Turn14 Distribution Integration</div>
<div style={{ fontSize: 15, opacity: 0.75, maxWidth: 520, lineHeight: 1.6 }}>
Sync product brands, manage collections, and automate catalog setup directly from Turn14 into your Shopify store.
</div>
<div style={{ marginTop: 20, display: "flex", gap: 12, flexWrap: "wrap" }}>
<button
onClick={() => navigate("/app/dashboard")}
style={{ background: "rgba(255,255,255,0.15)", border: "1px solid rgba(255,255,255,0.3)", borderRadius: 8, color: "#fff", padding: "9px 20px", cursor: "pointer", fontWeight: 700, fontSize: 14 }}
>
📊 Open Dashboard
</button>
<button
onClick={() => setActiveModal(true)}
style={{ background: isSubscribed ? "rgba(34,197,94,0.2)" : "#2563eb", border: isSubscribed ? "1px solid rgba(34,197,94,0.4)" : "none", borderRadius: 8, color: "#fff", padding: "9px 20px", cursor: "pointer", fontWeight: 700, fontSize: 14 }}
>
{isSubscribed ? "✅ Subscription Active" : "🚀 Start Free Trial"}
</button>
</div>
</div>
</div>
{/* Subscription status bar */}
<div style={{ background: statusStyle.bg, border: `1px solid ${statusStyle.border}`, borderRadius: 12, padding: "14px 20px", marginBottom: 24, display: "flex", gap: 24, flexWrap: "wrap", alignItems: "center" }}>
<div>
<div style={{ fontSize: 11, fontWeight: 700, color: "#9ca3af", textTransform: "uppercase", letterSpacing: "0.06em", marginBottom: 3 }}>Subscription Status</div>
<div style={{ fontSize: 15, fontWeight: 800, color: statusStyle.color }}>{isSubscribed ? subscription?.status : "NOT SUBSCRIBED"}</div>
</div>
<div style={{ width: 1, height: 36, background: "#e5e7eb" }} />
<div>
<div style={{ fontSize: 11, fontWeight: 700, color: "#9ca3af", textTransform: "uppercase", letterSpacing: "0.06em", marginBottom: 3 }}>Plan</div>
<div style={{ fontSize: 14, fontWeight: 600, color: "#374151" }}>{displayPlan}</div>
</div>
<div style={{ width: 1, height: 36, background: "#e5e7eb" }} />
<div>
<div style={{ fontSize: 11, fontWeight: 700, color: "#9ca3af", textTransform: "uppercase", letterSpacing: "0.06em", marginBottom: 3 }}>Price</div>
<div style={{ fontSize: 14, fontWeight: 600, color: "#374151" }}>{displayPrice}</div>
</div>
{trialDaysLeft != null && (
<>
<div style={{ width: 1, height: 36, background: "#e5e7eb" }} />
<div>
<div style={{ fontSize: 11, fontWeight: 700, color: "#9ca3af", textTransform: "uppercase", letterSpacing: "0.06em", marginBottom: 3 }}>Trial Days Left</div>
<div style={{ fontSize: 14, fontWeight: 700, color: "#d97706" }}>{trialDaysLeft} days</div>
</div>
</>
)}
<div style={{ marginLeft: "auto" }}>
<button onClick={() => setActiveModal(true)} style={{ background: "#f3f4f6", border: "1px solid #e5e7eb", borderRadius: 8, color: "#374151", padding: "7px 16px", cursor: "pointer", fontWeight: 600, fontSize: 13 }}>
{isSubscribed ? "View Details" : "Manage Billing →"}
</button>
</div>
</div>
{/* Nav cards grid */}
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(220px, 1fr))", gap: 16, marginBottom: 28 }}>
{navItems.map((item) => (
<NavCard key={item.title} {...item} />
))}
</div>
{/* Footer help */}
<div style={{ textAlign: "center", padding: "16px 0", fontSize: 13, color: "#9ca3af" }}>
Need help? Email <a href="mailto:support@data4autos.com" style={{ color: "#2563eb", fontWeight: 600 }}>support@data4autos.com</a> or visit the <a href="/app/help" style={{ color: "#2563eb", fontWeight: 600 }}>Help page</a>
</div>
{/* Billing modal */}
<Modal
open={activeModal}
onClose={() => setActiveModal(false)}
title="Subscription Details"
primaryAction={
hasConfirmationUrl ? undefined : isSubscribed ? undefined : {
content: cadence === "ANNUAL" ? `Start ${TRIAL_DAYS}-day Trial — $${ANNUAL_AMOUNT}/yr` : `Start ${TRIAL_DAYS}-day Trial — $${MONTHLY_AMOUNT}/mo`,
onAction: () => {
const form = document.getElementById("billing-form");
if (form) {
const hidden = document.getElementById("cadence-field");
if (hidden) hidden.value = cadence;
submit(form, { method: "post" });
}
},
}
}
secondaryActions={[{ content: "Close", onAction: () => setActiveModal(false) }]}
>
<Form id="billing-form" method="post">
<input type="hidden" name="cadence" id="cadence-field" value={cadence} readOnly />
<Modal.Section>
<BlockStack gap="300">
{errors.length > 0 && (
<Banner title="Couldn't create subscription" tone="critical">
<ul style={{ margin: 0, paddingLeft: "1rem" }}>
{errors.map((e, i) => <li key={i}>{e}</li>)}
</ul>
</Banner>
)}
{!hasConfirmationUrl && (
<>
<TextField label="Subscription Status" value={displayStatus} readOnly />
<TextField label="Plan" value={displayPlan} readOnly />
<TextField label="Billing Interval" value={displayInterval} readOnly />
<TextField label="Price" value={displayPrice} readOnly />
<TextField label="Trial" value={isSubscribed ? `${subscription?.trialDays || 0} day(s)` : `${TRIAL_DAYS}-day free trial`} readOnly />
<TextField label="Trial Days Left" value={trialDaysLeft != null ? `${trialDaysLeft} days left` : "N/A"} readOnly />
<TextField label="Next Renewal / Period End" value={displayNextRenewal} readOnly />
{subscription?.createdAt && <TextField label="Subscription Created" value={formatDate(subscription.createdAt)} readOnly />}
<Divider />
{!isSubscribed && (
<>
<div style={{ fontWeight: 700, fontSize: 15, marginBottom: 4 }}>Choose your billing plan</div>
<ChoiceList
title="Billing cadence"
choices={[
{ label: `Monthly — $${MONTHLY_AMOUNT}/mo`, value: "MONTHLY", helpText: "Flexible monthly billing. Cancel anytime during or after the trial." },
{ label: `Annual — $${ANNUAL_AMOUNT}/yr (save ~17%)`, value: "ANNUAL", helpText: "Best value. Billed annually after your free trial ends." },
]}
selected={[cadence]}
onChange={(selected) => {
const next = Array.isArray(selected) && selected[0] ? selected[0] : "MONTHLY";
setCadence(next);
const hidden = document.getElementById("cadence-field");
if (hidden) hidden.value = next;
}}
allowMultiple={false}
/>
</>
)}
</>
)}
{hasConfirmationUrl && (
<BlockStack gap="300">
<Banner title="Almost there!" tone="success">
Click the button below to open Shopify's billing confirmation in a new tab.
</Banner>
<Button url={actionData.confirmationUrl} target="_blank" external onClick={() => setActiveModal(false)} variant="primary" size="large">
Open Billing Confirmation
</Button>
<a href={actionData.confirmationUrl} target="_blank" rel="noopener noreferrer" style={{ wordBreak: "break-all", fontSize: 12, color: "#6b7280" }}>
{actionData.confirmationUrl}
</a>
</BlockStack>
)}
</BlockStack>
</Modal.Section>
</Form>
</Modal>
</Page>
);
}