mirror of
https://github.com/vercel/commerce.git
synced 2025-05-19 07:56:59 +00:00
wip: Story detail page
This commit is contained in:
parent
16e3bad165
commit
b4304bd6ea
@ -11,6 +11,7 @@ import NewsletterSignup from 'components/layout/newsletter-signup';
|
|||||||
import SagyobarPreview from 'components/layout/sagyobar-preview';
|
import SagyobarPreview from 'components/layout/sagyobar-preview';
|
||||||
import Shoplist from 'components/layout/shoplist';
|
import Shoplist from 'components/layout/shoplist';
|
||||||
import Stories from 'components/layout/stories';
|
import Stories from 'components/layout/stories';
|
||||||
|
import { BLOG_HANDLE } from 'lib/constants';
|
||||||
import { getCart } from 'lib/shopify';
|
import { getCart } from 'lib/shopify';
|
||||||
import { cookies } from 'next/headers';
|
import { cookies } from 'next/headers';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
@ -131,7 +132,7 @@ export default async function HomePage({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Stories handle="headless" articles={3} locale={locale} more />
|
<Stories handle={BLOG_HANDLE} articles={3} locale={locale} more />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
|
43
app/[locale]/stories/[handle]/layout.tsx
Normal file
43
app/[locale]/stories/[handle]/layout.tsx
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import Footer from 'components/layout/footer';
|
||||||
|
import { SupportedLocale } from 'components/layout/navbar/language-control';
|
||||||
|
|
||||||
|
import Navbar from 'components/layout/navbar';
|
||||||
|
import { getCart } from 'lib/shopify';
|
||||||
|
import { cookies } from 'next/headers';
|
||||||
|
import { ReactNode, Suspense } from 'react';
|
||||||
|
|
||||||
|
export const runtime = 'edge';
|
||||||
|
const { SITE_NAME } = process.env;
|
||||||
|
|
||||||
|
export const metadata = {
|
||||||
|
title: SITE_NAME,
|
||||||
|
description: SITE_NAME,
|
||||||
|
openGraph: {
|
||||||
|
type: 'website'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function BlogLayout({
|
||||||
|
params: { locale },
|
||||||
|
children
|
||||||
|
}: {
|
||||||
|
params: { locale?: SupportedLocale };
|
||||||
|
children: ReactNode[] | ReactNode | string;
|
||||||
|
}) {
|
||||||
|
const cartId = cookies().get('cartId')?.value;
|
||||||
|
let cart;
|
||||||
|
|
||||||
|
if (cartId) {
|
||||||
|
cart = await getCart(cartId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Navbar cart={cart} locale={locale} compact />
|
||||||
|
{children}
|
||||||
|
<Suspense>
|
||||||
|
<Footer cart={cart} />
|
||||||
|
</Suspense>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
97
app/[locale]/stories/[handle]/page.tsx
Normal file
97
app/[locale]/stories/[handle]/page.tsx
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
import type { Metadata } from 'next';
|
||||||
|
import { notFound } from 'next/navigation';
|
||||||
|
|
||||||
|
import { SupportedLocale } from 'components/layout/navbar/language-control';
|
||||||
|
import Prose from 'components/prose';
|
||||||
|
import { BLOG_HANDLE, HIDDEN_ARTICLE_TAG } from 'lib/constants';
|
||||||
|
import { getBlogArticle } from 'lib/shopify';
|
||||||
|
import { BlogArticle } from 'lib/shopify/types';
|
||||||
|
import Image from 'next/image';
|
||||||
|
export const runtime = 'edge';
|
||||||
|
|
||||||
|
export async function generateMetadata({
|
||||||
|
params
|
||||||
|
}: {
|
||||||
|
params: { handle: string; locale?: SupportedLocale };
|
||||||
|
}): Promise<Metadata> {
|
||||||
|
const article: BlogArticle | undefined = await getBlogArticle({
|
||||||
|
handle: BLOG_HANDLE,
|
||||||
|
articleHandle: params.handle,
|
||||||
|
language: params?.locale?.toUpperCase()
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!article) return notFound();
|
||||||
|
|
||||||
|
const { url, width, height, altText: alt } = article.image || {};
|
||||||
|
const indexable = !article?.tags?.includes(HIDDEN_ARTICLE_TAG);
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: article?.seo?.title || article?.title,
|
||||||
|
description: article?.seo?.description || article?.excerpt,
|
||||||
|
robots: {
|
||||||
|
index: indexable,
|
||||||
|
follow: indexable,
|
||||||
|
googleBot: {
|
||||||
|
index: indexable,
|
||||||
|
follow: indexable
|
||||||
|
}
|
||||||
|
},
|
||||||
|
openGraph: url
|
||||||
|
? {
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
url,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
alt
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function BlogArticlePage({
|
||||||
|
params
|
||||||
|
}: {
|
||||||
|
params: { handle: string; locale?: SupportedLocale };
|
||||||
|
}) {
|
||||||
|
const article: BlogArticle | undefined = await getBlogArticle({
|
||||||
|
handle: BLOG_HANDLE,
|
||||||
|
articleHandle: params.handle,
|
||||||
|
language: params?.locale?.toUpperCase()
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!article) return notFound();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="mx-auto max-w-screen-xl py-24">
|
||||||
|
<div className="flex flex-col space-y-12">
|
||||||
|
{!!article?.image && (
|
||||||
|
<div className="relative aspect-square h-full w-full">
|
||||||
|
<Image
|
||||||
|
src={article?.image?.url}
|
||||||
|
alt={article?.image?.altText}
|
||||||
|
height={article?.image.height}
|
||||||
|
width={article?.image.width}
|
||||||
|
className="h-full w-full object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex flex-col space-y-6 px-6 md:flex-row md:space-x-6 md:space-y-0">
|
||||||
|
<div className="md:w-1/2">
|
||||||
|
<h1 className="font-multilingual mb-2 text-5xl leading-normal">{article.title}</h1>
|
||||||
|
</div>
|
||||||
|
<div className="md:w-1/2">
|
||||||
|
<div className="flex flex-col space-y-6">
|
||||||
|
<Prose html={article.contentHtml} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -3,6 +3,7 @@ import { SupportedLocale } from 'components/layout/navbar/language-control';
|
|||||||
|
|
||||||
import Navbar from 'components/layout/navbar';
|
import Navbar from 'components/layout/navbar';
|
||||||
import Stories from 'components/layout/stories';
|
import Stories from 'components/layout/stories';
|
||||||
|
import { BLOG_HANDLE } from 'lib/constants';
|
||||||
import { getCart } from 'lib/shopify';
|
import { getCart } from 'lib/shopify';
|
||||||
import { cookies } from 'next/headers';
|
import { cookies } from 'next/headers';
|
||||||
import { Suspense } from 'react';
|
import { Suspense } from 'react';
|
||||||
@ -34,7 +35,7 @@ export default async function StoriesPage({
|
|||||||
<div>
|
<div>
|
||||||
<Navbar cart={cart} locale={locale} compact />
|
<Navbar cart={cart} locale={locale} compact />
|
||||||
<div className="py-24 md:py-48">
|
<div className="py-24 md:py-48">
|
||||||
<Stories handle="headless" locale={locale} />
|
<Stories handle={BLOG_HANDLE} locale={locale} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Suspense>
|
<Suspense>
|
||||||
|
@ -34,6 +34,7 @@ export default async function Stories({
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{blog?.articles?.map((article) => (
|
{blog?.articles?.map((article) => (
|
||||||
|
<Link href={`/stories/${article.handle}`}>
|
||||||
<div className="flex flex-col space-y-4 md:col-span-1">
|
<div className="flex flex-col space-y-4 md:col-span-1">
|
||||||
<div className="relative aspect-square overflow-hidden md:max-w-sm">
|
<div className="relative aspect-square overflow-hidden md:max-w-sm">
|
||||||
{!!article?.image?.url && (
|
{!!article?.image?.url && (
|
||||||
@ -52,6 +53,7 @@ export default async function Stories({
|
|||||||
<div className="max-w-sm text-lg">{article?.title}</div>
|
<div className="max-w-sm text-lg">{article?.title}</div>
|
||||||
<div className="max-w-sm">{article?.excerpt}</div>
|
<div className="max-w-sm">{article?.excerpt}</div>
|
||||||
</div>
|
</div>
|
||||||
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{more && (
|
{more && (
|
||||||
|
@ -26,5 +26,7 @@ export const TAGS = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const HIDDEN_PRODUCT_TAG = 'nextjs-frontend-hidden';
|
export const HIDDEN_PRODUCT_TAG = 'nextjs-frontend-hidden';
|
||||||
|
export const HIDDEN_ARTICLE_TAG = 'nextjs-frontend-hidden';
|
||||||
export const DEFAULT_OPTION = 'Default Title';
|
export const DEFAULT_OPTION = 'Default Title';
|
||||||
export const SHOPIFY_GRAPHQL_API_ENDPOINT = '/api/2023-01/graphql.json';
|
export const SHOPIFY_GRAPHQL_API_ENDPOINT = '/api/2023-01/graphql.json';
|
||||||
|
export const BLOG_HANDLE = 'headless';
|
||||||
|
@ -9,7 +9,7 @@ import {
|
|||||||
editCartItemsMutation,
|
editCartItemsMutation,
|
||||||
removeFromCartMutation
|
removeFromCartMutation
|
||||||
} from './mutations/cart';
|
} from './mutations/cart';
|
||||||
import { getBlogQuery } from './queries/blog';
|
import { getBlogArticleQuery, getBlogQuery } from './queries/blog';
|
||||||
import { getCartQuery } from './queries/cart';
|
import { getCartQuery } from './queries/cart';
|
||||||
import {
|
import {
|
||||||
getCollectionProductsQuery,
|
getCollectionProductsQuery,
|
||||||
@ -25,6 +25,7 @@ import {
|
|||||||
} from './queries/product';
|
} from './queries/product';
|
||||||
import {
|
import {
|
||||||
Blog,
|
Blog,
|
||||||
|
BlogArticle,
|
||||||
Cart,
|
Cart,
|
||||||
Collection,
|
Collection,
|
||||||
Connection,
|
Connection,
|
||||||
@ -34,6 +35,7 @@ import {
|
|||||||
Product,
|
Product,
|
||||||
ShopifyAddToCartOperation,
|
ShopifyAddToCartOperation,
|
||||||
ShopifyBlog,
|
ShopifyBlog,
|
||||||
|
ShopifyBlogArticleOperation,
|
||||||
ShopifyBlogOperation,
|
ShopifyBlogOperation,
|
||||||
ShopifyCart,
|
ShopifyCart,
|
||||||
ShopifyCartOperation,
|
ShopifyCartOperation,
|
||||||
@ -433,6 +435,25 @@ export async function getBlog({
|
|||||||
return reshapeBlog(res.body.data.blogByHandle);
|
return reshapeBlog(res.body.data.blogByHandle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getBlogArticle({
|
||||||
|
handle,
|
||||||
|
articleHandle,
|
||||||
|
language,
|
||||||
|
country
|
||||||
|
}: {
|
||||||
|
handle: string;
|
||||||
|
articleHandle: string;
|
||||||
|
language?: string;
|
||||||
|
country?: string;
|
||||||
|
}): Promise<BlogArticle | undefined> {
|
||||||
|
const res = await shopifyFetch<ShopifyBlogArticleOperation>({
|
||||||
|
query: getBlogArticleQuery,
|
||||||
|
variables: { handle, articleHandle, language, country }
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.body.data.blogByHandle.articleByHandle;
|
||||||
|
}
|
||||||
|
|
||||||
export async function getProduct({
|
export async function getProduct({
|
||||||
handle,
|
handle,
|
||||||
language,
|
language,
|
||||||
|
@ -36,6 +36,30 @@ const blogFragment = /* GraphQL */ `
|
|||||||
${seoFragment}
|
${seoFragment}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const articleFragment = /* GraphQL */ `
|
||||||
|
fragment article on Article {
|
||||||
|
... on Article {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
handle
|
||||||
|
excerpt
|
||||||
|
content
|
||||||
|
contentHtml
|
||||||
|
image {
|
||||||
|
url
|
||||||
|
altText
|
||||||
|
width
|
||||||
|
height
|
||||||
|
}
|
||||||
|
seo {
|
||||||
|
...seo
|
||||||
|
}
|
||||||
|
publishedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
${seoFragment}
|
||||||
|
`;
|
||||||
|
|
||||||
export const getBlogQuery = /* GraphQL */ `
|
export const getBlogQuery = /* GraphQL */ `
|
||||||
query getBlog($handle: String!, $articles: Int, $country: CountryCode, $language: LanguageCode)
|
query getBlog($handle: String!, $articles: Int, $country: CountryCode, $language: LanguageCode)
|
||||||
@inContext(country: $country, language: $language) {
|
@inContext(country: $country, language: $language) {
|
||||||
@ -46,6 +70,22 @@ export const getBlogQuery = /* GraphQL */ `
|
|||||||
${blogFragment}
|
${blogFragment}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const getBlogArticleQuery = /* GraphQL */ `
|
||||||
|
query getBlogArticle(
|
||||||
|
$handle: String!
|
||||||
|
$articleHandle: String!
|
||||||
|
$country: CountryCode
|
||||||
|
$language: LanguageCode
|
||||||
|
) @inContext(country: $country, language: $language) {
|
||||||
|
blogByHandle(handle: $handle) {
|
||||||
|
articleByHandle(handle: $articleHandle) {
|
||||||
|
...article
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
${articleFragment}
|
||||||
|
`;
|
||||||
|
|
||||||
export const getBlogsQuery = /* GraphQL */ `
|
export const getBlogsQuery = /* GraphQL */ `
|
||||||
query getBlogs($country: CountryCode, $language: LanguageCode)
|
query getBlogs($country: CountryCode, $language: LanguageCode)
|
||||||
@inContext(country: $country, language: $language) {
|
@inContext(country: $country, language: $language) {
|
||||||
|
@ -75,6 +75,7 @@ export type BlogArticle = {
|
|||||||
publishedAt: string;
|
publishedAt: string;
|
||||||
image?: Image;
|
image?: Image;
|
||||||
seo?: SEO;
|
seo?: SEO;
|
||||||
|
tags: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Product = Omit<ShopifyProduct, 'variants' | 'images'> & {
|
export type Product = Omit<ShopifyProduct, 'variants' | 'images'> & {
|
||||||
@ -114,6 +115,7 @@ export type ShopifyBlog = {
|
|||||||
articles: Connection<BlogArticle>;
|
articles: Connection<BlogArticle>;
|
||||||
seo?: SEO;
|
seo?: SEO;
|
||||||
image?: Image;
|
image?: Image;
|
||||||
|
tags: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ShopifyCart = {
|
export type ShopifyCart = {
|
||||||
@ -281,6 +283,16 @@ export type ShopifyBlogOperation = {
|
|||||||
variables: { handle: string; articles?: number; language?: string; country?: string };
|
variables: { handle: string; articles?: number; language?: string; country?: string };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ShopifyBlogArticleOperation = {
|
||||||
|
data: { blogByHandle: { articleByHandle: BlogArticle } };
|
||||||
|
variables: {
|
||||||
|
handle: string;
|
||||||
|
articleHandle: string;
|
||||||
|
language?: string;
|
||||||
|
country?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export type ShopifyProductOperation = {
|
export type ShopifyProductOperation = {
|
||||||
data: { product: ShopifyProduct };
|
data: { product: ShopifyProduct };
|
||||||
variables: {
|
variables: {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user