├── .eslintrc.json ├── jsconfig.json ├── app ├── components │ ├── yoda.png │ └── Navbar.jsx ├── (auth) │ └── login │ │ ├── page.jsx │ │ └── AuthForm.jsx ├── loading.jsx ├── local │ └── page.jsx ├── layout.jsx ├── post │ └── [uri] │ │ └── page.jsx ├── page.jsx ├── movies │ └── page.jsx └── globals.css ├── postcss.config.js ├── next.config.js ├── tailwind.config.js ├── .gitignore ├── package.json ├── public ├── vercel.svg ├── next.svg └── darth-vader.svg └── README.md /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "paths": { 4 | "@/*": ["./*"] 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /app/components/yoda.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fran-A-Dev/nextjs14-headlesswp-example/HEAD/app/components/yoda.png -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /app/(auth)/login/page.jsx: -------------------------------------------------------------------------------- 1 | import AuthForm from "./AuthForm"; 2 | 3 | export default async function Page() { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /app/loading.jsx: -------------------------------------------------------------------------------- 1 | export default function Loading() { 2 | return ( 3 |
4 |

Loading...

5 |

🔄

6 |
7 | ); 8 | } 9 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | const { withAtlasConfig } = require("@wpengine/atlas-next"); 2 | 3 | /** @type {import('next').NextConfig} */ 4 | const nextConfig = { 5 | // Your existing Next.js config 6 | }; 7 | 8 | module.exports = withAtlasConfig(nextConfig); 9 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | "./pages/**/*.{js,ts,jsx,tsx,mdx}", 5 | "./components/**/*.{js,ts,jsx,tsx,mdx}", 6 | "./app/**/*.{js,ts,jsx,tsx,mdx}", 7 | ], 8 | theme: { 9 | extend: { 10 | colors: { 11 | primary: "#002c3e", 12 | }, 13 | }, 14 | }, 15 | plugins: [], 16 | }; 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | -------------------------------------------------------------------------------- /app/local/page.jsx: -------------------------------------------------------------------------------- 1 | import { headers } from "next/headers"; 2 | 3 | export default async function LocalPage() { 4 | const country = headers().get("wpe-headless-country") || "No country data"; 5 | const region = headers().get("wpe-headless-region") || "No region data"; 6 | const timezone = headers().get("wpe-headless-timezone") || "No timezone data"; 7 | 8 | return ( 9 |
10 |

Geolocation Data

11 |

Country: {country}

12 |

Region: {region}

13 |

Timezone: {timezone}

14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ticketing-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@wpengine/atlas-next": "^1.1.0", 13 | "autoprefixer": "10.4.14", 14 | "eslint": "8.44.0", 15 | "eslint-config-next": "13.4.9", 16 | "next": "14.1.2", 17 | "postcss": "8.4.25", 18 | "react": "18.2.0", 19 | "react-dom": "18.2.0", 20 | "tailwindcss": "3.3.2" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/components/Navbar.jsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import Image from "next/image"; 3 | import Logo from "./yoda.png"; 4 | export default function Navbar() { 5 | return ( 6 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/layout.jsx: -------------------------------------------------------------------------------- 1 | import "./globals.css"; 2 | import { Rubik } from "next/font/google"; 3 | 4 | // components 5 | import Navbar from "./components/Navbar"; 6 | 7 | const rubik = Rubik({ subsets: ["latin"] }); 8 | 9 | export const metadata = { 10 | title: "Headless WordPress Example", 11 | description: "Generated by Franly the Manly", 12 | }; 13 | 14 | export default function RootLayout({ children }) { 15 | return ( 16 | 17 | 18 | 19 | {children} 20 | 25 | 26 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/post/[uri]/page.jsx: -------------------------------------------------------------------------------- 1 | import { Suspense } from "react"; 2 | import Loading from "../../loading"; 3 | 4 | async function getPost(uri) { 5 | const query = ` 6 | query GetPostByUri($uri: ID!) { 7 | post(id: $uri, idType: URI) { 8 | title 9 | content 10 | 11 | } 12 | } 13 | `; 14 | 15 | const variables = { 16 | uri, 17 | }; 18 | 19 | const res = await fetch(process.env.NEXT_PUBLIC_GRAPHQL_ENDPOINT, { 20 | method: "POST", 21 | headers: { 22 | "Content-Type": "application/json", 23 | }, 24 | next: { 25 | revalidate: 60, 26 | }, 27 | body: JSON.stringify({ query, variables }), 28 | }); 29 | 30 | const responseBody = await res.json(); 31 | 32 | if (responseBody && responseBody.data && responseBody.data.post) { 33 | return responseBody.data.post; 34 | } else { 35 | throw new Error("Failed to fetch the post"); 36 | } 37 | } 38 | 39 | export default async function PostDetails({ params }) { 40 | const post = await getPost(params.uri); 41 | 42 | return ( 43 |
44 | 47 | }> 48 |
49 |

50 |

51 |
52 |
53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /app/page.jsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import { Suspense } from "react"; 3 | import Loading from "./loading"; 4 | 5 | async function getPosts() { 6 | const query = ` 7 | { 8 | posts(first: 5) { 9 | nodes { 10 | title 11 | content 12 | uri 13 | } 14 | } 15 | } 16 | `; 17 | 18 | const res = await fetch( 19 | `${process.env.NEXT_PUBLIC_GRAPHQL_ENDPOINT}?query=${encodeURIComponent( 20 | query 21 | )}`, 22 | { next: { revalidate: 10 } }, 23 | { 24 | method: "GET", 25 | headers: { 26 | "Content-Type": "application/json", 27 | // ... any other headers you need to include (like authentication tokens) 28 | }, 29 | } 30 | ); 31 | 32 | const { data } = await res.json(); 33 | 34 | return data.posts.nodes; 35 | } 36 | 37 | export default async function PostList() { 38 | const posts = await getPosts(); 39 | 40 | return ( 41 | }> 42 |
43 | {posts.map((post) => ( 44 |
45 | 46 |

{post.title}

47 |
52 | 53 |
54 | ))} 55 |
56 | 57 | ); 58 | } 59 | -------------------------------------------------------------------------------- /app/movies/page.jsx: -------------------------------------------------------------------------------- 1 | async function getMovies() { 2 | const query = ` 3 | query getMovies { 4 | movies { 5 | nodes { 6 | movieFields { 7 | 8 | 9 | movieQuote 10 | } 11 | title 12 | uri 13 | } 14 | } 15 | } 16 | `; 17 | 18 | const res = await fetch( 19 | `${process.env.NEXT_PUBLIC_GRAPHQL_ENDPOINT}?query=${encodeURIComponent( 20 | query 21 | )}`, 22 | { next: { revalidate: 10 } }, 23 | { 24 | method: "GET", 25 | headers: { 26 | "Content-Type": "application/json", 27 | // ... any other headers you need to include (like authentication tokens) 28 | }, 29 | } 30 | ); 31 | 32 | const { data } = await res.json(); 33 | 34 | return data.movies.nodes; 35 | } 36 | 37 | export default async function MovieList() { 38 | const movies = await getMovies(); 39 | 40 | return ( 41 |
42 | {movies.map((movie) => { 43 | // Ensure that you are accessing the movieQuote from the movie.movieFields object 44 | const { movieFields: { movieQuote, title } = {} } = movie; 45 | return ( 46 |
47 |

{movie.title}

48 |
53 |
54 | ); 55 | })} 56 |
57 | ); 58 | } 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | ``` 14 | 15 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 16 | 17 | You can start editing the page by modifying `app/page.js`. The page auto-updates as you edit the file. 18 | 19 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 20 | 21 | ## Learn More 22 | 23 | To learn more about Next.js, take a look at the following resources: 24 | 25 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 26 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 27 | 28 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 29 | 30 | ## Deploy on Vercel 31 | 32 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 33 | 34 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 35 | # nextjs13-headlesswp-starter 36 | -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | /* base styles */ 6 | body { 7 | background: #ebf0fa; 8 | @apply text-gray-500 m-8; 9 | } 10 | h1, 11 | h2 { 12 | @apply font-bold text-primary text-lg; 13 | } 14 | main { 15 | @apply max-w-5xl my-12 mx-auto px-8; 16 | } 17 | main > h2 { 18 | @apply mb-4 pb-2 text-base; 19 | } 20 | p > a { 21 | @apply text-primary underline; 22 | } 23 | 24 | /* nav styles */ 25 | nav { 26 | @apply pb-4 27 | border-b-2 border-gray-200 28 | flex items-center gap-5 29 | my-10 mx-auto 30 | max-w-5xl; 31 | } 32 | nav a, 33 | nav span { 34 | @apply text-gray-500; 35 | } 36 | nav a:hover { 37 | @apply text-black; 38 | } 39 | main nav { 40 | @apply border-0; 41 | } 42 | 43 | /* button styles */ 44 | button { 45 | @apply px-3 py-2 46 | rounded-sm 47 | flex justify-between items-center gap-2 48 | text-sm; 49 | } 50 | button:hover { 51 | @apply bg-opacity-90; 52 | } 53 | .btn-primary { 54 | @apply bg-primary text-white; 55 | } 56 | .btn-secondary { 57 | @apply bg-gray-200 text-gray-900; 58 | } 59 | 60 | /* form styles */ 61 | form { 62 | @apply py-4 px-7 63 | bg-primary 64 | bg-opacity-5 65 | rounded-md 66 | block 67 | mx-auto 68 | min-w-fit w-1/4; 69 | } 70 | form label { 71 | @apply block; 72 | } 73 | form input, 74 | form textarea, 75 | form select { 76 | @apply block 77 | mt-2 my-4 px-2 py-1 78 | rounded-sm w-full; 79 | } 80 | form button { 81 | @apply block mx-auto; 82 | } 83 | 84 | /* feedback styles */ 85 | .error { 86 | @apply border-2 87 | border-red-500 88 | bg-red-300 89 | text-red-800 90 | py-1 px-2 91 | rounded-sm 92 | block 93 | max-w-fit 94 | my-4 mx-auto; 95 | } 96 | 97 | /* card styles */ 98 | .card { 99 | @apply bg-white 100 | shadow-sm 101 | rounded-md 102 | py-3 px-4 my-4 103 | relative 104 | overflow-hidden; 105 | } 106 | .card h3 { 107 | @apply font-bold text-gray-700 text-sm 108 | mb-0; 109 | } 110 | .card p { 111 | @apply my-4 text-sm leading-6; 112 | } 113 | 114 | /* pill styles */ 115 | .pill { 116 | @apply py-1 px-2 mt-3 117 | inline-block 118 | text-xs font-semibold; 119 | } 120 | .pill.high { 121 | @apply bg-red-300 text-red-600; 122 | } 123 | .pill.medium { 124 | @apply bg-blue-300 text-blue-600; 125 | } 126 | .pill.low { 127 | @apply bg-emerald-300 text-emerald-600; 128 | } 129 | .card .pill { 130 | @apply absolute bottom-0 right-0 131 | rounded-tl-md; 132 | } 133 | -------------------------------------------------------------------------------- /public/darth-vader.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/(auth)/login/AuthForm.jsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState, useEffect } from "react"; 4 | 5 | const AuthForm = () => { 6 | const [username, setUsername] = useState(""); 7 | const [password, setPassword] = useState(""); 8 | const [jwt, setJwt] = useState(""); 9 | const [posts, setPosts] = useState([]); 10 | const [title, setTitle] = useState(""); 11 | const [content, setContent] = useState(""); 12 | const [loginError, setLoginError] = useState(null); 13 | const [loading, setLoading] = useState(true); // Add a loading state 14 | const [userData, setUserData] = useState(null); // Add user data state 15 | 16 | useEffect(() => { 17 | // Check if a JWT token is stored in the browser's localStorage 18 | const storedToken = localStorage.getItem("authToken"); 19 | if (storedToken) { 20 | setJwt(storedToken); 21 | setLoading(false); // Set loading to false once token is found 22 | } else { 23 | setLoading(false); // Set loading to false even if token is not found 24 | } 25 | }, []); // Only run this effect once, on component mount 26 | 27 | useEffect(() => { 28 | // Check if JWT token is set 29 | if (jwt) { 30 | // Fetch posts data when JWT token is available 31 | fetch(process.env.NEXT_PUBLIC_GRAPHQL_ENDPOINT, { 32 | method: "post", 33 | headers: { 34 | "Content-Type": "application/json", 35 | Authorization: `Bearer ${jwt}`, 36 | }, 37 | body: JSON.stringify({ 38 | query: ` 39 | query GetAllPosts { 40 | posts(where: { status: DRAFT }) { 41 | nodes { 42 | title 43 | content 44 | author { 45 | node { 46 | username 47 | } 48 | } 49 | } 50 | } 51 | } 52 | `, 53 | }), 54 | }) 55 | .then((response) => response.json()) 56 | .then((data) => { 57 | setPosts(data?.data?.posts?.nodes || []); 58 | }) 59 | .catch((error) => { 60 | console.error("An error occurred:", error); 61 | }); 62 | 63 | // Fetch user data 64 | fetch(process.env.NEXT_PUBLIC_GRAPHQL_ENDPOINT, { 65 | method: "post", 66 | headers: { 67 | "Content-Type": "application/json", 68 | Authorization: `Bearer ${jwt}`, 69 | }, 70 | body: JSON.stringify({ 71 | query: ` 72 | query GetUserData { 73 | viewer { 74 | id 75 | username 76 | } 77 | } 78 | `, 79 | }), 80 | }) 81 | .then((response) => response.json()) 82 | .then((data) => { 83 | setUserData(data?.data?.viewer || null); 84 | }) 85 | .catch((error) => { 86 | console.error("An error occurred:", error); 87 | }); 88 | } 89 | }, [jwt]); 90 | 91 | const loginAndFetch = async (e) => { 92 | e.preventDefault(); 93 | console.log("Logging in user..."); 94 | 95 | try { 96 | const response = await fetch(process.env.NEXT_PUBLIC_GRAPHQL_ENDPOINT, { 97 | method: "post", 98 | headers: { "Content-Type": "application/json" }, 99 | body: JSON.stringify({ 100 | query: ` 101 | mutation LoginUser { 102 | login( input: { 103 | clientMutationId: "uniqueId", 104 | username: "${username}", 105 | password: "${password}" 106 | }){ 107 | authToken 108 | } 109 | } 110 | `, 111 | }), 112 | }); 113 | 114 | const { data } = await response.json(); 115 | 116 | if (data?.login?.authToken) { 117 | // Store JWT token in localStorage 118 | localStorage.setItem("authToken", data.login.authToken); 119 | setJwt(data.login.authToken); 120 | } 121 | } catch (error) { 122 | console.error("An error occurred:", error); 123 | } 124 | }; 125 | 126 | const createDraftPost = async () => { 127 | try { 128 | const response = await fetch(process.env.NEXT_PUBLIC_GRAPHQL_ENDPOINT, { 129 | method: "post", 130 | headers: { 131 | "Content-Type": "application/json", 132 | Authorization: `Bearer ${jwt}`, 133 | }, 134 | body: JSON.stringify({ 135 | query: ` 136 | mutation CreateDraftPost($title: String!, $content: String!) { 137 | createPost(input: { content: $content, title: $title, status: DRAFT, authorId: "1" }) { 138 | post { 139 | title 140 | content 141 | } 142 | } 143 | } 144 | `, 145 | variables: { 146 | title: title, 147 | content: content, 148 | }, 149 | }), 150 | }); 151 | 152 | const responseData = await response.json(); 153 | console.log("Created post:", responseData); 154 | 155 | alert("Draft post created successfully!"); 156 | setTitle(""); // Clear the title 157 | setContent(""); // Clear the content 158 | } catch (error) { 159 | console.error("An error occurred:", error); 160 | } 161 | }; 162 | 163 | const logout = () => { 164 | // Remove JWT token from localStorage 165 | localStorage.removeItem("authToken"); 166 | setJwt(""); 167 | }; 168 | 169 | return ( 170 | <> 171 | {loading ? ( 172 | // Display a loading indicator here 173 |

Loading...

174 | ) : jwt ? ( 175 |
176 | 183 | {userData && ( 184 | 185 | Welcome, {userData.username} 186 | 187 | )} 188 |
189 | ) : ( 190 |
191 | 192 | setUsername(e.target.value)} 197 | id="usernameInput" 198 | /> 199 | 200 | setPassword(e.target.value)} 205 | id="passwordInput" 206 | /> 207 | 208 |
209 | )} 210 | {loginError &&

{loginError}

} 211 | {jwt && ( 212 |
213 |

Draft Posts

214 |
    215 | {posts.map((post) => ( 216 |
  • 217 |

    {post.title}

    218 |

    219 | Author: {post.author.node.username} 220 |

  • 221 | ))} 222 |
223 |
224 | )} 225 | {jwt && ( 226 |
227 |
228 |

229 | Create A Draft Post 230 |

231 | 232 | setTitle(e.target.value)} 237 | id="titleInput" 238 | className="w-full rounded border border-gray-300 p-2" 239 | /> 240 |
241 | 242 |