Skip to content

Commit

Permalink
Add Ebook to precart
Browse files Browse the repository at this point in the history
  • Loading branch information
pookmish committed Jan 22, 2025
1 parent 116f1b8 commit 32538fb
Show file tree
Hide file tree
Showing 9 changed files with 4,005 additions and 18,000 deletions.
20 changes: 10 additions & 10 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@
"dependencies": {
"@formkit/auto-animate": "^0.8.2",
"@heroicons/react": "^2.2.0",
"@mui/base": "5.0.0-beta.68",
"@next/third-parties": "15.1.4",
"@mui/base": "5.0.0-beta.69",
"@next/third-parties": "15.1.6",
"@tailwindcss/container-queries": "^0.1.1",
"@types/node": "^22.10.7",
"@types/react": "^19.0.7",
"@types/react-dom": "19.0.3",
"algoliasearch": "5.19.0",
"algoliasearch": "5.20.0",
"autoprefixer": "^10.4.20",
"clsx": "^2.1.1",
"decanter": "^7.3.0",
Expand All @@ -32,15 +32,15 @@
"graphql-tag": "^2.12.6",
"html-entities": "^2.5.2",
"html-react-parser": "^5.2.2",
"next": "^15.2.0-canary.12",
"next": "^15.1.6",
"plaiceholder": "^3.0.0",
"postcss": "^8.5.1",
"qs": "^6.14.0",
"react": "19.0.0",
"react-dom": "19.0.0",
"react-focus-lock": "^2.13.5",
"react-instantsearch": "^7.15.0",
"react-instantsearch-nextjs": "^0.4.1",
"react-instantsearch": "^7.15.1",
"react-instantsearch-nextjs": "^0.4.2",
"react-slick": "^0.30.3",
"react-tiny-oembed": "^1.1.0",
"sharp": "^0.33.5",
Expand All @@ -55,7 +55,7 @@
"@graphql-codegen/import-types-preset": "^3.0.0",
"@graphql-codegen/typescript-graphql-request": "^6.2.0",
"@graphql-codegen/typescript-operations": "^4.4.0",
"@next/bundle-analyzer": "15.1.4",
"@next/bundle-analyzer": "15.1.6",
"@storybook/addon-essentials": "^8.5.0",
"@storybook/addon-interactions": "^8.5.0",
"@storybook/addon-links": "^8.5.0",
Expand All @@ -68,17 +68,17 @@
"concurrently": "^9.1.2",
"encoding": "^0.1.13",
"eslint": "9.18.0",
"eslint-config-next": "15.1.4",
"eslint-config-next": "15.1.6",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-prettier": "^5.2.2",
"eslint-plugin-prettier": "^5.2.3",
"eslint-plugin-storybook": "^0.11.2",
"prettier": "^3.4.2",
"prettier-plugin-tailwindcss": "^0.6.10",
"react-docgen": "^7.1.0",
"storybook": "^8.5.0",
"storybook-addon-module-mock": "^1.3.4",
"tsconfig-paths-webpack-plugin": "^4.2.0",
"typescript-eslint": "8.20.0"
"typescript-eslint": "8.21.0"
},
"packageManager": "[email protected]",
"resolutions": {
Expand Down
14 changes: 12 additions & 2 deletions src/components/nodes/pages/sup-book/book-page/book-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import {ArrowLongLeftIcon, BookmarkIcon, ClipboardIcon} from "@heroicons/react/2
import Link from "@components/elements/link"
import BookAwards from "@components/nodes/pages/sup-book/book-awards"
import BookPageImage from "@components/nodes/pages/sup-book/book-page-image"
import PrecartClient from "@components/nodes/pages/sup-book/precart/precart.client"
import PreCartClient from "@components/nodes/pages/sup-book/precart/precart.client"
import ExcerptButton from "@components/elements/excerpt-button"
import NodePageMetadata from "@components/nodes/pages/node-page-metadata"

type Props = HTMLAttributes<HTMLElement> & {
node: NodeSupBook
Expand All @@ -34,6 +35,14 @@ const BookPage = async ({node, ...props}: Props) => {

return (
<article {...props} className="centered">
<NodePageMetadata metatags={node.metatag} pageTitle={node.title} backupDescription={node.supBookSubtitle}>
{node.supBookAuthors?.map(author => (
<>
<meta property="book:author:profile:first_name" content={author.given || undefined} />
<meta property="book:author:profile:last_name" content={author.family || undefined} />
</>
))}
</NodePageMetadata>
<div className="mb-20 flex flex-col md:rs-mt-4 md:flex-row md:gap-32 lg:gap-[7.6rem]">
<div className="relative left-1/2 flex w-screen -translate-x-1/2 flex-col justify-center bg-fog-light px-20 md:hidden">
<div className="flex flex-row gap-24">
Expand Down Expand Up @@ -152,11 +161,12 @@ const BookPage = async ({node, ...props}: Props) => {

<div className="lg:w-3/8 xl:min-w-[200px] 2xl:min-w-[320px] 2xl:max-w-[370px]">
{!node.supBookNoCart && (node.supBookIsbn13Cloth || node.supBookIsbn13Paper) && (
<PrecartClient
<PreCartClient
priceId={node.supBookPriceData?.id}
bookTitle={node.title}
clothIsbn={node.supBookIsbn13Cloth}
paperIsbn={node.supBookIsbn13Paper}
ebookIsbn={node.supBookIsbn13Digital}
hasIntlCart={node.supBookPriceData?.supIntlCart}
/>
)}
Expand Down
163 changes: 85 additions & 78 deletions src/components/nodes/pages/sup-book/precart/precart.client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@

import useIsInternational from "@lib/hooks/useIsInternational"
import Button from "@components/elements/button"
import {useEffect, useState} from "react"
import {HTMLAttributes, useEffect, useState} from "react"
import {ArrowRightIcon} from "@heroicons/react/16/solid"
import {Maybe, PressPrice} from "@lib/gql/__generated__/drupal.d"
import {BookOpenIcon as BookOpenIconOutline} from "@heroicons/react/24/outline"
import {BookOpenIcon as BookOpenIconOutline, DeviceTabletIcon} from "@heroicons/react/24/outline"
import {BookOpenIcon} from "@heroicons/react/24/solid"
import {formatCurrency} from "@lib/utils/format-currency"
import {twMerge} from "tailwind-merge"
Expand All @@ -18,11 +18,12 @@ type Props = {
priceId?: string
clothIsbn?: Maybe<string>
paperIsbn?: Maybe<string>
ebookIsbn?: Maybe<string>
bookTitle: string
hasIntlCart?: Maybe<boolean>
}

const PrecartClient = ({priceId, clothIsbn, paperIsbn, hasIntlCart, bookTitle}: Props) => {
const PreCartClient = ({priceId, clothIsbn, paperIsbn, ebookIsbn, hasIntlCart, bookTitle}: Props) => {
const [priceData, setPriceData] = useState<PriceProps>()

useEffect(() => {
Expand All @@ -45,18 +46,20 @@ const PrecartClient = ({priceId, clothIsbn, paperIsbn, hasIntlCart, bookTitle}:
<input name="email" type="email" />
</label>

<fieldset className="rs-mb-1">
<fieldset className="rs-mb-1 space-y-4">
<legend className="sr-only">Format</legend>
{!isIntl && (
<UsFormatChoices
clothIsbn={clothIsbn}
clothPrice={clothIsbn ? priceData?.supClothSale || priceData?.supClothPrice || false : undefined}
paperIsbn={paperIsbn}
ebookIsbn={ebookIsbn}
clothPrice={clothIsbn ? priceData?.supClothSale || priceData?.supClothPrice || false : undefined}
paperPrice={paperIsbn ? priceData?.supPaperSale || priceData?.supPaperPrice || false : undefined}
ebookPrice={ebookIsbn ? priceData?.supDigitalPrice || priceData?.supDigitalPrice || false : undefined}
/>
)}

{isIntl && <IntlFormatChoices clothIsbn={clothIsbn} paperIsbn={paperIsbn} />}
{isIntl && <IntlFormatChoices clothIsbn={clothIsbn} paperIsbn={paperIsbn} ebookIsbn={ebookIsbn} />}
</fieldset>

{(hasIntlCart || priceData?.supIntlCart) && (
Expand Down Expand Up @@ -157,108 +160,112 @@ const UsFormatChoices = ({
clothIsbn,
clothPrice,
paperIsbn,
ebookIsbn,
ebookPrice,
paperPrice,
}: {
clothIsbn?: Maybe<string>
clothPrice?: Maybe<number | false>
paperIsbn?: Maybe<string>
ebookIsbn?: Maybe<string>
clothPrice?: Maybe<number | false>
ebookPrice?: Maybe<number | false>
paperPrice?: Maybe<number | false>
}) => {
const defaultChoice = clothIsbn ? "cloth" : "paper"

return (
<>
{clothIsbn && clothPrice !== undefined && (
<label className="mb-3 block cursor-pointer">
<input
className="peer sr-only"
type="radio"
name="format"
value={clothIsbn}
defaultChecked={defaultChoice === "cloth"}
/>
<span className="group rs-py-0 rs-px-1 flex items-center border-4 hover:bg-fog-light peer-checked:border-digital-red peer-focus-visible:bg-fog-light peer-focus-visible:underline">
<span className="flex w-full items-center gap-2">
<span className="flex w-full flex-col items-center justify-between gap-5 @lg:flex-row @lg:gap-0">
<span className="font-semibold group-hover:underline md:text-[0.85em]">Hardcover</span>
<span className="mr-2 text-press-sand-dark @lg:ml-2 @lg:text-center md:text-[0.85em]">US/CAN</span>
{clothPrice && (
<span className="text-press-sand-dark md:text-[0.85em]">{formatCurrency(clothPrice)}</span>
)}
</span>
<BookOpenIcon width={24} className="text-fog-dark" />
</span>
<FormatChoice typeName="cloth" isbn={clothIsbn} defaultChecked={defaultChoice === "cloth"}>
<span className="flex w-full flex-col items-center justify-between gap-5 @lg:flex-row @lg:gap-0">
<span className="font-semibold group-hover:underline md:text-[0.85em]">Hardcover</span>
<span className="mr-2 text-press-sand-dark @lg:ml-2 @lg:text-center md:text-[0.85em]">US/CAN</span>
{clothPrice && <span className="text-press-sand-dark md:text-[0.85em]">{formatCurrency(clothPrice)}</span>}
</span>
</label>
<BookOpenIcon width={24} className="text-fog-dark" />
</FormatChoice>
)}
{paperIsbn && paperPrice !== undefined && (
<label className="mb-3 block cursor-pointer">
<input
className="peer sr-only"
type="radio"
name="format"
value={paperIsbn}
defaultChecked={defaultChoice === "paper"}
/>
<span className="group rs-py-0 rs-px-1 flex items-center border-4 hover:bg-fog-light peer-checked:border-digital-red peer-focus-visible:bg-fog-light peer-focus-visible:underline">
<span className="flex w-full items-center gap-2">
<span className="flex w-full flex-col items-center justify-between gap-2 @lg:flex-row @lg:gap-0">
<span className="font-semibold group-hover:underline md:text-[0.85em]">Paperback</span>
<FormatChoice typeName="paper" isbn={paperIsbn} defaultChecked={defaultChoice === "paper"}>
<span className="flex w-full flex-col items-center justify-between gap-2 @lg:flex-row @lg:gap-0">
<span className="font-semibold group-hover:underline md:text-[0.85em]">Paperback</span>

<span className="mr-2 text-press-sand-dark @lg:ml-2 @lg:text-center md:text-[0.85em]">US/CAN</span>
{paperPrice && (
<span className="text-press-sand-dark md:text-[0.85em]">{formatCurrency(paperPrice)}</span>
)}
</span>
<BookOpenIconOutline width={24} className="text-fog-dark" />
</span>
<span className="mr-2 text-press-sand-dark @lg:ml-2 @lg:text-center md:text-[0.85em]">US/CAN</span>
{paperPrice && <span className="text-press-sand-dark md:text-[0.85em]">{formatCurrency(paperPrice)}</span>}
</span>
<BookOpenIconOutline width={24} className="text-fog-dark" />
</FormatChoice>
)}
{ebookIsbn && ebookPrice !== undefined && (
<FormatChoice typeName="ebook" isbn={ebookIsbn}>
<span className="flex w-full flex-col items-center justify-between gap-2 @lg:flex-row @lg:gap-0">
<span className="font-semibold group-hover:underline md:text-[0.85em]">EBook</span>

<span className="mr-2 text-press-sand-dark @lg:ml-2 @lg:text-center md:text-[0.85em]">US/CAN</span>
{ebookPrice && <span className="text-press-sand-dark md:text-[0.85em]">{formatCurrency(ebookPrice)}</span>}
</span>
</label>
<DeviceTabletIcon width={24} className="text-fog-dark" />
</FormatChoice>
)}
</>
)
}

const IntlFormatChoices = ({clothIsbn, paperIsbn}: {clothIsbn?: Maybe<string>; paperIsbn?: Maybe<string>}) => {
const IntlFormatChoices = ({
clothIsbn,
paperIsbn,
ebookIsbn,
}: {
clothIsbn?: Maybe<string>
paperIsbn?: Maybe<string>
ebookIsbn?: Maybe<string>
}) => {
const defaultChoice = clothIsbn ? "cloth" : "paper"
return (
<>
{clothIsbn && (
<label className="mb-3 block cursor-pointer">
<input
className="peer sr-only"
type="radio"
name="format"
value={clothIsbn}
defaultChecked={defaultChoice === "cloth"}
/>
<span className="group rs-py-0 rs-px-1 flex items-center border-4 hover:bg-fog-light peer-checked:border-digital-red peer-focus-visible:bg-fog-light peer-focus-visible:underline">
<span className="flex w-full items-center justify-between font-semibold">
<span className="group-hover:underline md:text-[0.85em]">Hardcover</span>
<BookOpenIcon width={24} className="text-fog-dark" />
</span>
</span>
</label>
<FormatChoice typeName="cloth" isbn={clothIsbn} defaultChecked={defaultChoice === "cloth"}>
<span className="font-semibold group-hover:underline md:text-[0.85em]">Hardcover</span>
<BookOpenIcon width={24} className="text-fog-dark" />
</FormatChoice>
)}
{paperIsbn && (
<label className="block cursor-pointer">
<input
className="peer sr-only"
type="radio"
name="format"
value={paperIsbn}
defaultChecked={defaultChoice === "paper"}
/>
<span className="group rs-py-0 rs-px-1 flex items-center border-4 hover:bg-fog-light peer-checked:border-digital-red peer-focus-visible:bg-fog-light peer-focus-visible:underline">
<span className="flex w-full items-center justify-between font-semibold">
<span className="group-hover:underline md:text-[0.85em]">Paperback</span>
<BookOpenIconOutline width={24} className="text-fog-dark" />
</span>
<FormatChoice typeName="paper" isbn={paperIsbn} defaultChecked={defaultChoice === "paper"}>
<span className="font-semibold group-hover:underline md:text-[0.85em]">Paperback</span>
<BookOpenIconOutline width={24} className="text-fog-dark" />
</FormatChoice>
)}
{ebookIsbn && (
<FormatChoice typeName="ebook" isbn={ebookIsbn}>
<span className="flex w-full flex-col items-center justify-between gap-2 @lg:flex-row @lg:gap-0">
<span className="font-semibold group-hover:underline md:text-[0.85em]">EBook</span>
</span>
</label>
<DeviceTabletIcon width={24} className="text-fog-dark" />
</FormatChoice>
)}
</>
)
}

export default PrecartClient
const FormatChoice = ({
typeName,
isbn,
defaultChecked,
children,
...props
}: {
typeName: string
isbn: string
defaultChecked?: boolean
} & HTMLAttributes<HTMLLabelElement>) => {
return (
<label {...props} className={twMerge("block cursor-pointer", props.className)}>
<input className="peer sr-only" type="radio" name="format" value={`${typeName}:${isbn}`} defaultChecked={defaultChecked} />
<span className="group rs-py-0 rs-px-1 flex items-center border-4 hover:bg-fog-light peer-checked:border-digital-red peer-focus-visible:bg-fog-light peer-focus-visible:underline">
<span className="flex w-full items-center justify-between">{children}</span>
</span>
</label>
)
}

export default PreCartClient
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@ export const submitForm = async (formData: FormData) => {
if (formData.get("email")) return

const bookTitle = formData.get("title") as string
const formatIsbn = formData.get("format") as string
const [format, isbn] = (formData.get("format") as string).split(":")
const isIntl = formData.get("intl") !== "us"

if (format === "ebook") redirect(`https://stanforduniversitypress.glassboxx.com/?add-to-cart-sku=${isbn}_PDF`)

if (isIntl) {
const title = bookTitle.replaceAll(/[^a-zA-Z\d\s:]/g, "").replaceAll(/\s/g, "-")
redirect(`https://www.combinedacademic.co.uk/${formatIsbn}/${title}`)
redirect(`https://www.combinedacademic.co.uk/${isbn}/${title}`)
}
redirect(`https://add-to-cart-2.supadu.com/add-to-cart?isbn=${formatIsbn}&client=indiepubs-stanford-university-press`)
redirect(`https://add-to-cart-2.supadu.com/add-to-cart?isbn=${isbn}&client=indiepubs-stanford-university-press`)
}
9 changes: 0 additions & 9 deletions src/components/nodes/pages/sup-book/sup-book-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import {HTMLAttributes} from "react"
import BookPage from "@components/nodes/pages/sup-book/book-page/book-page"
import DigitalProjectPage from "@components/nodes/pages/sup-book/digital-project-page/digital-project-page"
import AlgoliaRelatedBooks from "@components/algolia-search/algolia-related-books"
import NodePageMetadata from "@components/nodes/pages/node-page-metadata"

type Props = HTMLAttributes<HTMLElement> & {
node: NodeSupBook
Expand All @@ -13,14 +12,6 @@ const SupBookPage = async ({node, ...props}: Props) => {

return (
<div>
<NodePageMetadata metatags={node.metatag} pageTitle={node.title} backupDescription={node.supBookSubtitle}>
{node.supBookAuthors?.map(author => (
<>
<meta property="book:author:profile:first_name" content={author.given || undefined} />
<meta property="book:author:profile:last_name" content={author.family || undefined} />
</>
))}
</NodePageMetadata>
<BookPage node={node} {...props} />
<AlgoliaRelatedBooks objectId={node.id} />
</div>
Expand Down
Loading

0 comments on commit 32538fb

Please sign in to comment.