commit
0721b7dc8f
66 changed files with 2858 additions and 0 deletions
7
.env.example
Normal file
7
.env.example
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
NEXT_PUBLIC_DRUPAL_BASE_URL=http://localhost:8080
|
||||||
|
NEXT_IMAGE_DOMAIN=localhost
|
||||||
|
DRUPAL_CLIENT_ID=52ce1a10-bf5c-4c81-8edf-eea3af95da84
|
||||||
|
DRUPAL_CLIENT_SECRET=SA9AGbHnx6pOamaAus2f9LG9XudHFjKs
|
||||||
|
DRUPAL_SITE_ID=example_marketing
|
||||||
|
DRUPAL_PREVIEW_SECRET=secret
|
||||||
|
DRUPAL_FRONT_PAGE=/home
|
||||||
41
.gitignore
vendored
Normal file
41
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# next.js
|
||||||
|
/.next/
|
||||||
|
/out/
|
||||||
|
.next
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
# vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
/certificates/*
|
||||||
|
|
||||||
|
cypress/screenshots
|
||||||
|
cypress/videos
|
||||||
531
CHANGELOG.md
Normal file
531
CHANGELOG.md
Normal file
|
|
@ -0,0 +1,531 @@
|
||||||
|
# Change Log
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
# [1.5.0](https://github.com/chapter-three/next-drupal/compare/example-marketing@1.4.4...example-marketing@1.5.0) (2022-12-06)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package example-marketing
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [1.4.4](https://github.com/chapter-three/next-drupal/compare/example-marketing@1.4.3...example-marketing@1.4.4) (2022-12-06)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package example-marketing
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [1.4.3](https://github.com/chapter-three/next-drupal/compare/example-marketing@1.4.2...example-marketing@1.4.3) (2022-09-07)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package example-marketing
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [1.4.2](https://github.com/chapter-three/next-drupal/compare/example-marketing@1.4.1...example-marketing@1.4.2) (2022-08-09)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* Warning title element received an array with more than 1 element ([910ca41](https://github.com/chapter-three/next-drupal/commit/910ca41ca230e5948fbe9d880bed04232d004306))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [1.4.1](https://github.com/chapter-three/next-drupal/compare/example-marketing@1.4.0...example-marketing@1.4.1) (2022-07-29)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package example-marketing
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [1.4.0](https://github.com/chapter-three/next-drupal/compare/example-marketing@1.4.0-rc.0...example-marketing@1.4.0) (2022-06-14)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package example-marketing
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [1.4.0-rc.0](https://github.com/chapter-three/next-drupal/compare/example-marketing@1.3.3-rc.1...example-marketing@1.4.0-rc.0) (2022-06-14)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* rename Experimental_DrupalClient to DrupalClient ([fc549ec](https://github.com/chapter-three/next-drupal/commit/fc549ecab94a5a1e67f38b4e951351365adbb1f5))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [1.3.3-rc.1](https://github.com/chapter-three/next-drupal/compare/example-marketing@1.3.3-rc.0...example-marketing@1.3.3-rc.1) (2022-06-10)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package example-marketing
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [1.3.3-rc.0](https://github.com/chapter-three/next-drupal/compare/example-marketing@1.3.3-alpha.0...example-marketing@1.3.3-rc.0) (2022-06-06)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package example-marketing
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [1.3.3-alpha.0](https://github.com/chapter-three/next-drupal/compare/example-marketing@1.3.2...example-marketing@1.3.3-alpha.0) (2022-06-02)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package example-marketing
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [1.3.2](https://github.com/chapter-three/next-drupal/compare/example-marketing@1.3.1...example-marketing@1.3.2) (2022-05-02)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package example-marketing
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [1.3.1](https://github.com/chapter-three/next-drupal/compare/example-marketing@1.3.0...example-marketing@1.3.1) (2022-04-25)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package example-marketing
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [1.3.0](https://github.com/chapter-three/next-drupal/compare/example-marketing@1.3.0-rc.0...example-marketing@1.3.0) (2022-04-19)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package example-marketing
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [1.3.0-rc.0](https://github.com/chapter-three/next-drupal/compare/example-marketing@1.3.0-alpha.0...example-marketing@1.3.0-rc.0) (2022-04-19)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package example-marketing
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [1.3.0-alpha.0](https://github.com/chapter-three/next-drupal/compare/example-marketing@1.2.2...example-marketing@1.3.0-alpha.0) (2022-04-18)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **example-marketing:** update slug ([cd6f9cc](https://github.com/chapter-three/next-drupal/commit/cd6f9ccdb783f95bc95d5dc7a551edcd5b2601b2))
|
||||||
|
* update examples ([c00cafb](https://github.com/chapter-three/next-drupal/commit/c00cafbf3c667265fd6f0478164808664f778433))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add example-cache ([2b29d66](https://github.com/chapter-three/next-drupal/commit/2b29d66c8cddb66a331e3b588da8140a4e4ba61e))
|
||||||
|
* **next-drupal:** rename DrupalClient to Experimental_DrupalClient ([0d5cf2f](https://github.com/chapter-three/next-drupal/commit/0d5cf2f44b503a2d8e61eee19146fd5b797356ab))
|
||||||
|
|
||||||
|
|
||||||
|
### Reverts
|
||||||
|
|
||||||
|
* Revert "chore: revert example-marketing" ([88a9505](https://github.com/chapter-three/next-drupal/commit/88a950508617e3e94a2b6504bb0ea95d7574c3b9))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [1.2.2](https://github.com/chapter-three/next-drupal/compare/example-marketing@1.2.1...example-marketing@1.2.2) (2022-04-11)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package example-marketing
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [1.2.1](https://github.com/chapter-three/next-drupal/compare/example-marketing@1.2.0...example-marketing@1.2.1) (2022-03-28)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **example-blog:** switch to manual revalidation ([933ca86](https://github.com/chapter-three/next-drupal/commit/933ca86d47a35dc3bf2a120466f1fec197ca8b61))
|
||||||
|
* **example-marketing:** turn off revalidate for pages ([22e8bf1](https://github.com/chapter-three/next-drupal/commit/22e8bf1f333e08aa6f13b6038e362bf826133acb))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [1.2.0](https://github.com/chapter-three/next-drupal/compare/example-marketing@1.1.1...example-marketing@1.2.0) (2022-02-24)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **example-marketing:** add next-transpile-modules ([b574e30](https://github.com/chapter-three/next-drupal/commit/b574e3077ee90d742b9ec49713597bb4b8d8005e))
|
||||||
|
* **example-marketing:** add tailwindcss forms ([e9b583f](https://github.com/chapter-three/next-drupal/commit/e9b583f645e2a8aa9a5bf70cc132fc31b2ebb6b3))
|
||||||
|
* **example-marketing:** debug code for translatePath in getStaticProps ([c14b690](https://github.com/chapter-three/next-drupal/commit/c14b690fdc3bd2b1125ba080f0343293426f9e6b))
|
||||||
|
* **example-marketing:** remove next-transpile-modules ([5b248c2](https://github.com/chapter-three/next-drupal/commit/5b248c2e4d44c9e2acf363743904e4078c5ec610))
|
||||||
|
* **example-marketing:** remove tailwind forms ([630fc24](https://github.com/chapter-three/next-drupal/commit/630fc24e126560f1dccc6f495079e895fa4f70ca))
|
||||||
|
* **example-marketing:** switch to next/link ([b39da07](https://github.com/chapter-three/next-drupal/commit/b39da077b2bb4c6cbbbd51a533aa7d061842c532))
|
||||||
|
* **example-marketing:** update meta component ([4d4e99b](https://github.com/chapter-three/next-drupal/commit/4d4e99b7c9206bc05117ac10868c12e696f53dae))
|
||||||
|
* **example-marketing:** update tailwindcss ([943d7f1](https://github.com/chapter-three/next-drupal/commit/943d7f1c9a37418125053e897b67b070065479c4))
|
||||||
|
* **example-marketing:** use local version for next-drupal ([8903fc9](https://github.com/chapter-three/next-drupal/commit/8903fc9e79d29c0e4a87acd7357229c5fe1bc61b))
|
||||||
|
* update gitignore ([0e05896](https://github.com/chapter-three/next-drupal/commit/0e05896f06a6a48bf82db4830c085e6f9c5e7b84))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **example-marketing:** add revalidate api ([bf402b1](https://github.com/chapter-three/next-drupal/commit/bf402b1a8ae4937cb91cd636c1256161da7d612e))
|
||||||
|
* bump all examples to next 12.1.0 ([00b15f2](https://github.com/chapter-three/next-drupal/commit/00b15f2b308a0a9fcb298789a9ca712f4efa7eff))
|
||||||
|
* **example-marketing:** add meta og image ([116e29f](https://github.com/chapter-three/next-drupal/commit/116e29f3171b40b274435f1ac064567770ed8f32))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [1.1.1](https://github.com/chapter-three/next-drupal/compare/example-marketing@1.1.0...example-marketing@1.1.1) (2022-01-17)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package example-marketing
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [1.1.0](https://github.com/chapter-three/next-drupal/compare/example-marketing@1.0.1...example-marketing@1.1.0) (2022-01-12)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **example-marketing:** handle redirects ([ba12711](https://github.com/chapter-three/next-drupal/commit/ba1271155ebb6dda80dac77e29683b07021c8e73))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [1.0.1](https://github.com/chapter-three/next-drupal/compare/example-marketing@1.0.0...example-marketing@1.0.1) (2021-12-21)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package example-marketing
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [1.0.0](https://github.com/chapter-three/next-drupal/compare/example-marketing@0.17.0...example-marketing@1.0.0) (2021-12-03)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package example-marketing
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [0.17.0](https://github.com/chapter-three/next-drupal/compare/example-marketing@0.16.0...example-marketing@0.17.0) (2021-11-24)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* update .env files ([8c3207b](https://github.com/chapter-three/next-drupal/commit/8c3207b79bc641b605c11cec3fd556d0f71f1e72))
|
||||||
|
* update page for marketing ([e527b36](https://github.com/chapter-three/next-drupal/commit/e527b360da861d3c69ca9a01954e851231b3ac51))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **drupal:** update example modules ([3f2b578](https://github.com/chapter-three/next-drupal/commit/3f2b57822226e587e590fdcc5f760cae0b11d97f))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [0.16.0](https://github.com/chapter-three/next-drupal/compare/example-marketing@0.16.0-alpha.0...example-marketing@0.16.0) (2021-11-01)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package example-marketing
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [0.16.0-alpha.0](https://github.com/chapter-three/next-drupal/compare/example-marketing@0.15.2...example-marketing@0.16.0-alpha.0) (2021-11-01)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package example-marketing
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [0.15.2](https://github.com/chapter-three/next-drupal/compare/example-marketing@0.15.1...example-marketing@0.15.2) (2021-10-14)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package example-marketing
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [0.15.1](https://github.com/chapter-three/next-drupal/compare/example-marketing@0.15.0...example-marketing@0.15.1) (2021-10-14)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package example-marketing
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [0.15.0](https://github.com/chapter-three/next-drupal/compare/example-marketing@0.14.0...example-marketing@0.15.0) (2021-10-13)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* rename repo links ([48d52dd](https://github.com/chapter-three/next-drupal/commit/48d52dde79f69396ef706d152c03670117b6a480))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **example-marketing:** add drupal jsonapi params example ([e663466](https://github.com/chapter-three/next-drupal/commit/e6634662139c614b36fb76370020298f9f89352a))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [0.14.0](https://github.com/chapter-three/next-drupal/compare/example-marketing@0.13.2...example-marketing@0.14.0) (2021-08-11)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **example-marketing:** implement route sync ([b49e531](https://github.com/chapter-three/next-drupal/commit/b49e5314692a037b6e420167609dfec216551b77))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [0.13.2](https://github.com/chapter-three/next-drupal/compare/example-marketing@0.13.1...example-marketing@0.13.2) (2021-08-07)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package example-marketing
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [0.13.1](https://github.com/chapter-three/next-drupal/compare/example-marketing@0.13.0...example-marketing@0.13.1) (2021-06-22)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **example-marketing:** add missing dependency ([7a182f6](https://github.com/chapter-three/next-drupal/commit/7a182f68f0cf86b03977009c07660e886c9635eb))
|
||||||
|
* **example-marketing:** use fallback blocking ([be5a2d3](https://github.com/chapter-three/next-drupal/commit/be5a2d32764d1c510ffb335cb5076cc927c65e0e))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [0.13.0](https://github.com/chapter-three/next-drupal/compare/example-marketing@0.12.0...example-marketing@0.13.0) (2021-06-16)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* update to nextjs 11 ([1e46e44](https://github.com/chapter-three/next-drupal/commit/1e46e44ab5eb9d961e95dcc87d51282178f02bb2))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [0.12.0](https://github.com/chapter-three/next-drupal/compare/example-marketing@0.11.0...example-marketing@0.12.0) (2021-06-16)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **example-marketing:** add pagination to view ([feb6543](https://github.com/chapter-three/next-drupal/commit/feb6543be9a201f02b7afee38a3fed5c170de85b))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [0.11.0](https://github.com/chapter-three/next-drupal/compare/example-marketing@0.10.0...example-marketing@0.11.0) (2021-06-15)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **example-marketing:** remove unused locale ([412e39d](https://github.com/chapter-three/next-drupal/commit/412e39d7ce9103f845fafaff8172c1e07bddb442))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **example-marketing:** add properties view ([2b2804f](https://github.com/chapter-three/next-drupal/commit/2b2804f835a375bdc5076cbd4148604c964dbd06))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [0.10.0](https://github.com/chapter-three/next-drupal/compare/example-marketing@0.9.2...example-marketing@0.10.0) (2021-06-14)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **example-marketing:** update site to work with latest next-drupal ([9f27e7a](https://github.com/chapter-three/next-drupal/commit/9f27e7ae2b9c775ba4fe7d99fdfa0b8240a3dd90))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [0.9.2](https://github.com/chapter-three/next-drupal/compare/example-marketing@0.9.1...example-marketing@0.9.2) (2021-06-13)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package example-marketing
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [0.9.1](https://github.com/chapter-three/next-drupal/compare/example-marketing@0.9.0...example-marketing@0.9.1) (2021-06-13)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* update sites to handle unpublished entities ([93dd678](https://github.com/chapter-three/next-drupal/commit/93dd6786caff73398dd291c84b41d45c5bc50645))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [0.9.0](https://github.com/chapter-three/next-drupal/compare/example-marketing@0.8.0...example-marketing@0.9.0) (2021-06-13)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* update marketing site ([08863e6](https://github.com/chapter-three/next-drupal/commit/08863e6e8ab273799f1b1fcab793fdf51ad1d04b))
|
||||||
|
* update preview ([f01ca6b](https://github.com/chapter-three/next-drupal/commit/f01ca6b9f68ac92c587b11c6e05f1145a57e8995))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add support for basic page to example-marketing ([f9c5d1b](https://github.com/chapter-three/next-drupal/commit/f9c5d1b81697c59bc15b0372700fa2e979de9a00))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [0.8.0](https://github.com/chapter-three/next-drupal/compare/example-marketing@0.7.0...example-marketing@0.8.0) (2021-06-11)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* update all sites ([ca9b2e9](https://github.com/chapter-three/next-drupal/commit/ca9b2e964c5a7fe591602465f2c2516eb4a54a1b))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [0.7.0](https://github.com/chapter-three/next-drupal/compare/example-marketing@0.6.0...example-marketing@0.7.0) (2021-06-10)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **examples:** update examples to use the new menu hook ([f9169e3](https://github.com/chapter-three/next-drupal/commit/f9169e34ab76584db855f1a69df027024156afff))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [0.6.0](https://github.com/chapter-three/next-drupal/compare/example-marketing@0.5.0...example-marketing@0.6.0) (2021-05-17)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add getEntityByPath ([072ead7](https://github.com/chapter-three/next-drupal/commit/072ead7ecc3b7f158e4b81e03d17f0bf1a5b511c))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [0.5.0](https://github.com/chapter-three/next-drupal/compare/example-marketing@0.4.1...example-marketing@0.5.0) (2021-05-17)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* deserialize entities by default ([8b53ae2](https://github.com/chapter-three/next-drupal/commit/8b53ae222717b8983568194373be04903944a032))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [0.4.1](https://github.com/chapter-three/next-drupal/compare/example-marketing@0.4.0...example-marketing@0.4.1) (2021-05-07)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package example-marketing
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [0.4.0](https://github.com/chapter-three/next-drupal/compare/example-marketing@0.3.0...example-marketing@0.4.0) (2021-05-07)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add a no-preview example ([db48c6e](https://github.com/chapter-three/next-drupal/commit/db48c6e90ae5100eafb25d3b5688b5ef8131c477))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [0.3.0](https://github.com/chapter-three/next-drupal/compare/example-marketing@0.2.1...example-marketing@0.3.0) (2021-04-29)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* update .env.example ([467047b](https://github.com/chapter-three/next-drupal/commit/467047b010f54394c52760b9db960b06ee48db61))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **www:** update to reflexjs 2.0.0-alpha-5 ([3b28c84](https://github.com/chapter-three/next-drupal/commit/3b28c84e9b7eefd4892aaf22dea0dd2512091b93))
|
||||||
|
* update examples to reflexjs 2.0.0-alpha-4 ([49816ee](https://github.com/chapter-three/next-drupal/commit/49816ee6ba0f669d45cee6930b449547200ce1c7))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [0.2.1](https://github.com/chapter-three/next-drupal/compare/example-marketing@0.2.0...example-marketing@0.2.1) (2021-02-02)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package example-marketing
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [0.2.0](https://github.com/chapter-three/next-drupal/compare/example-marketing@0.1.1...example-marketing@0.2.0) (2021-02-01)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* make image domain configurable via env ([9b66159](https://github.com/chapter-three/next-drupal/commit/9b66159561a5e0bf17b4e73c4cde318e06fe938d))
|
||||||
|
* update examples ([e70ed45](https://github.com/chapter-three/next-drupal/commit/e70ed459294cb8945f42d04cf7bd20a54ab9fe77))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* implement filter by entity reference ([eeade94](https://github.com/chapter-three/next-drupal/commit/eeade9485caaff587735d5d8211a86a88ca8847f))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [0.1.1](https://github.com/chapter-three/next-drupal/compare/example-marketing@0.1.0...example-marketing@0.1.1) (2021-02-01)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* update example sites ([33143d0](https://github.com/chapter-three/next-drupal/commit/33143d0d5229be6424c41ace2ad846c0d85447d9))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# 0.1.0 (2021-01-31)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add marketing site ([8462d7c](https://github.com/chapter-three/next-drupal/commit/8462d7cfcf623a9e8ca03456ebed0bb6ab838e11))
|
||||||
11
README.md
Normal file
11
README.md
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
# example-marketing
|
||||||
|
|
||||||
|
An example marketing site built using Drupal + JSON:API.
|
||||||
|
|
||||||
|
Pages are built from the Landing page node type and paragraphs sourced from `/drupal`.
|
||||||
|
|
||||||
|
See https://demo.next-drupal.org
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Licensed under the [MIT license](https://github.com/chapter-three/next-drupal/blob/master/LICENSE).
|
||||||
31
components/footer.tsx
Normal file
31
components/footer.tsx
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
import Link from "next/link"
|
||||||
|
import { DrupalMenuLinkContent } from "next-drupal"
|
||||||
|
|
||||||
|
interface FooterProps {
|
||||||
|
links: DrupalMenuLinkContent[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Footer({ links }: FooterProps) {
|
||||||
|
return (
|
||||||
|
<footer className="border-t">
|
||||||
|
<div className="container px-6 py-12 mx-auto">
|
||||||
|
<div className="flex flex-col items-center justify-between text-sm md:flex-row">
|
||||||
|
<p className="mb-6 md:mb-0">
|
||||||
|
© {new Date().getFullYear()} Next.js + Drupal
|
||||||
|
</p>
|
||||||
|
{links?.length ? (
|
||||||
|
<ul className="flex gap-4">
|
||||||
|
{links.map((link) => (
|
||||||
|
<li key={link.id}>
|
||||||
|
<Link href={link.url} passHref>
|
||||||
|
<a>{link.title}</a>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
)
|
||||||
|
}
|
||||||
16
components/form-item.tsx
Normal file
16
components/form-item.tsx
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
interface FormItemProps {
|
||||||
|
label: string
|
||||||
|
name: string
|
||||||
|
children?: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FormItem({ label, name, children, ...props }: FormItemProps) {
|
||||||
|
return (
|
||||||
|
<div className="grid gap-2" {...props}>
|
||||||
|
<label className="text-sm font-semibold uppercase" htmlFor={name}>
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
64
components/formatted-text.tsx
Normal file
64
components/formatted-text.tsx
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
import Image from "next/image"
|
||||||
|
import { HTMLReactParserOptions, domToReact } from "html-react-parser"
|
||||||
|
import { Element } from "domhandler/lib/node"
|
||||||
|
import parse from "html-react-parser"
|
||||||
|
|
||||||
|
import { isRelative } from "lib/utils/is-relative"
|
||||||
|
import Link from "next/link"
|
||||||
|
|
||||||
|
const options: HTMLReactParserOptions = {
|
||||||
|
replace: (domNode) => {
|
||||||
|
if (domNode instanceof Element) {
|
||||||
|
if (domNode.name === "img") {
|
||||||
|
const { src, alt, width = "100px", height = "100px" } = domNode.attribs
|
||||||
|
|
||||||
|
if (isRelative(src)) {
|
||||||
|
return (
|
||||||
|
<Image
|
||||||
|
src={`${process.env.NEXT_PUBLIC_DRUPAL_BASE_URL}/${src}`}
|
||||||
|
width={`${width}px`}
|
||||||
|
height={`${height}px`}
|
||||||
|
alt={alt}
|
||||||
|
layout="intrinsic"
|
||||||
|
objectFit="cover"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (domNode.name === "a") {
|
||||||
|
const { href, class: className } = domNode.attribs
|
||||||
|
|
||||||
|
if (href && isRelative(href)) {
|
||||||
|
return (
|
||||||
|
<Link href={href} passHref>
|
||||||
|
<a className={className}>{domToReact(domNode.children)}</a>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (domNode.name === "input") {
|
||||||
|
if (domNode.attribs.value === "") {
|
||||||
|
delete domNode.attribs.value
|
||||||
|
}
|
||||||
|
|
||||||
|
return domNode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FormattedTextProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
|
format?: string
|
||||||
|
processed: string
|
||||||
|
value?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FormattedText({ processed, ...props }: FormattedTextProps) {
|
||||||
|
return (
|
||||||
|
<div data-cy="node--body" {...props}>
|
||||||
|
{parse(processed, options)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
23
components/layout.tsx
Normal file
23
components/layout.tsx
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
import * as React from "react"
|
||||||
|
import { DrupalMenuLinkContent } from "next-drupal"
|
||||||
|
|
||||||
|
import { Navbar } from "components/navbar"
|
||||||
|
import { Footer } from "components/footer"
|
||||||
|
|
||||||
|
export interface LayoutProps {
|
||||||
|
menus: {
|
||||||
|
main: DrupalMenuLinkContent[]
|
||||||
|
footer: DrupalMenuLinkContent[]
|
||||||
|
}
|
||||||
|
children?: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Layout({ menus, children }: LayoutProps) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col min-h-screen">
|
||||||
|
<Navbar links={menus.main} />
|
||||||
|
<main className="flex-1">{children}</main>
|
||||||
|
<Footer links={menus.footer} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
38
components/link.tsx
Normal file
38
components/link.tsx
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
import * as React from "react"
|
||||||
|
import NextLink from "next/link"
|
||||||
|
import type { LinkProps as NextLinkProps } from "next/link"
|
||||||
|
import { useRouter } from "next/router"
|
||||||
|
|
||||||
|
import { isRelative } from "lib/utils/is-relative"
|
||||||
|
|
||||||
|
interface LinkProps extends NextLinkProps {
|
||||||
|
href: string
|
||||||
|
children?: React.ReactElement
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Link({ href, passHref, as, children, ...props }: LinkProps) {
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
if (!href) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use Next Link for internal links, and <a> for others.
|
||||||
|
if (isRelative(href)) {
|
||||||
|
// Disable prefetching in preview mode.
|
||||||
|
// We do this here inside of inline `prefetch={!router.isPreview}`
|
||||||
|
// because `prefetch={true}` is not allowed.
|
||||||
|
// See https://nextjs.org/docs/messages/prefetch-true-deprecated
|
||||||
|
const linkProps = router.isPreview ? { prefetch: false, ...props } : props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NextLink as={as} href={href} passHref={passHref} {...linkProps}>
|
||||||
|
{children}
|
||||||
|
</NextLink>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return React.cloneElement(children, {
|
||||||
|
href,
|
||||||
|
})
|
||||||
|
}
|
||||||
33
components/links.tsx
Normal file
33
components/links.tsx
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
import classNames from "classnames"
|
||||||
|
import Link from "next/link"
|
||||||
|
|
||||||
|
export interface LinksProps {
|
||||||
|
links: {
|
||||||
|
title: string
|
||||||
|
uri: string
|
||||||
|
options?: []
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Links({ links }: LinksProps) {
|
||||||
|
if (!links.length) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid gap-4 mt-6 sm:grid-cols-2">
|
||||||
|
{links.map((link, index) => (
|
||||||
|
<Link href={link.uri} key={index} passHref>
|
||||||
|
<a
|
||||||
|
className={classNames(
|
||||||
|
"px-6 py-3 text-lg transition-colors rounded-md ",
|
||||||
|
index === 0
|
||||||
|
? "bg-blue-600 text-white hover:bg-blue-700"
|
||||||
|
: "bg-gray-100 hover:bg-gray-200 text-black"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{link.title}
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
25
components/locale-switcher.tsx
Normal file
25
components/locale-switcher.tsx
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
import classNames from "classnames"
|
||||||
|
import Link from "next/link"
|
||||||
|
import { useRouter } from "next/router"
|
||||||
|
|
||||||
|
export function LocaleSwitcher() {
|
||||||
|
const { locales, asPath, locale: currentLocale } = useRouter()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex">
|
||||||
|
{locales.map((locale) => (
|
||||||
|
<Link href={asPath} key={locale} locale={locale} passHref>
|
||||||
|
<a
|
||||||
|
data-cy={`local-switcher-${locale}`}
|
||||||
|
className={classNames(
|
||||||
|
"flex items-center justify-center p-2 uppercase",
|
||||||
|
locale === currentLocale ? "text-black" : "text-gray-500"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{locale}
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
34
components/media--image.tsx
Normal file
34
components/media--image.tsx
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
import Image, { ImageProps } from "next/image"
|
||||||
|
|
||||||
|
import { absoluteURL } from "lib/utils/absolute-url"
|
||||||
|
import { MediaProps } from "components/media"
|
||||||
|
|
||||||
|
interface MediaImageProps extends MediaProps, Partial<ImageProps> {}
|
||||||
|
|
||||||
|
export function MediaImage({
|
||||||
|
media,
|
||||||
|
layout = "responsive",
|
||||||
|
objectFit,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
...props
|
||||||
|
}: MediaImageProps) {
|
||||||
|
const image = media?.field_media_image
|
||||||
|
|
||||||
|
if (!image) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Image
|
||||||
|
src={absoluteURL(image.uri.url)}
|
||||||
|
layout={layout}
|
||||||
|
objectFit={objectFit}
|
||||||
|
width={width || image.resourceIdObjMeta.width}
|
||||||
|
height={height || image.resourceIdObjMeta.height}
|
||||||
|
alt={image.resourceIdObjMeta.alt || "Image"}
|
||||||
|
title={image.resourceIdObjMeta.title}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
25
components/media.tsx
Normal file
25
components/media.tsx
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { DrupalMedia } from "next-drupal"
|
||||||
|
|
||||||
|
import { MediaImage } from "components/media--image"
|
||||||
|
|
||||||
|
const mediaTypes = {
|
||||||
|
"media--image": MediaImage,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MediaProps {
|
||||||
|
media: DrupalMedia
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Media({ media, ...props }: MediaProps) {
|
||||||
|
if (!media) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const Component = mediaTypes[media.type]
|
||||||
|
|
||||||
|
if (!Component) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Component media={media} {...props} />
|
||||||
|
}
|
||||||
53
components/meta.tsx
Normal file
53
components/meta.tsx
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
import Head from "next/head"
|
||||||
|
import { useRouter } from "next/router"
|
||||||
|
import { DrupalMetatag } from "types/drupal"
|
||||||
|
|
||||||
|
interface MetaProps {
|
||||||
|
title?: string
|
||||||
|
path?: string
|
||||||
|
tags?: DrupalMetatag[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Meta({ title, tags }: MetaProps) {
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Head>
|
||||||
|
<link
|
||||||
|
rel="canonical"
|
||||||
|
href={`${process.env.NEXT_PUBLIC_BASE_URL}${
|
||||||
|
router.asPath !== "/" ? router.asPath : ""
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
{tags?.length ? (
|
||||||
|
tags.map((tag, index) => {
|
||||||
|
if (tag.attributes.rel === "canonical") {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tag.attributes.name === "title") {
|
||||||
|
return (
|
||||||
|
<title key={tag.attributes.name}>{tag.attributes.content}</title>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const Tag = tag.tag as keyof JSX.IntrinsicElements
|
||||||
|
return <Tag key={index} {...tag.attributes}></Tag>
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<title>{`${title} | Next.js for Drupal`}</title>
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="A Next.js blog powered by a Drupal backend."
|
||||||
|
/>
|
||||||
|
<meta
|
||||||
|
property="og:image"
|
||||||
|
content={`${process.env.NEXT_PUBLIC_BASE_URL}/images/meta.jpg`}
|
||||||
|
/>
|
||||||
|
<meta property="og:image:width" content="800" />
|
||||||
|
<meta property="og:image:height" content="600" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Head>
|
||||||
|
)
|
||||||
|
}
|
||||||
64
components/navbar.tsx
Normal file
64
components/navbar.tsx
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
import React from "react"
|
||||||
|
import { useRouter } from "next/router"
|
||||||
|
import { DrupalMenuLinkContent } from "next-drupal"
|
||||||
|
import classNames from "classnames"
|
||||||
|
|
||||||
|
import { LocaleSwitcher } from "components/locale-switcher"
|
||||||
|
import Link from "next/link"
|
||||||
|
|
||||||
|
interface NavbarProps {
|
||||||
|
links: DrupalMenuLinkContent[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Navbar({ links, ...props }: NavbarProps) {
|
||||||
|
const { locale } = useRouter()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<header
|
||||||
|
className="static top-0 z-50 flex-shrink-0 py-4 bg-white md:sticky"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div className="container flex flex-col items-start justify-between px-6 mx-auto md:flex-row md:items-center">
|
||||||
|
<Link href="/" locale={locale} passHref>
|
||||||
|
<a className="text-lg font-bold">Marketing</a>
|
||||||
|
</Link>
|
||||||
|
{links ? <Menu items={links} /> : null}
|
||||||
|
<div className="absolute flex justify-end md:static top-2 right-4">
|
||||||
|
<LocaleSwitcher />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Menu({ items }: { items: DrupalMenuLinkContent[] }) {
|
||||||
|
return (
|
||||||
|
<ul
|
||||||
|
className="grid grid-flow-col gap-4 mx-auto mt-6 md:mt-0 auto-cols-auto md:auto-rows-auto md:gap-8 lg:gap-12"
|
||||||
|
data-cy="navbar-menu"
|
||||||
|
>
|
||||||
|
{items.map((item) => (
|
||||||
|
<MenuLink link={item} key={item.id} />
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function MenuLink({ link }: { link: DrupalMenuLinkContent }) {
|
||||||
|
const { asPath } = useRouter()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li>
|
||||||
|
<Link href={link.url} passHref>
|
||||||
|
<a
|
||||||
|
className={classNames(
|
||||||
|
"py-4 hover:underline text-sm md:text-base",
|
||||||
|
link.url === asPath ? "font-semibold" : "font-normal"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{link.title}
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
}
|
||||||
108
components/node--article.tsx
Normal file
108
components/node--article.tsx
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
import Link from "next/link"
|
||||||
|
import Image from "next/image"
|
||||||
|
|
||||||
|
import { formatDate } from "lib/utils/format-date"
|
||||||
|
import { absoluteURL } from "lib/utils/absolute-url"
|
||||||
|
import { FormattedText } from "components/formatted-text"
|
||||||
|
import { NodeProps } from "components/node"
|
||||||
|
|
||||||
|
export function NodeArticle({ node, viewMode, ...props }: NodeProps) {
|
||||||
|
if (viewMode === "teaser") {
|
||||||
|
return <NodeArticleTeaser node={node} {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
if (viewMode === "full") {
|
||||||
|
return <NodeArticleFull node={node} {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NodeArticleFull({ node, ...props }) {
|
||||||
|
return (
|
||||||
|
<article data-cy="node--article" {...props}>
|
||||||
|
<div className="container max-w-3xl px-6 mx-auto my-10 md:my-18">
|
||||||
|
<h1 className="mb-4 text-2xl font-bold md:text-3xl lg:text-5xl">
|
||||||
|
{node.title}
|
||||||
|
</h1>
|
||||||
|
<div className="prose">
|
||||||
|
<div data-cy="node--meta" className="text-gray-600">
|
||||||
|
{node.uid?.field_name ? (
|
||||||
|
<span>
|
||||||
|
Posted by <strong>{node.uid?.field_name}</strong>
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
<span> - {formatDate(node.created)}</span>
|
||||||
|
</div>
|
||||||
|
{node.body?.summary ? <p>{node.body.summary}</p> : null}
|
||||||
|
{node.field_image?.uri && (
|
||||||
|
<Image
|
||||||
|
src={absoluteURL(node.field_image.uri.url)}
|
||||||
|
width={1200}
|
||||||
|
height={600}
|
||||||
|
layout="intrinsic"
|
||||||
|
objectFit="cover"
|
||||||
|
className="rounded-lg"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{node.body?.processed && (
|
||||||
|
<FormattedText processed={node.body.processed} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NodeArticleTeaser({ node, ...props }) {
|
||||||
|
return (
|
||||||
|
<article data-cy="node--article" {...props}>
|
||||||
|
{node.field_image?.uri && (
|
||||||
|
<div>
|
||||||
|
<Image
|
||||||
|
src={absoluteURL(node.field_image.uri.url)}
|
||||||
|
width={800}
|
||||||
|
height={450}
|
||||||
|
layout="intrinsic"
|
||||||
|
objectFit="cover"
|
||||||
|
className="rounded-lg"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<h2 className="my-4 text-2xl font-semibold md:text-3xl">
|
||||||
|
<Link href={node.path?.alias} passHref>
|
||||||
|
<a className="hover:text-blue-500">{node.title}</a>
|
||||||
|
</Link>
|
||||||
|
</h2>
|
||||||
|
<div data-cy="node--meta" className="text-gray-600">
|
||||||
|
{node.uid?.field_name ? (
|
||||||
|
<span>
|
||||||
|
Posted by <strong>{node.uid?.field_name}</strong>
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
<span> - {formatDate(node.created)}</span>
|
||||||
|
</div>
|
||||||
|
{node.body?.summary ? (
|
||||||
|
<p className="mt-4 leading-relaxed text-gray-600">
|
||||||
|
{node.body.summary}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
<Link href={node.path.alias} passHref>
|
||||||
|
<a className="flex items-center mt-4 text-sm hover:text-blue-500">
|
||||||
|
Read more
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
className="w-4 h-4 ml-2"
|
||||||
|
>
|
||||||
|
<path d="M5 12h14M12 5l7 7-7 7" />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
</article>
|
||||||
|
)
|
||||||
|
}
|
||||||
20
components/node--landing-page.tsx
Normal file
20
components/node--landing-page.tsx
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { DrupalNode } from "next-drupal"
|
||||||
|
|
||||||
|
import { Paragraph } from "components/paragraph"
|
||||||
|
|
||||||
|
interface NodeLandingPage {
|
||||||
|
node: DrupalNode
|
||||||
|
viewMode?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NodeLandingPage({ node }: NodeLandingPage) {
|
||||||
|
if (!node.field_sections?.length) return null
|
||||||
|
|
||||||
|
return node.field_sections.map((paragraph) => {
|
||||||
|
if (paragraph.type === "paragraph--from_library") {
|
||||||
|
paragraph = paragraph.field_reusable_paragraph.paragraphs
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Paragraph key={paragraph.id} paragraph={paragraph} />
|
||||||
|
})
|
||||||
|
}
|
||||||
17
components/node--page.tsx
Normal file
17
components/node--page.tsx
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { FormattedText } from "components/formatted-text"
|
||||||
|
import { NodeProps } from "components/node"
|
||||||
|
|
||||||
|
export function NodePage({ node, ...props }: NodeProps) {
|
||||||
|
delete props.viewMode
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container max-w-3xl px-6 pt-10 mx-auto md:pt-20" {...props}>
|
||||||
|
<h1 className="mb-4 text-2xl font-bold md:text-3xl lg:text-5xl">
|
||||||
|
{node.title}
|
||||||
|
</h1>
|
||||||
|
<div className="prose">
|
||||||
|
{node.body && <FormattedText processed={node.body.processed} />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
127
components/node--property.tsx
Normal file
127
components/node--property.tsx
Normal file
|
|
@ -0,0 +1,127 @@
|
||||||
|
import Image from "next/image"
|
||||||
|
|
||||||
|
import { NodeProps } from "components/node"
|
||||||
|
|
||||||
|
export function NodeProperty({ node, viewMode, ...props }: NodeProps) {
|
||||||
|
if (viewMode === "list") {
|
||||||
|
return <NodePropertyList node={node} {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
return <NodePropertyGrid node={node} {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NodePropertyGrid({ node }) {
|
||||||
|
const thumbnail = node.field_images?.[0].field_media_image
|
||||||
|
|
||||||
|
return (
|
||||||
|
<article
|
||||||
|
className="relative overflow-hidden bg-white border rounded-md"
|
||||||
|
data-cy="node--property"
|
||||||
|
>
|
||||||
|
{node.field_status ? (
|
||||||
|
<p className="absolute z-10 px-2 py-1 text-sm text-white bg-black rounded-md top-2 right-2">
|
||||||
|
{node.field_status === "rent" ? "For Rent" : "For Sale"}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
{thumbnail && (
|
||||||
|
<Image
|
||||||
|
src={`${process.env.NEXT_PUBLIC_DRUPAL_BASE_URL}${thumbnail.uri.url}`}
|
||||||
|
width={360}
|
||||||
|
height={240}
|
||||||
|
layout="responsive"
|
||||||
|
objectFit="cover"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<p className="text-gray-500">{node.field_location.name}</p>
|
||||||
|
{node.field_size && (
|
||||||
|
<p className="flex items-center">
|
||||||
|
{node.field_size}
|
||||||
|
<span className="ml-1 text-gray-500">sqft</span>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<h4 className="text-lg font-semibold">{node.title}</h4>
|
||||||
|
{node.field_teaser && (
|
||||||
|
<p className="hidden mt-2 lg:block">{node.field_teaser}</p>
|
||||||
|
)}
|
||||||
|
<hr className="my-4" />
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center">
|
||||||
|
{node.field_beds && (
|
||||||
|
<p className="flex items-center">
|
||||||
|
{node.field_beds}
|
||||||
|
<span className="ml-1 text-gray-500">beds</span>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{node.field_baths && (
|
||||||
|
<p className="flex items-center ml-4">
|
||||||
|
{node.field_baths}
|
||||||
|
<span className="ml-1 text-gray-500">baths</span>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NodePropertyList({ node }) {
|
||||||
|
const thumbnail = node.field_images?.[0].field_media_image
|
||||||
|
|
||||||
|
return (
|
||||||
|
<article className="relative overflow-hidden bg-white border rounded-md">
|
||||||
|
{node.field_status ? (
|
||||||
|
<p className="absolute z-10 px-2 py-1 text-sm text-white bg-black rounded-md top-8 left-8">
|
||||||
|
{node.field_status === "rent" ? "For Rent" : "For Sale"}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
<div className="grid items-start grid-cols-3 p-6">
|
||||||
|
{thumbnail && (
|
||||||
|
<Image
|
||||||
|
src={`${process.env.NEXT_PUBLIC_DRUPAL_BASE_URL}${thumbnail.uri.url}`}
|
||||||
|
width={310}
|
||||||
|
height={310}
|
||||||
|
layout="responsive"
|
||||||
|
objectFit="cover"
|
||||||
|
className="rounded-lg"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div className="col-span-2 px-6">
|
||||||
|
<div className="items-center justify-between hidden mb-2 md:flex">
|
||||||
|
<p className="text-gray-500">{node.field_location.name}</p>
|
||||||
|
{node.field_size && (
|
||||||
|
<p className="flex items-center">
|
||||||
|
{node.field_size}
|
||||||
|
<span className="ml-1 text-gray-500">sqft</span>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<h4 className="text-lg font-semibold">{node.title}</h4>
|
||||||
|
{node.field_teaser && (
|
||||||
|
<p className="hidden mt-2 md:block">{node.field_teaser}</p>
|
||||||
|
)}
|
||||||
|
<hr className="my-4" />
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center">
|
||||||
|
{node.field_beds && (
|
||||||
|
<p className="flex items-center">
|
||||||
|
{node.field_beds}
|
||||||
|
<span className="ml-1 text-gray-500">beds</span>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{node.field_baths && (
|
||||||
|
<p className="flex items-center ml-4">
|
||||||
|
{node.field_baths}
|
||||||
|
<span className="ml-1 text-gray-500">baths</span>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
)
|
||||||
|
}
|
||||||
32
components/node.tsx
Normal file
32
components/node.tsx
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { DrupalNode } from "next-drupal"
|
||||||
|
|
||||||
|
import { NodePage } from "components/node--page"
|
||||||
|
import { NodeArticle } from "components/node--article"
|
||||||
|
import { NodeLandingPage } from "components/node--landing-page"
|
||||||
|
import { NodeProperty } from "components/node--property"
|
||||||
|
|
||||||
|
const nodeTypes = {
|
||||||
|
"node--page": NodePage,
|
||||||
|
"node--article": NodeArticle,
|
||||||
|
"node--landing_page": NodeLandingPage,
|
||||||
|
"node--property_listing": NodeProperty,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NodeProps {
|
||||||
|
node: DrupalNode
|
||||||
|
viewMode?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Node({ node, viewMode = "full", ...props }: NodeProps) {
|
||||||
|
if (!node) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const Component = nodeTypes[node.type]
|
||||||
|
|
||||||
|
if (!Component) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Component node={node} viewMode={viewMode} {...props} />
|
||||||
|
}
|
||||||
80
components/pager.tsx
Normal file
80
components/pager.tsx
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
import classNames from "classnames"
|
||||||
|
|
||||||
|
import { usePagination, usePaginationProps } from "hooks/use-pagination"
|
||||||
|
import Link from "next/link"
|
||||||
|
|
||||||
|
export interface PagerProps extends React.HTMLAttributes<HTMLElement> {
|
||||||
|
current: number
|
||||||
|
total: number
|
||||||
|
href: usePaginationProps["href"]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Pager({ current, total, href, ...props }: PagerProps) {
|
||||||
|
const items = usePagination({
|
||||||
|
current,
|
||||||
|
total,
|
||||||
|
href,
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav role="navigation" aria-labelledby="pagination-heading" {...props}>
|
||||||
|
<h4 className="sr-only">Pagination</h4>
|
||||||
|
<ul className="flex items-center justify-center w-auto">
|
||||||
|
{items.map((link, index) => (
|
||||||
|
<li key={index}>
|
||||||
|
{link.type === "previous" && (
|
||||||
|
<Link href={link.href as string}>
|
||||||
|
<a className="flex items-center justify-center w-12 h-12 hover:text-blue-500">
|
||||||
|
<span className="sr-only">Previous page</span>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
className="w-4 h-4"
|
||||||
|
>
|
||||||
|
<path d="M19 12H5M12 19l-7-7 7-7" />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
{link.type === "page" && (
|
||||||
|
<Link href={link.href as string} passHref>
|
||||||
|
<a
|
||||||
|
className={classNames(
|
||||||
|
"flex items-center justify-center w-12 h-12 hover:text-blue-500",
|
||||||
|
{
|
||||||
|
"text-gray-500": link.isCurrent,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{link.display}
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
{link.type === "next" && (
|
||||||
|
<Link href={link.href as string}>
|
||||||
|
<a className="flex items-center justify-center w-12 h-12 hover:text-blue-500">
|
||||||
|
<span className="sr-only">Next page</span>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
className="w-4 h-4"
|
||||||
|
>
|
||||||
|
<path d="M5 12h14M12 5l7 7-7 7" />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
)
|
||||||
|
}
|
||||||
38
components/paragraph--cards.tsx
Normal file
38
components/paragraph--cards.tsx
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { ParagraphProps } from "components/paragraph"
|
||||||
|
import { SectionHeader } from "components/section-header"
|
||||||
|
import { FormattedText } from "components/formatted-text"
|
||||||
|
import { Section } from "components/section"
|
||||||
|
|
||||||
|
export function ParagraphCards({ paragraph, ...props }: ParagraphProps) {
|
||||||
|
return (
|
||||||
|
<Section
|
||||||
|
backgroundColor={
|
||||||
|
paragraph.field_background_color === "muted" && "bg-gray-50"
|
||||||
|
}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<SectionHeader
|
||||||
|
heading={paragraph.field_heading}
|
||||||
|
text={paragraph.field_text?.processed}
|
||||||
|
links={paragraph.field_links}
|
||||||
|
/>
|
||||||
|
<div className="container px-6 mx-auto">
|
||||||
|
{paragraph.field_items?.length && (
|
||||||
|
<div className="grid justify-center gap-20 pt-20 lg:grid-cols-3">
|
||||||
|
{paragraph.field_items.map((card) => (
|
||||||
|
<div key={card.id} className="max-w-sm text-center lg:max-w-none">
|
||||||
|
<h3 className="text-2xl font-bold">{card.field_heading}</h3>
|
||||||
|
{card.field_text?.processed && (
|
||||||
|
<FormattedText
|
||||||
|
processed={card.field_text?.processed}
|
||||||
|
className="pt-2 text-lg"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
)
|
||||||
|
}
|
||||||
41
components/paragraph--faq.tsx
Normal file
41
components/paragraph--faq.tsx
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
import { ParagraphProps } from "components/paragraph"
|
||||||
|
import { Section } from "components/section"
|
||||||
|
import { SectionHeader } from "components/section-header"
|
||||||
|
import { FormattedText } from "./formatted-text"
|
||||||
|
|
||||||
|
export function ParagraphFAQ({ paragraph, ...props }: ParagraphProps) {
|
||||||
|
return (
|
||||||
|
<Section
|
||||||
|
data-cy="paragraph-faq"
|
||||||
|
backgroundColor={
|
||||||
|
paragraph.field_background_color === "muted" && "bg-gray-50"
|
||||||
|
}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<SectionHeader
|
||||||
|
heading={paragraph.field_heading}
|
||||||
|
text={paragraph.field_text?.processed}
|
||||||
|
links={paragraph.field_links}
|
||||||
|
/>
|
||||||
|
<div className="container px-6 mx-auto">
|
||||||
|
{paragraph.field_items && (
|
||||||
|
<div className="grid gap-8 pt-10 md:py-20 md:gap-12 lg:gap-20 md:grid-cols-2">
|
||||||
|
{paragraph.field_items.map((card) => (
|
||||||
|
<div key={card.id}>
|
||||||
|
<h3 className="text-xl font-semibold md:text-2xl">
|
||||||
|
{card.field_heading}
|
||||||
|
</h3>
|
||||||
|
{card.field_text?.processed && (
|
||||||
|
<FormattedText
|
||||||
|
processed={card.field_text.processed}
|
||||||
|
className="mt-2 leading-relaxed"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
)
|
||||||
|
}
|
||||||
60
components/paragraph--feature.tsx
Normal file
60
components/paragraph--feature.tsx
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
import Image from "next/image"
|
||||||
|
import classNames from "classnames"
|
||||||
|
|
||||||
|
import { absoluteURL } from "lib/utils/absolute-url"
|
||||||
|
import { Links } from "components/links"
|
||||||
|
import { FormattedText } from "components/formatted-text"
|
||||||
|
import { ParagraphProps } from "components/paragraph"
|
||||||
|
import { Section } from "components/section"
|
||||||
|
|
||||||
|
export function ParagraphFeature({ paragraph, ...props }: ParagraphProps) {
|
||||||
|
return (
|
||||||
|
<Section
|
||||||
|
data-cy="paragraph-feature"
|
||||||
|
backgroundColor={
|
||||||
|
paragraph.field_background_color === "muted" && "bg-gray-50"
|
||||||
|
}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div className="container px-6 mx-auto">
|
||||||
|
<div className="grid items-center gap-8 md:grid-flow-col-dense md:grid-cols-2 md:gap-12">
|
||||||
|
{paragraph.field_media?.field_media_image && (
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
paragraph.field_media_position === "left"
|
||||||
|
? "md:col-start-1"
|
||||||
|
: "md:col-start-2"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src={absoluteURL(
|
||||||
|
paragraph.field_media.field_media_image.uri.url
|
||||||
|
)}
|
||||||
|
alt={
|
||||||
|
paragraph.field_media.field_media_image.resourceIdObjMeta.alt
|
||||||
|
}
|
||||||
|
width={500}
|
||||||
|
height={300}
|
||||||
|
layout="responsive"
|
||||||
|
objectFit="cover"
|
||||||
|
className="rounded-lg"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex flex-col items-center text-center md:items-start md:text-left">
|
||||||
|
{paragraph.field_heading && (
|
||||||
|
<h2 className="text-3xl font-black sm:text-4xl lg:text-5xl">
|
||||||
|
{paragraph.field_heading}
|
||||||
|
</h2>
|
||||||
|
)}
|
||||||
|
<FormattedText
|
||||||
|
className="max-w-md mt-4 text-lg font-light leading-relaxed text-gray-500 sm:text-xl lg:text-2xl"
|
||||||
|
processed={paragraph.field_text.processed}
|
||||||
|
/>
|
||||||
|
{paragraph.field_link && <Links links={[paragraph.field_link]} />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
)
|
||||||
|
}
|
||||||
38
components/paragraph--hero.tsx
Normal file
38
components/paragraph--hero.tsx
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
import classNames from "classnames"
|
||||||
|
|
||||||
|
import { ParagraphProps } from "components/paragraph"
|
||||||
|
import { MediaImage } from "components/media--image"
|
||||||
|
import { SectionHeader } from "components/section-header"
|
||||||
|
|
||||||
|
export function ParagraphHero({ paragraph, ...props }: ParagraphProps) {
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
data-cy="paragraph-hero"
|
||||||
|
className={classNames(
|
||||||
|
"pt-8 md:pt-12 lg:pt-20",
|
||||||
|
paragraph.field_media?.field_media_image
|
||||||
|
? "pb-0 md:pb-0 border-b"
|
||||||
|
: "pb-8 md:pb-12 lg:pb-20"
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<SectionHeader
|
||||||
|
level={1}
|
||||||
|
heading={paragraph.field_heading}
|
||||||
|
text={paragraph.field_text?.processed}
|
||||||
|
links={paragraph.field_links}
|
||||||
|
/>
|
||||||
|
<div className="container px-6 mx-auto">
|
||||||
|
{paragraph.field_media && (
|
||||||
|
<div className="w-full h-40 mt-6 overflow-hidden sm:rounded-t-xl md:mt-12 lg:mt-20 md:h-56 lg:h-80">
|
||||||
|
<MediaImage
|
||||||
|
media={paragraph.field_media}
|
||||||
|
objectFit="cover"
|
||||||
|
priority
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
9
components/paragraph--view.tsx
Normal file
9
components/paragraph--view.tsx
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { ViewPropertiesListing } from "components/view--properties_listing"
|
||||||
|
|
||||||
|
export function ParagraphView({ paragraph, ...props }) {
|
||||||
|
if (paragraph.field_view.name === "properties--listing") {
|
||||||
|
return <ViewPropertiesListing view={paragraph.field_view} {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
33
components/paragraph.tsx
Normal file
33
components/paragraph.tsx
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { DrupalParagraph } from "next-drupal"
|
||||||
|
|
||||||
|
import { ParagraphCards } from "components/paragraph--cards"
|
||||||
|
import { ParagraphFAQ } from "components/paragraph--faq"
|
||||||
|
import { ParagraphFeature } from "components/paragraph--feature"
|
||||||
|
import { ParagraphHero } from "components/paragraph--hero"
|
||||||
|
import { ParagraphView } from "components/paragraph--view"
|
||||||
|
|
||||||
|
const paragraphTypes = {
|
||||||
|
"paragraph--cards": ParagraphCards,
|
||||||
|
"paragraph--faq": ParagraphFAQ,
|
||||||
|
"paragraph--feature": ParagraphFeature,
|
||||||
|
"paragraph--hero": ParagraphHero,
|
||||||
|
"paragraph--view": ParagraphView,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ParagraphProps {
|
||||||
|
paragraph: DrupalParagraph
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Paragraph({ paragraph, ...props }: ParagraphProps) {
|
||||||
|
if (!paragraph) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const Component = paragraphTypes[paragraph.type]
|
||||||
|
|
||||||
|
if (!Component) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Component paragraph={paragraph} {...props} />
|
||||||
|
}
|
||||||
35
components/section-header.tsx
Normal file
35
components/section-header.tsx
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { LinksProps, Links } from "components/links"
|
||||||
|
import { FormattedText } from "components/formatted-text"
|
||||||
|
|
||||||
|
interface SectionHeaderProps {
|
||||||
|
level?: number
|
||||||
|
heading: string
|
||||||
|
text: string
|
||||||
|
links?: LinksProps["links"]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SectionHeader({
|
||||||
|
level = 2,
|
||||||
|
heading,
|
||||||
|
text,
|
||||||
|
links,
|
||||||
|
...props
|
||||||
|
}: SectionHeaderProps) {
|
||||||
|
const HeadingLevel = `h${level}` as keyof JSX.IntrinsicElements
|
||||||
|
return (
|
||||||
|
<div className="container px-6 mx-auto text-center" {...props}>
|
||||||
|
{heading && (
|
||||||
|
<HeadingLevel className="text-3xl font-black sm:text-4xl md:text-5xl lg:text-6xl">
|
||||||
|
{heading}
|
||||||
|
</HeadingLevel>
|
||||||
|
)}
|
||||||
|
{text && (
|
||||||
|
<FormattedText
|
||||||
|
className="max-w-xl mx-auto mt-2 text-lg font-light leading-tight text-gray-500 sm:text-xl md:text-2xl"
|
||||||
|
processed={text}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{links?.length ? <Links links={links} /> : null}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
17
components/section.tsx
Normal file
17
components/section.tsx
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
import classNames from "classnames"
|
||||||
|
|
||||||
|
interface SectionProps extends React.HTMLAttributes<HTMLElement> {
|
||||||
|
backgroundColor?: string
|
||||||
|
children?: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Section({ backgroundColor, children, ...props }: SectionProps) {
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
className={classNames("py-8 md:py-12 lg:py-20", backgroundColor)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
290
components/view--properties_listing.tsx
Normal file
290
components/view--properties_listing.tsx
Normal file
|
|
@ -0,0 +1,290 @@
|
||||||
|
import React from "react"
|
||||||
|
import { useQuery, useQueryClient } from "@tanstack/react-query"
|
||||||
|
import { useForm } from "react-hook-form"
|
||||||
|
import { deserialize } from "next-drupal"
|
||||||
|
import classNames from "classnames"
|
||||||
|
|
||||||
|
import { FormItem } from "components/form-item"
|
||||||
|
import { Section } from "components/section"
|
||||||
|
import { Node } from "components/node"
|
||||||
|
|
||||||
|
const filters = { location: "All", status: "All", beds: "1", baths: "1" }
|
||||||
|
|
||||||
|
async function fetchView(url, params) {
|
||||||
|
const _url = new URL(url)
|
||||||
|
_url.search = new URLSearchParams(params).toString()
|
||||||
|
|
||||||
|
const result = await fetch(_url.toString())
|
||||||
|
|
||||||
|
if (!result.ok) {
|
||||||
|
throw new Error(result.statusText)
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await result.json()
|
||||||
|
|
||||||
|
return {
|
||||||
|
results: deserialize(data),
|
||||||
|
meta: data.meta,
|
||||||
|
links: data.links,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ViewPropertiesListing({ view: initialView, ...props }) {
|
||||||
|
const [page, setPage] = React.useState(0)
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
const [display, setDisplay] = React.useState<"grid" | "list">("grid")
|
||||||
|
const { register, handleSubmit, getValues, formState, reset } = useForm({
|
||||||
|
defaultValues: filters,
|
||||||
|
})
|
||||||
|
const [locations, setLocations] = React.useState([])
|
||||||
|
const {
|
||||||
|
data: view,
|
||||||
|
isLoading,
|
||||||
|
isPreviousData,
|
||||||
|
} = useQuery(
|
||||||
|
[initialView.name, page],
|
||||||
|
async () => {
|
||||||
|
// Build params from form values.
|
||||||
|
const values = getValues()
|
||||||
|
const params = {
|
||||||
|
page: page + "",
|
||||||
|
include: "field_location,field_images.field_media_image",
|
||||||
|
}
|
||||||
|
for (const filter of Object.keys(filters)) {
|
||||||
|
if (values[filter]) {
|
||||||
|
params[`views-filter[${filter}]`] = values[filter]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetchView(initialView.links.self.href.split("?")[0], params)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
initialData: initialView,
|
||||||
|
keepPreviousData: true,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Build locations dropdown from view results.
|
||||||
|
React.useEffect(() => {
|
||||||
|
const allLocations: {
|
||||||
|
name: string
|
||||||
|
id: string
|
||||||
|
}[] = view.results.map((result) => ({
|
||||||
|
name: result.field_location.name,
|
||||||
|
id: result.field_location.drupal_internal__tid,
|
||||||
|
}))
|
||||||
|
|
||||||
|
setLocations(
|
||||||
|
Array.from(new Map(allLocations.map((item) => [item.id, item])).values())
|
||||||
|
)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
async function submitForm() {
|
||||||
|
setPage(0)
|
||||||
|
await queryClient.invalidateQueries(view.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resetForm() {
|
||||||
|
reset()
|
||||||
|
submitForm()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Section backgroundColor="bg-gray-50" {...props}>
|
||||||
|
<div className="container items-start gap-10 px-6 mx-auto md:grid md:grid-cols-3">
|
||||||
|
<div className="p-6 mb-10 bg-white rounded-lg">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h4 className="text-xl font-bold">Find your place</h4>
|
||||||
|
</div>
|
||||||
|
<hr className="my-6" />
|
||||||
|
<form onSubmit={handleSubmit(submitForm)}>
|
||||||
|
<div className="grid gap-8 auto-rows-auto">
|
||||||
|
<FormItem name="location" label="Locations">
|
||||||
|
<select
|
||||||
|
id="location"
|
||||||
|
name="location"
|
||||||
|
className="border border-gray-300 rounded-md appearance-none"
|
||||||
|
{...register("location")}
|
||||||
|
>
|
||||||
|
<option value="All">Select Location</option>
|
||||||
|
{locations.map((location) => (
|
||||||
|
<option key={location.id} value={location.id}>
|
||||||
|
{location.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</FormItem>
|
||||||
|
<FormItem name="status" label="Status">
|
||||||
|
<select
|
||||||
|
id="status"
|
||||||
|
name="status"
|
||||||
|
className="border border-gray-300 rounded-md appearance-none"
|
||||||
|
{...register("status")}
|
||||||
|
>
|
||||||
|
<option value="All">Select Status</option>
|
||||||
|
<option value="sale">For Sale</option>
|
||||||
|
<option value="rent">For Rent</option>
|
||||||
|
</select>
|
||||||
|
</FormItem>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<FormItem name="beds" label="Min Beds">
|
||||||
|
<select
|
||||||
|
id="beds"
|
||||||
|
name="beds"
|
||||||
|
className="border border-gray-300 rounded-md appearance-none"
|
||||||
|
{...register("beds")}
|
||||||
|
>
|
||||||
|
{[1, 2, 3, 4].map((value) => (
|
||||||
|
<option value={value} key={value}>
|
||||||
|
{value}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</FormItem>
|
||||||
|
<FormItem name="baths" label="Min Baths">
|
||||||
|
<select
|
||||||
|
id="baths"
|
||||||
|
name="baths"
|
||||||
|
className="border border-gray-300 rounded-md appearance-none"
|
||||||
|
{...register("baths")}
|
||||||
|
>
|
||||||
|
{[1, 2, 3, 4].map((value) => (
|
||||||
|
<option value={value} key={value}>
|
||||||
|
{value}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</FormItem>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr className="my-6" />
|
||||||
|
<div className="grid items-center justify-between grid-cols-2 gap-4">
|
||||||
|
<button
|
||||||
|
className="flex items-center justify-center px-4 py-2 text-white transition-colors bg-blue-600 rounded-md hover:bg-blue-700"
|
||||||
|
data-cy="submit"
|
||||||
|
>
|
||||||
|
Search
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={classNames(
|
||||||
|
"flex items-center justify-center px-4 py-2 text-black transition-colors bg-gray-100 rounded-md hover:bg-gray-200",
|
||||||
|
{
|
||||||
|
hidden: !formState.isSubmitted,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
type="button"
|
||||||
|
onClick={() => resetForm()}
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-2">
|
||||||
|
{view.results.length ? (
|
||||||
|
<>
|
||||||
|
<div className="flex items-center justify-between px-2 pb-4">
|
||||||
|
<h3 className="text-lg" data-cy="view--results">
|
||||||
|
Found {view.meta?.count} properties.
|
||||||
|
</h3>
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={classNames(
|
||||||
|
"border h-8 w-8 flex items-center justify-center bg-white rounded-md",
|
||||||
|
{
|
||||||
|
"opacity-50": display === "grid",
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
onClick={() => setDisplay("grid")}
|
||||||
|
aria-label="grid"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
className="w-5 h-5"
|
||||||
|
>
|
||||||
|
<path d="M3 3h7v7H3zM14 3h7v7h-7zM14 14h7v7h-7zM3 14h7v7H3z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={classNames(
|
||||||
|
"border h-8 w-8 flex items-center justify-center bg-white rounded-md",
|
||||||
|
{
|
||||||
|
"opacity-50": display === "list",
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
onClick={() => setDisplay("list")}
|
||||||
|
aria-label="list"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
className="w-5 h-5"
|
||||||
|
>
|
||||||
|
<path d="M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={classNames("grid grid-flow-row gap-10", {
|
||||||
|
"md:grid-cols-2": display === "grid",
|
||||||
|
"opacity-50": isLoading,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{view.results.map((node) => (
|
||||||
|
<Node key={node.id} viewMode={display} node={node} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<hr className="my-10" />
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<button
|
||||||
|
className={classNames(
|
||||||
|
"flex items-center bg-black justify-center px-4 py-2 transition-colors rounded-md ",
|
||||||
|
page == 0
|
||||||
|
? "bg-gray-100 hover:bg-gray-200 text-black opacity-50"
|
||||||
|
: "bg-black text-white"
|
||||||
|
)}
|
||||||
|
data-cy="pager-previous"
|
||||||
|
onClick={() => setPage((old) => Math.max(old - 1, 0))}
|
||||||
|
disabled={page === 0}
|
||||||
|
>
|
||||||
|
Previous
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={classNames(
|
||||||
|
"flex items-center bg-black justify-center px-4 py-2 transition-colors rounded-md ",
|
||||||
|
isPreviousData || !view?.links.next
|
||||||
|
? "bg-gray-100 hover:bg-gray-200 text-black opacity-50"
|
||||||
|
: "bg-black text-white"
|
||||||
|
)}
|
||||||
|
data-cy="pager-next"
|
||||||
|
onClick={() => {
|
||||||
|
if (!isPreviousData && view.links.next) {
|
||||||
|
setPage((old) => old + 1)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={isPreviousData || !view?.links.next}
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<p className="py-20 text-center">No properties found.</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
)
|
||||||
|
}
|
||||||
3
cypress.json
Normal file
3
cypress.json
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"baseUrl": "http://localhost:3001"
|
||||||
|
}
|
||||||
5
cypress/fixtures/example.json
Normal file
5
cypress/fixtures/example.json
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"name": "Using fixtures to represent data",
|
||||||
|
"email": "hello@cypress.io",
|
||||||
|
"body": "Fixtures are a great way to mock data for responses to routes"
|
||||||
|
}
|
||||||
14
cypress/integration/about.spec.tsx
Normal file
14
cypress/integration/about.spec.tsx
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
/// <reference types="cypress"/>
|
||||||
|
|
||||||
|
context("About", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit("/about")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should convert inline images to Next.js images", () => {
|
||||||
|
cy.get("img[data-nimg='intrinsic']")
|
||||||
|
cy.get("figcaption").contains("This is the caption")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
export {}
|
||||||
66
cypress/integration/blog.spec.tsx
Normal file
66
cypress/integration/blog.spec.tsx
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
/// <reference types="cypress"/>
|
||||||
|
|
||||||
|
context("Blog", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit("/blog")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should render articles", () => {
|
||||||
|
cy.get("h1").contains("Latest Articles.")
|
||||||
|
cy.get("[data-cy=node--article]").should("have.length.gt", 1)
|
||||||
|
|
||||||
|
cy.get("[data-cy=node--article] h2").contains(
|
||||||
|
"Dynamic Routing and Static Generation"
|
||||||
|
)
|
||||||
|
cy.get("[data-cy=node--article] [data-cy=node--meta]").contains(
|
||||||
|
"Posted by Arshad"
|
||||||
|
)
|
||||||
|
cy.get("[data-cy=node--article] [data-cy=node--meta]").contains(
|
||||||
|
"June 13, 2021"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
context("Translation", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit("/es/blog")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should render articles", () => {
|
||||||
|
cy.get("h1").contains("Últimas Publicaciones.")
|
||||||
|
cy.get("[data-cy=node--article]").should("have.length.gt", 1)
|
||||||
|
|
||||||
|
cy.get("[data-cy=node--article] h2").contains(
|
||||||
|
"Enrutamiento dinámico y Generación Estática"
|
||||||
|
)
|
||||||
|
cy.get("[data-cy=node--article] [data-cy=node--meta]").contains(
|
||||||
|
"Posted by Arshad"
|
||||||
|
)
|
||||||
|
cy.get("[data-cy=node--article] [data-cy=node--meta]").contains(
|
||||||
|
"June 14, 2021"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
context("Blog node", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit("/blog/learn-how-pre-render-pages-using-static-generation-nextjs")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should render article node", () => {
|
||||||
|
cy.get("h1").contains(
|
||||||
|
"Learn How to Pre-render Pages Using Static Generation with Next.js"
|
||||||
|
)
|
||||||
|
cy.get("[data-cy=node--article] [data-cy=node--body]").should(
|
||||||
|
"not.be.empty"
|
||||||
|
)
|
||||||
|
cy.get("[data-cy=node--article] [data-cy=node--meta]").contains(
|
||||||
|
"Posted by Arshad"
|
||||||
|
)
|
||||||
|
cy.get("[data-cy=node--article] [data-cy=node--meta]").contains(
|
||||||
|
"June 13, 2021"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
export {}
|
||||||
63
cypress/integration/home.spec.tsx
Normal file
63
cypress/integration/home.spec.tsx
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
/// <reference types="cypress"/>
|
||||||
|
|
||||||
|
context("Home", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit("")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should render paragraphs", () => {
|
||||||
|
cy.get("[data-cy=paragraph-hero] h1").contains("Build Something Amazing")
|
||||||
|
cy.get("[data-cy=paragraph-hero] p").contains(
|
||||||
|
"Must today firm from bag. Investment try cold a when capital. Everything wait person service."
|
||||||
|
)
|
||||||
|
cy.get("[data-cy=paragraph-hero] [alt=Hero]").should("be.visible")
|
||||||
|
|
||||||
|
cy.get("[data-cy=paragraph-feature] h2").contains("Marketing Strategy")
|
||||||
|
cy.get("[data-cy=paragraph-feature] img[alt=Feature]").should("be.visible")
|
||||||
|
|
||||||
|
cy.get("[data-cy=paragraph-faq] h2").contains("Frequently Asked Questions")
|
||||||
|
cy.get("[data-cy=paragraph-faq] h3").contains(
|
||||||
|
"Move weight here just either attorney?"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should render menu items", () => {
|
||||||
|
cy.get("[data-cy=navbar-menu] a").contains("Home")
|
||||||
|
cy.get("[data-cy=navbar-menu] a").contains("Blog")
|
||||||
|
cy.get("[data-cy=navbar-menu] a").contains("Properties")
|
||||||
|
cy.get("[data-cy=navbar-menu] a").contains("About")
|
||||||
|
cy.get("[data-cy=navbar-menu] a").contains("GitHub")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
context("Translation", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit("/es")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should render paragraphs", () => {
|
||||||
|
cy.get("[data-cy=paragraph-hero] h1").contains("Construye Algo Asombroso")
|
||||||
|
cy.get("[data-cy=paragraph-hero] p").contains(
|
||||||
|
"Dicta laboriosam magnam possimus ad. Ratione rem nihil nostrum dolore reiciendis enim."
|
||||||
|
)
|
||||||
|
cy.get("[data-cy=paragraph-hero] img[alt=Hero]").should("be.visible")
|
||||||
|
|
||||||
|
cy.get("[data-cy=paragraph-feature] h2").contains("Marketing Strategy")
|
||||||
|
cy.get("[data-cy=paragraph-feature] img[alt=Feature]").should("be.visible")
|
||||||
|
|
||||||
|
cy.get("[data-cy=paragraph-faq] h2").contains("Frequently Asked Questions")
|
||||||
|
cy.get("[data-cy=paragraph-faq] h3").contains(
|
||||||
|
"Move weight here just either attorney?"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should render menu items localized", () => {
|
||||||
|
cy.get("[data-cy=navbar-menu] a").contains("Inicio")
|
||||||
|
cy.get("[data-cy=navbar-menu] a").contains("Blog")
|
||||||
|
cy.get("[data-cy=navbar-menu] a").contains("Propiedades")
|
||||||
|
cy.get("[data-cy=navbar-menu] a").contains("Acerca")
|
||||||
|
cy.get("[data-cy=navbar-menu] a").contains("GitHub")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
export {}
|
||||||
19
cypress/integration/locale.spec.tsx
Normal file
19
cypress/integration/locale.spec.tsx
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
/// <reference types="cypress"/>
|
||||||
|
|
||||||
|
context("Home", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit("")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should switch locale when clicking on the locale switcher", () => {
|
||||||
|
cy.get("[data-cy=local-switcher-en]").contains("en")
|
||||||
|
cy.get("[data-cy=local-switcher-es]").contains("es")
|
||||||
|
|
||||||
|
cy.get("[data-cy=paragraph-hero] h1").contains("Build Something Amazing")
|
||||||
|
cy.get("[data-cy=local-switcher-es]").click()
|
||||||
|
cy.url().should("include", "/es")
|
||||||
|
cy.get("[data-cy=paragraph-hero] h1").contains("Construye Algo Asombroso")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
export {}
|
||||||
32
cypress/integration/properties.spec.tsx
Normal file
32
cypress/integration/properties.spec.tsx
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
/// <reference types="cypress"/>
|
||||||
|
|
||||||
|
context("Properties", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit("/properties")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should render view with meta and pager", () => {
|
||||||
|
cy.get("[data-cy=view--results]").contains("Found 12 properties.")
|
||||||
|
cy.get("[data-cy=node--property]").should("have.length", 4)
|
||||||
|
cy.get("[data-cy=node--property]")
|
||||||
|
.find("h4")
|
||||||
|
.contains("484 Robert Crest Apt. 875")
|
||||||
|
cy.get("[data-cy=pager-next]").should("not.be.disabled")
|
||||||
|
cy.get("[data-cy=pager-previous]").should("be.disabled")
|
||||||
|
|
||||||
|
cy.get("[data-cy=pager-next]").click()
|
||||||
|
cy.get("[data-cy=node--property]").should("have.length", 4)
|
||||||
|
cy.get("[data-cy=pager-next]").should("not.be.disabled")
|
||||||
|
cy.get("[data-cy=pager-previous]").should("not.be.disabled")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should allow view to be filtered", () => {
|
||||||
|
cy.get("select[name='location']").select("San Francisco, CA")
|
||||||
|
cy.get("select[name='status']").select("For Sale")
|
||||||
|
cy.get("select[name='beds']").select("4")
|
||||||
|
cy.get("[data-cy=submit]").contains("Search").click()
|
||||||
|
cy.get("[data-cy=node--property]").should("have.length", 1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
export {}
|
||||||
22
cypress/plugins/index.js
Normal file
22
cypress/plugins/index.js
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
/// <reference types="cypress" />
|
||||||
|
// ***********************************************************
|
||||||
|
// This example plugins/index.js can be used to load plugins
|
||||||
|
//
|
||||||
|
// You can change the location of this file or turn off loading
|
||||||
|
// the plugins file with the 'pluginsFile' configuration option.
|
||||||
|
//
|
||||||
|
// You can read more here:
|
||||||
|
// https://on.cypress.io/plugins-guide
|
||||||
|
// ***********************************************************
|
||||||
|
|
||||||
|
// This function is called when a project is opened or re-opened (e.g. due to
|
||||||
|
// the project's config changing)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {Cypress.PluginConfig}
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
module.exports = (on, config) => {
|
||||||
|
// `on` is used to hook into various events Cypress emits
|
||||||
|
// `config` is the resolved Cypress config
|
||||||
|
}
|
||||||
25
cypress/support/commands.js
Normal file
25
cypress/support/commands.js
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
// ***********************************************
|
||||||
|
// This example commands.js shows you how to
|
||||||
|
// create various custom commands and overwrite
|
||||||
|
// existing commands.
|
||||||
|
//
|
||||||
|
// For more comprehensive examples of custom
|
||||||
|
// commands please read more here:
|
||||||
|
// https://on.cypress.io/custom-commands
|
||||||
|
// ***********************************************
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// -- This is a parent command --
|
||||||
|
// Cypress.Commands.add('login', (email, password) => { ... })
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// -- This is a child command --
|
||||||
|
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// -- This is a dual command --
|
||||||
|
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// -- This will overwrite an existing command --
|
||||||
|
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
|
||||||
20
cypress/support/index.js
Normal file
20
cypress/support/index.js
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
// ***********************************************************
|
||||||
|
// This example support/index.js is processed and
|
||||||
|
// loaded automatically before your test files.
|
||||||
|
//
|
||||||
|
// This is a great place to put global configuration and
|
||||||
|
// behavior that modifies Cypress.
|
||||||
|
//
|
||||||
|
// You can change the location of this file or turn off
|
||||||
|
// automatically serving support files with the
|
||||||
|
// 'supportFile' configuration option.
|
||||||
|
//
|
||||||
|
// You can read more here:
|
||||||
|
// https://on.cypress.io/configuration
|
||||||
|
// ***********************************************************
|
||||||
|
|
||||||
|
// Import commands.js using ES2015 syntax:
|
||||||
|
import './commands'
|
||||||
|
|
||||||
|
// Alternatively you can use CommonJS syntax:
|
||||||
|
// require('./commands')
|
||||||
83
hooks/use-pagination.tsx
Normal file
83
hooks/use-pagination.tsx
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
import * as React from "react"
|
||||||
|
import { LinkProps } from "next/link"
|
||||||
|
|
||||||
|
type PagerItemType = "page" | "previous" | "next"
|
||||||
|
|
||||||
|
export interface PagerItem {
|
||||||
|
type: PagerItemType
|
||||||
|
page: number
|
||||||
|
display: string
|
||||||
|
href: LinkProps["href"]
|
||||||
|
isCurrent?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface usePaginationProps {
|
||||||
|
total: number
|
||||||
|
current: number
|
||||||
|
href: (page: PagerItem["page"]) => LinkProps["href"]
|
||||||
|
show?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export const usePagination = ({
|
||||||
|
total,
|
||||||
|
current,
|
||||||
|
href,
|
||||||
|
show = 9,
|
||||||
|
}: usePaginationProps): PagerItem[] => {
|
||||||
|
return React.useMemo<PagerItem[]>((): PagerItem[] => {
|
||||||
|
const pagerMiddle = Math.ceil(show / 2)
|
||||||
|
const pagerCurrent = current + 1
|
||||||
|
let pagerFirst = pagerCurrent - pagerMiddle + 1
|
||||||
|
let pagerLast = pagerCurrent + show - pagerMiddle
|
||||||
|
|
||||||
|
show = total < show ? total : show
|
||||||
|
|
||||||
|
// Adjust start end end based on position.
|
||||||
|
if (pagerLast > total) {
|
||||||
|
pagerFirst = pagerFirst + (total - pagerLast)
|
||||||
|
pagerLast = total
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pagerFirst <= 0) {
|
||||||
|
pagerFirst = 1
|
||||||
|
pagerLast = pagerLast + (1 - pagerFirst)
|
||||||
|
}
|
||||||
|
|
||||||
|
const items: PagerItem[] = []
|
||||||
|
|
||||||
|
if (current !== 0) {
|
||||||
|
items.push({
|
||||||
|
type: "previous",
|
||||||
|
display: "Previous",
|
||||||
|
page: current - 1,
|
||||||
|
href: href(current - 1),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
items.push(
|
||||||
|
...Array.from(Array(show).keys()).map(
|
||||||
|
(pageNumber: number): PagerItem | null => {
|
||||||
|
const page = pageNumber + pagerFirst - 1
|
||||||
|
return {
|
||||||
|
type: "page",
|
||||||
|
page,
|
||||||
|
display: `${page + 1}`,
|
||||||
|
isCurrent: page === current,
|
||||||
|
href: href(page),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (current !== total - 1) {
|
||||||
|
items.push({
|
||||||
|
type: "next",
|
||||||
|
display: "Next",
|
||||||
|
page: current + 1,
|
||||||
|
href: href(current + 1),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return items
|
||||||
|
}, [total, current])
|
||||||
|
}
|
||||||
13
lib/drupal.ts
Normal file
13
lib/drupal.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { DrupalClient } from "next-drupal"
|
||||||
|
|
||||||
|
export const drupal = new DrupalClient(
|
||||||
|
process.env.NEXT_PUBLIC_DRUPAL_BASE_URL,
|
||||||
|
{
|
||||||
|
frontPage: "/home",
|
||||||
|
previewSecret: "secret",
|
||||||
|
auth: {
|
||||||
|
clientId: process.env.DRUPAL_CLIENT_ID,
|
||||||
|
clientSecret: process.env.DRUPAL_CLIENT_SECRET,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
22
lib/get-menus.ts
Normal file
22
lib/get-menus.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { GetStaticPropsContext } from "next"
|
||||||
|
import { DrupalMenuLinkContent } from "next-drupal"
|
||||||
|
import { drupal } from "lib/drupal"
|
||||||
|
|
||||||
|
export async function getMenus(context: GetStaticPropsContext): Promise<{
|
||||||
|
main: DrupalMenuLinkContent[]
|
||||||
|
footer: DrupalMenuLinkContent[]
|
||||||
|
}> {
|
||||||
|
const { tree: main } = await drupal.getMenu("main", {
|
||||||
|
locale: context.locale,
|
||||||
|
defaultLocale: context.defaultLocale,
|
||||||
|
})
|
||||||
|
const { tree: footer } = await drupal.getMenu("footer", {
|
||||||
|
locale: context.locale,
|
||||||
|
defaultLocale: context.defaultLocale,
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
main,
|
||||||
|
footer,
|
||||||
|
}
|
||||||
|
}
|
||||||
39
lib/get-params.ts
Normal file
39
lib/get-params.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
import { DrupalJsonApiParams } from "drupal-jsonapi-params"
|
||||||
|
|
||||||
|
export function getParams(resourceType: string) {
|
||||||
|
const apiParams = new DrupalJsonApiParams().addFilter(
|
||||||
|
"field_site.meta.drupal_internal__target_id",
|
||||||
|
process.env.DRUPAL_SITE_ID
|
||||||
|
)
|
||||||
|
|
||||||
|
if (resourceType === "node--landing_page") {
|
||||||
|
apiParams
|
||||||
|
.addInclude([
|
||||||
|
"field_sections",
|
||||||
|
"field_sections.field_media.field_media_image",
|
||||||
|
"field_sections.field_items",
|
||||||
|
"field_sections.field_reusable_paragraph.paragraphs.field_items",
|
||||||
|
])
|
||||||
|
.addFields("node--landing_page", [
|
||||||
|
"title",
|
||||||
|
"field_sections",
|
||||||
|
"path",
|
||||||
|
"status",
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resourceType === "node--article") {
|
||||||
|
apiParams.addInclude(["field_image", "uid"])
|
||||||
|
apiParams.addFields(resourceType, [
|
||||||
|
"title",
|
||||||
|
"body",
|
||||||
|
"uid",
|
||||||
|
"created",
|
||||||
|
"field_image",
|
||||||
|
"status",
|
||||||
|
"metatag",
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
return apiParams.getQueryObject()
|
||||||
|
}
|
||||||
3
lib/utils/absolute-url.ts
Normal file
3
lib/utils/absolute-url.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export function absoluteURL(uri: string) {
|
||||||
|
return `${process.env.NEXT_PUBLIC_DRUPAL_BASE_URL}${uri}`
|
||||||
|
}
|
||||||
8
lib/utils/format-date.ts
Normal file
8
lib/utils/format-date.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
export function formatDate(input: string): string {
|
||||||
|
const date = new Date(input)
|
||||||
|
return date.toLocaleDateString("en-US", {
|
||||||
|
month: "long",
|
||||||
|
day: "numeric",
|
||||||
|
year: "numeric",
|
||||||
|
})
|
||||||
|
}
|
||||||
3
lib/utils/is-relative.ts
Normal file
3
lib/utils/is-relative.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export function isRelative(url: string) {
|
||||||
|
return !new RegExp("^(?:[a-z]+:)?//", "i").test(url)
|
||||||
|
}
|
||||||
5
next-env.d.ts
vendored
Normal file
5
next-env.d.ts
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
/// <reference types="next" />
|
||||||
|
/// <reference types="next/image-types/global" />
|
||||||
|
|
||||||
|
// NOTE: This file should not be edited
|
||||||
|
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||||
28
next.config.js
Normal file
28
next.config.js
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
module.exports = {
|
||||||
|
swcMinify: true,
|
||||||
|
i18n: {
|
||||||
|
locales: ["en", "es"],
|
||||||
|
defaultLocale: "en",
|
||||||
|
},
|
||||||
|
images: {
|
||||||
|
domains: [process.env.NEXT_IMAGE_DOMAIN],
|
||||||
|
},
|
||||||
|
async rewrites() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
source: "/blog",
|
||||||
|
destination: "/blog/page/0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: "/es",
|
||||||
|
destination: "/es/home",
|
||||||
|
locale: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: "/en/principal",
|
||||||
|
destination: "/",
|
||||||
|
locale: false,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
40
package.json
Normal file
40
package.json
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
{
|
||||||
|
"name": "example-marketing",
|
||||||
|
"version": "1.5.0",
|
||||||
|
"private": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev -p 3001",
|
||||||
|
"build": "next build",
|
||||||
|
"preview": "next build && next start -p 3001",
|
||||||
|
"lint": "next lint",
|
||||||
|
"cy:open": "cypress open",
|
||||||
|
"cy:run": "cypress run",
|
||||||
|
"test:e2e": "start-server-and-test 'yarn preview' http://localhost:3001 cy:open",
|
||||||
|
"test:e2e:ci": "start-server-and-test 'yarn preview' http://localhost:3001 cy:run"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@tailwindcss/forms": "^0.4.0",
|
||||||
|
"@tailwindcss/typography": "^0.5.1",
|
||||||
|
"@tanstack/react-query": "^4.0.10",
|
||||||
|
"classnames": "^2.3.1",
|
||||||
|
"drupal-jsonapi-params": "^1.2.2",
|
||||||
|
"html-react-parser": "^1.2.7",
|
||||||
|
"next": "^12.2.3",
|
||||||
|
"next-drupal": "^1.6.0",
|
||||||
|
"nprogress": "^0.2.0",
|
||||||
|
"react": "^17.0.2",
|
||||||
|
"react-dom": "^17.0.2",
|
||||||
|
"react-hook-form": "^7.8.6"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/core": "^7.12.9",
|
||||||
|
"@types/node": "^17.0.21",
|
||||||
|
"@types/react": "^17.0.0",
|
||||||
|
"autoprefixer": "^10.4.2",
|
||||||
|
"eslint-config-next": "^12.0.10",
|
||||||
|
"postcss": "^8.4.5",
|
||||||
|
"tailwindcss": "^3.0.15",
|
||||||
|
"typescript": "^4.5.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
112
pages/[[...slug]].tsx
Normal file
112
pages/[[...slug]].tsx
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
import * as React from "react"
|
||||||
|
import {
|
||||||
|
GetStaticPathsContext,
|
||||||
|
GetStaticPathsResult,
|
||||||
|
GetStaticPropsContext,
|
||||||
|
GetStaticPropsResult,
|
||||||
|
} from "next"
|
||||||
|
import Head from "next/head"
|
||||||
|
import { useRouter } from "next/router"
|
||||||
|
import { DrupalNode } from "next-drupal"
|
||||||
|
|
||||||
|
import { drupal } from "lib/drupal"
|
||||||
|
import { getMenus } from "lib/get-menus"
|
||||||
|
import { absoluteURL } from "lib/utils/absolute-url"
|
||||||
|
import { getParams } from "lib/get-params"
|
||||||
|
import { Node } from "components/node"
|
||||||
|
import { Layout, LayoutProps } from "components/layout"
|
||||||
|
import { Meta } from "components/meta"
|
||||||
|
|
||||||
|
const RESOURCE_TYPES = ["node--page", "node--landing_page", "node--article"]
|
||||||
|
|
||||||
|
interface NodePageProps extends LayoutProps {
|
||||||
|
node: DrupalNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function NodePage({ node, menus }: NodePageProps) {
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout menus={menus}>
|
||||||
|
<Meta title={node.title} tags={node.metatag} path={node.path?.alias} />
|
||||||
|
<Head>
|
||||||
|
{node.content_translations?.map((translation, index) =>
|
||||||
|
translation.langcode !== router.locale ? (
|
||||||
|
<link
|
||||||
|
key={index}
|
||||||
|
rel="alternate"
|
||||||
|
hrefLang={translation.langcode}
|
||||||
|
href={absoluteURL(translation.path)}
|
||||||
|
/>
|
||||||
|
) : null
|
||||||
|
)}
|
||||||
|
</Head>
|
||||||
|
<Node node={node} />
|
||||||
|
</Layout>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getStaticPaths(
|
||||||
|
context: GetStaticPathsContext
|
||||||
|
): Promise<GetStaticPathsResult> {
|
||||||
|
return {
|
||||||
|
paths: await drupal.getStaticPathsFromContext(RESOURCE_TYPES, context, {
|
||||||
|
params: {
|
||||||
|
filter: {
|
||||||
|
"field_site.meta.drupal_internal__target_id":
|
||||||
|
process.env.DRUPAL_SITE_ID,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
fallback: "blocking",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getStaticProps(
|
||||||
|
context: GetStaticPropsContext
|
||||||
|
): Promise<GetStaticPropsResult<NodePageProps>> {
|
||||||
|
const path = await drupal.translatePathFromContext(context)
|
||||||
|
|
||||||
|
if (!path || !RESOURCE_TYPES.includes(path.jsonapi.resourceName)) {
|
||||||
|
return {
|
||||||
|
notFound: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const type = path.jsonapi.resourceName
|
||||||
|
|
||||||
|
const node = await drupal.getResourceFromContext<DrupalNode>(path, context, {
|
||||||
|
params: getParams(type),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!node || (!context.preview && node?.status === false)) {
|
||||||
|
return {
|
||||||
|
notFound: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load initial view data.
|
||||||
|
if (type === "node--landing_page") {
|
||||||
|
for (const section of node.field_sections) {
|
||||||
|
if (section.type === "paragraph--view" && section.field_view) {
|
||||||
|
const view = await drupal.getView(section.field_view, {
|
||||||
|
params: {
|
||||||
|
include: "field_location,field_images.field_media_image",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
section.field_view = {
|
||||||
|
name: section.field_view,
|
||||||
|
...view,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
node,
|
||||||
|
menus: await getMenus(context),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
31
pages/_app.tsx
Normal file
31
pages/_app.tsx
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
import * as React from "react"
|
||||||
|
import Router from "next/router"
|
||||||
|
import { QueryClient, QueryClientProvider, Hydrate } from "@tanstack/react-query"
|
||||||
|
import NProgress from "nprogress"
|
||||||
|
import { syncDrupalPreviewRoutes } from "next-drupal"
|
||||||
|
import "nprogress/nprogress.css"
|
||||||
|
|
||||||
|
import "styles/globals.css"
|
||||||
|
|
||||||
|
NProgress.configure({ showSpinner: false })
|
||||||
|
|
||||||
|
Router.events.on("routeChangeStart", function (path) {
|
||||||
|
syncDrupalPreviewRoutes(path)
|
||||||
|
NProgress.start()
|
||||||
|
})
|
||||||
|
Router.events.on("routeChangeComplete", () => NProgress.done())
|
||||||
|
Router.events.on("routeChangeError", () => NProgress.done())
|
||||||
|
|
||||||
|
export default function App({ Component, pageProps }) {
|
||||||
|
const queryClientRef = React.useRef<QueryClient>()
|
||||||
|
if (!queryClientRef.current) {
|
||||||
|
queryClientRef.current = new QueryClient()
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<QueryClientProvider client={queryClientRef.current}>
|
||||||
|
<Hydrate state={pageProps.dehydratedState}>
|
||||||
|
<Component {...pageProps} />
|
||||||
|
</Hydrate>
|
||||||
|
</QueryClientProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
17
pages/_document.tsx
Normal file
17
pages/_document.tsx
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
import NextDocument, { Html, Main, NextScript, Head } from "next/document"
|
||||||
|
|
||||||
|
export default class Document extends NextDocument {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Html lang="en" dir="ltr">
|
||||||
|
<Head>
|
||||||
|
<meta charSet="utf-8" />
|
||||||
|
</Head>
|
||||||
|
<body>
|
||||||
|
<Main />
|
||||||
|
<NextScript />
|
||||||
|
</body>
|
||||||
|
</Html>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
8
pages/api/exit-preview.ts
Normal file
8
pages/api/exit-preview.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { NextApiResponse } from "next"
|
||||||
|
|
||||||
|
export default async function exit(_, response: NextApiResponse) {
|
||||||
|
response.clearPreviewData()
|
||||||
|
|
||||||
|
response.writeHead(307, { Location: "/" })
|
||||||
|
response.end()
|
||||||
|
}
|
||||||
5
pages/api/preview.ts
Normal file
5
pages/api/preview.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { drupal } from "lib/drupal"
|
||||||
|
|
||||||
|
export default async function (request, response) {
|
||||||
|
return drupal.preview(request, response)
|
||||||
|
}
|
||||||
34
pages/api/revalidate.ts
Normal file
34
pages/api/revalidate.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
import { NextApiRequest, NextApiResponse } from "next"
|
||||||
|
|
||||||
|
export default async function handler(
|
||||||
|
request: NextApiRequest,
|
||||||
|
response: NextApiResponse
|
||||||
|
) {
|
||||||
|
let slug = request.query.slug as string
|
||||||
|
const secret = request.query.secret as string
|
||||||
|
|
||||||
|
// Validate secret.
|
||||||
|
if (secret !== process.env.DRUPAL_PREVIEW_SECRET) {
|
||||||
|
return response.status(401).json({ message: "Invalid secret." })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate slug.
|
||||||
|
if (!slug) {
|
||||||
|
return response.status(400).json({ message: "Invalid slug." })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fix for home slug.
|
||||||
|
if (slug === process.env.DRUPAL_FRONT_PAGE) {
|
||||||
|
slug = "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await response.revalidate(slug)
|
||||||
|
|
||||||
|
return response.json({})
|
||||||
|
} catch (error) {
|
||||||
|
return response.status(404).json({
|
||||||
|
message: error.message,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
126
pages/blog/page/[page].tsx
Normal file
126
pages/blog/page/[page].tsx
Normal file
|
|
@ -0,0 +1,126 @@
|
||||||
|
import * as React from "react"
|
||||||
|
import { GetStaticPathsResult, GetStaticPropsResult } from "next"
|
||||||
|
import { useRouter } from "next/router"
|
||||||
|
import { DrupalNode, JsonApiResponse } from "next-drupal"
|
||||||
|
import { DrupalJsonApiParams } from "drupal-jsonapi-params"
|
||||||
|
|
||||||
|
import { drupal } from "lib/drupal"
|
||||||
|
import { getMenus } from "lib/get-menus"
|
||||||
|
import { Layout, LayoutProps } from "components/layout"
|
||||||
|
import { Pager, PagerProps } from "components/pager"
|
||||||
|
import { Node } from "components/node"
|
||||||
|
import { Meta } from "components/meta"
|
||||||
|
|
||||||
|
export const NUMBER_OF_POSTS_PER_PAGE = 2
|
||||||
|
|
||||||
|
export interface BlogPageProps extends LayoutProps {
|
||||||
|
page: Pick<PagerProps, "current" | "total">
|
||||||
|
nodes: DrupalNode[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function BlogPage({ nodes, menus, page }: BlogPageProps) {
|
||||||
|
const { locale } = useRouter()
|
||||||
|
const title = locale === "en" ? "Latest Articles." : "Últimas Publicaciones."
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout menus={menus}>
|
||||||
|
<Meta title={title} />
|
||||||
|
<div className="container max-w-6xl px-6 pt-10 mx-auto md:py-20">
|
||||||
|
<h1 className="mb-10 text-3xl font-black sm:text-4xl md:text-5xl lg:text-6xl">
|
||||||
|
{title}
|
||||||
|
</h1>
|
||||||
|
{nodes.length ? (
|
||||||
|
<div className="grid gap-20 md:grid-cols-2">
|
||||||
|
{nodes.map((article) => (
|
||||||
|
<Node viewMode="teaser" key={article.id} node={article} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="py-6">No posts found</p>
|
||||||
|
)}
|
||||||
|
{page ? (
|
||||||
|
<Pager
|
||||||
|
current={page.current}
|
||||||
|
total={page.total}
|
||||||
|
href={(page) => (page === 0 ? `/blog` : `/blog/page/${page}`)}
|
||||||
|
className="py-8 mt-8"
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getStaticPaths(): Promise<GetStaticPathsResult> {
|
||||||
|
// Use SSG for the first pages, then fallback to SSR for other pages.
|
||||||
|
const paths = Array(5)
|
||||||
|
.fill(0)
|
||||||
|
.map((_, page) => ({
|
||||||
|
params: {
|
||||||
|
page: `${page + 1}`,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
return {
|
||||||
|
paths,
|
||||||
|
fallback: "blocking",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getStaticProps(
|
||||||
|
context
|
||||||
|
): Promise<GetStaticPropsResult<BlogPageProps>> {
|
||||||
|
const current = parseInt(context.params.page)
|
||||||
|
|
||||||
|
const params = new DrupalJsonApiParams()
|
||||||
|
.addFilter(
|
||||||
|
"field_site.meta.drupal_internal__target_id",
|
||||||
|
process.env.DRUPAL_SITE_ID
|
||||||
|
)
|
||||||
|
.addInclude(["uid", "field_image"])
|
||||||
|
.addFields("node--article", [
|
||||||
|
"title",
|
||||||
|
"path",
|
||||||
|
"body",
|
||||||
|
"uid",
|
||||||
|
"created",
|
||||||
|
"field_image",
|
||||||
|
])
|
||||||
|
.addFields("user--user", ["field_name"])
|
||||||
|
.addFilter("status", "1")
|
||||||
|
.addSort("created", "DESC")
|
||||||
|
|
||||||
|
const result = await drupal.getResourceCollectionFromContext<JsonApiResponse>(
|
||||||
|
"node--article",
|
||||||
|
context,
|
||||||
|
{
|
||||||
|
deserialize: false,
|
||||||
|
params: {
|
||||||
|
...params.getQueryObject(),
|
||||||
|
page: {
|
||||||
|
limit: NUMBER_OF_POSTS_PER_PAGE,
|
||||||
|
offset: context.params.page ? NUMBER_OF_POSTS_PER_PAGE * current : 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!result.data?.length) {
|
||||||
|
return {
|
||||||
|
notFound: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodes = drupal.deserialize(result) as DrupalNode[]
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
nodes,
|
||||||
|
page: {
|
||||||
|
current,
|
||||||
|
total: Math.ceil(result.meta.count / NUMBER_OF_POSTS_PER_PAGE),
|
||||||
|
},
|
||||||
|
menus: await getMenus(context),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
6
postcss.config.js
Normal file
6
postcss.config.js
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
BIN
public/meta.jpg
Normal file
BIN
public/meta.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 65 KiB |
2
public/robots.txt
Normal file
2
public/robots.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
User-agent: *
|
||||||
|
Allow: /
|
||||||
3
styles/globals.css
Normal file
3
styles/globals.css
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
14
tailwind.config.js
Normal file
14
tailwind.config.js
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
module.exports = {
|
||||||
|
mode: "jit",
|
||||||
|
content: [
|
||||||
|
"./pages/**/*.{js,ts,jsx,tsx}",
|
||||||
|
"./components/**/*.{js,ts,jsx,tsx}",
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
variants: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [require("@tailwindcss/forms"), require("@tailwindcss/typography")],
|
||||||
|
}
|
||||||
29
tsconfig.json
Normal file
29
tsconfig.json
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es5",
|
||||||
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": false,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"baseUrl": "./",
|
||||||
|
"paths": {
|
||||||
|
"@/components/*": ["src/components/*"],
|
||||||
|
"@/nodes/*": ["src/nodes/*"],
|
||||||
|
"@/paragraphs/*": ["src/paragraphs/*"],
|
||||||
|
"@/views/*": ["src/views/*"],
|
||||||
|
"@utils/*": ["src/utils/*"],
|
||||||
|
"@/config": ["src/config"]
|
||||||
|
},
|
||||||
|
"incremental": true
|
||||||
|
},
|
||||||
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
0
types/.gitkeep
Normal file
0
types/.gitkeep
Normal file
17
types/drupal.d.ts
vendored
Normal file
17
types/drupal.d.ts
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
interface NodeProperty {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
field_location: {
|
||||||
|
name: string
|
||||||
|
drupal_internal__tid: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DrupalMetatag {
|
||||||
|
tag: string
|
||||||
|
attributes: {
|
||||||
|
content: string
|
||||||
|
name?: string
|
||||||
|
rel?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue