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 { authenticate } from "../shopify.server";
|
||||||
import { useLoaderData } from "react-router";
|
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 }) => {
|
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
|
// Fetch some reviews to show in the admin dashboard
|
||||||
const response = await admin.graphql(`#graphql
|
const response = await admin.graphql(`#graphql
|
||||||
query {
|
query {
|
||||||
metaobjects(first: 10, type: "custom_product_review") {
|
metaobjects(first: 50, type: "custom_product_review") {
|
||||||
edges {
|
edges {
|
||||||
node {
|
node {
|
||||||
id
|
id
|
||||||
@ -31,39 +37,24 @@ export default function Index() {
|
|||||||
const { reviews } = useLoaderData();
|
const { reviews } = useLoaderData();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<s-page heading="Product Reviews Dashboard">
|
<Page title="Product Reviews Dashboard">
|
||||||
<s-section heading="Manage your reviews">
|
<Layout>
|
||||||
<s-paragraph>
|
<Layout.Section>
|
||||||
All reviews are stored directly in Shopify Metaobjects. You can view, edit, or delete them here.
|
<BlockStack gap="400">
|
||||||
</s-paragraph>
|
<DashboardMetrics reviews={reviews} />
|
||||||
|
<Card padding="0">
|
||||||
{reviews.length === 0 ? (
|
<ReviewList reviews={reviews} />
|
||||||
<s-paragraph>No reviews found yet. Try adding one from your store!</s-paragraph>
|
</Card>
|
||||||
) : (
|
</BlockStack>
|
||||||
<s-stack direction="block" gap="base">
|
</Layout.Section>
|
||||||
{reviews.map((r) => (
|
|
||||||
<s-box padding="base" borderWidth="base" borderRadius="base" background="subdued" key={r.id}>
|
<Layout.Section variant="oneThird">
|
||||||
<s-stack direction="block" gap="small">
|
<BlockStack gap="500">
|
||||||
<s-text style={{ fontWeight: 'bold' }}>{r.customer_name || 'Anonymous'}</s-text>
|
<TechStackCard />
|
||||||
<s-text>{r.content}</s-text>
|
<StorefrontSetupCard />
|
||||||
<s-text style={{ color: '#666', fontSize: '0.8em' }}>Product ID: {r.product_id}</s-text>
|
</BlockStack>
|
||||||
</s-stack>
|
</Layout.Section>
|
||||||
</s-box>
|
</Layout>
|
||||||
))}
|
</Page>
|
||||||
</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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user