├── .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 |
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 |
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 |
259 | )}
260 | >
261 | );
262 | };
263 |
264 | export default AuthForm;
265 |
--------------------------------------------------------------------------------