diff --git a/preview/astro.config.mjs b/preview/astro.config.mjs index 4f25053f1..de0324fa4 100644 --- a/preview/astro.config.mjs +++ b/preview/astro.config.mjs @@ -149,9 +149,7 @@ export default defineConfig({ { // Beautify html fences before highlighting. preprocess(code) { - if (this.options.lang === 'html') { - return beautify.html(code, { indent_size: 2, wrap_line_length: 80 }) - } + return this.options.lang === 'html' ? beautify.html(code, { indent_size: 2, wrap_line_length: 80 }) : code }, // Keep shiki classes only (drop Astro's astro-code / data-language). pre(node) { diff --git a/preview/js/demo.ts b/preview/js/demo.ts index 4c8382936..2d1d1c76c 100644 --- a/preview/js/demo.ts +++ b/preview/js/demo.ts @@ -26,13 +26,12 @@ const parseUrl = (): void => { const search = window.location.search.substring(1) const params = search.split('&') - for (let i = 0; i < params.length; i++) { - const arr = params[i].split('=') - const key = arr[0] - const value = arr[1] + for (const param of params) { + const [key, value] = param.split('=') + const item = key !== undefined ? items[key] : undefined - if (!!items[key]) { - localStorage.setItem(items[key].localStorage, value) + if (item && key !== undefined && value !== undefined) { + localStorage.setItem(item.localStorage, value) config[key] = value } } diff --git a/preview/pages/all-elements.astro b/preview/pages/all-elements.astro index f00eb7c58..0fd92858a 100644 --- a/preview/pages/all-elements.astro +++ b/preview/pages/all-elements.astro @@ -42,9 +42,12 @@ import ListGroup from '@ui/ListGroup.astro' import ListGroupItem from '@ui/ListGroupItem.astro' import payments from '@data/payments.json' -const paymentNames = Object.fromEntries((payments as { name: string; logo: string }[]).map((p) => [p.logo, p.name])) +const PAYMENT_IDS = ['visa', 'mastercard', 'paypal', 'americanexpress', 'applepay', 'google-pay', 'stripe', 'discover', 'jcb', 'maestro', 'dinersclub', 'bitcoin', 'ethereum', 'klarna', 'venmo', 'square'] as const +type PaymentId = (typeof PAYMENT_IDS)[number] -const countryNames: Record = { +const paymentNames = Object.fromEntries((payments as { name: string; logo: string }[]).map((p) => [p.logo, p.name])) as Record + +const countryNames = { us: 'United States', gb: 'United Kingdom', de: 'Germany', @@ -574,7 +577,7 @@ function greetUser(name) {
{ - (['visa', 'mastercard', 'paypal', 'americanexpress', 'applepay', 'google-pay', 'stripe', 'discover', 'jcb', 'maestro', 'dinersclub', 'bitcoin', 'ethereum', 'klarna', 'venmo', 'square'] as const).map((payment) => ( + PAYMENT_IDS.map((payment) => ( <> diff --git a/preview/pages/card-actions.astro b/preview/pages/card-actions.astro index d58bc8735..2d9d01008 100644 --- a/preview/pages/card-actions.astro +++ b/preview/pages/card-actions.astro @@ -10,9 +10,10 @@ import Button from '@ui/Button.astro' import Avatar from '@ui/Avatar.astro' import Icon from '@ui/Icon.astro' import people from '@data/people.json' +import { requireIndex } from '@shared/lib/array' -const person0 = people[0] -const person3 = people[3] +const person0 = requireIndex(people, 0) +const person3 = requireIndex(people, 3) --- diff --git a/preview/pages/card-gradients.astro b/preview/pages/card-gradients.astro index 0d9edc4b3..a7d393e7f 100644 --- a/preview/pages/card-gradients.astro +++ b/preview/pages/card-gradients.astro @@ -11,6 +11,7 @@ import Weather from '@shared/components/cards/Weather.astro' import ProfileContact from '@shared/components/cards/ProfileContact.astro' import SmallStats from '@shared/components/cards/SmallStats.astro' import people from '@data/people.json' +import { requireIndex } from '@shared/lib/array' --- @@ -74,7 +75,7 @@ import people from '@data/people.json'
- +
diff --git a/preview/pages/datatables.astro b/preview/pages/datatables.astro index fb0f03c64..10f3f3379 100644 --- a/preview/pages/datatables.astro +++ b/preview/pages/datatables.astro @@ -8,9 +8,16 @@ import Progress from '@ui/Progress.astro' import TablerList from '@shared/components/js/TablerList.astro' import rollercoasters from '@data/rollercoasters.json' +interface Rollercoaster { + name: string + city: string + type: string + score: string +} + const id = 'default' -const rows = (rollercoasters as Record[]).map((rc, i) => { +const rows = (rollercoasters as Rollercoaster[]).map((rc, i) => { const index = i + 1 const progress = randomNumber(index, 0, 100) const date = randomDate(index) diff --git a/preview/pages/form-elements.astro b/preview/pages/form-elements.astro index 6fde2806d..3dcb61b6f 100644 --- a/preview/pages/form-elements.astro +++ b/preview/pages/form-elements.astro @@ -19,6 +19,7 @@ import CardFooter from '@ui/CardFooter.astro' import CardTitle from '@ui/CardTitle.astro' import FormGroup from '@ui/FormGroup.astro' import people from '@data/people.json' +import { requireIndex } from '@shared/lib/array' const httpMethods = ['GET', 'POST', 'PUT', 'HEAD', 'DELETE', 'PATCH'] const comparisons = [ @@ -142,7 +143,7 @@ const rows = [
-
+
diff --git a/preview/pages/gallery.astro b/preview/pages/gallery.astro index db21d3cdb..164ffcd9e 100644 --- a/preview/pages/gallery.astro +++ b/preview/pages/gallery.astro @@ -4,6 +4,7 @@ import GalleryPhoto from '@shared/components/cards/GalleryPhoto.astro' import Pagination from '@ui/Pagination.astro' import photos from '@data/photos.json' import people from '@data/people.json' +import { requireIndex } from '@shared/lib/array' // Horizontal photos, limit 15; person = people[loop index]. const galleryPhotos = photos.filter((photo) => photo.horizontal).slice(0, 15) @@ -14,7 +15,7 @@ const galleryPhotos = photos.filter((photo) => photo.horizontal).slice(0, 15) { galleryPhotos.map((photo, index) => (
- +
)) } diff --git a/preview/pages/illustrations.astro b/preview/pages/illustrations.astro index 737dc938f..9770507c2 100644 --- a/preview/pages/illustrations.astro +++ b/preview/pages/illustrations.astro @@ -9,12 +9,13 @@ import CaptureScript from '@shared/components/CaptureScript.astro' import freeIllustrations from '@data/free-illustrations.json' import illustrationsList from '@data/illustrations.json' import siteData from '@data/site.json' +import { requireIndex } from '@shared/lib/array' const autodark = (freeIllustrations as { autodark: Record }).autodark const autodarkEntries = Object.entries(autodark) // Last autodark entry (loop overwrites each pass). -const firstIllustration = autodarkEntries.length ? autodarkEntries[autodarkEntries.length - 1][1] : '' +const firstIllustration = autodarkEntries.length ? requireIndex(autodarkEntries, autodarkEntries.length - 1)[1] : '' const withClass = (svg: string) => svg.replaceAll(' requireIndex(photosData, i) --- @@ -11,42 +13,42 @@ import photos from '@data/photos.json' diff --git a/preview/pages/search-results.astro b/preview/pages/search-results.astro index b833593bb..2a5b68697 100644 --- a/preview/pages/search-results.astro +++ b/preview/pages/search-results.astro @@ -4,6 +4,7 @@ import NavAside from '@shared/components/parts/NavAside.astro' import GalleryPhoto from '@shared/components/cards/GalleryPhoto.astro' import photos from '@data/photos.json' import people from '@data/people.json' +import { requireIndex } from '@shared/lib/array' const resultPhotos = photos.filter((photo) => photo.horizontal).slice(0, 18) --- @@ -18,7 +19,7 @@ const resultPhotos = photos.filter((photo) => photo.horizontal).slice(0, 18) { resultPhotos.map((photo, index) => (
- +
)) } diff --git a/preview/pages/settings.astro b/preview/pages/settings.astro index a8bdcc244..8b86b3c25 100644 --- a/preview/pages/settings.astro +++ b/preview/pages/settings.astro @@ -8,8 +8,9 @@ import Check from '@ui/form/Check.astro' import FormGroup from '@ui/FormGroup.astro' import CardTitle from '@ui/CardTitle.astro' import people from '@data/people.json' +import { requireIndex } from '@shared/lib/array' -const person = people[0] +const person = requireIndex(people, 0) --- diff --git a/preview/pages/sign-in-cover.astro b/preview/pages/sign-in-cover.astro index b1ebdeddb..7a0b4d715 100644 --- a/preview/pages/sign-in-cover.astro +++ b/preview/pages/sign-in-cover.astro @@ -5,6 +5,7 @@ import Logo from '@shared/components/navbar/Logo.astro' import SignInForm from '@shared/components/cards/SignInForm.astro' import Photo from '@ui/Photo.astro' import photos from '@data/photos.json' +import { requireIndex } from '@shared/lib/array' --- @@ -25,7 +26,7 @@ import photos from '@data/photos.json'
- +
diff --git a/preview/pages/tags.astro b/preview/pages/tags.astro index 1e360b348..59da87efd 100644 --- a/preview/pages/tags.astro +++ b/preview/pages/tags.astro @@ -13,7 +13,7 @@ const tagIcons = ['bold', 'italic', 'underline', 'copy', 'scissors', 'file-plus' const range = (a: number, b: number) => Array.from({ length: b - a + 1 }, (_, i) => a + i) -const flags9 = flags.slice(0, 9) +const flags9 = flags.slice(0, 9) as { name: string; flag: string }[] const people8 = people.slice(0, 8) // site.colors yields value objects with .class / .title. @@ -38,7 +38,7 @@ const colors = Object.values(siteData.colors) as { class: string; title: string Tags with flag - {flags9.map((country) => )} + {flags9.map((country) => )} diff --git a/preview/pages/tasks-list.astro b/preview/pages/tasks-list.astro index ae969031a..46e86b6b0 100644 --- a/preview/pages/tasks-list.astro +++ b/preview/pages/tasks-list.astro @@ -10,6 +10,7 @@ import CaptureModal from '@shared/components/CaptureModal.astro' import FormGroup from '@ui/FormGroup.astro' import tasks from '@data/tasks.json' import people from '@data/people.json' +import { requireIndex } from '@shared/lib/array' interface Person { id?: string @@ -36,7 +37,7 @@ const columns = (tasks as { columns: Column[] }).columns const peopleList = people as Person[] // Assignable people for the add-task modal. -const selectedPeople = [5, 6, 2, 3].map((id) => peopleList[id - 1]) +const selectedPeople = [5, 6, 2, 3].map((id) => requireIndex(peopleList, id - 1)) --- diff --git a/preview/tsconfig.json b/preview/tsconfig.json index 49d930145..ea7772dfc 100644 --- a/preview/tsconfig.json +++ b/preview/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "astro/tsconfigs/strict", + "extends": "astro/tsconfigs/strictest", "include": [ ".astro/types.d.ts", "**/*" diff --git a/shared/lib/array.test.ts b/shared/lib/array.test.ts new file mode 100644 index 000000000..ce368cf0d --- /dev/null +++ b/shared/lib/array.test.ts @@ -0,0 +1,12 @@ +import { describe, expect, it } from 'vitest' +import { requireIndex } from './array' + +describe('requireIndex', () => { + it('returns the entry at the given index', () => { + expect(requireIndex(['a', 'b', 'c'], 1)).toBe('b') + }) + + it('throws when the index is out of bounds', () => { + expect(() => requireIndex(['a'], 5)).toThrow('Expected an entry at index 5') + }) +}) diff --git a/shared/lib/array.ts b/shared/lib/array.ts new file mode 100644 index 000000000..73268af1b --- /dev/null +++ b/shared/lib/array.ts @@ -0,0 +1,11 @@ +// Indexes into demo-data arrays (people.json, photos.json, …) that pages trust to have +// an entry at a given position. Throws instead of silently rendering `undefined` if that +// assumption is ever violated (e.g. a seed file shrinks). + +export function requireIndex(array: readonly T[], index: number): T { + const item = array[index] + if (item === undefined) { + throw new Error(`Expected an entry at index ${index}, got undefined (array has ${array.length} entries)`) + } + return item +} diff --git a/shared/ui/Avatar.astro b/shared/ui/Avatar.astro index bb78084a2..f732ffa4d 100644 --- a/shared/ui/Avatar.astro +++ b/shared/ui/Avatar.astro @@ -11,7 +11,7 @@ interface Person { interface Props { src?: string - placeholder?: string + placeholder?: string | undefined personId?: number person?: Person link?: boolean @@ -27,7 +27,7 @@ interface Props { statusIcon?: string statusLabel?: string brand?: string - icon?: string + icon?: string | undefined /** asset URL base — '.' for root-level pages, '..' for pages one level deep (e.g. marketing/) */ base?: string /** remaining attributes (data-bs-toggle for tooltips, aria-*, …) are forwarded to the element */ diff --git a/shared/ui/Flag.astro b/shared/ui/Flag.astro index 11a4725e9..65e5edebc 100644 --- a/shared/ui/Flag.astro +++ b/shared/ui/Flag.astro @@ -3,7 +3,7 @@ interface Props { flag?: string size?: string class?: string - label?: string + label?: string | undefined } const { flag = 'pl', size, class: className, label } = Astro.props diff --git a/shared/ui/Payment.astro b/shared/ui/Payment.astro index 073d2cd4d..d16b68cec 100644 --- a/shared/ui/Payment.astro +++ b/shared/ui/Payment.astro @@ -4,7 +4,7 @@ interface Props { dark?: boolean size?: string class?: string - label?: string + label?: string | undefined } const { payment = 'visa', dark, size, class: className, label } = Astro.props diff --git a/shared/ui/Tag.astro b/shared/ui/Tag.astro index 31f05eb1c..85ddb8038 100644 --- a/shared/ui/Tag.astro +++ b/shared/ui/Tag.astro @@ -13,7 +13,7 @@ interface Person { interface Props { text?: string - flag?: string + flag?: string | undefined icon?: string person?: Person personId?: number