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