├── .env.sample ├── .eslintrc.json ├── .gitignore ├── README.md ├── components.json ├── django-nextjs-frontend.code-workspace ├── jsconfig.json ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── public ├── next.svg └── vercel.svg ├── src ├── app │ ├── api │ │ ├── hello │ │ │ └── route.jsx │ │ ├── login │ │ │ └── route.jsx │ │ ├── logout │ │ │ └── route.jsx │ │ ├── page.jsx │ │ ├── proxy.jsx │ │ └── waitlists │ │ │ ├── [id] │ │ │ └── route.jsx │ │ │ └── route.jsx │ ├── favicon.ico │ ├── globals.css │ ├── layout.js │ ├── login │ │ ├── page.jsx │ │ └── pageOld.jsx │ ├── logout │ │ └── page.jsx │ ├── page.js │ └── waitlists │ │ ├── [id] │ │ └── page.jsx │ │ ├── card.jsx │ │ ├── forms.jsx │ │ ├── page.js │ │ └── table.jsx ├── components │ ├── authProvider.jsx │ ├── layout │ │ ├── AccountDropdown.jsx │ │ ├── BaseLayout.jsx │ │ ├── BrandLink.jsx │ │ ├── MobileNavbar.jsx │ │ ├── NavLinks.jsx │ │ └── Navbar.jsx │ ├── themeProvider.jsx │ ├── themeToggleButton.jsx │ └── ui │ │ ├── button.jsx │ │ ├── card.jsx │ │ ├── dropdown-menu.jsx │ │ ├── input.jsx │ │ ├── label.jsx │ │ ├── sheet.jsx │ │ ├── table.jsx │ │ └── textarea.jsx ├── config │ └── defaults.jsx └── lib │ ├── auth.jsx │ ├── fetcher.js │ └── utils.js └── tailwind.config.js /.env.sample: -------------------------------------------------------------------------------- 1 | DJANGO_BASE_URL="http://127.0.0.1:8111" -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | 3 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 4 | 5 | # dependencies 6 | /node_modules 7 | /.pnp 8 | .pnp.js 9 | .yarn/install-state.gz 10 | 11 | # testing 12 | /coverage 13 | 14 | # next.js 15 | /.next/ 16 | /out/ 17 | 18 | # production 19 | /build 20 | 21 | # misc 22 | .DS_Store 23 | *.pem 24 | 25 | # debug 26 | npm-debug.log* 27 | yarn-debug.log* 28 | yarn-error.log* 29 | 30 | # local env files 31 | .env*.local 32 | 33 | # vercel 34 | .vercel 35 | 36 | # typescript 37 | *.tsbuildinfo 38 | next-env.d.ts 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Django <> Nextjs - Frontend 2 | 3 | 4 | ## Getting Started 5 | 6 | Clone, install, then run: 7 | 8 | ```bash 9 | git clone https://github.com/codingforentrepreneurs/django-nextjs-frontend 10 | 11 | npm install 12 | 13 | npm run dev 14 | ``` -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": false, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "src/app/globals.css", 9 | "baseColor": "stone", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /django-nextjs-frontend.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "." 5 | } 6 | ], 7 | "settings": {} 8 | } -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "paths": { 4 | "@/*": ["./src/*"] 5 | } 6 | } 7 | } -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {}; 3 | 4 | export default nextConfig; 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "django-nextjs-frontend", 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 | "@radix-ui/react-dialog": "^1.1.1", 13 | "@radix-ui/react-dropdown-menu": "^2.1.1", 14 | "@radix-ui/react-icons": "^1.3.0", 15 | "@radix-ui/react-label": "^2.1.0", 16 | "@radix-ui/react-slot": "^1.1.0", 17 | "class-variance-authority": "^0.7.0", 18 | "clsx": "^2.1.1", 19 | "lucide-react": "^0.399.0", 20 | "next": "14.2.4", 21 | "next-themes": "^0.3.0", 22 | "react": "^18", 23 | "react-dom": "^18", 24 | "swr": "^2.2.5", 25 | "tailwind-merge": "^2.3.0", 26 | "tailwindcss-animate": "^1.0.7" 27 | }, 28 | "devDependencies": { 29 | "eslint": "^8", 30 | "eslint-config-next": "14.2.4", 31 | "postcss": "^8", 32 | "tailwindcss": "^3.4.1" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/api/hello/route.jsx: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import ApiProxy from "../proxy"; 3 | import { DJANGO_API_ENDPOINT } from "@/config/defaults"; 4 | 5 | 6 | export async function GET(request){ 7 | const data = {apiEndpoint: DJANGO_API_ENDPOINT} 8 | return NextResponse.json(data, {status: 200}) 9 | } 10 | -------------------------------------------------------------------------------- /src/app/api/login/route.jsx: -------------------------------------------------------------------------------- 1 | "use server" 2 | import { DJANGO_API_ENDPOINT } from '@/config/defaults' 3 | import { setRefreshToken, setToken } from '@/lib/auth' 4 | import { NextResponse } from 'next/server' 5 | 6 | const DJANGO_API_LOGIN_URL = `${DJANGO_API_ENDPOINT}/token/pair` 7 | 8 | export async function POST(request) { 9 | const requestData = await request.json() 10 | const jsonData = JSON.stringify(requestData) 11 | const requestOptions = { 12 | method: "POST", 13 | headers: { 14 | "Content-Type": "application/json" 15 | }, 16 | body: jsonData 17 | } 18 | const response = await fetch(DJANGO_API_LOGIN_URL, requestOptions) 19 | const responseData = await response.json() 20 | if (response.ok) { 21 | console.log("logged in") 22 | const {username, access, refresh} = responseData 23 | setToken(access) 24 | setRefreshToken(refresh) 25 | return NextResponse.json({"loggedIn": true, "username": username}, {status: 200}) 26 | } 27 | return NextResponse.json({"loggedIn": false, ...responseData}, {status: 400}) 28 | } -------------------------------------------------------------------------------- /src/app/api/logout/route.jsx: -------------------------------------------------------------------------------- 1 | import { deleteTokens } from "@/lib/auth"; 2 | import { NextResponse } from "next/server"; 3 | 4 | 5 | export async function POST(request) { 6 | deleteTokens() 7 | return NextResponse.json({}, {status: 200}) 8 | } -------------------------------------------------------------------------------- /src/app/api/page.jsx: -------------------------------------------------------------------------------- 1 | "use server" 2 | 3 | export default async function Page({ params, searchParams }) { 4 | return

