feat: redesign admin dashboard using polaris layouts and extracted custom components
This commit is contained in:
parent
d478e42889
commit
23f448ecd9
34
app/components/DashboardMetrics.jsx
Normal file
34
app/components/DashboardMetrics.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
68
app/components/ReviewList.jsx
Normal file
68
app/components/ReviewList.jsx
Normal 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>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
20
app/components/StorefrontSetupCard.jsx
Normal file
20
app/components/StorefrontSetupCard.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
23
app/components/TechStackCard.jsx
Normal file
23
app/components/TechStackCard.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user