feat: Add stories

This commit is contained in:
Sol Irvine 2023-08-20 13:25:41 +09:00
parent ab8923080b
commit b994e62c21
16 changed files with 380 additions and 4 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 783 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 MiB

View File

@ -4,9 +4,13 @@ import { SupportedLocale } from 'components/layout/navbar/language-control';
import clsx from 'clsx'; import clsx from 'clsx';
import AboutNaraiPreview from 'components/layout/about-narai-preview'; import AboutNaraiPreview from 'components/layout/about-narai-preview';
import ConceptPreview from 'components/layout/concept-preview';
import LocationPreview from 'components/layout/location-preview';
import Navbar from 'components/layout/navbar'; import Navbar from 'components/layout/navbar';
import NewsletterSignup from 'components/layout/newsletter-signup'; import NewsletterSignup from 'components/layout/newsletter-signup';
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 { 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';
@ -14,6 +18,11 @@ import { Suspense } from 'react';
import HomeImage001 from './images/home-image-001.webp'; import HomeImage001 from './images/home-image-001.webp';
import HomeImage002 from './images/home-image-002.webp'; import HomeImage002 from './images/home-image-002.webp';
import HomeImage003 from './images/home-image-003.webp'; import HomeImage003 from './images/home-image-003.webp';
import HomeImage004 from './images/home-image-004.webp';
import HomeImage005 from './images/home-image-005.webp';
import HomeImage006 from './images/home-image-006.jpg';
import HomeImage007 from './images/home-image-007.webp';
import HomeImage008 from './images/home-image-008.webp';
export const runtime = 'edge'; export const runtime = 'edge';
const { SITE_NAME } = process.env; const { SITE_NAME } = process.env;
@ -58,6 +67,7 @@ export default async function HomePage({
<div className="py-24"> <div className="py-24">
<Shoplist /> <Shoplist />
</div> </div>
<div className="relative pb-48"> <div className="relative pb-48">
<Image <Image
src={HomeImage002} src={HomeImage002}
@ -66,6 +76,7 @@ export default async function HomePage({
className={clsx('h-full w-full object-cover')} className={clsx('h-full w-full object-cover')}
/> />
</div> </div>
<div className="relative mx-auto max-w-screen-xl"> <div className="relative mx-auto max-w-screen-xl">
<Image <Image
src={HomeImage003} src={HomeImage003}
@ -76,6 +87,62 @@ export default async function HomePage({
<AboutNaraiPreview /> <AboutNaraiPreview />
</div> </div>
<div className="relative mx-auto max-w-screen-xl">
<Image
src={HomeImage004}
priority={true}
alt="A picture of the main road in Narai-juku, Nagano Prefecture, Japan."
className={clsx('h-full w-full object-cover')}
/>
<LocationPreview />
</div>
<div className="relative pb-24">
<Image
src={HomeImage005}
priority={true}
alt="A picture of mountain tops."
className={clsx('h-full w-full object-cover')}
/>
</div>
<div className="relative mx-auto max-w-screen-xl">
<Image
src={HomeImage006}
priority={true}
alt="A picture of the interior of the Sagyobar."
className={clsx('h-full w-full object-cover')}
/>
<SagyobarPreview />
</div>
<div className="relative mx-auto max-w-screen-xl">
<Image
src={HomeImage007}
priority={true}
alt="A picture of the interior of the brewery."
className={clsx('h-full w-full object-cover')}
/>
<ConceptPreview />
</div>
<div className="relative">
<Stories handle="headless" articles={3} locale={locale} />
</div>
<div className="relative">
<Image
src={HomeImage008}
priority={true}
alt="A picture of a forest in Nagano, Japan."
className={clsx('h-full w-full object-cover')}
/>
</div>
<Suspense> <Suspense>
<Footer cart={cart} /> <Footer cart={cart} />
</Suspense> </Suspense>

View File

@ -5,12 +5,12 @@ export default function AboutNaraiPreview() {
const t = useTranslations('Index'); const t = useTranslations('Index');
return ( return (
<div className="flex flex-col space-y-4 px-6 py-24 md:flex-row md:space-x-2 md:space-y-0 md:p-24"> <div className="flex flex-col space-y-4 px-6 py-24 md:flex-row md:space-x-12 md:space-y-0 md:p-24">
<div className="font-multilingual flex flex-col space-y-2 font-extralight md:w-1/2"> <div className="font-multilingual flex flex-col space-y-2 font-extralight md:w-1/2">
<div className="text-5xl">{t('home.previews.about-narai.title')}</div> <div className="max-w-sm text-5xl">{t('home.previews.about-narai.title')}</div>
<div className="text-5xl">{t('home.previews.about-narai.subtitle')}</div> <div className="max-w-sm text-5xl">{t('home.previews.about-narai.subtitle')}</div>
</div> </div>
<div className="font-multilingual flex flex-col space-y-2 font-extralight md:w-1/2"> <div className="font-multilingual flex flex-col space-y-6 font-extralight md:w-1/2">
<div>{t('home.previews.about-narai.body')}</div> <div>{t('home.previews.about-narai.body')}</div>
</div> </div>
</div> </div>

View File

@ -0,0 +1,25 @@
'use client';
import { useTranslations } from 'next-intl';
import Link from 'next/link';
export default function ConceptPreview() {
const t = useTranslations('Index');
return (
<div className="flex flex-col space-y-4 px-6 py-24 md:flex-row md:space-x-12 md:space-y-0 md:p-24">
<div className="font-multilingual flex flex-col space-y-2 font-extralight md:w-1/2">
<div className="max-w-sm text-5xl">{t('home.previews.concept.title')}</div>
<div className="max-w-sm text-5xl">{t('home.previews.concept.subtitle')}</div>
</div>
<div className="font-multilingual flex flex-col space-y-6 font-extralight md:w-1/2">
<div>{t('home.previews.concept.body')}</div>
<Link
href="/location"
className="max-w-sm border border-white px-6 py-3 text-center text-lg transition-colors duration-150 hover:border-white/50"
>
{t('home.previews.concept.button')}
</Link>
</div>
</div>
);
}

View File

@ -0,0 +1,25 @@
'use client';
import { useTranslations } from 'next-intl';
import Link from 'next/link';
export default function LocationPreview() {
const t = useTranslations('Index');
return (
<div className="flex flex-col space-y-4 px-6 py-24 md:flex-row md:space-x-12 md:space-y-0 md:p-24">
<div className="font-multilingual flex flex-col space-y-2 font-extralight md:w-1/2">
<div className="max-w-sm text-5xl">{t('home.previews.location.title')}</div>
<div className="max-w-sm text-5xl">{t('home.previews.location.subtitle')}</div>
</div>
<div className="font-multilingual flex flex-col space-y-6 font-extralight md:w-1/2">
<div>{t('home.previews.location.body')}</div>
<Link
href="/location"
className="max-w-sm border border-white px-6 py-3 text-center text-lg transition-colors duration-150 hover:border-white/50"
>
{t('home.previews.location.button')}
</Link>
</div>
</div>
);
}

View File

@ -0,0 +1,25 @@
'use client';
import { useTranslations } from 'next-intl';
import Link from 'next/link';
export default function SagyobarPreview() {
const t = useTranslations('Index');
return (
<div className="flex flex-col space-y-4 px-6 py-24 md:flex-row md:space-x-12 md:space-y-0 md:p-24">
<div className="font-multilingual flex flex-col space-y-2 font-extralight md:w-1/2">
<div className="max-w-sm text-5xl">{t('home.previews.bar.title')}</div>
<div className="max-w-sm text-5xl">{t('home.previews.bar.subtitle')}</div>
</div>
<div className="font-multilingual flex flex-col space-y-6 font-extralight md:w-1/2">
<div>{t('home.previews.bar.body')}</div>
<Link
href="/location"
className="max-w-sm border border-white px-6 py-3 text-center text-lg transition-colors duration-150 hover:border-white/50"
>
{t('home.previews.bar.button')}
</Link>
</div>
</div>
);
}

View File

@ -0,0 +1,68 @@
import clsx from 'clsx';
import { getBlog } from 'lib/shopify';
import Image from 'next/image';
import Link from 'next/link';
import { SupportedLocale } from './navbar/language-control';
export default async function Stories({
locale,
handle,
articles
}: {
locale?: SupportedLocale;
handle: string;
articles?: number;
}) {
const blog = await getBlog({
handle: 'headless',
articles: articles || 3,
language: locale?.toUpperCase()
});
console.debug({ blog });
if (!blog) return null;
return (
<div className="bg-white px-6 text-black md:py-24">
<div className="mx-auto flex max-w-screen-xl flex-col space-y-6">
<h3 className="font-serif text-5xl">stories</h3>
<div
className={clsx(
'font-multilingual',
'font-extralight',
'flex flex-col space-x-6 space-y-6 md:flex-row md:space-y-0'
)}
>
{blog?.articles?.map((article) => (
<div className="flex flex-col space-y-4 md:w-1/3">
<div className="relative aspect-square max-w-sm overflow-hidden">
{!!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>
<div className="flex w-full flex-row justify-center pt-12">
<Link
href="/stories"
className="mx-auto max-w-sm border border-dark px-24 py-3 text-center text-lg transition-colors duration-150 hover:border-dark/40"
>
more stories
</Link>
</div>
</div>
</div>
);
}

View File

@ -9,6 +9,7 @@ import {
editCartItemsMutation, editCartItemsMutation,
removeFromCartMutation removeFromCartMutation
} from './mutations/cart'; } from './mutations/cart';
import { getBlogQuery } from './queries/blog';
import { getCartQuery } from './queries/cart'; import { getCartQuery } from './queries/cart';
import { import {
getCollectionProductsQuery, getCollectionProductsQuery,
@ -23,6 +24,7 @@ import {
getProductsQuery getProductsQuery
} from './queries/product'; } from './queries/product';
import { import {
Blog,
Cart, Cart,
Collection, Collection,
Connection, Connection,
@ -31,6 +33,8 @@ import {
Page, Page,
Product, Product,
ShopifyAddToCartOperation, ShopifyAddToCartOperation,
ShopifyBlog,
ShopifyBlogOperation,
ShopifyCart, ShopifyCart,
ShopifyCartOperation, ShopifyCartOperation,
ShopifyCollection, ShopifyCollection,
@ -167,6 +171,19 @@ const reshapeImages = (images: Connection<Image>, productTitle: string) => {
}); });
}; };
const reshapeBlog = (blog: ShopifyBlog) => {
if (!blog) {
return undefined;
}
const { articles, ...rest } = blog;
return {
...rest,
articles: removeEdgesAndNodes(articles)
};
};
const reshapeProduct = (product: ShopifyProduct, filterHiddenProducts: boolean = true) => { const reshapeProduct = (product: ShopifyProduct, filterHiddenProducts: boolean = true) => {
if (!product || (filterHiddenProducts && product.tags.includes(HIDDEN_PRODUCT_TAG))) { if (!product || (filterHiddenProducts && product.tags.includes(HIDDEN_PRODUCT_TAG))) {
return undefined; return undefined;
@ -397,6 +414,25 @@ export async function getPages({
return removeEdgesAndNodes(res.body.data.pages); return removeEdgesAndNodes(res.body.data.pages);
} }
export async function getBlog({
handle,
articles,
language,
country
}: {
handle: string;
articles?: number;
language?: string;
country?: string;
}): Promise<Blog | undefined> {
const res = await shopifyFetch<ShopifyBlogOperation>({
query: getBlogQuery,
variables: { handle, articles, language, country }
});
return reshapeBlog(res.body.data.blogByHandle);
}
export async function getProduct({ export async function getProduct({
handle, handle,
language, language,

View File

@ -0,0 +1,61 @@
import seoFragment from '../fragments/seo';
const blogFragment = /* GraphQL */ `
fragment blog on Blog {
... on Blog {
id
title
handle
articles(first: $articles) {
edges {
node {
id
title
handle
excerpt
content
contentHtml
image {
url
altText
width
height
}
seo {
...seo
}
publishedAt
}
}
}
seo {
...seo
}
}
}
${seoFragment}
`;
export const getBlogQuery = /* GraphQL */ `
query getBlog($handle: String!, $articles: Int, $country: CountryCode, $language: LanguageCode)
@inContext(country: $country, language: $language) {
blogByHandle(handle: $handle) {
...blog
}
}
${blogFragment}
`;
export const getBlogsQuery = /* GraphQL */ `
query getBlogs($country: CountryCode, $language: LanguageCode)
@inContext(country: $country, language: $language) {
blogs(first: 100) {
edges {
node {
...blog
}
}
}
}
${blogFragment}
`;

View File

@ -61,6 +61,22 @@ export type Page = {
updatedAt: string; updatedAt: string;
}; };
export type Blog = Omit<ShopifyBlog, 'articles'> & {
articles: BlogArticle[];
};
export type BlogArticle = {
id: string;
title: string;
handle: string;
content: string;
contentHtml: string;
excerpt: string;
publishedAt: string;
image?: Image;
seo?: SEO;
};
export type Product = Omit<ShopifyProduct, 'variants' | 'images'> & { export type Product = Omit<ShopifyProduct, 'variants' | 'images'> & {
variants: ProductVariant[]; variants: ProductVariant[];
images: Image[]; images: Image[];
@ -88,6 +104,18 @@ export type SEO = {
description: string; description: string;
}; };
export type ShopifyBlog = {
id: string;
title: string;
handle: string;
content: string;
contentHtml: string;
excerpt: string;
articles: Connection<BlogArticle>;
seo?: SEO;
image?: Image;
};
export type ShopifyCart = { export type ShopifyCart = {
id: string; id: string;
checkoutUrl: string; checkoutUrl: string;
@ -248,6 +276,11 @@ export type ShopifyPagesOperation = {
}; };
}; };
export type ShopifyBlogOperation = {
data: { blogByHandle: ShopifyBlog };
variables: { handle: string; articles?: number; language?: string; country?: string };
};
export type ShopifyProductOperation = { export type ShopifyProductOperation = {
data: { product: ShopifyProduct }; data: { product: ShopifyProduct };
variables: { variables: {

View File

@ -30,6 +30,24 @@
"title": "water of the mountains,", "title": "water of the mountains,",
"subtitle": "sake of the skies", "subtitle": "sake of the skies",
"body": "We brew our sake in one of the highest breweries in Japan, standing at an altitude of 940m and surrounded by an abundance of nature. While many breweries typically use a stable water source such as well water, narai uses the fresh water from the mountains, flowing from an altitude of over 1,000m. The water originates from a spring near the watershed of the Shinano River and Kiso River, and is characterized by its clarity and smooth, rounded texture brought about by a rare “water hardness” of less than 25." "body": "We brew our sake in one of the highest breweries in Japan, standing at an altitude of 940m and surrounded by an abundance of nature. While many breweries typically use a stable water source such as well water, narai uses the fresh water from the mountains, flowing from an altitude of over 1,000m. The water originates from a spring near the watershed of the Shinano River and Kiso River, and is characterized by its clarity and smooth, rounded texture brought about by a rare “water hardness” of less than 25."
},
"location": {
"title": "brewed in Narai-juku, Nagano",
"subtitle": "",
"body": "Our brewery is nestled in the historic townscape of Narai-juku, a well-preserved post town in Nagano stretching for about 1km the longest of its kind in Japan. In Narai-juku, winter temperatures drop nearly 20°C below zero, causing the mountain water to freeze. After the cold winter, the town gets enveloped in fresh greenery as the air turns more clear and pleasant. In autumn, the mountains are adorned with vibrant foliage. Blessed with an abundance of nature, it is a place to truly experience the changing of seasons.",
"button": "about narai"
},
"bar": {
"title": "sagyobar : a brewery-operated bar & workspace",
"subtitle": "",
"body": "Introducing sagyobar: a place where brewery operations (sagyo) and drinking (bar) come together. We have combined our brewery workspace, where we pack boxes and fulfill orders, with a place to enjoy sake served from our very own suginomori wagon. It is a renovated warehouse located a one-minute walk from the brewery, just across the railway tracks.",
"button": "about sagyobar"
},
"concept": {
"title": "beyond brewing",
"subtitle": "",
"body": "We are driven by our mission to preserve Japanese sake culture for future generations. To achieve this, we reexamine conventional practices of the sake industry and experiment with new endeavors. We are dedicated to exploring sake with a free and creative approach, going beyond brewing and spreading its charm to the world.",
"button": "concept"
} }
} }
}, },

View File

@ -30,6 +30,24 @@
"title": "山の水、", "title": "山の水、",
"subtitle": "空に一番近い酒", "subtitle": "空に一番近い酒",
"body": "標高約940mの日本でも有数の空に近い自然豊かな環境で醸造しています。多くの酒蔵が水の性質が安定している井戸水を使用しますが、naraiは標高1,000m以上から流れる天然の山水を使用。信濃川と木曽川の分水嶺付近の湧き水であるこの山水は、日本でも有数な「硬度25以下」の透明感と丸みのある滑らかな舌触りが特徴です。" "body": "標高約940mの日本でも有数の空に近い自然豊かな環境で醸造しています。多くの酒蔵が水の性質が安定している井戸水を使用しますが、naraiは標高1,000m以上から流れる天然の山水を使用。信濃川と木曽川の分水嶺付近の湧き水であるこの山水は、日本でも有数な「硬度25以下」の透明感と丸みのある滑らかな舌触りが特徴です。"
},
"location": {
"title": "長野",
"subtitle": "奈良井宿で醸す",
"body": "⻑野県に位置する日本最長の宿場町「奈良井宿」の歴史的な街並みの中で醸造しています。奈良井宿の冬は氷点下20度近くまで冷え込み、山の水は凍ります。寒い冬を越えると、新緑に囲まれ、空気が清らかで過ごしやすい季節が続きます。秋には紅葉で山が鮮やかに染まります。日本らしい四季の移ろいを感じられる自然豊かな立地です。",
"button": "naraiについて"
},
"bar": {
"title": "酒蔵直営の角打ち&作業場",
"subtitle": "sagyobar",
"body": "ここは、ボトルの箱詰めや出荷などの酒蔵作業 (sagyo) と、日本酒移動販売車「suginomori wagon」から提供されるSAKEを楽しむこともできる場(bar)が融合した、弊蔵の直営店です。酒蔵から徒歩1分。線路を挟み向かいの倉庫をリノベーションしました。",
"button": "sagyobarについて"
},
"concept": {
"title": "醸造のその先へ",
"subtitle": "",
"body": "私たちには、日本酒文化を未来に継承したいという信念があります。そのためには、これまでの常識をもう一度見つめ直すことや、新しい試みにも挑戦する。醸造のその先へ、自由な発想でSAKEを探究し、その魅力を伝えていきます。",
"button": "concept"
} }
} }
}, },