├── .eslintrc.json ├── .github └── workflows │ ├── playwright.yml │ └── run-tests.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .tool-versions ├── README.md ├── components ├── About │ ├── CollectionGrid.tsx │ └── CollectionGridStyled.ts ├── Chat │ ├── Chat.test.tsx │ ├── Chat.tsx │ ├── Conversation.styled.tsx │ ├── Conversation.test.tsx │ ├── Conversation.tsx │ ├── Feedback │ │ ├── Feedback.tsx │ │ ├── OptIn.test.tsx │ │ ├── OptIn.tsx │ │ ├── Option.test.tsx │ │ ├── Option.tsx │ │ └── TextArea.tsx │ ├── Response │ │ ├── Aggregations.test.tsx │ │ ├── Aggregations.tsx │ │ ├── Images.test.tsx │ │ ├── Images.tsx │ │ ├── Interstitial.styled.tsx │ │ ├── Interstitial.test.tsx │ │ ├── Interstitial.tsx │ │ ├── Markdown.test.tsx │ │ ├── Markdown.tsx │ │ ├── Options.test.tsx │ │ ├── Options.tsx │ │ ├── Response.styled.tsx │ │ ├── Response.test.tsx │ │ └── Response.tsx │ └── Stack │ │ ├── Stack.styled.tsx │ │ ├── Stack.test.tsx │ │ └── Stack.tsx ├── Clover │ ├── SliderWrapper.tsx │ ├── ViewerWrapper.styled.ts │ ├── ViewerWrapper.test.tsx │ ├── ViewerWrapper.tsx │ └── __mocks__ │ │ ├── SliderWrapper.tsx │ │ └── ViewerWrapper.tsx ├── Collection │ ├── Collection.styled.ts │ ├── Item │ │ ├── Item.styled.ts │ │ └── Item.tsx │ ├── NavTabs.styled.ts │ └── Tabs │ │ ├── Explore.test.tsx │ │ ├── Explore.tsx │ │ ├── Metadata.styled.ts │ │ ├── Metadata.tsx │ │ ├── Organization.styled.ts │ │ └── Organization.tsx ├── Facets │ ├── Facet │ │ ├── Facet.tsx │ │ ├── GenericFacet.styled.ts │ │ ├── GenericFacet.test.tsx │ │ ├── GenericFacet.tsx │ │ ├── Option.test.tsx │ │ ├── Option.tsx │ │ └── Options.tsx │ ├── Facets.styled.ts │ ├── Facets.tsx │ ├── Filter │ │ ├── Clear.styled.ts │ │ ├── Clear.tsx │ │ ├── Filter.styled.ts │ │ ├── Filter.tsx │ │ ├── GroupList.styled.ts │ │ ├── GroupList.test.tsx │ │ ├── GroupList.tsx │ │ ├── Modal.test.tsx │ │ ├── Modal.tsx │ │ ├── Preview.styled.ts │ │ ├── Preview.test.tsx │ │ ├── Preview.tsx │ │ ├── Submit.test.tsx │ │ └── Submit.tsx │ ├── UserFacets │ │ ├── UserFacets.styled.ts │ │ ├── UserFacets.test.tsx │ │ ├── UserFacets.tsx │ │ ├── Value.test.tsx │ │ └── Value.tsx │ ├── WorkType │ │ ├── RadioGroup.tsx │ │ ├── WorkType.styled.ts │ │ └── WorkType.tsx │ └── index.tsx ├── Figure │ ├── Figure.styled.ts │ └── Figure.tsx ├── Fonts.tsx ├── Footer │ ├── Footer.styled.ts │ ├── Footer.tsx │ └── SiteContentMessage │ │ ├── SiteContentMessage.styled.ts │ │ └── SiteContentMessage.tsx ├── Grid │ ├── Feature.styled.ts │ ├── Feature.tsx │ ├── Grid.styled.ts │ ├── Grid.tsx │ ├── Item.test.tsx │ └── Item.tsx ├── Header │ ├── Header.styled.ts │ ├── Header.tsx │ ├── Lockup.tsx │ ├── Primary.test.tsx │ ├── Primary.tsx │ ├── Super.test.tsx │ └── Super.tsx ├── Heading │ ├── Heading.styled.ts │ ├── Heading.tsx │ ├── SlashTitle.styled.ts │ └── Subhead.tsx ├── Hero │ ├── Basic.styled.ts │ ├── Basic.tsx │ ├── Hero.styled.ts │ └── Hero.tsx ├── Homepage │ ├── Collections.styled.ts │ ├── Collections.tsx │ ├── Hero.styled.ts │ ├── Hero.tsx │ ├── Overview.styled.ts │ ├── Overview.tsx │ ├── Works.styled.ts │ ├── Works.tsx │ └── index.ts ├── Nav │ ├── Nav.styled.ts │ └── Nav.tsx ├── Search │ ├── GenerativeAI.styled.ts │ ├── GenerativeAIToggle.test.tsx │ ├── GenerativeAIToggle.tsx │ ├── JumpTo.styled.ts │ ├── JumpTo.test.tsx │ ├── JumpTo.tsx │ ├── JumpToList.test.tsx │ ├── JumpToList.tsx │ ├── Options.styled.tsx │ ├── Options.tsx │ ├── Pagination.styled.ts │ ├── Pagination.tsx │ ├── PaginationAltCounts.test.tsx │ ├── PaginationAltCounts.tsx │ ├── Panel.styled.tsx │ ├── Panel.test.tsx │ ├── Panel.tsx │ ├── PublicOnlyWorks.tsx │ ├── Results.tsx │ ├── Search.styled.ts │ ├── Search.test.tsx │ ├── Search.tsx │ ├── Similar.test.tsx │ ├── Similar.tsx │ ├── TextArea.styled.ts │ └── TextArea.tsx ├── Shared │ ├── Announcement.tsx │ ├── AuthDialog.styled.ts │ ├── AuthDialog.test.tsx │ ├── AuthDialog.tsx │ ├── BlockStyled.ts │ ├── BlurredBgImage.styled.ts │ ├── BlurredBgImage.test.tsx │ ├── BlurredBgImage.tsx │ ├── BouncingLoader.tsx │ ├── Card.styled.ts │ ├── Card.test.tsx │ ├── Card.tsx │ ├── Checkbox.styled.tsx │ ├── Container.tsx │ ├── CopyText.styled.ts │ ├── CopyText.tsx │ ├── DefinitionList.styled.ts │ ├── Dialog.styled.ts │ ├── Dialog.test.tsx │ ├── Dialog.tsx │ ├── ErrorFallback.tsx │ ├── Expand │ │ ├── Expand.styled.ts │ │ ├── Expand.test.tsx │ │ └── Expand.tsx │ ├── ExpandableList.styled.ts │ ├── ExpandableList.tsx │ ├── Facts.styled.ts │ ├── Facts.tsx │ ├── Form.styled.ts │ ├── IIIF │ │ ├── HelperLink.tsx │ │ ├── Share.test.tsx │ │ ├── Share.tsx │ │ ├── ViewerLink.test.tsx │ │ └── ViewerLink.tsx │ ├── Icon.tsx │ ├── LinkStyled.ts │ ├── Loader.styled.ts │ ├── PhotoFeature │ │ └── PhotoFeature.tsx │ ├── PlaceholderBlock.styled.ts │ ├── ReadMore.tsx │ ├── RelatedItems.styled.ts │ ├── RelatedItems.test.tsx │ ├── RelatedItems.tsx │ ├── SVG │ │ ├── IIIF.tsx │ │ ├── Icons.tsx │ │ └── Northwestern.tsx │ ├── SectionHeading.tsx │ ├── SectionTop │ │ └── SectionTop.tsx │ ├── Select.styled.ts │ ├── Select.tsx │ ├── SimpleSelect.styled.ts │ ├── Social.styled.tsx │ ├── Social.tsx │ ├── Switch.styled.ts │ ├── TextArea.test.tsx │ ├── TextArea.tsx │ ├── Tooltip.styled.tsx │ ├── UnorderedList.ts │ └── WorkCount │ │ ├── WorkCount.styled.ts │ │ ├── WorkCount.test.tsx │ │ └── WorkCount.tsx ├── SharedLink │ ├── SharedLink.test.tsx │ └── SharedLink.tsx ├── Transition.tsx ├── Work │ ├── ActionsDialog │ │ ├── ActionsDialog.styled.ts │ │ ├── ActionsDialog.test.tsx │ │ ├── ActionsDialog.tsx │ │ ├── Aside.tsx │ │ ├── Cite.test.tsx │ │ ├── Cite.tsx │ │ ├── ContentState │ │ │ ├── ActiveCanvas.tsx │ │ │ ├── ContentState.styled.tsx │ │ │ └── ContentState.tsx │ │ ├── DownloadAndShare │ │ │ ├── DownloadAndShare.styled.ts │ │ │ ├── DownloadAndShare.test.tsx │ │ │ ├── DownloadAndShare.tsx │ │ │ ├── EmbedResources.test.tsx │ │ │ ├── EmbedResources.tsx │ │ │ ├── EmbedViewer.test.tsx │ │ │ ├── EmbedViewer.tsx │ │ │ ├── IIIFManifest.tsx │ │ │ └── MiradorLink.tsx │ │ ├── Find.test.tsx │ │ └── Find.tsx │ ├── Metadata.styled.ts │ ├── Metadata.test.tsx │ ├── Metadata.tsx │ ├── RestrictedDisplay.test.tsx │ ├── RestrictedDisplay.tsx │ ├── TopInfo.styled.ts │ ├── TopInfo.test.tsx │ ├── TopInfo.tsx │ └── Work.styled.ts └── layout.tsx ├── context ├── filter-context.tsx ├── home-context.tsx ├── search-context.tsx ├── user-context.tsx └── work-context.tsx ├── global.d.ts ├── hooks ├── useChatSocket.ts ├── useCopyToClipboard.ts ├── useElementPosition.ts ├── useEventCallback.ts ├── useEventListener.ts ├── useGenerativeAISearchToggle.ts ├── useIsomorphicLayoutEffect.ts ├── useLocalStorage.ts ├── useQueryParams.test.ts ├── useQueryParams.ts ├── useSessionStorage.ts └── useWorkAuth.ts ├── jest.config.js ├── jest.setup.js ├── lib ├── chat-helpers.ts ├── collection-helpers.test.ts ├── collection-helpers.ts ├── constants │ ├── bucket.ts │ ├── common.tsx │ ├── endpoints.ts │ ├── error.ts │ ├── facets-model.ts │ ├── head-meta.ts │ ├── homepage.ts │ └── works.ts ├── dc-api.test.ts ├── dc-api.ts ├── ga │ └── data-layer.ts ├── homepage-helpers.ts ├── honeybadger │ └── config.js ├── iiif │ ├── collection-helpers.test.js │ ├── collection-helpers.ts │ ├── content-state-helpers.ts │ └── manifest-helpers.ts ├── json-ld.test.ts ├── json-ld.ts ├── open-graph.test.ts ├── open-graph.ts ├── queries │ ├── aggs.test.ts │ ├── aggs.ts │ ├── builder.ts │ ├── facet.test.ts │ ├── facet.ts │ └── search.ts ├── user-helpers.ts ├── utils │ ├── array-helpers.js │ ├── count-helpers.test.ts │ ├── count-helpers.ts │ ├── date-helpers.test.ts │ ├── date-helpers.ts │ ├── debounce.js │ ├── facet-helpers.test.ts │ ├── facet-helpers.ts │ ├── get-url-search-params.test.ts │ ├── get-url-search-params.ts │ └── time-helpers.ts └── work-helpers.ts ├── mocks ├── aggregation.ts ├── private-unpublished-work.ts ├── sample-collection1.ts ├── sample-public-work.ts ├── sample-search-shape.ts ├── sample-work-image.ts ├── sample-work1.ts ├── sample-work2.ts ├── search-response1.ts ├── shared-link │ └── work.ts ├── use-markdown.js └── work-page │ ├── download-and-share.ts │ ├── work-manifest1.ts │ └── work1.ts ├── next-env.d.ts ├── next.config.js ├── package-lock.json ├── package.json ├── pages ├── 403.tsx ├── 404.tsx ├── _app.tsx ├── _document.tsx ├── _error.js ├── about.tsx ├── api │ └── sitemap │ │ └── [filename].tsx ├── collections │ ├── [id].tsx │ └── index.tsx ├── contact.tsx ├── embedded-viewer │ └── [manifestId].tsx ├── index.tsx ├── items │ └── [...id].tsx ├── legacy-pid │ └── [pid].tsx ├── search.tsx └── shared │ └── [id].tsx ├── playwright.config.ts ├── public ├── favicon.ico ├── fixtures │ └── iiif │ │ ├── collection │ │ ├── football.json │ │ └── masks.json │ │ └── manifest │ │ ├── ohio-state-1937.json │ │ └── wisconsin-1937.json └── images │ ├── book_placeholder.png │ ├── curt__O8A9877_final.jpg │ ├── feature-box-collection-bursars-office-550x310.jpg │ ├── feature-box-collection-cassas-550x310.jpg │ ├── feature-box-collection-colonialism-550x310.jpg │ ├── feature-box-collection-iranian-cinema-550x310.jpg │ ├── feature-box-collection-road-trip2-550x310.png │ ├── feature-box-collection-wwII-550x310.jpg │ ├── icons │ └── ltpurple-slash.svg │ └── liz__O8A9903_final.jpg ├── server.js ├── stitches.config.ts ├── styles ├── colors.ts ├── containers.ts ├── fonts.ts ├── global.ts ├── media.ts ├── sizes.ts └── transitions.ts ├── terraform ├── .terraform.lock.hcl ├── main.tf ├── sitemap_bucket.tf └── variables.tf ├── test-utils.tsx ├── tests ├── 404.spec.ts ├── fixtures │ ├── open-graph.ts │ ├── search-page.ts │ ├── work-page.ts │ └── works │ │ └── canary-work.ts ├── home.spec.ts ├── layout.spec.ts ├── search.spec.ts └── work.spec.ts ├── tsconfig.json └── types ├── api ├── request.ts └── response.ts ├── components ├── chat.ts ├── facets.ts ├── search.ts └── works.ts ├── context ├── filter-context.ts ├── search-context.ts ├── user.ts └── work-context.ts ├── declarations.d.ts └── index.ts /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["@typescript-eslint", "testing-library"], 3 | "extends": [ 4 | "plugin:@typescript-eslint/recommended", 5 | "next/core-web-vitals", 6 | "prettier" 7 | ], 8 | "overrides": [ 9 | // Overrides for Playwright tests 10 | { 11 | "files": ["tests/**/*.[jt]s?(x)"], 12 | "rules": { 13 | "testing-library/prefer-screen-queries": "off" 14 | } 15 | } 16 | ], 17 | "rules": { 18 | "sort-keys": 2, 19 | "sort-imports": 2, 20 | "@typescript-eslint/no-var-requires": "warn", 21 | "@typescript-eslint/no-unused-vars": "error", 22 | "@typescript-eslint/no-explicit-any": "warn", 23 | "@typescript-eslint/ban-ts-comment": "warn" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.github/workflows/playwright.yml: -------------------------------------------------------------------------------- 1 | name: Playwright Tests 2 | on: 3 | push: 4 | branches: [] 5 | pull_request: 6 | branches: [main, deploy/staging] 7 | workflow_dispatch: 8 | jobs: 9 | test: 10 | timeout-minutes: 10 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: actions/setup-node@v4 15 | with: 16 | node-version: lts/* 17 | - name: Install dependencies 18 | run: npm ci --force 19 | 20 | - name: Install Playwright Browsers 21 | run: npx playwright install --with-deps 22 | 23 | - name: Build app 24 | run: npm run build 25 | env: 26 | NEXT_PUBLIC_DCAPI_ENDPOINT: ${{ secrets.NEXT_PUBLIC_DCAPI_ENDPOINT }} 27 | 28 | - name: Start app 29 | run: npm run start:playwright & 30 | 31 | - name: Wait for server 32 | run: npx wait-on http://localhost:3000 33 | 34 | - name: Run Playwright tests 35 | run: npx playwright test 36 | env: 37 | BASE_URL: ${{ secrets.BASE_URL }} 38 | 39 | - uses: actions/upload-artifact@v4 40 | if: always() 41 | with: 42 | name: playwright-report 43 | path: playwright-report/ 44 | retention-days: 30 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | 36 | # typescript 37 | *.tsbuildinfo 38 | 39 | # terraform 40 | *.tfvars 41 | /terraform/.terraform 42 | /terraform/*.plan 43 | 44 | # ephemeral build artifacts 45 | /lib/honeybadger/config.vars.js 46 | 47 | # vscode 48 | /.vscode/* 49 | 50 | # playwright 51 | /test-results/ 52 | /playwright-report/* 53 | /blob-report/ 54 | /playwright/.cache/ 55 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | playwright-report/ -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2 3 | } 4 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | nodejs 20.5.0 2 | -------------------------------------------------------------------------------- /components/About/CollectionGrid.tsx: -------------------------------------------------------------------------------- 1 | import PhotoFeature, { 2 | PhotoFeatureProps, 3 | } from "@/components/Shared/PhotoFeature/PhotoFeature"; 4 | 5 | import { CollectionGridStyled } from "components/About/CollectionGridStyled"; 6 | 7 | interface Props { 8 | items: PhotoFeatureProps[]; 9 | } 10 | 11 | const AboutCollectionGrid: React.FC = ({ items = [] }) => { 12 | return ( 13 | 14 | {items.map(({ callToAction, href, imgAlt, imgSrc, title }) => ( 15 | 23 | ))} 24 | 25 | ); 26 | }; 27 | 28 | export default AboutCollectionGrid; 29 | -------------------------------------------------------------------------------- /components/About/CollectionGridStyled.ts: -------------------------------------------------------------------------------- 1 | import { styled } from "@/stitches.config"; 2 | 3 | /* eslint sort-keys: 0 */ 4 | 5 | const CollectionGridStyled = styled("div", { 6 | display: "grid", 7 | gridTemplateColumns: "repeat(2, 1fr)", 8 | gap: "$gr3", 9 | justifyItems: "center", 10 | justifyContent: "center", 11 | maxWidth: "1440px", 12 | margin: "0 auto", 13 | position: "relative", 14 | zIndex: "0", 15 | 16 | "@sm": { 17 | gridTemplateColumns: "1fr", 18 | }, 19 | }); 20 | 21 | export { CollectionGridStyled }; 22 | -------------------------------------------------------------------------------- /components/Chat/Feedback/OptIn.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from "@testing-library/react"; 2 | 3 | import ChatFeedbackOptIn from "@/components/Chat/Feedback/OptIn"; 4 | import React from "react"; 5 | import { UserContext } from "@/context/user-context"; 6 | 7 | const mockUserContextValue = { 8 | user: { 9 | email: "foo@bar.com", 10 | isLoggedIn: true, 11 | isReadingRoom: false, 12 | name: "foo", 13 | sub: "123", 14 | }, 15 | }; 16 | 17 | describe("ChatFeedbackOptIn", () => { 18 | it("renders a checkbox input with the user email value", () => { 19 | render( 20 | 21 | 22 | , 23 | ); 24 | 25 | const checkbox = screen.getByRole("checkbox"); 26 | expect(checkbox).toHaveAttribute("value", "foo@bar.com"); 27 | expect(checkbox).toBeInTheDocument(); 28 | }); 29 | 30 | it("renders a label", () => { 31 | render( 32 | 33 | 34 | , 35 | ); 36 | const label = screen.getByText(/please follow up with me/i); 37 | expect(label).toBeInTheDocument(); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /components/Chat/Feedback/OptIn.tsx: -------------------------------------------------------------------------------- 1 | import { UserContext } from "@/context/user-context"; 2 | import { styled } from "@/stitches.config"; 3 | import { useContext } from "react"; 4 | 5 | const ChatFeedbackOptIn = () => { 6 | const { user } = useContext(UserContext); 7 | 8 | return ( 9 | 10 | Please follow 11 | up with me regarding this issue. 12 | 13 | ); 14 | }; 15 | 16 | /* eslint-disable sort-keys */ 17 | const StyledChatFeedbackOptIn = styled("label", { 18 | display: "block", 19 | margin: "$gr3 0", 20 | fontSize: "$gr2", 21 | }); 22 | 23 | export default ChatFeedbackOptIn; 24 | -------------------------------------------------------------------------------- /components/Chat/Feedback/Option.test.tsx: -------------------------------------------------------------------------------- 1 | // test ChatFeedbackOption.tsx 2 | 3 | import { render, screen } from "@testing-library/react"; 4 | 5 | import ChatFeedbackOption from "@/components/Chat/Feedback/Option"; 6 | 7 | describe("ChatFeedbackOption", () => { 8 | it("renders a checkbox input", () => { 9 | render(); 10 | const checkbox = screen.getByTestId("chat-feedback-option-test"); 11 | expect(checkbox).toHaveAttribute("aria-checked", "false"); 12 | expect(checkbox).toHaveAttribute("tabindex", "0"); 13 | expect(checkbox).toBeInTheDocument(); 14 | }); 15 | 16 | it("renders a label", () => { 17 | render(); 18 | const label = screen.getByText(/this is a test/i); 19 | expect(label).toBeInTheDocument(); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /components/Chat/Feedback/TextArea.tsx: -------------------------------------------------------------------------------- 1 | import { styled } from "@/stitches.config"; 2 | 3 | const ChatFeedbackTextArea = () => { 4 | return ( 5 | 6 | Add additional specific details (optional) 7 |