wip: Story detail page

This commit is contained in:
Sol Irvine 2023-08-20 14:30:44 +09:00
parent 16e3bad165
commit b4304bd6ea
9 changed files with 239 additions and 20 deletions

View File

@ -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">

View 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>
);
}

View 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>
</>
);
}

View File

@ -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>

View File

@ -34,24 +34,26 @@ export default async function Stories({
)} )}
> >
{blog?.articles?.map((article) => ( {blog?.articles?.map((article) => (
<div className="flex flex-col space-y-4 md:col-span-1"> <Link href={`/stories/${article.handle}`}>
<div className="relative aspect-square overflow-hidden md:max-w-sm"> <div className="flex flex-col space-y-4 md:col-span-1">
{!!article?.image?.url && ( <div className="relative aspect-square overflow-hidden md:max-w-sm">
<Image {!!article?.image?.url && (
src={article?.image?.url} <Image
width={article?.image?.width} src={article?.image?.url}
height={article?.image?.height} width={article?.image?.width}
alt={article?.image?.altText || `image-for-${article?.handle}`} height={article?.image?.height}
className={clsx( alt={article?.image?.altText || `image-for-${article?.handle}`}
'h-full w-full object-cover', className={clsx(
'transition duration-300 ease-in-out hover:scale-105' '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>
<div className="max-w-sm text-lg">{article?.title}</div> </Link>
<div className="max-w-sm">{article?.excerpt}</div>
</div>
))} ))}
</div> </div>
{more && ( {more && (

View File

@ -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';

View File

@ -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,

View File

@ -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) {

View File

@ -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: {