diff --git a/app/components/AccountDetails.tsx b/app/components/AccountDetails.tsx index ce757b9318..57a95a57b7 100644 --- a/app/components/AccountDetails.tsx +++ b/app/components/AccountDetails.tsx @@ -1,6 +1,6 @@ -import { Link, Outlet, useOutlet } from "@remix-run/react"; +import { Outlet, useOutlet } from "@remix-run/react"; import type { Customer } from "@shopify/hydrogen-ui-alpha/storefront-api-types"; -import { Modal } from "~/components"; +import { Modal, LinkI18n } from "~/components"; import type { AccountDetailsOutletContext } from "~/routes/account/edit"; export function AccountDetails({ customer }: { customer: Customer }) { @@ -20,9 +20,9 @@ export function AccountDetails({ customer }: { customer: Customer }) {

Profile & Security

- + Edit - +
Name

diff --git a/app/components/CartDetails.tsx b/app/components/CartDetails.tsx index 1fdfa25f68..4bf90e6c4c 100644 --- a/app/components/CartDetails.tsx +++ b/app/components/CartDetails.tsx @@ -3,9 +3,9 @@ import { useScroll } from "react-use"; import { flattenConnection, Money } from "@shopify/hydrogen-ui-alpha"; import { type FetcherWithComponents, - Link, useFetcher, useLocation, + Link, } from "@remix-run/react"; import { @@ -15,6 +15,7 @@ import { ProductCard, Skeleton, Text, + LinkI18n, } from "~/components"; import type { Cart, @@ -185,9 +186,9 @@ function CartLineItem({

- + {merchandise.product.title} - +
diff --git a/app/components/CountrySelector.tsx b/app/components/CountrySelector.tsx index 17b0688888..ac39744617 100644 --- a/app/components/CountrySelector.tsx +++ b/app/components/CountrySelector.tsx @@ -1,25 +1,20 @@ -import { Link, useAsyncValue, useLocation, Await } from "@remix-run/react"; +import { Link, useAsyncValue, useLocation, Await, useParams } from "@remix-run/react"; import { Country } from "@shopify/hydrogen-ui-alpha/storefront-api-types"; import {Listbox} from '@headlessui/react'; import { useState, Suspense } from "react"; import { IconCaret, IconCheck } from "./Icon"; +import { getLocalizationFromLang } from "~/lib/utils"; export function CountrySelector({ - defaultCountry, countries, }: { - defaultCountry: Country; countries: Array ; }) { - const selectedCountry = defaultCountry.isoCode; return ( }> - + ) @@ -32,7 +27,7 @@ function CountrySelectorFallback() { - -- + -- @@ -40,17 +35,15 @@ function CountrySelectorFallback() { ) } -function CountrySelectorElement({ - defaultCountry, - selectedCountry -}: { - defaultCountry: Country; - selectedCountry: string; -}) { +function CountrySelectorElement() { const [listboxOpen, setListboxOpen] = useState(false); const countries = useAsyncValue>(); const { pathname } = useLocation(); - const currentCountry = countries.find(country => country.isoCode === selectedCountry) || defaultCountry; + const { lang } = useParams(); + const { language, country } = getLocalizationFromLang(lang); + const languageIsoCode = language.toLowerCase(); + const strippedPathname = pathname.replace(new RegExp(`^\/${lang}\/`), '/') + const currentCountry = countries.find(c => c.isoCode === country); return (
@@ -64,7 +57,11 @@ function CountrySelectorElement({ open ? 'rounded-b md:rounded-t md:rounded-b-none' : 'rounded' } border-contrast/30 dark:border-white`} > - {currentCountry.name} ({currentCountry.currency.isoCode} {currentCountry.currency.symbol}) + { + currentCountry ? + `${currentCountry.name} (${currentCountry.currency.isoCode} ${currentCountry.currency.symbol})` : + '--' + } @@ -77,13 +74,13 @@ function CountrySelectorElement({ }`} > {listboxOpen && Object.values(countries).map(country => { - const isSelected = country.isoCode === currentCountry.isoCode; - const countryIsoCode = country.isoCode.toLocaleLowerCase(); + const isSelected = country.isoCode === currentCountry?.isoCode; + const countryIsoCode = country.isoCode.toLowerCase(); return ( {({active}) => ( ; - defaultCountry: Country; cart: Promise; }; }) { - const { layout, countries, defaultCountry, cart } = data || {}; + const { layout, countries, cart } = data || {}; return ( <> @@ -60,7 +61,6 @@ export function Layout({
); @@ -75,12 +75,9 @@ function Header({ menu?: EnhancedMenu; cart?: Promise; }) { - const { pathname } = useLocation(); - - // TODO: Ensure locale support like in Hydrogen - const isHome = pathname === "/"; - const localeMatch = /^\/([a-z]{2})(\/|$)/i.exec(pathname); - const countryCode = localeMatch ? localeMatch[1] : null; + const { lang } = useParams(); + const { country } = getLocalizationFromLang(lang); + const isHome = isHomePath(); const { isOpen: isCartOpen, @@ -101,7 +98,6 @@ function Header({ )} ; - defaultCountry?: Country; }) { - const { pathname } = useLocation(); - - // TODO: Ensure locale support like in Hydrogen - const localeMatch = /^\/([a-z]{2})(\/|$)/i.exec(pathname); - const countryCode = localeMatch ? localeMatch[1] : null; - - const isHome = pathname === `/${countryCode ? countryCode + "/" : ""}`; + const isHome = isHomePath(); const itemsCount = menu ? menu?.items?.length + 1 > 4 ? 4 @@ -152,14 +139,13 @@ function Footer({ bg-primary dark:bg-contrast dark:text-primary text-contrast overflow-hidden`} > - {countries && defaultCountry && ( + {countries && (
Country
)} @@ -256,25 +242,23 @@ function MenuMobileNav({ ); } function MobileHeader({ - countryCode, title, isHome, openCart, openMenu, cart, }: { - countryCode?: string | null; title: string; isHome: boolean; openCart: () => void; @@ -301,7 +285,7 @@ function MobileHeader({
- {title} - +
- + - + - + - +
diff --git a/app/components/OrderCard.tsx b/app/components/OrderCard.tsx index 0670dbe67f..80b44345ac 100644 --- a/app/components/OrderCard.tsx +++ b/app/components/OrderCard.tsx @@ -1,10 +1,9 @@ -import { Link } from "@remix-run/react"; import { flattenConnection } from "@shopify/hydrogen-ui-alpha"; import type { Order, OrderLineItem, } from "@shopify/hydrogen-ui-alpha/storefront-api-types"; -import { Heading, Text } from "~/components"; +import { Heading, Text, LinkI18n } from "~/components"; import { statusMessage } from "~/lib/utils"; export function OrderCard({ order }: { order: Order }) { @@ -14,7 +13,7 @@ export function OrderCard({ order }: { order: Order }) { return (
  • - @@ -68,16 +67,16 @@ export function OrderCard({ order }: { order: Order }) {
  • - +
    - View Details - +
    ); diff --git a/app/components/ProductCard.tsx b/app/components/ProductCard.tsx index 795bfd4f6e..df7c6c2e2b 100644 --- a/app/components/ProductCard.tsx +++ b/app/components/ProductCard.tsx @@ -6,7 +6,7 @@ import { useMoney, } from "@shopify/hydrogen-ui-alpha"; -import { Text } from "~/components"; +import { Text, LinkI18n } from "~/components"; import { isDiscounted, isNewArrival } from "~/lib/utils"; import { getProductPlaceholder } from "~/lib/placeholders"; import type { @@ -15,7 +15,6 @@ import type { ProductVariant, ProductVariantConnection, } from "@shopify/hydrogen-ui-alpha/storefront-api-types"; -import { Link } from "@remix-run/react"; export function ProductCard({ product, @@ -53,7 +52,7 @@ export function ProductCard({ const styles = clsx("grid gap-6", className); return ( -
    - + ); } diff --git a/app/components/ProductGrid.tsx b/app/components/ProductGrid.tsx index be2b4f3c3f..b3f6c52420 100644 --- a/app/components/ProductGrid.tsx +++ b/app/components/ProductGrid.tsx @@ -1,10 +1,10 @@ -import { Button, Grid, ProductCard } from "~/components"; +import { Button, Grid, ProductCard, LinkI18n } from "~/components"; import { getImageLoadingPriority } from "~/lib/const"; import type { Collection, Product, } from "@shopify/hydrogen-ui-alpha/storefront-api-types"; -import { Link, useFetcher } from "@remix-run/react"; +import { useFetcher } from "@remix-run/react"; import { useEffect, useState } from "react"; export function ProductGrid({ @@ -43,9 +43,9 @@ export function ProductGrid({ return ( <>

    No products found on this collection

    - +

    Browse catalog

    - +
    ); } diff --git a/app/components/index.ts b/app/components/index.ts index bad412b063..5f660f33f2 100644 --- a/app/components/index.ts +++ b/app/components/index.ts @@ -14,5 +14,6 @@ export { CartDetails, CartEmpty } from "./CartDetails"; export { OrderCard } from "./OrderCard"; export { AccountDetails } from "./AccountDetails"; export { Modal } from "./Modal"; +export { LinkI18n } from './LinkI18n'; // Sue me export * from "./Icon"; diff --git a/app/data/index.ts b/app/data/index.ts index 10c3300a97..030fb3a62c 100644 --- a/app/data/index.ts +++ b/app/data/index.ts @@ -10,7 +10,6 @@ import type { ProductConnection, ProductVariant, SelectedOptionInput, - LanguageCode, Blog, PageConnection, Shop, @@ -27,10 +26,11 @@ import { getPublicTokenHeaders, getStorefrontApiUrl, } from "~/lib/shopify-client"; -import { type EnhancedMenu, parseMenu, getApiErrorMessage } from "~/lib/utils"; +import { type EnhancedMenu, parseMenu, getApiErrorMessage, getLocalizationFromLang } from "~/lib/utils"; import invariant from "tiny-invariant"; import { logout } from "~/routes/account.logout"; import type { AppLoadContext } from "@remix-run/cloudflare"; +import { type Params } from "@remix-run/react"; type StorefrontApiResponse = StorefrontApiResponseOk; @@ -78,8 +78,8 @@ export interface LayoutData { cart?: Promise; } -export async function getLayoutData() { - const languageCode = "EN"; +export async function getLayoutData(params: Params) { + const { language } = getLocalizationFromLang(params.lang); const HEADER_MENU_HANDLE = "main-menu"; const FOOTER_MENU_HANDLE = "footer"; @@ -87,7 +87,7 @@ export async function getLayoutData() { const { data } = await getStorefrontData({ query: LAYOUT_QUERY, variables: { - language: languageCode, + language: language, headerMenuHandle: HEADER_MENU_HANDLE, footerMenuHandle: FOOTER_MENU_HANDLE, }, @@ -188,11 +188,10 @@ const COUNTRIES_QUERY = `#graphql export async function getProductData( handle: string, - searchParams: URLSearchParams + searchParams: URLSearchParams, + params: Params ) { - // TODO: Figure out localization stuff - const languageCode = "EN"; - const countryCode = "US"; + const { language, country } = getLocalizationFromLang(params.lang); let selectedOptions: SelectedOptionInput[] = []; searchParams.forEach((value, name) => { @@ -205,8 +204,8 @@ export async function getProductData( }>({ query: PRODUCT_QUERY, variables: { - country: countryCode, - language: languageCode, + country, + language, selectedOptions, handle, }, @@ -374,9 +373,9 @@ const RECOMMENDED_PRODUCTS_QUERY = `#graphql query productRecommendations( $productId: ID! $count: Int - $countryCode: CountryCode - $languageCode: LanguageCode - ) @inContext(country: $countryCode, language: $languageCode) { + $country: CountryCode + $language: LanguageCode + ) @inContext(country: $country, language: $language) { recommended: productRecommendations(productId: $productId) { ...ProductCard } @@ -388,11 +387,12 @@ const RECOMMENDED_PRODUCTS_QUERY = `#graphql } `; -export async function getRecommendedProducts(productId: string, count = 12) { - // TODO: You know what to do - const languageCode = "EN"; - const countryCode = "US"; - +export async function getRecommendedProducts( + productId: string, + params: Params, + count = 12 +) { + const { language, country } = getLocalizationFromLang(params.lang); const { data: products } = await getStorefrontData<{ recommended: Product[]; additional: ProductConnection; @@ -401,8 +401,8 @@ export async function getRecommendedProducts(productId: string, count = 12) { variables: { productId, count, - language: languageCode, - country: countryCode, + language, + country, }, }); @@ -453,11 +453,10 @@ const COLLECTIONS_QUERY = `#graphql `; export async function getCollections( + params: Params, { paginationSize } = { paginationSize: 8 } ) { - // TODO: You know what to do - const languageCode = "EN"; - const countryCode = "US"; + const { language, country } = getLocalizationFromLang(params.lang); const { data } = await getStorefrontData<{ collections: CollectionConnection; @@ -465,8 +464,8 @@ export async function getCollections( query: COLLECTIONS_QUERY, variables: { pageBy: paginationSize, - country: countryCode, - language: languageCode, + country, + language, }, }); @@ -517,14 +516,14 @@ export async function getCollection({ handle, paginationSize = 48, cursor, + params, }: { handle: string; paginationSize?: number; cursor?: string; + params: Params; }) { - // TODO: You know what to do - const languageCode = "EN"; - const countryCode = "US"; + const { language, country } = getLocalizationFromLang(params.lang); const { data } = await getStorefrontData<{ collection: Collection; @@ -533,8 +532,8 @@ export async function getCollection({ variables: { handle, cursor, - language: languageCode, - country: countryCode, + language, + country, pageBy: paginationSize, }, }); @@ -572,13 +571,13 @@ const ALL_PRODUCTS_QUERY = `#graphql export async function getAllProducts({ paginationSize = 48, cursor, + params, }: { paginationSize?: number; cursor?: string; + params: Params; }) { - // TODO: You know what to do - const languageCode = "EN"; - const countryCode = "US"; + const { language, country } = getLocalizationFromLang(params.lang); const { data } = await getStorefrontData<{ products: ProductConnection; @@ -586,8 +585,8 @@ export async function getAllProducts({ query: ALL_PRODUCTS_QUERY, variables: { cursor, - language: languageCode, - country: countryCode, + language, + country, pageBy: paginationSize, }, }); @@ -709,9 +708,8 @@ mutation CartCreate($input: CartInput!, $country: CountryCode = ZZ) @inContext(c } `; -export async function createCart({ cart }: { cart: CartInput }) { - // TODO: You know what to do - const countryCode = "US"; +export async function createCart({ cart, params }: { cart: CartInput, params: Params }) { + const { country } = getLocalizationFromLang(params.lang); const { data } = await getStorefrontData<{ cartCreate: { @@ -721,7 +719,7 @@ export async function createCart({ cart }: { cart: CartInput }) { query: CREATE_CART_MUTATION, variables: { input: cart, - country: countryCode, + country, }, }); @@ -743,12 +741,13 @@ const ADD_LINE_ITEM_QUERY = `#graphql export async function addLineItem({ cartId, lines, + params }: { cartId: string; lines: CartLineInput[]; + params: Params; }) { - // TODO: You know what to do - const countryCode = "US"; + const { country } = getLocalizationFromLang(params.lang); const { data } = await getStorefrontData<{ cartLinesAdd: { @@ -756,7 +755,7 @@ export async function addLineItem({ }; }>({ query: ADD_LINE_ITEM_QUERY, - variables: { cartId, lines, country: countryCode }, + variables: { cartId, lines, country }, }); invariant(data, "No data returned from Shopify API"); @@ -774,15 +773,17 @@ const CART_QUERY = `#graphql ${CART_FRAGMENT} `; -export async function getCart({ cartId }: { cartId: string }) { - // TODO: Yes - const countryCode = "US"; +export async function getCart({ cartId, params }: { + cartId: string; + params: Params; +}) { + const { country } = getLocalizationFromLang(params.lang); const { data } = await getStorefrontData<{ cart: Cart }>({ query: CART_QUERY, variables: { cartId, - country: countryCode, + country, }, }); @@ -806,11 +807,13 @@ const UPDATE_LINE_ITEM_QUERY = `#graphql export async function updateLineItem({ cartId, lineItem, + params, }: { cartId: string; lineItem: CartLineUpdateInput; + params: Params; }) { - const countryCode = "US"; + const { country } = getLocalizationFromLang(params.lang); const { data } = await getStorefrontData<{ cartLinesUpdate: { cart: Cart } }>( { @@ -818,7 +821,7 @@ export async function updateLineItem({ variables: { cartId, lines: [lineItem], - country: countryCode, + country, }, } ); @@ -832,9 +835,9 @@ const TOP_PRODUCTS_QUERY = `#graphql ${PRODUCT_CARD_FRAGMENT} query topProducts( $count: Int - $countryCode: CountryCode - $languageCode: LanguageCode - ) @inContext(country: $countryCode, language: $languageCode) { + $country: CountryCode + $language: LanguageCode + ) @inContext(country: $country, language: $language) { products(first: $count, sortKey: BEST_SELLING) { nodes { ...ProductCard @@ -843,9 +846,11 @@ const TOP_PRODUCTS_QUERY = `#graphql } `; -export async function getTopProducts({ count = 4 }: { count?: number } = {}) { - const countryCode = "US"; - const languageCode = "EN"; +export async function getTopProducts({ params, count = 4 }: { + params: Params; + count?: number +}) { + const { language, country } = getLocalizationFromLang(params.lang); const { data } = await getStorefrontData<{ products: ProductConnection; @@ -853,8 +858,8 @@ export async function getTopProducts({ count = 4 }: { count?: number } = {}) { query: TOP_PRODUCTS_QUERY, variables: { count, - countryCode, - languageCode, + country, + language, }, }); @@ -913,13 +918,20 @@ interface SitemapQueryData { pages: PageConnection; } -export async function getSitemap(variables: { - language: string; +export async function getSitemap({ + params, + urlLimits, +}: { + params: Params; urlLimits: number; }) { + const { language } = getLocalizationFromLang(params.lang); const { data } = await getStorefrontData({ query: SITEMAP_QUERY, - variables, + variables: { + urlLimits, + language, + }, }); invariant(data, "Sitemap data is missing"); @@ -961,14 +973,15 @@ query Blog( `; export async function getBlog({ - language, + params, paginationSize, blogHandle, }: { - language: LanguageCode; + params: Params; blogHandle: string; paginationSize: number; }) { + const { language } = getLocalizationFromLang(params.lang); const { data } = await getStorefrontData<{ blog: Blog; }>({ @@ -1015,16 +1028,25 @@ const ARTICLE_QUERY = `#graphql } `; -export async function getArticle(variables: { - language: LanguageCode; +export async function getArticle({ + params, + blogHandle, + articleHandle, +}: { + params: Params; blogHandle: string; articleHandle: string; }) { + const { language } = getLocalizationFromLang(params.lang); const { data } = await getStorefrontData<{ blog: Blog; }>({ query: ARTICLE_QUERY, - variables, + variables: { + blogHandle, + articleHandle, + language + }, }); invariant(data, "No data returned from Shopify API"); @@ -1037,8 +1059,8 @@ export async function getArticle(variables: { } const PAGE_QUERY = `#graphql - query PageDetails($languageCode: LanguageCode, $handle: String!) - @inContext(language: $languageCode) { + query PageDetails($language: LanguageCode, $handle: String!) + @inContext(language: $language) { page(handle: $handle) { id title @@ -1051,13 +1073,20 @@ const PAGE_QUERY = `#graphql } `; -export async function getPageData(variables: { - language: LanguageCode; +export async function getPageData({ + params, + handle, +}: { + params: Params; handle: string; }) { + const { language } = getLocalizationFromLang(params.lang); const { data } = await getStorefrontData<{ page: Page }>({ query: PAGE_QUERY, - variables, + variables: { + language, + handle + }, }); invariant(data, "No data returned from Shopify API"); @@ -1093,16 +1122,19 @@ const NOT_FOUND_QUERY = `#graphql } `; -export async function getFeaturedData(variables: { - language: LanguageCode; - country: CountryCode; +export async function getFeaturedData({params}: { + params: Params; }) { + const { language, country } = getLocalizationFromLang(params.lang); const { data } = await getStorefrontData<{ featuredCollections: CollectionConnection; featuredProducts: ProductConnection; }>({ query: NOT_FOUND_QUERY, - variables, + variables: { + language, + country, + }, }); return data; @@ -1254,13 +1286,14 @@ export async function getCustomer({ request, context, customerAccessToken, + params, }: { request: Request; context: AppLoadContext; customerAccessToken: string; + params: Params; }) { - const countryCode = "US"; - const languageCode = "EN"; + const { language, country } = getLocalizationFromLang(params.lang); const { data } = await getStorefrontData<{ customer: Customer; @@ -1268,8 +1301,8 @@ export async function getCustomer({ query: CUSTOMER_QUERY, variables: { customerAccessToken, - country: countryCode, - language: languageCode, + country, + language, }, }); diff --git a/app/lib/utils.ts b/app/lib/utils.ts index 50b60d2878..c3b7824ff9 100644 --- a/app/lib/utils.ts +++ b/app/lib/utils.ts @@ -1,8 +1,11 @@ +import { useLocation, useParams, type Params } from "@remix-run/react"; import type { MenuItem, Menu, MoneyV2, UserError, + CountryCode, + LanguageCode, } from "@shopify/hydrogen-ui-alpha/storefront-api-types"; // @ts-expect-error types not available @@ -250,3 +253,29 @@ export function getApiErrorMessage( return data[field].customerUserErrors[0].message; return null; } + +export function getLocalizationFromLang(lang?: String): { + language: LanguageCode, + country: CountryCode +} { + if (lang) { + const [language, country] = lang.split('-'); + + return { + language: language.toUpperCase() as LanguageCode, + country: (country.toUpperCase() || 'US') as CountryCode, + } + } + return { + language: 'EN' as LanguageCode, + country: 'US' as CountryCode, + } +} + +export function isHomePath() { + const { pathname } = useLocation(); + const { lang } = useParams(); + const strippedPathname = pathname.replace(new RegExp(`^\/${lang}\/`), '/') + + return strippedPathname === '/'; +} diff --git a/app/root.tsx b/app/root.tsx index 1a10e1eda0..ee3b3946dc 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -47,22 +47,15 @@ export const meta: MetaFunction = () => ({ export const loader: LoaderFunction = async function loader({ request, context, + params }) { const session = await getSession(request, context); const cartId = await session.get("cartId"); return defer({ - layout: await getLayoutData(), - defaultCountry: await { - currency: { - isoCode: "USD", - symbol: "$", - }, - isoCode: "US", - name: "United States", - }, + layout: await getLayoutData(params), countries: getCountries(), - cart: cartId ? getCart({ cartId }) : undefined, + cart: cartId ? getCart({ cartId, params }) : undefined, }); }; diff --git a/app/routes/$lang/[robots.txt].tsx b/app/routes/$lang/[robots.txt].tsx new file mode 100644 index 0000000000..c4ce41ce3d --- /dev/null +++ b/app/routes/$lang/[robots.txt].tsx @@ -0,0 +1 @@ +export { loader } from '~/routes/[robots.txt]'; diff --git a/app/routes/$lang/[sitemap.xml].tsx b/app/routes/$lang/[sitemap.xml].tsx new file mode 100644 index 0000000000..4a1afbd015 --- /dev/null +++ b/app/routes/$lang/[sitemap.xml].tsx @@ -0,0 +1 @@ +export { loader } from '~/routes/[sitemap.xml]'; diff --git a/app/routes/$lang/account.login.tsx b/app/routes/$lang/account.login.tsx new file mode 100644 index 0000000000..da16b3f5bd --- /dev/null +++ b/app/routes/$lang/account.login.tsx @@ -0,0 +1,4 @@ +import Component, { loader, action } from '~/routes/account.login'; + +export { loader, action }; +export default Component; diff --git a/app/routes/$lang/account.logout.tsx b/app/routes/$lang/account.logout.tsx new file mode 100644 index 0000000000..2bfccac9e5 --- /dev/null +++ b/app/routes/$lang/account.logout.tsx @@ -0,0 +1 @@ +export { logout, loader, action } from '~/routes/account.logout'; diff --git a/app/routes/$lang/account.tsx b/app/routes/$lang/account.tsx new file mode 100644 index 0000000000..0468a5ac0b --- /dev/null +++ b/app/routes/$lang/account.tsx @@ -0,0 +1,4 @@ +import Component, { loader } from '~/routes/account'; + +export { loader }; +export default Component; diff --git a/app/routes/$lang/account/edit.tsx b/app/routes/$lang/account/edit.tsx new file mode 100644 index 0000000000..f41c1ecf9d --- /dev/null +++ b/app/routes/$lang/account/edit.tsx @@ -0,0 +1,4 @@ +import Component, { action } from '~/routes/account/edit'; + +export { action }; +export default Component; diff --git a/app/routes/$lang/cart.tsx b/app/routes/$lang/cart.tsx new file mode 100644 index 0000000000..2e66254c82 --- /dev/null +++ b/app/routes/$lang/cart.tsx @@ -0,0 +1,4 @@ +import Component, { loader, action } from '~/routes/cart'; + +export { loader, action }; +export default Component; diff --git a/app/routes/$lang/collections/$collectionHandle.tsx b/app/routes/$lang/collections/$collectionHandle.tsx new file mode 100644 index 0000000000..cf02ccbf86 --- /dev/null +++ b/app/routes/$lang/collections/$collectionHandle.tsx @@ -0,0 +1,4 @@ +import Component, { loader, meta } from '~/routes/collections/$collectionHandle'; + +export { loader, meta }; +export default Component; diff --git a/app/routes/$lang/collections/all.tsx b/app/routes/$lang/collections/all.tsx new file mode 100644 index 0000000000..fc179d414e --- /dev/null +++ b/app/routes/$lang/collections/all.tsx @@ -0,0 +1 @@ +export { loader } from '~/routes/collections/all'; diff --git a/app/routes/$lang/collections/index.tsx b/app/routes/$lang/collections/index.tsx new file mode 100644 index 0000000000..54a4f9448a --- /dev/null +++ b/app/routes/$lang/collections/index.tsx @@ -0,0 +1,4 @@ +import Component, { loader, meta } from '~/routes/collections/index'; + +export { loader, meta }; +export default Component; diff --git a/app/routes/$lang/index.tsx b/app/routes/$lang/index.tsx new file mode 100644 index 0000000000..114bf5d14d --- /dev/null +++ b/app/routes/$lang/index.tsx @@ -0,0 +1,7 @@ +import Component from '~/routes/index'; + +export default Component; + +// Everything in the $lang folder is a re-export of the route folder +// Wondering if we can just have it as a build process (dynamically creates these +// files based on the files in the routes folder) during build/hot reload process diff --git a/app/routes/$lang/journal/$journalHandle.tsx b/app/routes/$lang/journal/$journalHandle.tsx new file mode 100644 index 0000000000..f92ace8742 --- /dev/null +++ b/app/routes/$lang/journal/$journalHandle.tsx @@ -0,0 +1,4 @@ +import Component, { loader, meta, links, CatchBoundary } from '~/routes/journal/$journalHandle'; + +export { loader, meta, links, CatchBoundary }; +export default Component; diff --git a/app/routes/$lang/journal/index.tsx b/app/routes/$lang/journal/index.tsx new file mode 100644 index 0000000000..2754f4a262 --- /dev/null +++ b/app/routes/$lang/journal/index.tsx @@ -0,0 +1,4 @@ +import Component, { loader, meta } from '~/routes/journal/index'; + +export { loader, meta }; +export default Component; diff --git a/app/routes/$lang/products/$productHandle.tsx b/app/routes/$lang/products/$productHandle.tsx new file mode 100644 index 0000000000..3b405d6c74 --- /dev/null +++ b/app/routes/$lang/products/$productHandle.tsx @@ -0,0 +1,4 @@ +import Component, { loader, action, ProductForm } from '~/routes/products/$productHandle'; + +export { loader, action, ProductForm }; +export default Component; diff --git a/app/routes/$lang/products/index.tsx b/app/routes/$lang/products/index.tsx new file mode 100644 index 0000000000..d7886eba8e --- /dev/null +++ b/app/routes/$lang/products/index.tsx @@ -0,0 +1,4 @@ +import Component, { loader, meta } from '~/routes/products/index'; + +export { loader, meta }; +export default Component; diff --git a/app/routes/[sitemap.xml].tsx b/app/routes/[sitemap.xml].tsx index c2ac98a48e..9c23b88448 100644 --- a/app/routes/[sitemap.xml].tsx +++ b/app/routes/[sitemap.xml].tsx @@ -4,9 +4,9 @@ import { getSitemap } from "~/data"; const MAX_URLS = 250; // the google limit is 50K, however, SF API only allow querying for 250 resources each time -export async function loader({ request }: LoaderArgs) { +export async function loader({ request, params }: LoaderArgs) { const data = await getSitemap({ - language: "EN", + params, urlLimits: MAX_URLS, }); diff --git a/app/routes/account.login.tsx b/app/routes/account.login.tsx index 1ddd67a6ab..e1fca2d1ec 100644 --- a/app/routes/account.login.tsx +++ b/app/routes/account.login.tsx @@ -5,11 +5,12 @@ import { type ActionFunction, type LoaderArgs, } from "@remix-run/cloudflare"; -import { Form, Link, useActionData, useLoaderData } from "@remix-run/react"; +import { Form, useActionData, useLoaderData } from "@remix-run/react"; import { useState } from "react"; import { login, StorefrontApiError } from "~/data"; import { getSession } from "~/lib/session.server"; import { getInputStyleClasses } from "~/lib/utils"; +import { LinkI18n } from '~/components'; export async function loader({ request, context }: LoaderArgs) { const session = await getSession(request, context); @@ -173,20 +174,20 @@ export default function Login() {

    New to {shopName}?   - + Create an account - +

    - Forgot password - +
    diff --git a/app/routes/account/edit.tsx b/app/routes/account/edit.tsx index 5d192cfd8a..e0cba527da 100644 --- a/app/routes/account/edit.tsx +++ b/app/routes/account/edit.tsx @@ -43,7 +43,7 @@ const formDataHas = (formData: FormData, key: string) => { return typeof value === "string" && value.length > 0; }; -export const action: ActionFunction = async ({ request, context }) => { +export const action: ActionFunction = async ({ request, context, params }) => { const [formData, session] = await Promise.all([ request.formData(), getSession(request, context), @@ -58,7 +58,7 @@ export const action: ActionFunction = async ({ request, context }) => { // Double-check current user is logged in. // Will throw a logout redirect if not. - await getCustomer({ customerAccessToken, request, context }); + await getCustomer({ customerAccessToken, request, context, params }); if ( formDataHas(formData, "newPassword") && diff --git a/app/routes/cart.tsx b/app/routes/cart.tsx index 81b8df00d1..548b0e86cf 100644 --- a/app/routes/cart.tsx +++ b/app/routes/cart.tsx @@ -1,15 +1,15 @@ -import { type ActionFunction, json, defer } from "@remix-run/cloudflare"; +import { type ActionFunction, json, defer, LoaderArgs } from "@remix-run/cloudflare"; import invariant from "tiny-invariant"; import { getTopProducts, updateLineItem } from "~/data"; import { getSession } from "~/lib/session.server"; -export async function loader() { +export async function loader({params}: LoaderArgs) { return defer({ - topProducts: getTopProducts(), + topProducts: getTopProducts({params}), }); } -export const action: ActionFunction = async ({ request, context }) => { +export const action: ActionFunction = async ({ request, context, params }) => { const [session, formData] = await Promise.all([ getSession(request, context), new URLSearchParams(await request.text()), @@ -30,7 +30,7 @@ export const action: ActionFunction = async ({ request, context }) => { switch (intent) { case "set-quantity": { const quantity = Number(formData.get("quantity")); - await updateLineItem({ cartId, lineItem: { id: lineId, quantity } }); + await updateLineItem({ cartId, lineItem: { id: lineId, quantity }, params }); break; } @@ -39,7 +39,7 @@ export const action: ActionFunction = async ({ request, context }) => { * We're re-using the same mutation as setting a quantity of 0, * but theoretically we could use the `cartLinesRemove` mutation. */ - await updateLineItem({ cartId, lineItem: { id: lineId, quantity: 0 } }); + await updateLineItem({ cartId, lineItem: { id: lineId, quantity: 0 }, params }); break; } } diff --git a/app/routes/collections/$collectionHandle.tsx b/app/routes/collections/$collectionHandle.tsx index 9542f5e265..48711d7210 100644 --- a/app/routes/collections/$collectionHandle.tsx +++ b/app/routes/collections/$collectionHandle.tsx @@ -17,7 +17,7 @@ export async function loader({ params, request }: LoaderArgs) { invariant(collectionHandle, "Missing collectionHandle param"); const cursor = new URL(request.url).searchParams.get("cursor") ?? undefined; - const collection = await getCollection({ handle: collectionHandle, cursor }); + const collection = await getCollection({ handle: collectionHandle, cursor, params }); return json({ collection }); } diff --git a/app/routes/collections/index.tsx b/app/routes/collections/index.tsx index 9510d04061..81b52f8521 100644 --- a/app/routes/collections/index.tsx +++ b/app/routes/collections/index.tsx @@ -1,12 +1,12 @@ -import { json, type MetaFunction } from "@remix-run/cloudflare"; -import { Link, useLoaderData } from "@remix-run/react"; +import { json, LoaderArgs, type MetaFunction } from "@remix-run/cloudflare"; +import { useLoaderData } from "@remix-run/react"; import type { Collection } from "@shopify/hydrogen-ui-alpha/storefront-api-types"; -import { Grid, Heading, PageHeader, Section } from "~/components"; +import { Grid, Heading, PageHeader, Section, LinkI18n } from "~/components"; import { getCollections } from "~/data"; import { getImageLoadingPriority } from "~/lib/const"; -export const loader = async () => { - const collections = await getCollections(); +export const loader = async ({ params }: LoaderArgs) => { + const collections = await getCollections(params); return json({ collections }); }; @@ -46,7 +46,7 @@ function CollectionCard({ loading?: HTMLImageElement["loading"]; }) { return ( - +
    {collection?.image && ( {collection.title} - + ); } diff --git a/app/routes/featured-products.tsx b/app/routes/featured-products.tsx index 125b539df8..a75ffbacb9 100644 --- a/app/routes/featured-products.tsx +++ b/app/routes/featured-products.tsx @@ -1,6 +1,6 @@ -import { json } from "@remix-run/cloudflare"; +import { json, LoaderArgs } from "@remix-run/cloudflare"; import { getFeaturedData } from "~/data"; -export async function loader() { - return json(await getFeaturedData({ language: "EN", country: "US" })); +export async function loader({params}: LoaderArgs) { + return json(await getFeaturedData({ params })); } diff --git a/app/routes/journal/$journalHandle.tsx b/app/routes/journal/$journalHandle.tsx index 93114be941..915868e4f7 100644 --- a/app/routes/journal/$journalHandle.tsx +++ b/app/routes/journal/$journalHandle.tsx @@ -11,25 +11,24 @@ import invariant from "tiny-invariant"; import { PageHeader, Section } from "~/components"; import { getArticle } from "~/data"; import { ATTR_LOADING_EAGER } from "~/lib/const"; +import { getLocalizationFromLang } from "~/lib/utils"; import styles from "../../styles/custom-font.css"; const BLOG_HANDLE = "journal"; -export async function loader({ request, params }: LoaderArgs) { - // TODO figure out localization - const languageCode = "EN"; - const countryCode = "US"; +export async function loader({ params }: LoaderArgs) { + const { language, country } = getLocalizationFromLang(params.lang); invariant(params.journalHandle, "Missing journal handle"); const article = await getArticle({ blogHandle: BLOG_HANDLE, articleHandle: params.journalHandle, - language: languageCode, + params, }); const formattedDate = new Intl.DateTimeFormat( - `${languageCode}-${countryCode}`, + `${language}-${country}`, { year: "numeric", month: "long", diff --git a/app/routes/journal/index.tsx b/app/routes/journal/index.tsx index 62a24baf8d..c26dd1f897 100644 --- a/app/routes/journal/index.tsx +++ b/app/routes/journal/index.tsx @@ -1,29 +1,28 @@ -import { json, type MetaFunction } from "@remix-run/cloudflare"; -import { Link, useLoaderData } from "@remix-run/react"; +import { json, LoaderArgs, type MetaFunction } from "@remix-run/cloudflare"; +import { useLoaderData } from "@remix-run/react"; import { flattenConnection, Image } from "@shopify/hydrogen-ui-alpha"; import type { Article } from "@shopify/hydrogen-ui-alpha/storefront-api-types"; -import { Grid, PageHeader, Section } from "~/components"; +import { Grid, PageHeader, Section, LinkI18n } from "~/components"; import { getBlog } from "~/data"; import { getImageLoadingPriority, PAGINATION_SIZE } from "~/lib/const"; +import { getLocalizationFromLang } from "~/lib/utils"; const BLOG_HANDLE = "Journal"; -export const loader = async () => { - // TODO figure out localization - const languageCode = "EN"; - const countryCode = "US"; - +export const loader = async ({params}: LoaderArgs) => { const journals = await getBlog({ - language: languageCode, + params, blogHandle: BLOG_HANDLE, paginationSize: PAGINATION_SIZE, }); + const { language, country } = getLocalizationFromLang(params.lang); + const articles = flattenConnection(journals).map((article) => { const { publishedAt } = article; return { ...article, - publishedAt: new Intl.DateTimeFormat(`${languageCode}-${countryCode}`, { + publishedAt: new Intl.DateTimeFormat(`${language}-${country}`, { year: "numeric", month: "long", day: "numeric", @@ -80,7 +79,7 @@ function ArticleCard({ }) { return (
  • - + {article.image && (
    {article.title} {article.publishedAt} - +
  • ); } diff --git a/app/routes/pages/$pageHandle.tsx b/app/routes/pages/$pageHandle.tsx index 12e957d297..92932bcb31 100644 --- a/app/routes/pages/$pageHandle.tsx +++ b/app/routes/pages/$pageHandle.tsx @@ -10,14 +10,11 @@ import { PageHeader } from "~/components"; import { getPageData } from "~/data"; export async function loader({ params }: LoaderArgs) { - // TODO figure out localization - const languageCode = "EN"; - invariant(params.pageHandle, "Missing page handle"); const page = await getPageData({ handle: params.pageHandle, - language: languageCode, + params, }); return json( diff --git a/app/routes/products/$productHandle.tsx b/app/routes/products/$productHandle.tsx index d147eea10d..11e70bdf11 100644 --- a/app/routes/products/$productHandle.tsx +++ b/app/routes/products/$productHandle.tsx @@ -6,7 +6,6 @@ import { redirect, } from "@remix-run/cloudflare"; import { - Link, useLoaderData, Await, useSearchParams, @@ -26,6 +25,7 @@ import { Section, Skeleton, Text, + LinkI18n, } from "~/components"; import { addLineItem, @@ -44,13 +44,14 @@ export const loader = async ({ params, request }: LoaderArgs) => { const { shop, product } = await getProductData( productHandle, - new URL(request.url).searchParams + new URL(request.url).searchParams, + params ); return defer({ product, shop, - recommended: getRecommendedProducts(product.id), + recommended: getRecommendedProducts(product.id, params), }); }; @@ -75,6 +76,7 @@ export const action: ActionFunction = async ({ request, context, params }) => { if (!cartId) { const cart = await createCart({ cart: { lines: [{ merchandiseId: variantId }] }, + params }); session.set("cartId", cart.id); @@ -84,6 +86,7 @@ export const action: ActionFunction = async ({ request, context, params }) => { await addLineItem({ cartId, lines: [{ merchandiseId: variantId }], + params, }); } @@ -380,7 +383,7 @@ function ProductOptionLink({ clonedSearchParams.set(optionName, optionValue); return ( - {children ?? optionValue} - + ); } @@ -428,12 +431,12 @@ function ProductDetail({ /> {learnMore && (
    - Learn more - +
    )} diff --git a/app/routes/products/index.tsx b/app/routes/products/index.tsx index 40b4bc62c8..d4a154390c 100644 --- a/app/routes/products/index.tsx +++ b/app/routes/products/index.tsx @@ -4,9 +4,9 @@ import type { Collection } from "@shopify/hydrogen-ui-alpha/storefront-api-types import { PageHeader, Section, ProductGrid } from "~/components"; import { getAllProducts } from "~/data"; -export async function loader({ request }: LoaderArgs) { +export async function loader({ request, params }: LoaderArgs) { const cursor = new URL(request.url).searchParams.get("cursor") ?? undefined; - const products = await getAllProducts({ cursor }); + const products = await getAllProducts({ cursor, params }); return products; }