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 Shoplist from 'components/layout/shoplist';
|
||||
import Stories from 'components/layout/stories';
|
||||
import { BLOG_HANDLE } from 'lib/constants';
|
||||
import { getCart } from 'lib/shopify';
|
||||
import { cookies } from 'next/headers';
|
||||
import Image from 'next/image';
|
||||
@ -131,7 +132,7 @@ export default async function HomePage({
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
<Stories handle="headless" articles={3} locale={locale} more />
|
||||
<Stories handle={BLOG_HANDLE} articles={3} locale={locale} more />
|
||||
</div>
|
||||
|
||||
<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 Stories from 'components/layout/stories';
|
||||
import { BLOG_HANDLE } from 'lib/constants';
|
||||
import { getCart } from 'lib/shopify';
|
||||
import { cookies } from 'next/headers';
|
||||
import { Suspense } from 'react';
|
||||
@ -34,7 +35,7 @@ export default async function StoriesPage({
|
||||
<div>
|
||||
<Navbar cart={cart} locale={locale} compact />
|
||||
<div className="py-24 md:py-48">
|
||||
<Stories handle="headless" locale={locale} />
|
||||
<Stories handle={BLOG_HANDLE} locale={locale} />
|
||||
</div>
|
||||
|
||||
<Suspense>
|
||||
|
@ -34,24 +34,26 @@ export default async function Stories({
|
||||
)}
|
||||
>
|
||||
{blog?.articles?.map((article) => (
|
||||
<div className="flex flex-col space-y-4 md:col-span-1">
|
||||
<div className="relative aspect-square overflow-hidden md:max-w-sm">
|
||||
{!!article?.image?.url && (
|
||||
<Image
|
||||
src={article?.image?.url}
|
||||
width={article?.image?.width}
|
||||
height={article?.image?.height}
|
||||
alt={article?.image?.altText || `image-for-${article?.handle}`}
|
||||
className={clsx(
|
||||
'h-full w-full object-cover',
|
||||
'transition duration-300 ease-in-out hover:scale-105'
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<Link href={`/stories/${article.handle}`}>
|
||||
<div className="flex flex-col space-y-4 md:col-span-1">
|
||||
<div className="relative aspect-square overflow-hidden md:max-w-sm">
|
||||
{!!article?.image?.url && (
|
||||
<Image
|
||||
src={article?.image?.url}
|
||||
width={article?.image?.width}
|
||||
height={article?.image?.height}
|
||||
alt={article?.image?.altText || `image-for-${article?.handle}`}
|
||||
className={clsx(
|
||||
'h-full w-full object-cover',
|
||||
'transition duration-300 ease-in-out hover:scale-105'
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="max-w-sm text-lg">{article?.title}</div>
|
||||
<div className="max-w-sm">{article?.excerpt}</div>
|
||||
</div>
|
||||
<div className="max-w-sm text-lg">{article?.title}</div>
|
||||
<div className="max-w-sm">{article?.excerpt}</div>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
{more && (
|
||||
|
@ -26,5 +26,7 @@ export const TAGS = {
|
||||
};
|
||||
|
||||
export const HIDDEN_PRODUCT_TAG = 'nextjs-frontend-hidden';
|
||||
export const HIDDEN_ARTICLE_TAG = 'nextjs-frontend-hidden';
|
||||
export const DEFAULT_OPTION = 'Default Title';
|
||||
export const SHOPIFY_GRAPHQL_API_ENDPOINT = '/api/2023-01/graphql.json';
|
||||
export const BLOG_HANDLE = 'headless';
|
||||
|
@ -9,7 +9,7 @@ import {
|
||||
editCartItemsMutation,
|
||||
removeFromCartMutation
|
||||
} from './mutations/cart';
|
||||
import { getBlogQuery } from './queries/blog';
|
||||
import { getBlogArticleQuery, getBlogQuery } from './queries/blog';
|
||||
import { getCartQuery } from './queries/cart';
|
||||
import {
|
||||
getCollectionProductsQuery,
|
||||
@ -25,6 +25,7 @@ import {
|
||||
} from './queries/product';
|
||||
import {
|
||||
Blog,
|
||||
BlogArticle,
|
||||
Cart,
|
||||
Collection,
|
||||
Connection,
|
||||
@ -34,6 +35,7 @@ import {
|
||||
Product,
|
||||
ShopifyAddToCartOperation,
|
||||
ShopifyBlog,
|
||||
ShopifyBlogArticleOperation,
|
||||
ShopifyBlogOperation,
|
||||
ShopifyCart,
|
||||
ShopifyCartOperation,
|
||||
@ -433,6 +435,25 @@ export async function getBlog({
|
||||
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({
|
||||
handle,
|
||||
language,
|
||||
|
@ -36,6 +36,30 @@ const blogFragment = /* GraphQL */ `
|
||||
${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 */ `
|
||||
query getBlog($handle: String!, $articles: Int, $country: CountryCode, $language: LanguageCode)
|
||||
@inContext(country: $country, language: $language) {
|
||||
@ -46,6 +70,22 @@ export const getBlogQuery = /* GraphQL */ `
|
||||
${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 */ `
|
||||
query getBlogs($country: CountryCode, $language: LanguageCode)
|
||||
@inContext(country: $country, language: $language) {
|
||||
|
@ -75,6 +75,7 @@ export type BlogArticle = {
|
||||
publishedAt: string;
|
||||
image?: Image;
|
||||
seo?: SEO;
|
||||
tags: string[];
|
||||
};
|
||||
|
||||
export type Product = Omit<ShopifyProduct, 'variants' | 'images'> & {
|
||||
@ -114,6 +115,7 @@ export type ShopifyBlog = {
|
||||
articles: Connection<BlogArticle>;
|
||||
seo?: SEO;
|
||||
image?: Image;
|
||||
tags: string[];
|
||||
};
|
||||
|
||||
export type ShopifyCart = {
|
||||
@ -281,6 +283,16 @@ export type ShopifyBlogOperation = {
|
||||
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 = {
|
||||
data: { product: ShopifyProduct };
|
||||
variables: {
|
||||
|
Loading…
x
Reference in New Issue
Block a user