Initial commit

Created from https://vercel.com/new
This commit is contained in:
vespo92 2023-08-09 21:34:23 +00:00
commit 0721b7dc8f
66 changed files with 2858 additions and 0 deletions

7
.env.example Normal file
View 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
View 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
View 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
View 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
View 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
View 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>
)
}

View 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
View 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
View 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
View 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>
)
}

View 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>
)
}

View 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
View 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
View 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
View 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>
)
}

View 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>
)
}

View 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
View 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>
)
}

View 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
View 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
View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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
View 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} />
}

View 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
View 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>
)
}

View 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
View file

@ -0,0 +1,3 @@
{
"baseUrl": "http://localhost:3001"
}

View 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"
}

View 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 {}

View 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 {}

View 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 {}

View 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 {}

View 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
View 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
}

View 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
View 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
View 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
View 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
View 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
View 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()
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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>
)
}
}

View 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
View 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
View 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
View 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
View file

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
public/meta.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

2
public/robots.txt Normal file
View file

@ -0,0 +1,2 @@
User-agent: *
Allow: /

3
styles/globals.css Normal file
View file

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

14
tailwind.config.js Normal file
View 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
View 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
View file

17
types/drupal.d.ts vendored Normal file
View 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
}
}