mirror of
https://github.com/vercel/commerce.git
synced 2025-06-07 16:56:59 +00:00
feat: Add stories
This commit is contained in:
parent
ab8923080b
commit
b994e62c21
BIN
app/[locale]/images/home-image-004.webp
Normal file
BIN
app/[locale]/images/home-image-004.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.3 MiB |
BIN
app/[locale]/images/home-image-005.webp
Normal file
BIN
app/[locale]/images/home-image-005.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.0 MiB |
BIN
app/[locale]/images/home-image-006.jpg
Normal file
BIN
app/[locale]/images/home-image-006.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 783 KiB |
BIN
app/[locale]/images/home-image-007.webp
Normal file
BIN
app/[locale]/images/home-image-007.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.4 MiB |
BIN
app/[locale]/images/home-image-008.webp
Normal file
BIN
app/[locale]/images/home-image-008.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.5 MiB |
@ -4,9 +4,13 @@ import { SupportedLocale } from 'components/layout/navbar/language-control';
|
||||
|
||||
import clsx from 'clsx';
|
||||
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 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 { getCart } from 'lib/shopify';
|
||||
import { cookies } from 'next/headers';
|
||||
import Image from 'next/image';
|
||||
@ -14,6 +18,11 @@ import { Suspense } from 'react';
|
||||
import HomeImage001 from './images/home-image-001.webp';
|
||||
import HomeImage002 from './images/home-image-002.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';
|
||||
const { SITE_NAME } = process.env;
|
||||
@ -58,6 +67,7 @@ export default async function HomePage({
|
||||
<div className="py-24">
|
||||
<Shoplist />
|
||||
</div>
|
||||
|
||||
<div className="relative pb-48">
|
||||
<Image
|
||||
src={HomeImage002}
|
||||
@ -66,6 +76,7 @@ export default async function HomePage({
|
||||
className={clsx('h-full w-full object-cover')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="relative mx-auto max-w-screen-xl">
|
||||
<Image
|
||||
src={HomeImage003}
|
||||
@ -76,6 +87,62 @@ export default async function HomePage({
|
||||
|
||||
<AboutNaraiPreview />
|
||||
</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>
|
||||
<Footer cart={cart} />
|
||||
</Suspense>
|
||||
|
@ -5,12 +5,12 @@ export default function AboutNaraiPreview() {
|
||||
const t = useTranslations('Index');
|
||||
|
||||
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="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.title')}</div>
|
||||
<div className="max-w-sm text-5xl">{t('home.previews.about-narai.subtitle')}</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>
|
||||
</div>
|
||||
|
25
components/layout/concept-preview.tsx
Normal file
25
components/layout/concept-preview.tsx
Normal 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>
|
||||
);
|
||||
}
|
25
components/layout/location-preview.tsx
Normal file
25
components/layout/location-preview.tsx
Normal 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>
|
||||
);
|
||||
}
|
25
components/layout/sagyobar-preview.tsx
Normal file
25
components/layout/sagyobar-preview.tsx
Normal 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>
|
||||
);
|
||||
}
|
68
components/layout/stories.tsx
Normal file
68
components/layout/stories.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -9,6 +9,7 @@ import {
|
||||
editCartItemsMutation,
|
||||
removeFromCartMutation
|
||||
} from './mutations/cart';
|
||||
import { getBlogQuery } from './queries/blog';
|
||||
import { getCartQuery } from './queries/cart';
|
||||
import {
|
||||
getCollectionProductsQuery,
|
||||
@ -23,6 +24,7 @@ import {
|
||||
getProductsQuery
|
||||
} from './queries/product';
|
||||
import {
|
||||
Blog,
|
||||
Cart,
|
||||
Collection,
|
||||
Connection,
|
||||
@ -31,6 +33,8 @@ import {
|
||||
Page,
|
||||
Product,
|
||||
ShopifyAddToCartOperation,
|
||||
ShopifyBlog,
|
||||
ShopifyBlogOperation,
|
||||
ShopifyCart,
|
||||
ShopifyCartOperation,
|
||||
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) => {
|
||||
if (!product || (filterHiddenProducts && product.tags.includes(HIDDEN_PRODUCT_TAG))) {
|
||||
return undefined;
|
||||
@ -397,6 +414,25 @@ export async function getPages({
|
||||
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({
|
||||
handle,
|
||||
language,
|
||||
|
61
lib/shopify/queries/blog.ts
Normal file
61
lib/shopify/queries/blog.ts
Normal 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}
|
||||
`;
|
@ -61,6 +61,22 @@ export type Page = {
|
||||
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'> & {
|
||||
variants: ProductVariant[];
|
||||
images: Image[];
|
||||
@ -88,6 +104,18 @@ export type SEO = {
|
||||
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 = {
|
||||
id: 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 = {
|
||||
data: { product: ShopifyProduct };
|
||||
variables: {
|
||||
|
@ -30,6 +30,24 @@
|
||||
"title": "water of the mountains,",
|
||||
"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."
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -30,6 +30,24 @@
|
||||
"title": "山の水、",
|
||||
"subtitle": "空に一番近い酒",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
Loading…
x
Reference in New Issue
Block a user