diff options
| author | Joe Carstairs <65492573+joeacarstairs@users.noreply.github.com> | 2024-01-25 17:02:50 +0000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-01-25 17:02:50 +0000 |
| commit | 64c5ee53a0d158b38caff3285dac008826d7ae88 (patch) | |
| tree | 7542f8c5b82b3327d82cc5defe2e84f86d6f11e7 | |
| parent | 7140956da0820d4b818119eff7dfc72345f3328e (diff) | |
Update from staging (#70)
* 33 / Pay annual subscription by PayPal (#68)
* Refactors paypal-buttons -> paypal-product-buttons WebComponent
* Pulls out address to data file
* Defines annual subscription prices data hash
* Styles paypal-product-buttons
* Styles <address>
* Details have cursor:pointer on the summary
* Moves Jyne section to separate page
* Rewrites payment options using details components
* Reworks translation typing
* Defines MembershipType enum
* Adds translations for MembershipType enum
* Can pay annual subs by PayPal
* Styles tables better
* Replaces a few straggler --margins with --spacing (#67)
---------
Co-authored-by: Joe Carstairs <118172583+jcarstairs-scottlogic@users.noreply.github.com>
37 files changed, 763 insertions, 285 deletions
diff --git a/scss/base/_address.scss b/scss/base/_address.scss new file mode 100644 index 0000000..0206b83 --- /dev/null +++ b/scss/base/_address.scss @@ -0,0 +1,5 @@ +address { + margin-block: var(--spacing-block-md); + font-style: normal; + margin-inline-start: var(--spacing-inline-lg); +} diff --git a/scss/base/_details.scss b/scss/base/_details.scss new file mode 100644 index 0000000..6d0b1d4 --- /dev/null +++ b/scss/base/_details.scss @@ -0,0 +1,4 @@ +details summary { + cursor: pointer; +} + diff --git a/scss/base/_index.scss b/scss/base/_index.scss index f338772..527b4c7 100644 --- a/scss/base/_index.scss +++ b/scss/base/_index.scss @@ -1,4 +1,9 @@ +@use 'address'; +@use 'details'; @use 'dl'; @use 'list'; +@use 'paypal-product-buttons'; +@use 'table'; @use 'theme'; @use 'typography'; + diff --git a/scss/base/_paypal-product-buttons.scss b/scss/base/_paypal-product-buttons.scss new file mode 100644 index 0000000..7339ede --- /dev/null +++ b/scss/base/_paypal-product-buttons.scss @@ -0,0 +1,4 @@ +paypal-product-buttons div { + margin-block-start: var(--spacing-block-md); +} + diff --git a/scss/base/_table.scss b/scss/base/_table.scss new file mode 100644 index 0000000..ca4f957 --- /dev/null +++ b/scss/base/_table.scss @@ -0,0 +1,23 @@ +table { + border-collapse: collapse; + background-color: var(--background-color); // side-scrolling may be needed on narrow screens + + caption { + font-style: italic; + font-size: var(--font-size-0); + } +} + +thead, th { + border-block-end: var(--border-width) solid var(--text-color); +} + +:is(th, td) { + padding-inline: var(--spacing-inline-sm); + padding-block: var(--spacing-block-sm); + + + :is(th, td) { + border-inline-start: calc(var(--border-width) / 2) solid var(--text-color); + } +} + diff --git a/scss/modules/_index.scss b/scss/modules/_index.scss index 6e67241..822295f 100644 --- a/scss/modules/_index.scss +++ b/scss/modules/_index.scss @@ -5,6 +5,7 @@ @use 'link-gallery' as *; @use 'nav' as *; @use 'news-index' as *; +@use 'payment-methods-list' as *; @use 'product' as *; @use 'schedule' as *; @use 'skip-to-content' as *; diff --git a/scss/modules/_link-gallery.xs.scss b/scss/modules/_link-gallery.xs.scss index b2bf86c..805073d 100644 --- a/scss/modules/_link-gallery.xs.scss +++ b/scss/modules/_link-gallery.xs.scss @@ -1,7 +1,7 @@ .link-gallery { grid-auto-rows: calc( var(--gallery-row-image-height) + - var(--margin-block-sm) + + var(--spacing-block-sm) + var(--gallery-row-title-height) ); --gallery-item-column-span: 2; diff --git a/scss/modules/_nav.scss b/scss/modules/_nav.scss index f2f32a9..e2c984e 100644 --- a/scss/modules/_nav.scss +++ b/scss/modules/_nav.scss @@ -1,6 +1,6 @@ .nav { & > :not(:last-child) { - margin-block: 0 var(--margin-block-sm); + margin-block: 0 var(--spacing-block-sm); } li { diff --git a/scss/modules/_nav.sm.scss b/scss/modules/_nav.sm.scss index db540bd..688f947 100644 --- a/scss/modules/_nav.sm.scss +++ b/scss/modules/_nav.sm.scss @@ -9,7 +9,7 @@ + li::before { content: '/'; - margin-inline: var(--margin-inline-md); + margin-inline: var(--spacing-inline-md); } } } diff --git a/scss/modules/_payment-methods-list.scss b/scss/modules/_payment-methods-list.scss new file mode 100644 index 0000000..a59831a --- /dev/null +++ b/scss/modules/_payment-methods-list.scss @@ -0,0 +1,8 @@ +.payment-methods-list { + list-style: none; + + details[open] :last-child { + margin-block-end: var(--spacing-block-sm); + } +} + diff --git a/scss/modules/_product.scss b/scss/modules/_product.scss index bc05dfd..cb74573 100644 --- a/scss/modules/_product.scss +++ b/scss/modules/_product.scss @@ -10,7 +10,7 @@ flex-direction: column; margin-inline: auto; width: fit-content; - gap: var(--margin-block-sm); + gap: var(--spacing-block-sm); } .product__action-block img { diff --git a/scss/modules/_schedule.scss b/scss/modules/_schedule.scss index 40a9fee..fcb1304 100644 --- a/scss/modules/_schedule.scss +++ b/scss/modules/_schedule.scss @@ -1,10 +1,10 @@ .schedule:not(:first-child) { - margin-block-start: var(--margin-block-sm); + margin-block-start: var(--spacing-block-sm); } .schedule dt { font-weight: 700; - margin-block-start: var(--margin-block-sm); + margin-block-start: var(--spacing-block-sm); text-align: center; text-decoration: none; } diff --git a/scss/modules/_skip-to-content.scss b/scss/modules/_skip-to-content.scss index a752b29..0a5f365 100644 --- a/scss/modules/_skip-to-content.scss +++ b/scss/modules/_skip-to-content.scss @@ -7,7 +7,7 @@ } .skip-to-content:is(:focus, :focus-visible, :focus-within) { - top: var(--margin-block-md); + top: var(--spacing-block-md); opacity: 1; transition: top 100ms; -}
\ No newline at end of file +} diff --git a/scss/values/spacing/_constants.scss b/scss/values/spacing/_constants.scss index 9a990b8..c89fdc8 100644 --- a/scss/values/spacing/_constants.scss +++ b/scss/values/spacing/_constants.scss @@ -42,7 +42,7 @@ $grid-padding-er: ( ); $spacing-factor: ( - 'inline': (lg: 4, md: 2, sm: 1), + 'inline': (lg: 12, md: 6, sm: 3), 'block': (lg: 16, md: 10, sm: 5) ); diff --git a/src/components/LallansIssue.astro b/src/components/LallansIssue.astro index c8bc6eb..317ff2d 100644 --- a/src/components/LallansIssue.astro +++ b/src/components/LallansIssue.astro @@ -7,7 +7,7 @@ import type LallansIssue from '$types/LallansIssue'; import { Image } from 'astro:assets'; import LallansIssueContributions from './LallansIssueContributions.astro'; import LallansIssueContributors from './LallansIssueContributors.astro'; -import PayPalButtons from './PayPalButtons.astro'; +import PaypalProductButtons from './PaypalProductButtons.astro'; interface Props { issue: LallansIssue; @@ -27,7 +27,7 @@ const t = translate(locale); <div class="product__action-block"> <Image alt="" src={issue.img.width274} /> - <PayPalButtons + <PaypalProductButtons successPageUrl={relativePath(Astro.url.pathname, `payment-success-confirmation`)} productDescription={`Lallans ${issue.issueNumber} | ${issue.issueName}`} shortDescription={`Lallans ${issue.issueNumber}`} diff --git a/src/components/Navbar/NavLinks.astro b/src/components/Navbar/NavLinks.astro index 798c24e..36c7f6f 100644 --- a/src/components/Navbar/NavLinks.astro +++ b/src/components/Navbar/NavLinks.astro @@ -4,6 +4,7 @@ import translate from '$i18n/translate'; import tNews from '$i18n/translations/pages/news'; import tFurthsettins from '$i18n/translations/pages/furthsettins'; import tHame from '$i18n/translations/pages/home'; +import tJyne from '$i18n/translations/pages/jyne'; const locale = getLocaleFromPathOrDefault(Astro.url.pathname); const t = translate(locale); @@ -20,7 +21,7 @@ const t = translate(locale); <a class="btn" href={`/${locale}/furthsettins`}>{t(tFurthsettins, { key: 'title' })}</a> </li> <li> - <a class="btn" href={`/${locale}/#jyne`}>{t(tHame, { key: 'jyne' })}</a> + <a class="btn" href={`/${locale}/jyne`}>{t(tJyne, { key: 'title' })}</a> </li> <li> <a class="btn" href={`/${locale}/#contact`}>{t(tHame, { key: 'contact' })}</a> diff --git a/src/components/PayPalButtons.astro b/src/components/PayPalButtons.astro deleted file mode 100644 index cdea5cb..0000000 --- a/src/components/PayPalButtons.astro +++ /dev/null @@ -1,150 +0,0 @@ ---- -import env from '$lib/env'; -import type Price from '$types/Price'; - -interface Props { - successPageUrl: string; - productDescription: string; - shortDescription: string; - totalPrice: Price; -} - -const { successPageUrl, productDescription, shortDescription, totalPrice } = Astro.props; - -const PAYPAL_CLIENT_ID = - env.ENVIRONMENT === 'prod' ? env.PAYPAL_LIVE_CLIENT_ID : env.PAYPAL_SANDBOX_CLIENT_ID; ---- - -<paypal-buttons - data-paypal-client-id={PAYPAL_CLIENT_ID} - data-success-page-url={successPageUrl} - data-product-description={productDescription} - data-short-description={shortDescription} - data-total-price={totalPrice} -> - <div id="paypal-button-container"></div> -</paypal-buttons> - -<script> - import { PayPalNamespace, loadScript } from '@paypal/paypal-js'; - - const backendUrl = '/api'; - - class PayPalButtons extends HTMLElement { - constructor() { - super(); - - const paypalClientId = this.getStringDataAttribute('paypalClientId'); - - const paypalOptions = { - clientId: paypalClientId, - locale: 'en_GB', - commit: false, - currency: 'GBP', - intent: 'capture', - }; - - loadScript(paypalOptions) - .catch((err) => { - throw new Error('Failed to load the PayPal JS SDK script. ', err); - }) - .then((paypal) => { - const constructButtons = paypal?.Buttons; - if (paypal && constructButtons) { - const successPageUrl = this.dataset['successPageUrl'] ?? '.'; - this.makePaypalButtons(constructButtons, successPageUrl).render( - '#paypal-button-container' - ); - } - }); - } - - makePaypalButtons( - constructButtons: Required<PayPalNamespace>['Buttons'], - successPageUrl: string - ) { - const productDescription = this.getStringDataAttribute('productDescription'); - const shortDescription = this.getStringDataAttribute('shortDescription'); - const totalPrice = this.getStringDataAttribute('totalPrice'); - - return constructButtons({ - // This function is called to set up the transaction details without actually carrying it out - async createOrder() { - const body = JSON.stringify({ - productDescription, - shortDescription, - totalPrice, - }); - const headers = { - 'Content-Type': 'application/json', - }; - - let response; - try { - response = await fetch(`${backendUrl}/order`, { method: 'POST', body, headers }); - } catch (err) { - throw new Error(`Failed to create order. ${err}`); - } - - if (!response.ok) { - throw new Error(` - Failed to create order. Backend response was - ${response.status} ${response.statusText}. ${await response.text()} - `); - } - - const { orderId, approvalLink } = await response.json(); - console.info(` - Successfully created order with ID - ${orderId}. Approval link: ${approvalLink} - `); - return orderId; - }, - - // This function is called after payment is confirmed - async onApprove(data) { - const body = JSON.stringify({ - id: data.orderID, - isApproved: true, - }); - const headers = { - 'Content-Type': 'application/json', - }; - - let response; - try { - response = await fetch(`${backendUrl}/order`, { method: 'PATCH', body, headers }); - } catch (err) { - throw new Error(`Failed to capture order. ${err}`); - } - - if (!response.ok) { - throw new Error(` - Failed to capture order. Backend response was ${response.status} - ${response.statusText}. ${await response.text()} - `); - } - - const { orderId, requestStatus } = await response.json(); - console.info(` - Successfully captured order with ID ${orderId}, - request status is ${requestStatus}. - `); - - console.info('Redirecting to confirmation page...'); - window.location.href = successPageUrl; - }, - }); - } - - getStringDataAttribute(key: string): string { - const value = this.dataset[key]; - if (!value || typeof value !== 'string') { - throw new Error(`data-${key} attribute not provided to PayPalButtons component.`); - } - return value; - } - } - - customElements.define('paypal-buttons', PayPalButtons); -</script> diff --git a/src/components/PaypalProductButtons.astro b/src/components/PaypalProductButtons.astro new file mode 100644 index 0000000..0ebbd3f --- /dev/null +++ b/src/components/PaypalProductButtons.astro @@ -0,0 +1,26 @@ +--- +import env from '$lib/env'; +import type Price from '$types/Price'; + +interface Props { + successPageUrl: string; + productDescription: string; + shortDescription: string; + totalPrice: Price; +} + +const { successPageUrl, productDescription, shortDescription, totalPrice } = Astro.props; + +const PAYPAL_CLIENT_ID = + env.ENVIRONMENT === 'prod' ? env.PAYPAL_LIVE_CLIENT_ID : env.PAYPAL_SANDBOX_CLIENT_ID; +--- + +<paypal-product-buttons + paypal-client-id={PAYPAL_CLIENT_ID} + success-page-url={successPageUrl} + product-description={productDescription} + short-description={shortDescription} + total-price={totalPrice} +> + Loading PayPal buttons… +</paypal-product-buttons> diff --git a/src/components/Scotsoun.astro b/src/components/Scotsoun.astro index cca330d..7acae8b 100644 --- a/src/components/Scotsoun.astro +++ b/src/components/Scotsoun.astro @@ -4,7 +4,7 @@ import translate from '$i18n/translate'; import tSite from '$i18n/translations/site'; import tComponent from '$i18n/translations/components/scotsoun'; import type Scotsoun from '$types/Scotsoun'; -import PayPalButtons from './PayPalButtons.astro'; +import PaypalProductButtons from './PaypalProductButtons.astro'; import relativePath from '$lib/relativePath'; import { getLocaleFromPathOrThrow } from '$lib/getLocaleFromPath'; @@ -27,7 +27,7 @@ const t = translate(locale); </header> <div class="product__action-block"> <Image src={scotsoun.img.width192} alt="" /> - <PayPalButtons + <PaypalProductButtons successPageUrl={relativePath(Astro.url.pathname, `payment-success-confirmation`)} productDescription={`SSCD${scotsoun.scotsounId} | ${scotsoun.longName}`} shortDescription={`SSCD${scotsoun.scotsounId}`} diff --git a/src/data/contactDetails.ts b/src/data/contactDetails.ts index f5de6f5..2357b6e 100644 --- a/src/data/contactDetails.ts +++ b/src/data/contactDetails.ts @@ -1,8 +1,24 @@ export default { membershipSecretary: { emailAddress: 'lallans@hotmail.co.uk', + address: [ + 'Scots Language Society', + 'c/o 61 Cliffburn Road', + 'Arbroath', + 'Angus', + 'DD11 5BA', + 'United Kingdom', + ], }, generalEnquiries: { emailAddress: 'lallans@hotmail.co.uk', + address: [ + 'Scots Language Society', + 'c/o 61 Cliffburn Road', + 'Arbroath', + 'Angus', + 'DD11 5BA', + 'United Kingdom', + ], }, }; diff --git a/src/data/subscriptionPrices.ts b/src/data/subscriptionPrices.ts new file mode 100644 index 0000000..c52d902 --- /dev/null +++ b/src/data/subscriptionPrices.ts @@ -0,0 +1,7 @@ +import MembershipType from '$enums/MembershipType'; + +export const annualSubscriptionPrice: { [key in MembershipType]: number } = { + [MembershipType.IndividualUk]: 20.0, + [MembershipType.IndividualNonUk]: 30.0, + [MembershipType.InstitutionUk]: 25.0, +}; diff --git a/src/enums/MembershipType.ts b/src/enums/MembershipType.ts new file mode 100644 index 0000000..e153755 --- /dev/null +++ b/src/enums/MembershipType.ts @@ -0,0 +1,42 @@ +enum MembershipType { + IndividualUk, + IndividualNonUk, + InstitutionUk, +} + +export type MembershipTypeSnakeCase = 'individual-uk' | 'individual-non-uk' | 'institution-uk'; + +const allMembershipTypesSnakeCaseHash: { [key in MembershipTypeSnakeCase]: null } = { + 'individual-uk': null, + 'individual-non-uk': null, + 'institution-uk': null, +}; +export const allMembershipTypesSnakeCase = Object.keys( + allMembershipTypesSnakeCaseHash +) as MembershipTypeSnakeCase[]; + +export function membershipTypeToSnakeCase(membershipType: MembershipType): MembershipTypeSnakeCase { + switch (membershipType) { + case MembershipType.IndividualUk: + return 'individual-uk'; + case MembershipType.IndividualNonUk: + return 'individual-non-uk'; + case MembershipType.InstitutionUk: + return 'institution-uk'; + } +} + +export function snakeCaseToMembershipType(membershipType: MembershipTypeSnakeCase): MembershipType { + switch (membershipType) { + case 'individual-uk': + return MembershipType.IndividualUk; + case 'individual-non-uk': + return MembershipType.IndividualNonUk; + case 'institution-uk': + return MembershipType.InstitutionUk; + } +} + +export const allMembershipTypes = allMembershipTypesSnakeCase.map(snakeCaseToMembershipType); + +export default MembershipType; diff --git a/src/i18n/translate.ts b/src/i18n/translate.ts index 8f27e4b..0366798 100644 --- a/src/i18n/translate.ts +++ b/src/i18n/translate.ts @@ -1,23 +1,23 @@ -import type CommitteeRole from '$enums/CommitteeRole'; import type Locale from '$types/Locale'; -import type { TranslationsDictionary, Translation } from '$types/TranslationsDictionary'; +import type { + TranslationsDictionary, + Translation, + LocalesTranslationHash, +} from '$types/TranslationsDictionary'; import { defaultLocale } from './locales'; type TranslationsDictionaryWith< - Key extends string | CommitteeRole, - Params, + Key extends string | number, + ParamsHash extends object, > = TranslationsDictionary<{ - [key in Key]: { [locale in typeof defaultLocale]: Translation<Params> } & { - [locale in Locale]?: Translation<Params>; - }; + [key in Key]: LocalesTranslationHash<ParamsHash>; }>; export default function (locale: Locale) { - return function < - Key extends string, - Params, - Dict extends TranslationsDictionaryWith<Key, Params>, - >(dict: Dict, params: { key: Key } & { [Param in keyof Params]: string }) { + return function <Key extends string | number, ParamsHash extends object>( + dict: TranslationsDictionaryWith<Key, ParamsHash>, + params: { key: Key } & ParamsHash + ) { if (!(params.key in dict)) { throw new Error(` Could not find translation for key '${params.key}'. Available keys were: @@ -26,7 +26,9 @@ export default function (locale: Locale) { .join(', ')} `); } - const translation = dict[params.key][locale] ?? dict[params.key][defaultLocale]; + const localesTranslationHash: LocalesTranslationHash<ParamsHash> = dict[params.key]; + const translation: Translation<ParamsHash> = + localesTranslationHash[locale] ?? localesTranslationHash[defaultLocale]; return translation(params); }; } diff --git a/src/i18n/translations/components/breadcrumbs.ts b/src/i18n/translations/components/breadcrumbs.ts index 5aeff0e..8e3d027 100644 --- a/src/i18n/translations/components/breadcrumbs.ts +++ b/src/i18n/translations/components/breadcrumbs.ts @@ -22,6 +22,26 @@ const tBreadcrumbs = { sco: () => 'Scotsoun', 'en-GB': () => 'Scotsoun', }, + '/jyne/': { + sco: () => 'Jyne', + 'en-GB': () => 'Join', + }, + '/jyne/paypal/': { + sco: () => 'PayPal', + 'en-GB': () => 'PayPal', + }, + '/jyne/paypal/individual-uk/': { + sco: () => 'Individual (UK)', + 'en-GB': () => 'Individual (UK)', + }, + '/jyne/paypal/individual-non-uk/': { + sco: () => 'Individual (non-UK)', + 'en-GB': () => 'Individual (non-UK)', + }, + '/jyne/paypal/institution-uk/': { + sco: () => 'Institutional (UK)', + 'en-GB': () => 'Institutional (UK)', + }, '/news/': { sco: () => 'News', 'en-GB': () => 'News', diff --git a/src/i18n/translations/enums/membershipType.ts b/src/i18n/translations/enums/membershipType.ts new file mode 100644 index 0000000..0e1db06 --- /dev/null +++ b/src/i18n/translations/enums/membershipType.ts @@ -0,0 +1,21 @@ +import type { TranslationsDictionary, LocalesTranslationHash } from '$types/TranslationsDictionary'; + +import MembershipType from '$enums/MembershipType'; + +const tEnum: { [key in MembershipType]: LocalesTranslationHash<object> } = { + [MembershipType.IndividualUk]: { + sco: () => 'Individual (UK)', + 'en-GB': () => 'Individual (UK)', + }, + [MembershipType.IndividualNonUk]: { + sco: () => 'Individual (non-UK)', + 'en-GB': () => 'Individual (non-UK)', + }, + [MembershipType.InstitutionUk]: { + sco: () => 'Institutional (UK)', + 'en-GB': () => 'Institutional (UK)', + }, +}; + +type Raw = typeof tEnum; +export default tEnum as TranslationsDictionary<Raw>; diff --git a/src/i18n/translations/pages/home.ts b/src/i18n/translations/pages/home.ts index b577a46..6628682 100644 --- a/src/i18n/translations/pages/home.ts +++ b/src/i18n/translations/pages/home.ts @@ -119,82 +119,6 @@ const tPage = { sco: () => 'Aw furthsettins', 'en-GB': () => 'All publications', }, - jyne: { - sco: () => 'Jyne', - 'en-GB': () => 'Join', - }, - 'why-jyne': { - sco: () => 'Why jyne the Scots Leid Associe?', - 'en-GB': () => 'Why join the Scots Language Society?', - }, - 'why-jyne-para-1': { - sco: () => ` - Bi jynin us, ye ar helpin us tae heize up the Scots leid, maistly bi giein - Scots scrievers a place tae lairn frae anither an finnd their ain Scots vyce. - `, - 'en-GB': () => ` - By joining us, you are helping us to champion the Scots language, - especially by giving Scots writers a place to learn from one - another and to find their own Scots voice. - `, - }, - 'why-jyne-para-2': { - sco: () => ` - Forby, memmers get biannual issues o our aesome <i>Lallans</i> magazine gratis, - an hae the richt tae atten our Annual Collogues, whaur we haud our AGM an - enjoy talks frae kenspeckle spikkers on thochtie maiters effeirin til Scots - leid an leiteratur. - `, - 'en-GB': () => ` - In addition, members get biannual issues of our unique <i>Lallans</i> - magazine gratis, and have the right to attend our Annual Conferences, where - we hold our AGM and enjoy talks from highly regarded speakers on - thought-provoking subjects relating to Scots language and literature. - `, - }, - 'why-jyne-para-3': { - sco: () => ` - Gin whit ye ar seekin is a community o prose-scrievers, makars, pleywreichts - an owersetters, aw wi muckle care fur the Scots leid, the ar nae peer wi hus, - the auldest Scots-leid activist organisation that’s about eenou an the anely - fully dedicatit til the forderance o new Scots leiteratur. - `, - 'en-GB': () => ` - If what you’re looking for is a community of prose-writers, poets, - playwrights and translators with a passion for the Scots language, we have - no equal. We are the oldest extant Scots-language activist organisation and - the only one solely dedicated to the development of new Scots literature. - `, - }, - 'hou-jyne': { - sco: () => 'Hou can A jyne?', - 'en-GB': () => 'How can I join?', - }, - 'hou-jyne-para-1': { - sco: () => ` - Fur individuals, memmership costs £20 per annum. Fur institutions, the stent is £25 per annum. - `, - 'en-GB': () => ` - For individuals, membership costs £20 per annum. For institutions, the fee is £25 per annum. - `, - }, - 'hou-jyne-para-2': { - sco: ({ membershipEmail }: { membershipEmail: string }) => ` - Fur tae become a memmer, simply get in titch wi us at - <a href="mailto:${membershipEmail}">${membershipEmail}</a> - an we’ll provide ye wi our bank details; or ye can jyne bi sendin us a cheque - made peyable til <b>The Scots Language Society</b>. See - <a href="#contact">Contact us</a> - fur our address. - `, - 'en-GB': ({ membershipEmail }: { membershipEmail: string }) => ` - To become a member, simply get in touch with us at - <a href="mailto:${membershipEmail}">${membershipEmail}</a> - and we’ll provide you with our bank details; or you can join by sending us - a cheque made payable to <b>The Scots Language Society</b>. See - <a href="#contact">Contact us</a> for our address. - `, - }, contact: { sco: () => 'Contact', 'en-GB': () => 'Contact', diff --git a/src/i18n/translations/pages/jyne.ts b/src/i18n/translations/pages/jyne.ts new file mode 100644 index 0000000..bbcc67c --- /dev/null +++ b/src/i18n/translations/pages/jyne.ts @@ -0,0 +1,190 @@ +import type { TranslationsDictionary } from '$types/TranslationsDictionary'; + +import MembershipType from '$enums/MembershipType'; +import { annualSubscriptionPrice } from '$data/subscriptionPrices'; +import tMembershipType from '$i18n/translations/enums/membershipType'; +import translate from '$i18n/translate'; + +const tPage = { + title: { + sco: () => 'Jyne', + 'en-GB': () => 'Join', + }, + 'why-jyne': { + sco: () => 'Why jyne the Scots Leid Associe?', + 'en-GB': () => 'Why join the Scots Language Society?', + }, + 'why-jyne-para-1': { + sco: () => ` + Bi jynin us, ye ar helpin us tae heize up the Scots leid, maistly bi giein + Scots scrievers a place tae lairn frae anither an finnd their ain Scots vyce. + `, + 'en-GB': () => ` + By joining us, you are helping us to champion the Scots language, + especially by giving Scots writers a place to learn from one + another and to find their own Scots voice. + `, + }, + 'why-jyne-para-2': { + sco: () => ` + Forby, memmers get biannual issues o our aesome <i>Lallans</i> magazine gratis, + an hae the richt tae atten our Annual Collogues, whaur we haud our AGM an + enjoy talks frae kenspeckle spikkers on thochtie maiters effeirin til Scots + leid an leiteratur. + `, + 'en-GB': () => ` + In addition, members get biannual issues of our unique <i>Lallans</i> + magazine gratis, and have the right to attend our Annual Conferences, where + we hold our AGM and enjoy talks from highly regarded speakers on + thought-provoking subjects relating to Scots language and literature. + `, + }, + 'why-jyne-para-3': { + sco: () => ` + Gin whit ye ar seekin is a community o prose-scrievers, makars, pleywreichts + an owersetters, aw wi muckle care fur the Scots leid, the ar nae peer wi hus, + the auldest Scots-leid activist organisation that’s about eenou an the anely + fully dedicatit til the forderance o new Scots leiteratur. + `, + 'en-GB': () => ` + If what you’re looking for is a community of prose-writers, poets, + playwrights and translators with a passion for the Scots language, we have + no equal. We are the oldest extant Scots-language activist organisation and + the only one solely dedicated to the development of new Scots literature. + `, + }, + 'hou-jyne': { + sco: () => 'Hou can A jyne?', + 'en-GB': () => 'How can I join?', + }, + 'hou-jyne-intro': { + sco: () => ` + For tae jyne, ye can pey by PayPal, bank transfer or cheque. The fee is + as ablo. + `, + 'en-GB': () => ` + To join, you can pay by PayPal, bank transfer or cheque. The fee is as below. + `, + }, + 'subscription-price-table-caption': { + sco: () => 'Annual subscription price per membership type', + 'en-GB': () => 'Annual subscription price per membership type', + }, + 'hou-pey-by-paypal-title': { + sco: () => 'PayPal', + 'en-GB': () => 'PayPal', + }, + 'hou-pey-by-paypal-para': { + sco: () => 'Select a membership type', + 'en-GB': () => 'Select a membership type', + }, + 'membership-type-individual-uk': { + sco: () => 'Ane year individual UK membership', + 'en-GB': () => 'One year individual UK membership', + }, + 'membership-type-individual-uk-para': { + sco: () => ` + Here, ye can pey for ane year’s annual subscription til the Scots Leid Associe + gin ye ar an individual and ye want yer copies o <i>Lallans</i> sent til + a UK address. This will cost + £${annualSubscriptionPrice[MembershipType.IndividualUk]}. Gin ye want + some ither kind o membership, + <a href="/sco/jyne/paypal">see the ither options</a>. + `, + 'en-GB': () => ` + Here, you can pay for one year’s annual subscription to the Scots Language + Society if you are an individual and you want your copies of <i>Lallans</i> + sent to a UK address. This will cost + £${annualSubscriptionPrice[MembershipType.IndividualNonUk]}. If you want + some other kind of membership, + <a href="/en-GB/jyne/paypal">see the other options</a>. + `, + }, + 'membership-type-individual-non-uk': { + sco: () => 'Ane year individual non-UK membership', + 'en-GB': () => 'One year individual non-UK membership', + }, + 'membership-type-individual-non-uk-para': { + sco: () => ` + Here, ye can pey for ane year’s annual subscription til the Scots Leid Associe + gin ye ar an individual an ye want yer copies o <i>Lallans</i> sent til + a non-UK address. This will cost + £${annualSubscriptionPrice[MembershipType.IndividualNonUk]}. Gin ye want + some ither kind o membership, + <a href="/sco/jyne/paypal">see the ither options</a>. + `, + 'en-GB': () => ` + Here, you can pay for one year’s annual subscription to the Scots Language + Society if you are an individual and you want your copies of <i>Lallans</i> + sent to a non-UK address. This will cost + £${annualSubscriptionPrice[MembershipType.IndividualNonUk]}. If you want + some other kind of membership, + <a href="/en-GB/jyne/paypal">see the other options</a>. + `, + }, + 'membership-type-institution-uk': { + sco: () => 'Ane year UK institutional membership', + 'en-GB': () => 'One year UK institutional membership', + }, + 'membership-type-institution-uk-para': { + sco: () => ` + Here, ye can pey for ane year’s annual subscription til the Scots Leid Associe + gin ye ar an institution. This will cost + £${annualSubscriptionPrice[MembershipType.InstitutionUk]}. Gin ye want + some ither kind o membership, + <a href="/sco/jyne/paypal">see the ither options</a>. + `, + 'en-GB': () => ` + Here, you can pay for one year’s annual subscription to the Scots + Language Society if you are an institution based in the UK. This will + cost £${annualSubscriptionPrice[MembershipType.InstitutionUk]}. If you + want some other kind of membership, + <a href="/en-GB/jyne/paypal">see the other options</a>. + `, + }, + 'hou-pey-by-bank-transfer-title': { + sco: () => 'Bank transfer', + 'en-GB': () => 'Bank transfer', + }, + 'hou-pey-by-bank-transfer-para': { + sco: ({ emailAddress }: { emailAddress: string }) => ` + Send us an email at <a href="mailto:${emailAddress}">${emailAddress}</a> an we’ll send ye oor bank details. + `, + 'en-GB': ({ emailAddress }: { emailAddress: string }) => ` + Send us an email at <a href="mailto:${emailAddress}">${emailAddress}</a> and we’ll send you our bank details. + `, + }, + 'hou-pey-by-cheque-title': { + sco: () => 'Cheque', + 'en-GB': () => 'Cheque', + }, + 'hou-pey-by-cheque-para': { + sco: ({ address }: { address: string[] }) => ` + Mak out a cheque til <b>The Scots Language Society</b>, syne send it til us at the follaein address: + <address>${address.join('<br />')}</address> + `, + 'en-GB': ({ address }: { address: string[] }) => ` + Make out a cheque to <b>The Scots Language Society</b>, then send it to us at the following address: + <address>${address.join('<br />')}</address> + `, + }, + success: { + sco: () => 'Successfullie bocht membership', + 'en-GB': () => 'Successfully bought membership', + }, + 'success-para': { + sco: ({ membershipType }: { membershipType: MembershipType }) => ` + Ye hae successfullie bocht ane year’s + ${translate('sco')(tMembershipType, { key: membershipType })} + membership. We shuid be in contact wi ye some time shuin. + `, + 'en-GB': ({ membershipType }: { membershipType: MembershipType }) => ` + You have successfully bought one year’s + ${translate('en-GB')(tMembershipType, { key: membershipType })} + membership. We should be in contact with you some time soon. + `, + }, +}; + +type Raw = typeof tPage; +export default tPage as TranslationsDictionary<Raw>; diff --git a/src/pages/[locale]/furthsettins/lallans/[issueNumber]/index.astro b/src/pages/[locale]/furthsettins/lallans/[issueNumber]/index.astro index 48f1947..82fabf0 100644 --- a/src/pages/[locale]/furthsettins/lallans/[issueNumber]/index.astro +++ b/src/pages/[locale]/furthsettins/lallans/[issueNumber]/index.astro @@ -23,4 +23,5 @@ const issue = await getLallansIssue[issueNumber](); <Page title={t(tFurthsettins, { key: 'title' })}> <LallansIssue issue={issue} /> + <script src="$scripts/wc/paypal-product-buttons.ts"></script> </Page> diff --git a/src/pages/[locale]/furthsettins/scotsoun/[scotsounId]/index.astro b/src/pages/[locale]/furthsettins/scotsoun/[scotsounId]/index.astro index 90053fe..4244f70 100644 --- a/src/pages/[locale]/furthsettins/scotsoun/[scotsounId]/index.astro +++ b/src/pages/[locale]/furthsettins/scotsoun/[scotsounId]/index.astro @@ -24,4 +24,5 @@ const scotsoun = await getScotsoun[scotsounId](); <Page title={t(tPage, { key: 'title', scotsounId, name: scotsoun.title })}> <Scotsoun scotsoun={scotsoun} /> + <script src="$scripts/wc/paypal-product-buttons.ts"></script> </Page> diff --git a/src/pages/[locale]/index.astro b/src/pages/[locale]/index.astro index e8b9cf0..d930c20 100644 --- a/src/pages/[locale]/index.astro +++ b/src/pages/[locale]/index.astro @@ -55,36 +55,11 @@ const mostRecentNewsItem = await getMostRecentNewsItem(locale); </Button> </section> - <section id="jyne"> - <h2 set:html={t(tPage, { key: 'jyne' })} /> - - <h3 set:html={t(tPage, { key: 'why-jyne' })} /> - <p set:html={t(tPage, { key: 'why-jyne-para-1' })} /> - <p set:html={t(tPage, { key: 'why-jyne-para-2' })} /> - <p set:html={t(tPage, { key: 'why-jyne-para-3' })} /> - - <h3 set:html={t(tPage, { key: 'hou-jyne' })} /> - <p set:html={t(tPage, { key: 'hou-jyne-para-1' })} /> - <p - set:html={t(tPage, { - key: 'hou-jyne-para-2', - membershipEmail: contactDetails.membershipSecretary.emailAddress, - })} - /> - </section> - <section id="contact"> <h2 set:html={t(tPage, { key: 'contact' })} /> <a href={`${locale}/mailto:${contactDetails.generalEnquiries.emailAddress}`}> {contactDetails.generalEnquiries.emailAddress} </a> - <address> - Scots Language Society<br /> - c/o 61 Cliffburn Road<br /> - Arbroath<br /> - Angus<br /> - DD11 5BA<br /> - United Kingdom<br /> - </address> + <address set:html={contactDetails.generalEnquiries.address.join('<br />')} /> </section> </Page> diff --git a/src/pages/[locale]/jyne/index.astro b/src/pages/[locale]/jyne/index.astro new file mode 100644 index 0000000..7a1b4f6 --- /dev/null +++ b/src/pages/[locale]/jyne/index.astro @@ -0,0 +1,104 @@ +--- +import type Locale from '$types/Locale'; + +import { annualSubscriptionPrice } from '$data/subscriptionPrices'; +import contactDetails from '$data/contactDetails'; +import Page from '$layouts/Page.astro'; +import localeStaticPaths from '$lib/localeStaticPaths'; +import translate from '$i18n/translate'; +import tPage from '$i18n/translations/pages/jyne'; +import tMembershipType from '$i18n/translations/enums/membershipType'; +import { allMembershipTypes } from '$enums/MembershipType'; + +export async function getStaticPaths() { + return localeStaticPaths; +} + +const locale = Astro.params.locale as Locale; +const t = translate(locale); +--- + +<Page title={t(tPage, { key: 'title' })}> + <header> + <h1 set:html={t(tPage, { key: 'title' })} /> + </header> + + <section> + <h2 set:html={t(tPage, { key: 'hou-jyne' })} /> + <p set:html={t(tPage, { key: 'hou-jyne-intro' })} /> + + <table> + <caption>{t(tPage, { key: 'subscription-price-table-caption' })}</caption> + <thead> + <tr> + { + allMembershipTypes.map((membershipType) => ( + <th scope="col">{t(tMembershipType, { key: membershipType })}</th> + )) + } + </tr> + </thead> + + <tbody> + <tr> + { + allMembershipTypes.map((membershipType) => ( + <td> + {new Intl.NumberFormat('en-GB', { + style: 'currency', + currency: 'GBP', + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }).format(annualSubscriptionPrice[membershipType])} + </td> + )) + } + </tr> + </tbody> + </table> + + <ul class="payment-methods-list"> + <li> + <details> + <summary set:html={t(tPage, { key: 'hou-pey-by-paypal-title' })} /> + <ul> + <li><a href={`/${locale}/jyne/paypal/individual-uk`}>Individual (UK)</a></li> + <li><a href={`/${locale}/jyne/paypal/individual-non-uk`}>Individual (non-UK)</a></li> + <li><a href={`/${locale}/jyne/paypal/institution-uk`}>Institutional (UK)</a></li> + </ul> + </details> + </li> + + <li> + <details> + <summary set:html={t(tPage, { key: 'hou-pey-by-bank-transfer-title' })} /> + <p + set:html={t(tPage, { + key: 'hou-pey-by-bank-transfer-para', + emailAddress: contactDetails.membershipSecretary.emailAddress, + })} + /> + </details> + </li> + + <li> + <details> + <summary set:html={t(tPage, { key: 'hou-pey-by-cheque-title' })} /> + <p + set:html={t(tPage, { + key: 'hou-pey-by-cheque-para', + address: contactDetails.membershipSecretary.address, + })} + /> + </details> + </li> + </ul> + </section> + + <section> + <h2 set:html={t(tPage, { key: 'why-jyne' })} /> + <p set:html={t(tPage, { key: 'why-jyne-para-1' })} /> + <p set:html={t(tPage, { key: 'why-jyne-para-2' })} /> + <p set:html={t(tPage, { key: 'why-jyne-para-3' })} /> + </section> +</Page> diff --git a/src/pages/[locale]/jyne/paypal/[membershipType]/index.astro b/src/pages/[locale]/jyne/paypal/[membershipType]/index.astro new file mode 100644 index 0000000..0934854 --- /dev/null +++ b/src/pages/[locale]/jyne/paypal/[membershipType]/index.astro @@ -0,0 +1,41 @@ +--- +import type Locale from '$types/Locale'; +import { + type MembershipTypeSnakeCase, + allMembershipTypesSnakeCase, + snakeCaseToMembershipType, +} from '$enums/MembershipType'; +import { annualSubscriptionPrice } from '$data/subscriptionPrices'; +import Page from '$layouts/Page.astro'; +import PaypalProductButtons from '$components/PaypalProductButtons.astro'; +import localeStaticPaths from '$lib/localeStaticPaths'; +import translate from '$i18n/translate'; +import tJyne from '$i18n/translations/pages/jyne'; + +export async function getStaticPaths() { + return allMembershipTypesSnakeCase.flatMap((membershipType) => + localeStaticPaths.map((p) => ({ ...p, params: { ...p.params, membershipType } })) + ); +} + +const locale = Astro.params.locale as Locale; +const t = translate(locale); + +const membershipType = Astro.params.membershipType as MembershipTypeSnakeCase; +--- + +<Page title={t(tJyne, { key: `membership-type-${membershipType}` })}> + <section> + <h1>{t(tJyne, { key: `membership-type-${membershipType}` })}</h1> + + <p set:html={t(tJyne, { key: `membership-type-${membershipType}-para` })} /> + + <PaypalProductButtons + successPageUrl={`/${locale}/jyne/paypal/${membershipType}/success`} + productDescription={t(tJyne, { key: `membership-type-${membershipType}` })} + shortDescription="SLA 1y subscription" + totalPrice={annualSubscriptionPrice[snakeCaseToMembershipType(membershipType)]} + /> + <script src="$scripts/wc/paypal-product-buttons"></script> + </section> +</Page> diff --git a/src/pages/[locale]/jyne/paypal/[membershipType]/success.astro b/src/pages/[locale]/jyne/paypal/[membershipType]/success.astro new file mode 100644 index 0000000..569e294 --- /dev/null +++ b/src/pages/[locale]/jyne/paypal/[membershipType]/success.astro @@ -0,0 +1,30 @@ +--- +import type Locale from '$types/Locale'; +import { type MembershipTypeSnakeCase } from '$enums/MembershipType'; + +import Page from '$layouts/Page.astro'; +import { allMembershipTypesSnakeCase, snakeCaseToMembershipType } from '$enums/MembershipType'; +import localeStaticPaths from '$lib/localeStaticPaths'; +import translate from '$i18n/translate'; +import tJyne from '$i18n/translations/pages/jyne'; + +export async function getStaticPaths() { + return allMembershipTypesSnakeCase.flatMap((membershipType) => + localeStaticPaths.map((p) => ({ ...p, params: { ...p.params, membershipType } })) + ); +} + +const locale = Astro.params.locale as Locale; +const t = translate(locale); + +const membershipTypeSnakeCase = Astro.params.membershipType as MembershipTypeSnakeCase; +const membershipType = snakeCaseToMembershipType(membershipTypeSnakeCase); +--- + +<Page title={t(tJyne, { key: 'success' })}> + <section> + <h1>{t(tJyne, { key: 'success' })}</h1> + + <p set:html={t(tJyne, { key: 'success-para', membershipType })} /> + </section> +</Page> diff --git a/src/pages/[locale]/jyne/paypal/index.astro b/src/pages/[locale]/jyne/paypal/index.astro new file mode 100644 index 0000000..55905d1 --- /dev/null +++ b/src/pages/[locale]/jyne/paypal/index.astro @@ -0,0 +1,27 @@ +--- +import type Locale from '$types/Locale'; + +import Page from '$layouts/Page.astro'; +import localeStaticPaths from '$lib/localeStaticPaths'; +import translate from '$i18n/translate'; +import tJyne from '$i18n/translations/pages/jyne'; + +export async function getStaticPaths() { + return localeStaticPaths; +} + +const locale = Astro.params.locale as Locale; +const t = translate(locale); +--- + +<Page title={t(tJyne, { key: 'hou-pey-by-paypal-title' })}> + <section> + <h1 set:html={t(tJyne, { key: 'hou-pey-by-paypal-title' })} /> + <p set:html={t(tJyne, { key: 'hou-pey-by-paypal-para' })} /> + <ul> + <li><a href={`/${locale}/jyne/paypal/individual-uk`}>Individual (UK)</a></li> + <li><a href={`/${locale}/jyne/paypal/individual-non-uk`}>Individual (non-UK)</a></li> + <li><a href={`/${locale}/jyne/paypal/institution-uk`}>Institutional (UK)</a></li> + </ul> + </section> +</Page> diff --git a/src/scripts/wc/paypal-product-buttons.ts b/src/scripts/wc/paypal-product-buttons.ts new file mode 100644 index 0000000..ca06524 --- /dev/null +++ b/src/scripts/wc/paypal-product-buttons.ts @@ -0,0 +1,146 @@ +import { type PayPalNamespace, loadScript } from '@paypal/paypal-js'; + +const backendUrl = '/api'; + +class PaypalProductButtons extends HTMLElement { + constructor() { + super(); + + try { + const paypalClientId = this.getAttribute('paypal-client-id'); + + const paypalOptions = { + clientId: paypalClientId, + locale: 'en_GB', + commit: false, + currency: 'GBP', + intent: 'capture', + }; + + loadScript(paypalOptions) + .catch((err) => { + this.innerHTML = ` + <details> + <summary>Failed to load PayPal buttons</summary> + <p>${err}</p> + </details> + `; + console.error(`Failed to load the PayPal JS SDK script. ${err}`); + }) + .then((paypal) => { + const constructButtons = paypal?.Buttons; + if (paypal && constructButtons) { + const successPageUrl = this.getAttribute('success-page-url'); + this.innerHTML = ''; + const div = document.createElement('div'); + this.appendChild(div); + this.makePaypalButtons(constructButtons, successPageUrl).render(div); + } + }) + .catch((err) => { + this.innerHTML = ` + <details> + <summary>Failed to load PayPal buttons</summary> + <p>${err}</p> + </details> + `; + console.error(`Failed to construct the PayPal buttons. ${err}`); + }); + } catch (err) { + this.innerHTML = ` + <details> + <summary>Failed to load PayPal buttons</summary> + <p>${err}</p> + </details> + `; + } + } + + makePaypalButtons( + constructButtons: Required<PayPalNamespace>['Buttons'], + successPageUrl: string + ) { + const productDescription = this.getAttribute('product-description'); + const shortDescription = this.getAttribute('short-description'); + const totalPrice = this.getAttribute('total-price'); + + return constructButtons({ + // This function is called to set up the transaction details without actually carrying it out + async createOrder() { + const body = JSON.stringify({ + productDescription, + shortDescription, + totalPrice, + }); + const headers = { + 'Content-Type': 'application/json', + }; + + let response: Response; + try { + response = await fetch(`${backendUrl}/order`, { method: 'POST', body, headers }); + } catch (err) { + throw new Error(`Failed to create order. ${err}`); + } + + if (!response.ok) { + throw new Error(` + Failed to create order. Backend response was + ${response.status} ${response.statusText}. ${await response.text()} + `); + } + + const { orderId, approvalLink } = await response.json(); + console.info(` + Successfully created order with ID + ${orderId}. Approval link: ${approvalLink} + `); + return orderId; + }, + + // This function is called after payment is confirmed + async onApprove(data) { + const body = JSON.stringify({ + id: data.orderID, + isApproved: true, + }); + const headers = { + 'Content-Type': 'application/json', + }; + + let response: Response; + try { + response = await fetch(`${backendUrl}/order`, { method: 'PATCH', body, headers }); + } catch (err) { + throw new Error(`Failed to capture order. ${err}`); + } + + if (!response.ok) { + throw new Error(` + Failed to capture order. Backend response was ${response.status} + ${response.statusText}. ${await response.text()} + `); + } + + const { orderId, requestStatus } = await response.json(); + console.info(` + Successfully captured order with ID ${orderId}, + request status is ${requestStatus}. + `); + + console.info(`Redirecting to confirmation page, ${successPageUrl}...`); + window.location.href = successPageUrl; + }, + }); + } + + getAttribute(key: string): string { + const value = super.getAttribute(key); + if (!value || typeof value !== 'string') { + throw new Error(`${key} attribute not provided to PaypalProductButtons component.`); + } + return value; + } +} + +customElements.define('paypal-product-buttons', PaypalProductButtons); diff --git a/src/types/TranslationsDictionary.d.ts b/src/types/TranslationsDictionary.d.ts index 5033ecd..47ee7f4 100644 --- a/src/types/TranslationsDictionary.d.ts +++ b/src/types/TranslationsDictionary.d.ts @@ -1,12 +1,15 @@ import type { defaultLocale } from '$i18n/locales'; import type Locale from './Locale'; -export type Translation<Params> = (params: { [key in keyof Params]: string }) => string; +export type Translation<ParamsHash extends object> = (params: ParamsHash) => string; +export type LocalesTranslationHash<ParamsHash extends object> = { + [locale in Locale]?: Translation<ParamsHash>; +} & { + [locale in typeof defaultLocale]: Translation<ParamsHash>; +}; export type TranslationsDictionary<Raw> = { - readonly [key in keyof Raw]: { - [locale in Locale]?: Translation<Parameters<Raw[key][typeof defaultLocale]>[0]>; - } & { - [locale in typeof defaultLocale]: Translation<Parameters<Raw[key][typeof defaultLocale]>[0]>; - }; + readonly [key in keyof Raw]: LocalesTranslationHash< + Parameters<Raw[key][typeof defaultLocale]>[0] + >; }; diff --git a/tsconfig.json b/tsconfig.json index 61fcd49..956d1c9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,6 +11,7 @@ "$layouts/*": ["./layouts/*"], "$lib/*": ["./lib/*"], "$pages/*": ["./pages/*"], + "$scripts/*": ["./scripts/*"], "$types/*": ["./types/*"] }, "strictNullChecks": true |