Api

5 | } -------------------------------------------------------------------------------- /src/app/api/proxy.jsx: -------------------------------------------------------------------------------- 1 | import { getToken } from "@/lib/auth" 2 | 3 | 4 | export default class ApiProxy { 5 | 6 | static async getHeaders(requireAuth) { 7 | let headers = { 8 | "Content-Type": "application/json", 9 | "Accept": "application/json", 10 | } 11 | const authToken = getToken() 12 | if (authToken && requireAuth === true) { 13 | headers["Authorization"] = `Bearer ${authToken}` 14 | } 15 | return headers 16 | } 17 | 18 | static async handleFetch(endpoint, requestOptions) { 19 | let data = {} 20 | let status = 500 21 | try { 22 | const response = await fetch(endpoint, requestOptions) 23 | data = await response.json() 24 | status = response.status 25 | } catch (error) { 26 | data = {message: "Cannot reach API server", error: error} 27 | status = 500 28 | } 29 | return {data, status} 30 | 31 | } 32 | 33 | static async put(endpoint, object, requireAuth) { 34 | const jsonData = JSON.stringify(object) 35 | const headers = await ApiProxy.getHeaders(requireAuth) 36 | const requestOptions = { 37 | method: "PUT", 38 | headers: headers, 39 | body: jsonData 40 | } 41 | return await ApiProxy.handleFetch(endpoint, requestOptions) 42 | } 43 | 44 | static async delete(endpoint, requireAuth) { 45 | const headers = await ApiProxy.getHeaders(requireAuth) 46 | const requestOptions = { 47 | method: "DELETE", 48 | headers: headers, 49 | } 50 | return await ApiProxy.handleFetch(endpoint, requestOptions) 51 | } 52 | 53 | static async post(endpoint, object, requireAuth) { 54 | const jsonData = JSON.stringify(object) 55 | const headers = await ApiProxy.getHeaders(requireAuth) 56 | const requestOptions = { 57 | method: "POST", 58 | headers: headers, 59 | body: jsonData 60 | } 61 | return await ApiProxy.handleFetch(endpoint, requestOptions) 62 | } 63 | 64 | static async get(endpoint, requireAuth) { 65 | const headers = await ApiProxy.getHeaders(requireAuth) 66 | const requestOptions = { 67 | method: "GET", 68 | headers: headers 69 | } 70 | return await ApiProxy.handleFetch(endpoint, requestOptions) 71 | } 72 | } -------------------------------------------------------------------------------- /src/app/api/waitlists/[id]/route.jsx: -------------------------------------------------------------------------------- 1 | "use server" 2 | 3 | import { DJANGO_API_ENDPOINT } from "@/config/defaults"; 4 | import { NextResponse } from "next/server"; 5 | import ApiProxy from "../../proxy"; 6 | 7 | const DJANGO_API_WAITLISTS_URL=`${DJANGO_API_ENDPOINT}/waitlists/` 8 | 9 | export async function GET(request, {params}) { 10 | const endpoint = params?.id ? `${DJANGO_API_WAITLISTS_URL}${params.id}/` : null 11 | if (!endpoint){ 12 | return NextResponse.json({}, {status: 400}) 13 | } 14 | const {data, status} = await ApiProxy.get(endpoint, true) 15 | return NextResponse.json(data, {status: status}) 16 | } 17 | 18 | 19 | export async function PUT(request, {params}) { 20 | const endpoint = params?.id ? `${DJANGO_API_WAITLISTS_URL}${params.id}/` : null 21 | const requestData = await request.json() 22 | const {data, status} = await ApiProxy.put(endpoint, requestData, true ) 23 | return NextResponse.json(data, {status: status}) 24 | } 25 | 26 | export async function DELETE(request, {params}) { 27 | const endpoint = params?.id ? `${DJANGO_API_WAITLISTS_URL}${params.id}/delete/` : null 28 | const {data, status} = await ApiProxy.delete(endpoint, true ) 29 | return NextResponse.json(data, {status: status}) 30 | } -------------------------------------------------------------------------------- /src/app/api/waitlists/route.jsx: -------------------------------------------------------------------------------- 1 | import { getToken } from "@/lib/auth"; 2 | import { NextResponse } from "next/server"; 3 | import ApiProxy from "../proxy"; 4 | import { DJANGO_API_ENDPOINT } from "@/config/defaults"; 5 | 6 | const DJANGO_API_WAITLISTS_URL=`${DJANGO_API_ENDPOINT}/waitlists/` 7 | 8 | export async function GET(request){ 9 | const {data, status} = await ApiProxy.get(DJANGO_API_WAITLISTS_URL, true) 10 | return NextResponse.json(data, {status: status}) 11 | } 12 | 13 | 14 | export async function POST(request) { 15 | const requestData = await request.json() 16 | const {data, status} = await ApiProxy.post(DJANGO_API_WAITLISTS_URL, requestData, true ) 17 | return NextResponse.json(data, {status: status}) 18 | } -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/django-nextjs-frontend/c7e592025e1b633b78edc6eb2ce21cfbcffec2f0/src/app/favicon.ico -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 20 14.3% 4.1%; 9 | 10 | --card: 0 0% 100%; 11 | --card-foreground: 20 14.3% 4.1%; 12 | 13 | --popover: 0 0% 100%; 14 | --popover-foreground: 20 14.3% 4.1%; 15 | 16 | --primary: 24 9.8% 10%; 17 | --primary-foreground: 60 9.1% 97.8%; 18 | 19 | --secondary: 60 4.8% 95.9%; 20 | --secondary-foreground: 24 9.8% 10%; 21 | 22 | --muted: 60 4.8% 95.9%; 23 | --muted-foreground: 25 5.3% 44.7%; 24 | 25 | --accent: 60 4.8% 95.9%; 26 | --accent-foreground: 24 9.8% 10%; 27 | 28 | --destructive: 0 84.2% 60.2%; 29 | --destructive-foreground: 60 9.1% 97.8%; 30 | 31 | --border: 20 5.9% 90%; 32 | --input: 20 5.9% 90%; 33 | --ring: 20 14.3% 4.1%; 34 | 35 | --radius: 0.5rem; 36 | } 37 | 38 | .dark { 39 | --background: 20 14.3% 4.1%; 40 | --foreground: 60 9.1% 97.8%; 41 | 42 | --card: 20 14.3% 4.1%; 43 | --card-foreground: 60 9.1% 97.8%; 44 | 45 | --popover: 20 14.3% 4.1%; 46 | --popover-foreground: 60 9.1% 97.8%; 47 | 48 | --primary: 60 9.1% 97.8%; 49 | --primary-foreground: 24 9.8% 10%; 50 | 51 | --secondary: 12 6.5% 15.1%; 52 | --secondary-foreground: 60 9.1% 97.8%; 53 | 54 | --muted: 12 6.5% 15.1%; 55 | --muted-foreground: 24 5.4% 63.9%; 56 | 57 | --accent: 12 6.5% 15.1%; 58 | --accent-foreground: 60 9.1% 97.8%; 59 | 60 | --destructive: 0 62.8% 30.6%; 61 | --destructive-foreground: 60 9.1% 97.8%; 62 | 63 | --border: 12 6.5% 15.1%; 64 | --input: 12 6.5% 15.1%; 65 | --ring: 24 5.7% 82.9%; 66 | } 67 | } 68 | 69 | @layer base { 70 | * { 71 | @apply border-border; 72 | } 73 | body { 74 | @apply bg-background text-foreground; 75 | } 76 | } -------------------------------------------------------------------------------- /src/app/layout.js: -------------------------------------------------------------------------------- 1 | import { Inter } from "next/font/google"; 2 | import "./globals.css"; 3 | import { AuthProvider } from "@/components/authProvider"; 4 | 5 | import { Inter as FontSans } from "next/font/google" 6 | 7 | import { cn } from "@/lib/utils" 8 | import { ThemeProvider } from "@/components/themeProvider"; 9 | import BaseLayout from "@/components/layout/BaseLayout"; 10 | import { Suspense } from "react"; 11 | 12 | const fontSans = FontSans({ 13 | subsets: ["latin"], 14 | variable: "--font-sans", 15 | }) 16 | 17 | 18 | const inter = Inter({ subsets: ["latin"] }); 19 | 20 | export const metadata = { 21 | title: "Django <> NextJS SaaS Platform", 22 | description: "Django <> NextJS SaaS Platform", 23 | }; 24 | 25 | export default function RootLayout({ children }) { 26 | return ( 27 | 28 | 32 | Loading...}> 33 | 37 | 38 | 39 | {children} 40 | 41 | 42 | 43 | 44 | 45 | 46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /src/app/login/page.jsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import Image from "next/image" 4 | import Link from "next/link" 5 | 6 | import { Button } from "@/components/ui/button" 7 | import { Input } from "@/components/ui/input" 8 | import { Label } from "@/components/ui/label" 9 | import { useAuth } from "@/components/authProvider" 10 | 11 | const LOGIN_URL = "/api/login/" 12 | 13 | 14 | export default function Page() { 15 | const auth = useAuth() 16 | async function handleSubmit (event) { 17 | event.preventDefault() 18 | console.log(event, event.target) 19 | const formData = new FormData(event.target) 20 | const objectFromForm = Object.fromEntries(formData) 21 | const jsonData = JSON.stringify(objectFromForm) 22 | const requestOptions = { 23 | method: "POST", 24 | headers: { 25 | "Content-Type": "application/json" 26 | }, 27 | body: jsonData 28 | } 29 | const response = await fetch(LOGIN_URL, requestOptions) 30 | let data = {} 31 | try { 32 | data = await response.json() 33 | } catch (error) { 34 | 35 | } 36 | // const data = await response.json() 37 | if (response.ok) { 38 | console.log("logged in") 39 | auth.login(data?.username) 40 | } else { 41 | console.log(await response.json()) 42 | } 43 | } 44 | return ( 45 |
46 |
47 |
48 |
49 |

Login

50 |

51 | Enter your email below to login to your account 52 |

53 |
54 |
55 |
56 |
57 | 58 | 65 |
66 |
67 |
68 | 69 | 73 | Forgot your password? 74 | 75 |
76 | 77 |
78 | 81 |
82 |
83 |
84 | Don't have an account?{" "} 85 | 86 | Sign up 87 | 88 |
89 |
90 |
91 |
92 | Image 99 |
100 |
101 | ) 102 | } 103 | -------------------------------------------------------------------------------- /src/app/login/pageOld.jsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | import { useAuth } from "@/components/authProvider" 3 | const LOGIN_URL = "/api/login/" 4 | 5 | 6 | export default function Page() { 7 | const auth = useAuth() 8 | async function handleSubmit (event) { 9 | event.preventDefault() 10 | console.log(event, event.target) 11 | const formData = new FormData(event.target) 12 | const objectFromForm = Object.fromEntries(formData) 13 | const jsonData = JSON.stringify(objectFromForm) 14 | const requestOptions = { 15 | method: "POST", 16 | headers: { 17 | "Content-Type": "application/json" 18 | }, 19 | body: jsonData 20 | } 21 | const response = await fetch(LOGIN_URL, requestOptions) 22 | // const data = await response.json() 23 | if (response.ok) { 24 | console.log("logged in") 25 | auth.login() 26 | } 27 | } 28 | return
29 |
30 |

Login Here

31 |
32 | 33 | 34 | 35 | 36 |
37 |
38 |
39 | } -------------------------------------------------------------------------------- /src/app/logout/page.jsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useAuth } from "@/components/authProvider" 4 | const LOGOUT_URL = "/api/logout/" 5 | 6 | 7 | export default function Page() { 8 | const auth = useAuth() 9 | async function handleClick (event) { 10 | event.preventDefault() 11 | const requestOptions = { 12 | method: "POST", 13 | headers: { 14 | "Content-Type": "application/json" 15 | }, 16 | body: "" 17 | } 18 | const response = await fetch(LOGOUT_URL, requestOptions) 19 | if (response.ok) { 20 | console.log("logged out") 21 | auth.logout() 22 | } 23 | } 24 | return
25 |
26 |

Are you sure you want to logout?

27 | 28 |
29 |
30 | } -------------------------------------------------------------------------------- /src/app/page.js: -------------------------------------------------------------------------------- 1 | "use client" 2 | import {useState} from 'react'; 3 | import Image from "next/image"; 4 | import useSWR from 'swr'; 5 | import { useAuth } from '@/components/authProvider'; 6 | import { ThemeToggleButton } from '@/components/themeToggleButton'; 7 | import WaitlistForm from './waitlists/forms'; 8 | 9 | 10 | const fetcher = (...args) => fetch(...args).then(res => res.json()) 11 | 12 | 13 | export default function Home() { 14 | const auth = useAuth() 15 | const {data, error, isLoading} = useSWR("/api/hello", fetcher) 16 | // if (error) return
failed to load
17 | // if (isLoading) return
loading...
18 | 19 | 20 | return ( 21 |
22 |
{data && data.apiEndpoint}
23 |
24 | 25 |
26 |
27 | {auth.isAuthenticated ? "Hello user" : "Hello guest"} 28 |
29 |
30 | 31 |
32 | 33 |
34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /src/app/waitlists/[id]/page.jsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import fetcher from "@/lib/fetcher" 4 | import useSWR from "swr" 5 | import { WaitlistCard } from "../card" 6 | 7 | 8 | export default function Page({params}) { 9 | const lookupId = params ? params.id : 0 10 | const {data, error, isLoading} = useSWR(`/api/waitlists/${lookupId}`, fetcher) 11 | console.log(data, error, isLoading) 12 | return
13 | {isLoading ?
Loading
14 | : 15 | 16 | } 17 |
18 | } 19 | -------------------------------------------------------------------------------- /src/app/waitlists/card.jsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import {useState} from "react" 4 | 5 | import { 6 | Card, 7 | CardContent, 8 | CardDescription, 9 | CardHeader, 10 | CardTitle, 11 | } from "@/components/ui/card" 12 | import { Button } from "@/components/ui/button" 13 | import { Input } from "@/components/ui/input" 14 | import { Textarea } from "@/components/ui/textarea" 15 | const WAITLIST_API_URL = "/api/waitlists/" 16 | 17 | 18 | 19 | export function WaitlistCard({waitlistEvent}) { 20 | const [message, setMessage] = useState('') 21 | const [errors, setErrors] = useState({}) 22 | const [error, setError] = useState('') 23 | 24 | if (!waitlistEvent && !waitlistEvent.email ){ 25 | return null 26 | } 27 | 28 | 29 | async function handleSubmit (event) { 30 | event.preventDefault() 31 | setMessage('') 32 | setErrors({}) 33 | setError('') 34 | const formData = new FormData(event.target) 35 | const objectFromForm = Object.fromEntries(formData) 36 | const jsonData = JSON.stringify(objectFromForm) 37 | const requestOptions = { 38 | method: "PUT", 39 | headers: { 40 | "Content-Type": "application/json" 41 | }, 42 | body: jsonData 43 | } 44 | const response = await fetch(`${WAITLIST_API_URL}${waitlistEvent.id}/`, requestOptions) 45 | const data = await response.json() 46 | console.log(data) 47 | if (response.status === 201 || response.status === 200) { 48 | setMessage("Data changed") 49 | } 50 | } 51 | return ( 52 | 53 | 54 | {waitlistEvent.email} 55 | {waitlistEvent.id} 56 | 57 | 58 | {message &&

{message}

} 59 |
60 |