Skip to content

Commit b9d82c9

Browse files
OpenStaxClaudeclaudeRoyEJohnson
authored
Add audiobook purchase button to book details pages (#2813)
* Add audiobook purchase button to book details pages This adds a new audiobook purchase option box that appears above the Individual and Bookstore print options when an audiobook link is configured in the CMS. Changes: - Import faVolumeUp icon for audiobook representation - Fetch audiobook_link from CMS API - Add audiobook entry to purchase options (appears first when present) - Update button styling to accommodate 3 boxes in grid layout - Add translations for "Audiobook" and "Purchase options" (en/es) - Add specific analytics tracking for audiobook clicks The audiobook box only appears when the book has an audiobook_link configured in the CMS, ensuring backwards compatibility. Related to: CORE-1476 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * Move audiobook box to end of purchase options list Per review feedback, the audiobook option now appears after Individual and Bookstore boxes instead of before them. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com> Co-authored-by: Roy Johnson <roy.e.johnson@rice.edu>
1 parent b988b5b commit b9d82c9

4 files changed

Lines changed: 55 additions & 8 deletions

File tree

src/app/lang/en.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,10 @@ export default {
7575
'printcopy.individual': 'Individual',
7676
'printcopy.disclosure': '***',
7777
'printcopy.bookstore': 'Bookstore',
78+
'printcopy.audiobook': 'Audiobook',
7879
'printcopy.button1': 'Buy a print copy',
7980
'printcopy.button2': 'Order options',
81+
'printcopy.audiobook-button': 'Purchase options',
8082
'pubInfo.pdf.heading': 'PDF Version Last Updated:',
8183
'pubInfo.pdf.see': 'See changes in the',
8284
'pubInfo.pdf.notes': 'Revision Notes',

src/app/lang/es.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,10 @@ export default {
7575
'printcopy.individual': 'Individuo',
7676
'printcopy.disclosure': 'Como afiliados de Amazon ganamos con compras calificadas',
7777
'printcopy.bookstore': 'Libreria',
78+
'printcopy.audiobook': 'Audiolibro',
7879
'printcopy.button1': 'Comprar una copia impresa',
7980
'printcopy.button2': 'Opciones para solicitar',
81+
'printcopy.audiobook-button': 'Opciones de compra',
8082
'pubInfo.pdf.heading': 'Última actualización de la versión PDF:',
8183
'pubInfo.pdf.see': 'Ver las modificaciones en las',
8284
'pubInfo.pdf.notes': 'Notas de revisión',

src/app/pages/details/common/get-this-title-files/order-print-copy/order-print-copy.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,5 +118,9 @@
118118
grid-template-columns: 1fr 1fr;
119119
padding: $normal-margin;
120120
width: max-content;
121+
122+
&.boxes-3 {
123+
grid-template-columns: 1fr 1fr 1fr;
124+
}
121125
}
122126
}

src/app/pages/details/common/get-this-title-files/order-print-copy/order-print-copy.tsx

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React from 'react';
22
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
33
import {faUser} from '@fortawesome/free-solid-svg-icons/faUser';
44
import {faUsers} from '@fortawesome/free-solid-svg-icons/faUsers';
5+
import {faVolumeUp} from '@fortawesome/free-solid-svg-icons/faVolumeUp';
56
import {useIntl} from 'react-intl';
67
import './order-print-copy.scss';
78
import cmsFetch from '~/helpers/cms-fetch';
@@ -12,6 +13,7 @@ type Content = {
1213
headerIcon: typeof faUser;
1314
buttonText: string;
1415
buttonUrl: string;
16+
trackingLabel: string;
1517
};
1618

1719
function Header({entry}: {entry: Content}) {
@@ -27,7 +29,7 @@ function Header({entry}: {entry: Content}) {
2729

2830
function PhoneBox({entry}: {entry: Content}) {
2931
return (
30-
<a className="box" href={entry.buttonUrl} data-track="Print">
32+
<a className="box" href={entry.buttonUrl} data-track={entry.trackingLabel}>
3133
<Header entry={entry} />
3234
</a>
3335
);
@@ -46,21 +48,23 @@ function PhoneBoxes({contentArray}: {contentArray: Content[]}) {
4648
function Button({
4749
href,
4850
text,
49-
buttonClass
51+
buttonClass,
52+
trackingLabel
5053
}: {
5154
href: string;
5255
text: string;
5356
buttonClass: string;
57+
trackingLabel: string;
5458
}) {
5559
return (
56-
<a className={`btn ${buttonClass}`} href={href} data-track="Print">
60+
<a className={`btn ${buttonClass}`} href={href} data-track={trackingLabel}>
5761
{text}
5862
</a>
5963
);
6064
}
6165

6266
function DesktopBox({index, entry}: {index: number; entry: Content}) {
63-
const buttonClass = ['primary', 'secondary'][index];
67+
const buttonClass = ['primary', 'primary', 'secondary'][index] || 'primary';
6468

6569
return (
6670
<div className="box" key={entry.headerText}>
@@ -69,6 +73,7 @@ function DesktopBox({index, entry}: {index: number; entry: Content}) {
6973
buttonClass={buttonClass}
7074
href={entry.buttonUrl}
7175
text={entry.buttonText}
76+
trackingLabel={entry.trackingLabel}
7277
/>
7378
</div>
7479
);
@@ -94,9 +99,20 @@ function useBookstoreContentLink(slug: string) {
9499
return url;
95100
}
96101

102+
function useAudiobookLink(slug: string) {
103+
const [url, setUrl] = React.useState<string | null>(null);
104+
105+
React.useEffect(() => {
106+
cmsFetch(slug).then((data) => setUrl(data.audiobook_link));
107+
}, [slug]);
108+
109+
return url;
110+
}
111+
97112
export default function OrderPrintCopy({slug, campaign}: {slug: string; campaign: UtmCampaign}) {
98113
const {formatMessage} = useIntl();
99114
const bookstoreLink = useBookstoreContentLink(slug);
115+
const audiobookLink = useAudiobookLink(slug);
100116
const contentArray = React.useMemo(() => {
101117
if (!bookstoreLink) {
102118
return null;
@@ -117,23 +133,46 @@ export default function OrderPrintCopy({slug, campaign}: {slug: string; campaign
117133
id: 'printcopy.button2',
118134
defaultMessage: 'Order options'
119135
});
136+
const audiobook = formatMessage({
137+
id: 'printcopy.audiobook',
138+
defaultMessage: 'Audiobook'
139+
});
140+
const audiobookButtonText = formatMessage({
141+
id: 'printcopy.audiobook-button',
142+
defaultMessage: 'Purchase options'
143+
});
120144

121-
return [
145+
const content = [
122146
{
123147
headerText: individual,
124148
headerIcon: faUser,
125149
buttonText: button1Text,
126-
buttonUrl: linkHelper.setUtmCampaign(bookstoreLink, campaign)
150+
buttonUrl: linkHelper.setUtmCampaign(bookstoreLink, campaign),
151+
trackingLabel: 'Print'
127152
},
128153
{
129154
headerText: bookstore,
130155
headerIcon: faUsers,
131156
buttonText: button2Text,
132157
buttonUrl:
133-
'https://he.kendallhunt.com/sites/default/files/uploadedFiles/Kendall_Hunt/OPENSTAX_PRICE_LIST_and_ORDER_FORM.pdf'
158+
'https://he.kendallhunt.com/sites/default/files/uploadedFiles/Kendall_Hunt/OPENSTAX_PRICE_LIST_and_ORDER_FORM.pdf',
159+
trackingLabel: 'Print'
134160
}
135161
];
136-
}, [formatMessage, bookstoreLink, campaign]);
162+
163+
// Add audiobook option at the end if link is available
164+
if (audiobookLink) {
165+
content.push({
166+
headerText: audiobook,
167+
headerIcon: faVolumeUp,
168+
buttonText: audiobookButtonText,
169+
buttonUrl: audiobookLink,
170+
trackingLabel: 'Audiobook'
171+
});
172+
}
173+
174+
return content;
175+
}, [formatMessage, bookstoreLink, audiobookLink, campaign]);
137176

138177
if (!contentArray) {
139178
return null;

0 commit comments

Comments
 (0)