feat: redesign admin dashboard using polaris layouts and extracted custom components

This commit is contained in:
Divya Pahuja 2026-03-15 16:19:55 +05:30
parent d478e42889
commit 23f448ecd9
5 changed files with 172 additions and 36 deletions

View File

@ -0,0 +1,34 @@
import { Card, BlockStack, Text, InlineGrid, Box } from "@shopify/polaris";
export function DashboardMetrics({ reviews }) {
const totalReviews = reviews.length;
// Calculate unique products
const uniqueProducts = new Set(reviews.map(r => r.product_id).filter(Boolean)).size;
return (
<InlineGrid columns={{ xs: 1, sm: 2 }} gap="400">
<Card>
<BlockStack gap="200">
<Text as="h3" variant="headingSm" tone="subdued">
Total Reviews
</Text>
<Text as="p" variant="heading3xl">
{totalReviews}
</Text>
</BlockStack>
</Card>
<Card>
<BlockStack gap="200">
<Text as="h3" variant="headingSm" tone="subdued">
Products Reviewed
</Text>
<Text as="p" variant="heading3xl">
{uniqueProducts}
</Text>
</BlockStack>
</Card>
</InlineGrid>
);
}

View File

@ -0,0 +1,68 @@
import {
EmptyState,
ResourceList,
ResourceItem,
InlineStack,
Avatar,
BlockStack,
Text,
Badge
} from "@shopify/polaris";
export function ReviewList({ reviews }) {
if (reviews.length === 0) {
return (
<EmptyState
heading="Manage your customer reviews"
action={{ content: "View Online Store", url: "https://admin.shopify.com", external: true }}
image="https://cdn.shopify.com/s/files/1/0262/4071/2726/files/emptystate-files.png"
>
<p>Track your customer feedback in one place. Your store has no reviews yet.</p>
</EmptyState>
);
}
return (
<ResourceList
resourceName={{ singular: 'review', plural: 'reviews' }}
items={reviews}
renderItem={(item) => {
const { id, customer_name, content, rating, product_id } = item;
const productIdNum = product_id?.split('/').pop() || '';
return (
<ResourceItem
id={id}
onClick={() => {
window.open(`shopify:admin/products/${productIdNum}`, "_top");
}}
accessibilityLabel={`View details for ${customer_name}`}
>
<InlineStack align="space-between" blockAlign="center">
<InlineStack gap="400" blockAlign="center">
<Avatar size="md" initials={customer_name ? customer_name.charAt(0).toUpperCase() : 'A'} name={customer_name || 'Anonymous'} />
<BlockStack gap="100">
<Text variant="bodyMd" fontWeight="bold" as="h3">
{customer_name || 'Anonymous'}
</Text>
<Text variant="bodySm" as="p" tone="subdued">
{content}
</Text>
</BlockStack>
</InlineStack>
<InlineStack gap="300" blockAlign="center">
<Badge tone="success">
{rating || "5"} / 5
</Badge>
<Text variant="bodySm" as="span" tone="subdued">
Product ID: {productIdNum}
</Text>
</InlineStack>
</InlineStack>
</ResourceItem>
);
}}
/>
);
}

View File

@ -0,0 +1,20 @@
import { Card, BlockStack, Text, Button } from "@shopify/polaris";
import { ExternalIcon } from "@shopify/polaris-icons";
export function StorefrontSetupCard() {
return (
<Card>
<BlockStack gap="200">
<Text as="h2" variant="headingMd">
Storefront Setup
</Text>
<Text as="p" variant="bodyMd">
Ensure the "Reviews" block is added to your Default Product Template in the Online Store editor.
</Text>
<Button target="_blank" url="shopify:admin/themes/current/editor?context=product" icon={ExternalIcon}>
Open Theme Editor
</Button>
</BlockStack>
</Card>
);
}

View File

@ -0,0 +1,23 @@
import { Card, BlockStack, Text, Box, List } from "@shopify/polaris";
export function TechStackCard() {
return (
<Card>
<BlockStack gap="200">
<Text as="h2" variant="headingMd">
Tech Stack
</Text>
<Box paddingBlockStart="200">
<Text as="p" variant="bodyMd" tone="subdued">
This app is running 100% database-less, powered only by Shopify.
</Text>
</Box>
<List type="bullet">
<List.Item>Storage: Shopify Metaobjects</List.Item>
<List.Item>Auth: App Proxy & Admin API</List.Item>
<List.Item>Design: Shopify Polaris</List.Item>
</List>
</BlockStack>
</Card>
);
}

View File

@ -1,13 +1,19 @@
import { authenticate } from "../shopify.server";
import { useLoaderData } from "react-router";
import { Page, Layout, Card, BlockStack } from "@shopify/polaris";
import { DashboardMetrics } from "../components/DashboardMetrics";
import { ReviewList } from "../components/ReviewList";
import { TechStackCard } from "../components/TechStackCard";
import { StorefrontSetupCard } from "../components/StorefrontSetupCard";
export const loader = async ({ request }) => {
const { admin, session } = await authenticate.admin(request);
const { admin } = await authenticate.admin(request);
// Fetch some reviews to show in the admin dashboard
const response = await admin.graphql(`#graphql
query {
metaobjects(first: 10, type: "custom_product_review") {
metaobjects(first: 50, type: "custom_product_review") {
edges {
node {
id
@ -31,39 +37,24 @@ export default function Index() {
const { reviews } = useLoaderData();
return (
<s-page heading="Product Reviews Dashboard">
<s-section heading="Manage your reviews">
<s-paragraph>
All reviews are stored directly in Shopify Metaobjects. You can view, edit, or delete them here.
</s-paragraph>
{reviews.length === 0 ? (
<s-paragraph>No reviews found yet. Try adding one from your store!</s-paragraph>
) : (
<s-stack direction="block" gap="base">
{reviews.map((r) => (
<s-box padding="base" borderWidth="base" borderRadius="base" background="subdued" key={r.id}>
<s-stack direction="block" gap="small">
<s-text style={{ fontWeight: 'bold' }}>{r.customer_name || 'Anonymous'}</s-text>
<s-text>{r.content}</s-text>
<s-text style={{ color: '#666', fontSize: '0.8em' }}>Product ID: {r.product_id}</s-text>
</s-stack>
</s-box>
))}
</s-stack>
)}
</s-section>
<s-section slot="aside" heading="Tech Stack">
<s-paragraph>
This app is 100% database-less.
</s-paragraph>
<s-unordered-list>
<s-list-item>Storage: Shopify Metaobjects</s-list-item>
<s-list-item>Auth: App Proxy & Admin API</s-list-item>
<s-list-item>Framework: React Router</s-list-item>
</s-unordered-list>
</s-section>
</s-page>
<Page title="Product Reviews Dashboard">
<Layout>
<Layout.Section>
<BlockStack gap="400">
<DashboardMetrics reviews={reviews} />
<Card padding="0">
<ReviewList reviews={reviews} />
</Card>
</BlockStack>
</Layout.Section>
<Layout.Section variant="oneThird">
<BlockStack gap="500">
<TechStackCard />
<StorefrontSetupCard />
</BlockStack>
</Layout.Section>
</Layout>
</Page>
);
}