Update React Editor integration and clean up template
This commit is contained in:
@@ -1,9 +1,6 @@
|
||||
import PageEditor from '@/components/page-editor';
|
||||
import pageData from '../page.json';
|
||||
|
||||
// Editor for /about. Sitting under the route it edits means the preview
|
||||
// resolves the same params the public page gets, and `../page.json` is the
|
||||
// very file publishing writes back to.
|
||||
export default function EditorPage() {
|
||||
return <PageEditor routeKey="/about" page={pageData} />;
|
||||
return <PageEditor pagePath="app/about/page.json" page={pageData} />;
|
||||
}
|
||||
|
||||
@@ -9,10 +9,8 @@ import {
|
||||
getCollectionProductsPage,
|
||||
isDefaultTitleOption,
|
||||
isDefaultTitleSelection,
|
||||
} from '@/services/shopify/catalog';
|
||||
} from '@/services/shopify/shop';
|
||||
|
||||
// Streaming needs the Node runtime here because the Storefront helpers run
|
||||
// server-side on each tool call.
|
||||
export const maxDuration = 30;
|
||||
|
||||
const MODEL = process.env.OPENROUTER_MODEL ?? 'openai/gpt-5.6-luna-pro';
|
||||
@@ -36,7 +34,6 @@ Guidelines:
|
||||
- You cannot place orders, change carts, process payments, or look up customer
|
||||
or order data. Say so and point the shopper to the relevant page instead.`;
|
||||
|
||||
// Trim the Storefront payloads to what the model actually needs to answer.
|
||||
const summariseProduct = (product: {
|
||||
id: string;
|
||||
title: string;
|
||||
@@ -88,12 +85,9 @@ export async function POST(req: Request) {
|
||||
const openrouter = createOpenRouter({ apiKey });
|
||||
|
||||
const result = streamText({
|
||||
// `reasoning` asks OpenRouter to stream the model's thinking; the UI
|
||||
// renders it via the Reasoning component.
|
||||
model: openrouter(MODEL, { reasoning: { enabled: true, effort: 'medium' } }),
|
||||
system: SYSTEM_PROMPT,
|
||||
messages: await convertToModelMessages(messages),
|
||||
// Let the model call a tool, read the result, then answer.
|
||||
stopWhen: stepCountIs(5),
|
||||
tools: {
|
||||
searchCatalogue: tool({
|
||||
@@ -201,7 +195,5 @@ export async function POST(req: Request) {
|
||||
},
|
||||
});
|
||||
|
||||
// sendReasoning forwards reasoning parts to the client; without it the
|
||||
// stream carries text and tool calls only.
|
||||
return result.toUIMessageStreamResponse({ sendReasoning: true });
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import PageEditor from '@/components/page-editor';
|
||||
import pageData from '../page.json';
|
||||
|
||||
// Editor for /collections/[handle]. Sitting under the route it edits means the preview
|
||||
// resolves the same params the public page gets, and `../page.json` is the
|
||||
// very file publishing writes back to.
|
||||
export default function EditorPage() {
|
||||
return <PageEditor routeKey="/collections/[handle]" page={pageData} />;
|
||||
return (
|
||||
<PageEditor
|
||||
pagePath="app/collections/[handle]/page.json"
|
||||
page={pageData}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Metadata } from 'next';
|
||||
import PageRender from '@/components/page-render';
|
||||
import { pageMetadata } from '@/lib/page-metadata';
|
||||
import { getCollectionProductsPage } from '@/services/shopify/catalog';
|
||||
import { getCollectionProductsPage } from '@/services/shopify/shop';
|
||||
import { truncate } from '@/lib/utils';
|
||||
import page from './page.json';
|
||||
|
||||
@@ -12,14 +12,11 @@ export async function generateMetadata({
|
||||
}): Promise<Metadata> {
|
||||
const { handle } = await params;
|
||||
|
||||
// No collection-only query exists, so ask for the smallest page of products
|
||||
// and use just the collection node off it.
|
||||
// Shopify exposes collection metadata through the products connection.
|
||||
const { collection } = await getCollectionProductsPage(handle, {
|
||||
first: 1,
|
||||
}).catch(() => ({ collection: null }));
|
||||
|
||||
// The blocks fetch client-side and render their own empty state, so a failed
|
||||
// lookup falls back to the page.json tags rather than a 500.
|
||||
if (!collection) return pageMetadata(page);
|
||||
|
||||
return pageMetadata(page, {
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import PageEditor from '@/components/page-editor';
|
||||
import pageData from '../page.json';
|
||||
|
||||
// Editor for /collections. Sitting under the route it edits means the preview
|
||||
// resolves the same params the public page gets, and `../page.json` is the
|
||||
// very file publishing writes back to.
|
||||
export default function EditorPage() {
|
||||
return <PageEditor routeKey="/collections" page={pageData} />;
|
||||
return <PageEditor pagePath="app/collections/page.json" page={pageData} />;
|
||||
}
|
||||
|
||||
+1
-4
@@ -1,9 +1,6 @@
|
||||
import PageEditor from '@/components/page-editor';
|
||||
import pageData from '../page.json';
|
||||
|
||||
// Editor for /. Sitting under the route it edits means the preview
|
||||
// resolves the same params the public page gets, and `../page.json` is the
|
||||
// very file publishing writes back to.
|
||||
export default function EditorPage() {
|
||||
return <PageEditor routeKey="/" page={pageData} />;
|
||||
return <PageEditor pagePath="app/page.json" page={pageData} />;
|
||||
}
|
||||
|
||||
+2
-27
@@ -3,53 +3,41 @@
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@theme {
|
||||
/* Refined neutral modern palette */
|
||||
--color-background: hsl(0 0% 100%);
|
||||
--color-foreground: hsl(240 10% 3.9%);
|
||||
|
||||
/* Card */
|
||||
--color-card: hsl(0 0% 100%);
|
||||
--color-card-foreground: hsl(240 10% 3.9%);
|
||||
|
||||
/* Popover */
|
||||
--color-popover: hsl(0 0% 100%);
|
||||
--color-popover-foreground: hsl(240 10% 3.9%);
|
||||
|
||||
/* Primary - Deep neutral slate for professional look */
|
||||
--color-primary: hsl(240 5.9% 10%);
|
||||
--color-primary-foreground: hsl(0 0% 98%);
|
||||
|
||||
/* Secondary */
|
||||
--color-secondary: hsl(240 4.8% 95.9%);
|
||||
--color-secondary-foreground: hsl(240 5.9% 10%);
|
||||
|
||||
/* Muted */
|
||||
--color-muted: hsl(240 4.8% 95.9%);
|
||||
--color-muted-foreground: hsl(240 3.8% 46.1%);
|
||||
|
||||
/* Accent - Subtle warm gray */
|
||||
--color-accent: hsl(30 6.7% 95%);
|
||||
--color-accent-foreground: hsl(240 5.9% 10%);
|
||||
|
||||
/* Destructive */
|
||||
--color-destructive: hsl(0 84.2% 60.2%);
|
||||
--color-destructive-foreground: hsl(0 0% 98%);
|
||||
|
||||
/* Shop (accelerated checkout) purple */
|
||||
--color-shop: #5a31f4;
|
||||
|
||||
/* Border */
|
||||
--color-border: hsl(240 5.9% 90%);
|
||||
--color-input: hsl(240 5.9% 90%);
|
||||
--color-ring: hsl(240 5.9% 10%);
|
||||
|
||||
/* Radius - Modern larger corners */
|
||||
--radius-sm: 0.5rem;
|
||||
--radius-md: 0.75rem;
|
||||
--radius-lg: 1rem;
|
||||
--radius-xl: 1.25rem;
|
||||
|
||||
/* Typography — Geist Sans / Geist Mono */
|
||||
--font-sans: var(--font-geist-sans), system-ui, sans-serif;
|
||||
--font-mono: var(--font-geist-mono), ui-monospace, SFMono-Regular, monospace;
|
||||
--font-heading: var(--font-geist-sans), system-ui, sans-serif;
|
||||
@@ -57,10 +45,7 @@
|
||||
--font-poppins: var(--font-geist-sans), system-ui, sans-serif;
|
||||
}
|
||||
|
||||
/* Base styles for light, modern Shopify storefront */
|
||||
/* Tailwind v4 defaults an unqualified `border` to currentColor. shadcn
|
||||
components (e.g. Button's outline variant) rely on this base layer to pick up
|
||||
the theme's border colour instead of the text colour. */
|
||||
/* Tailwind v4 otherwise defaults unqualified borders to currentColor. */
|
||||
@layer base {
|
||||
* {
|
||||
border-color: var(--color-border);
|
||||
@@ -75,7 +60,6 @@ body {
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
/* Modern card design for products and collections */
|
||||
.card-modern {
|
||||
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
border: 1px solid var(--color-border);
|
||||
@@ -87,7 +71,6 @@ body {
|
||||
border-color: hsl(240 5.9% 85%);
|
||||
}
|
||||
|
||||
/* Heading styles — regular weight, slightly tightened like the Geist reference */
|
||||
h1,
|
||||
h2,
|
||||
.font-heading {
|
||||
@@ -95,7 +78,6 @@ h2,
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
/* Swipeable rows (mobile galleries) without a visible scrollbar */
|
||||
.no-scrollbar {
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
@@ -105,7 +87,6 @@ h2,
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Diagonal strike marking an unavailable product option (size pill, swatch) */
|
||||
.option-unavailable {
|
||||
background-image: linear-gradient(
|
||||
to top right,
|
||||
@@ -116,7 +97,6 @@ h2,
|
||||
);
|
||||
}
|
||||
|
||||
/* Product description (HTML returned by the Storefront API) */
|
||||
.product-description p {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
@@ -125,7 +105,6 @@ h2,
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Shop policy body (HTML returned by the Storefront API) */
|
||||
.policy-body p,
|
||||
.policy-body ul,
|
||||
.policy-body ol {
|
||||
@@ -164,7 +143,6 @@ h2,
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Enhanced button styles */
|
||||
button,
|
||||
.btn-modern {
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
@@ -177,12 +155,10 @@ button,
|
||||
0 4px 6px -4px rgb(0 0 0 / 0.1);
|
||||
}
|
||||
|
||||
/* Hero section styles */
|
||||
.hero-bg {
|
||||
background: linear-gradient(135deg, #f8f9fa 0%, #f1f3f5 100%);
|
||||
}
|
||||
|
||||
/* Product gallery improvements */
|
||||
.product-gallery-main {
|
||||
transition: box-shadow 0.3s ease;
|
||||
}
|
||||
@@ -191,7 +167,6 @@ button,
|
||||
box-shadow: 0 0 0 1px hsl(240 5.9% 80%) inset;
|
||||
}
|
||||
|
||||
/* Clean price display */
|
||||
.price-display {
|
||||
font-feature-settings: 'tnum';
|
||||
}
|
||||
@@ -227,4 +202,4 @@ button,
|
||||
--color-3: oklch(69.6% 0.165 251);
|
||||
--color-4: oklch(80.2% 0.134 225);
|
||||
--color-5: oklch(90.7% 0.231 133);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,8 +14,6 @@ const geistMono = Geist_Mono({
|
||||
variable: '--font-geist-mono',
|
||||
});
|
||||
|
||||
// Per-page tags come from each page.json's root props via pageMetadata(); this
|
||||
// is the fallback for routes that don't set their own.
|
||||
export const metadata: Metadata = {
|
||||
metadataBase: new URL(site.url),
|
||||
title: {
|
||||
@@ -44,9 +42,6 @@ export default function RootLayout({
|
||||
}) {
|
||||
return (
|
||||
<html lang="en" className={`${geist.variable} ${geistMono.variable}`}>
|
||||
{/* Header, footer and the store assistant are editable blocks now, so
|
||||
each page.json carries its own chrome rather than the layout doing
|
||||
it. Keeps what the editor shows identical to what the route ships. */}
|
||||
<body className="font-body antialiased bg-background text-foreground m-0 p-0 flex min-h-screen flex-col">
|
||||
<div className="flex-1">{children}</div>
|
||||
</body>
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import PageEditor from '@/components/page-editor';
|
||||
import pageData from '../page.json';
|
||||
|
||||
// Editor for /policies/[handle]. Sitting under the route it edits means the preview
|
||||
// resolves the same params the public page gets, and `../page.json` is the
|
||||
// very file publishing writes back to.
|
||||
export default function EditorPage() {
|
||||
return <PageEditor routeKey="/policies/[handle]" page={pageData} />;
|
||||
return (
|
||||
<PageEditor pagePath="app/policies/[handle]/page.json" page={pageData} />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import PageEditor from '@/components/page-editor';
|
||||
import pageData from '../page.json';
|
||||
|
||||
// Editor for /products/[handle]. Sitting under the route it edits means the preview
|
||||
// resolves the same params the public page gets, and `../page.json` is the
|
||||
// very file publishing writes back to.
|
||||
export default function EditorPage() {
|
||||
return <PageEditor routeKey="/products/[handle]" page={pageData} />;
|
||||
return (
|
||||
<PageEditor pagePath="app/products/[handle]/page.json" page={pageData} />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Metadata } from 'next';
|
||||
import PageRender from '@/components/page-render';
|
||||
import { pageMetadata } from '@/lib/page-metadata';
|
||||
import { getProduct } from '@/services/shopify/catalog';
|
||||
import { getProduct } from '@/services/shopify/shop';
|
||||
import { truncate } from '@/lib/utils';
|
||||
import page from './page.json';
|
||||
|
||||
@@ -12,8 +12,6 @@ export async function generateMetadata({
|
||||
}): Promise<Metadata> {
|
||||
const { handle } = await params;
|
||||
|
||||
// The blocks fetch client-side and render their own empty state, so a failed
|
||||
// lookup falls back to the page.json tags rather than a 500.
|
||||
const product = await getProduct(handle).catch(() => null);
|
||||
if (!product) return pageMetadata(page);
|
||||
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import PageEditor from '@/components/page-editor';
|
||||
import pageData from '../page.json';
|
||||
|
||||
// Editor for /search. Sitting under the route it edits means the preview
|
||||
// resolves the same params the public page gets, and `../page.json` is the
|
||||
// very file publishing writes back to.
|
||||
export default function EditorPage() {
|
||||
return <PageEditor routeKey="/search" page={pageData} />;
|
||||
return <PageEditor pagePath="app/search/page.json" page={pageData} />;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import type { Metadata } from 'next';
|
||||
import PageRender from '@/components/page-render';
|
||||
import { pageMetadata } from '@/lib/page-metadata';
|
||||
// Alias of /collections — same page.json, so editing that route updates both.
|
||||
import page from '../../collections/page.json';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
...pageMetadata(page),
|
||||
// Point crawlers at the canonical listing.
|
||||
alternates: { canonical: '/collections' },
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user