├── .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 |
83 |
84 | Don't have an account?{" "}
85 |
86 | Sign up
87 |
88 |
89 |
90 |
91 |
92 |
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 |
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 |
69 |
70 |
71 |
72 |
73 |
74 | )
75 | }
76 |
--------------------------------------------------------------------------------
/src/app/waitlists/forms.jsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { Button } from "@/components/ui/button"
4 | import { Input } from "@/components/ui/input"
5 | import { Label } from "@/components/ui/label"
6 | import { useState } from "react"
7 |
8 | const WAITLIST_API_URL = "/api/waitlists/"
9 |
10 | export default function WaitlistForm() {
11 | const [message, setMessage] = useState('')
12 | const [errors, setErrors] = useState({})
13 | const [error, setError] = useState('')
14 | async function handleSubmit (event) {
15 | event.preventDefault()
16 | setMessage('')
17 | setErrors({})
18 | setError('')
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(WAITLIST_API_URL, requestOptions)
30 | // const data = await response.json()
31 | if (response.status === 201 || response.status === 200) {
32 | setMessage("Thank you for joining")
33 | } else {
34 | const data = await response.json()
35 | setErrors(data)
36 | if (!data.email) {
37 | setError("There was an error with your request. Please try again.")
38 | }
39 | }
40 | }
41 | return
67 | }
68 |
--------------------------------------------------------------------------------
/src/app/waitlists/page.js:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import WaitlistTable from './table';
4 |
5 |
6 |
7 | export default function Page() {
8 | return (
9 |
10 |
11 |
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/src/app/waitlists/table.jsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useAuth } from "@/components/authProvider"
4 | import {
5 | Table,
6 | TableBody,
7 | TableCaption,
8 | TableCell,
9 | TableFooter,
10 | TableHead,
11 | TableHeader,
12 | TableRow,
13 | } from "@/components/ui/table"
14 | import fetcher from "@/lib/fetcher"
15 | import { useRouter } from "next/navigation"
16 | import { useEffect } from "react"
17 | import useSWR from "swr"
18 |
19 |
20 | const WAITLIST_API_URL = "/api/waitlists/"
21 |
22 |
23 | export default function WaitlistTable() {
24 | const router = useRouter()
25 | const {data, error, isLoading} = useSWR(WAITLIST_API_URL, fetcher)
26 | const auth = useAuth()
27 | useEffect(()=>{
28 | if (error?.status === 401) {
29 | auth.loginRequiredRedirect()
30 | }
31 | }, [auth, error])
32 | if (error) return failed to load
33 | if (isLoading) return loading...
34 | return (
35 |
36 | A list of your waitlist entries.
37 |
38 |
39 | ID
40 | Email
41 | Description
42 |
43 |
44 |
45 | {data.map((item, idx) => (
46 | router.push(`/waitlists/${item.id}`)}>
47 | {item.id}
48 | {item.email}
49 | {item.description}
50 |
51 | ))}
52 |
53 |
54 | )
55 | }
56 |
--------------------------------------------------------------------------------
/src/components/authProvider.jsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { usePathname, useRouter, useSearchParams } from "next/navigation";
4 |
5 | const { createContext, useContext, useState, useEffect } = require("react");
6 | const AuthContext = createContext(null);
7 |
8 | const LOGIN_REDIRECT_URL = "/"
9 | const LOGOUT_REDIRECT_URL = "/login"
10 | const LOGIN_REQUIRED_URL = "/login"
11 | const LOCAL_STORAGE_KEY = "is-logged-in"
12 | const LOCAL_USERNAME_KEY = "username"
13 |
14 | export function AuthProvider({children}) {
15 | const [isAuthenticated, setIsAuthenticated] = useState(false)
16 | const [username, setUsername] = useState("")
17 | const router = useRouter()
18 | const pathname = usePathname()
19 | const searchParams = useSearchParams()
20 |
21 | useEffect(()=>{
22 | const storedAuthStatus = localStorage.getItem(LOCAL_STORAGE_KEY)
23 | if (storedAuthStatus) {
24 | const storedAuthStatusInt = parseInt(storedAuthStatus)
25 | setIsAuthenticated(storedAuthStatusInt===1)
26 | }
27 | const storedUn = localStorage.getItem(LOCAL_USERNAME_KEY)
28 | if (storedUn) {
29 | setUsername(storedUn)
30 | }
31 | },[])
32 |
33 | const login = (username) => {
34 | setIsAuthenticated(true)
35 | localStorage.setItem(LOCAL_STORAGE_KEY, "1")
36 | if (username) {
37 | localStorage.setItem(LOCAL_USERNAME_KEY, `${username}`)
38 | setUsername(username)
39 | } else {
40 | localStorage.removeItem(LOCAL_USERNAME_KEY)
41 | }
42 | const nextUrl = searchParams.get("next")
43 | const invalidNextUrl = ['/login', '/logout']
44 | const nextUrlValid = nextUrl && nextUrl.startsWith("/") && !invalidNextUrl.includes(nextUrl)
45 | if (nextUrlValid) {
46 | router.replace(nextUrl)
47 | return
48 | } else {
49 | router.replace(LOGIN_REDIRECT_URL)
50 | return
51 | }
52 | }
53 | const logout = () => {
54 | setIsAuthenticated(false)
55 | localStorage.setItem(LOCAL_STORAGE_KEY, "0")
56 | router.replace(LOGOUT_REDIRECT_URL)
57 | }
58 | const loginRequiredRedirect = () => {
59 | // user is not logged in via API
60 | setIsAuthenticated(false)
61 | localStorage.setItem(LOCAL_STORAGE_KEY, "0")
62 | let loginWithNextUrl = `${LOGIN_REQUIRED_URL}?next=${pathname}`
63 | if (LOGIN_REQUIRED_URL === pathname) {
64 | loginWithNextUrl = `${LOGIN_REQUIRED_URL}`
65 | }
66 | router.replace(loginWithNextUrl)
67 | }
68 | return
69 | {children}
70 |
71 | }
72 |
73 | export function useAuth(){
74 | return useContext(AuthContext)
75 | }
--------------------------------------------------------------------------------
/src/components/layout/AccountDropdown.jsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import Link from "next/link"
4 | import { CircleUser, Menu, Package2, Search } from "lucide-react"
5 |
6 | import { Button } from "@/components/ui/button"
7 | import {
8 | DropdownMenu,
9 | DropdownMenuContent,
10 | DropdownMenuItem,
11 | DropdownMenuLabel,
12 | DropdownMenuSeparator,
13 | DropdownMenuTrigger,
14 | } from "@/components/ui/dropdown-menu"
15 | import { Input } from "@/components/ui/input"
16 | import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet"
17 | import { useAuth } from "../authProvider"
18 |
19 | import NavLinks, {NonUserLinks} from './NavLinks'
20 | import BrandLink from "./BrandLink"
21 | import MobileNavbar from "./MobileNavbar"
22 | import { useRouter } from "next/navigation"
23 |
24 |
25 |
26 | export default function AccountDropdown({className}) {
27 | const auth = useAuth()
28 | const router = useRouter()
29 |
30 | return
31 |
32 |
36 |
37 |
38 | {auth.username? auth.username : "Account"}
39 | router.push('/logout')}>Logout
40 |
41 |
42 | }
--------------------------------------------------------------------------------
/src/components/layout/BaseLayout.jsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import Navbar from './Navbar'
4 |
5 |
6 | export default function BaseLayout({ children, className}) {
7 | const mainClassName = className ? className : "flex min-h-[calc(100vh_-_theme(spacing.16))] flex-1 flex-col gap-4 bg-muted/40 p-4 md:gap-8 md:p-10"
8 | return (
9 |
10 |
11 |
12 | {children}
13 |
14 |
15 | )
16 | }
17 |
--------------------------------------------------------------------------------
/src/components/layout/BrandLink.jsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import Link from "next/link"
4 | import { Package2 } from "lucide-react"
5 |
6 | export default function BrandLink({displayName, className}){
7 | const finalClass = className ? className : "flex items-center gap-2 text-lg font-semibold md:text-base"
8 | return
12 |
13 | {displayName ?
14 | SaaS
15 | :
16 | SaaS
17 | }
18 |
19 | }
--------------------------------------------------------------------------------
/src/components/layout/MobileNavbar.jsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import Link from "next/link"
4 | import { Menu } from "lucide-react"
5 |
6 | import { Button } from "@/components/ui/button"
7 | import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet"
8 | import { useAuth } from "../authProvider"
9 |
10 | import NavLinks, {NonUserLinks} from './NavLinks'
11 | import BrandLink from "./BrandLink"
12 |
13 |
14 |
15 | export default function MobileNavbar({className}) {
16 | const auth = useAuth()
17 | return
18 |
19 |
27 |
28 |
29 |
62 |
63 |
64 | }
--------------------------------------------------------------------------------
/src/components/layout/NavLinks.jsx:
--------------------------------------------------------------------------------
1 |
2 |
3 | const NavLinks = [
4 | {
5 | label: "Dashboard",
6 | authRequired: false,
7 | href: "/"
8 | },
9 | {
10 | label: "Waitlist",
11 | authRequired: true,
12 | href: "/waitlists"
13 | }
14 | ]
15 |
16 | export const NonUserLinks = [
17 | {
18 | label: "Signup",
19 | authRequired: false,
20 | href: "/signup"
21 | },
22 | {
23 | label: "Login",
24 | authRequired: false,
25 | href: "/login"
26 | }
27 | ]
28 | export default NavLinks
--------------------------------------------------------------------------------
/src/components/layout/Navbar.jsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import Link from "next/link"
4 | import { useAuth } from "../authProvider"
5 | import NavLinks, {NonUserLinks} from './NavLinks'
6 | import BrandLink from "./BrandLink"
7 | import MobileNavbar from "./MobileNavbar"
8 | import AccountDropdown from "./AccountDropdown"
9 |
10 |
11 | export default function Navbar({className}) {
12 | const auth = useAuth()
13 | const finalClass = className ? className : "sticky top-0 flex h-16 items-center gap-4 border-b bg-background px-4 md:px-6"
14 | return
53 | }
--------------------------------------------------------------------------------
/src/components/themeProvider.jsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import { ThemeProvider as NextThemesProvider } from "next-themes"
5 |
6 | export function ThemeProvider({ children, ...props }) {
7 | return {children}
8 | }
--------------------------------------------------------------------------------
/src/components/themeToggleButton.jsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import { Moon, Sun } from "lucide-react"
5 | import { useTheme } from "next-themes"
6 |
7 | import { Button } from "@/components/ui/button"
8 | import {
9 | DropdownMenu,
10 | DropdownMenuContent,
11 | DropdownMenuItem,
12 | DropdownMenuTrigger,
13 | } from "@/components/ui/dropdown-menu"
14 |
15 | export function ThemeToggleButton() {
16 | const { setTheme } = useTheme()
17 |
18 | return (
19 |
20 |
21 |
26 |
27 |
28 | setTheme("light")}>
29 | Light
30 |
31 | setTheme("dark")}>
32 | Dark
33 |
34 | setTheme("system")}>
35 | System
36 |
37 |
38 |
39 | )
40 | }
41 |
--------------------------------------------------------------------------------
/src/components/ui/button.jsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { cva } from "class-variance-authority";
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
9 | {
10 | variants: {
11 | variant: {
12 | default:
13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90",
14 | destructive:
15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
16 | outline:
17 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
18 | secondary:
19 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
20 | ghost: "hover:bg-accent hover:text-accent-foreground",
21 | link: "text-primary underline-offset-4 hover:underline",
22 | },
23 | size: {
24 | default: "h-9 px-4 py-2",
25 | sm: "h-8 rounded-md px-3 text-xs",
26 | lg: "h-10 rounded-md px-8",
27 | icon: "h-9 w-9",
28 | },
29 | },
30 | defaultVariants: {
31 | variant: "default",
32 | size: "default",
33 | },
34 | }
35 | )
36 |
37 | const Button = React.forwardRef(({ className, variant, size, asChild = false, ...props }, ref) => {
38 | const Comp = asChild ? Slot : "button"
39 | return (
40 | ()
44 | );
45 | })
46 | Button.displayName = "Button"
47 |
48 | export { Button, buttonVariants }
49 |
--------------------------------------------------------------------------------
/src/components/ui/card.jsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Card = React.forwardRef(({ className, ...props }, ref) => (
6 |
10 | ))
11 | Card.displayName = "Card"
12 |
13 | const CardHeader = React.forwardRef(({ className, ...props }, ref) => (
14 |
18 | ))
19 | CardHeader.displayName = "CardHeader"
20 |
21 | const CardTitle = React.forwardRef(({ className, ...props }, ref) => (
22 |
26 | ))
27 | CardTitle.displayName = "CardTitle"
28 |
29 | const CardDescription = React.forwardRef(({ className, ...props }, ref) => (
30 |
34 | ))
35 | CardDescription.displayName = "CardDescription"
36 |
37 | const CardContent = React.forwardRef(({ className, ...props }, ref) => (
38 |
39 | ))
40 | CardContent.displayName = "CardContent"
41 |
42 | const CardFooter = React.forwardRef(({ className, ...props }, ref) => (
43 |
47 | ))
48 | CardFooter.displayName = "CardFooter"
49 |
50 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
51 |
--------------------------------------------------------------------------------
/src/components/ui/dropdown-menu.jsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
5 | import {
6 | CheckIcon,
7 | ChevronRightIcon,
8 | DotFilledIcon,
9 | } from "@radix-ui/react-icons"
10 |
11 | import { cn } from "@/lib/utils"
12 |
13 | const DropdownMenu = DropdownMenuPrimitive.Root
14 |
15 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
16 |
17 | const DropdownMenuGroup = DropdownMenuPrimitive.Group
18 |
19 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal
20 |
21 | const DropdownMenuSub = DropdownMenuPrimitive.Sub
22 |
23 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
24 |
25 | const DropdownMenuSubTrigger = React.forwardRef(({ className, inset, children, ...props }, ref) => (
26 |
34 | {children}
35 |
36 |
37 | ))
38 | DropdownMenuSubTrigger.displayName =
39 | DropdownMenuPrimitive.SubTrigger.displayName
40 |
41 | const DropdownMenuSubContent = React.forwardRef(({ className, ...props }, ref) => (
42 |
49 | ))
50 | DropdownMenuSubContent.displayName =
51 | DropdownMenuPrimitive.SubContent.displayName
52 |
53 | const DropdownMenuContent = React.forwardRef(({ className, sideOffset = 4, ...props }, ref) => (
54 |
55 |
64 |
65 | ))
66 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
67 |
68 | const DropdownMenuItem = React.forwardRef(({ className, inset, ...props }, ref) => (
69 |
77 | ))
78 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
79 |
80 | const DropdownMenuCheckboxItem = React.forwardRef(({ className, children, checked, ...props }, ref) => (
81 |
89 |
90 |
91 |
92 |
93 |
94 | {children}
95 |
96 | ))
97 | DropdownMenuCheckboxItem.displayName =
98 | DropdownMenuPrimitive.CheckboxItem.displayName
99 |
100 | const DropdownMenuRadioItem = React.forwardRef(({ className, children, ...props }, ref) => (
101 |
108 |
109 |
110 |
111 |
112 |
113 | {children}
114 |
115 | ))
116 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
117 |
118 | const DropdownMenuLabel = React.forwardRef(({ className, inset, ...props }, ref) => (
119 |
123 | ))
124 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
125 |
126 | const DropdownMenuSeparator = React.forwardRef(({ className, ...props }, ref) => (
127 |
131 | ))
132 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
133 |
134 | const DropdownMenuShortcut = ({
135 | className,
136 | ...props
137 | }) => {
138 | return (
139 | ()
142 | );
143 | }
144 | DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
145 |
146 | export {
147 | DropdownMenu,
148 | DropdownMenuTrigger,
149 | DropdownMenuContent,
150 | DropdownMenuItem,
151 | DropdownMenuCheckboxItem,
152 | DropdownMenuRadioItem,
153 | DropdownMenuLabel,
154 | DropdownMenuSeparator,
155 | DropdownMenuShortcut,
156 | DropdownMenuGroup,
157 | DropdownMenuPortal,
158 | DropdownMenuSub,
159 | DropdownMenuSubContent,
160 | DropdownMenuSubTrigger,
161 | DropdownMenuRadioGroup,
162 | }
163 |
--------------------------------------------------------------------------------
/src/components/ui/input.jsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Input = React.forwardRef(({ className, type, ...props }, ref) => {
6 | return (
7 | ()
15 | );
16 | })
17 | Input.displayName = "Input"
18 |
19 | export { Input }
20 |
--------------------------------------------------------------------------------
/src/components/ui/label.jsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as LabelPrimitive from "@radix-ui/react-label"
5 | import { cva } from "class-variance-authority";
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const labelVariants = cva(
10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
11 | )
12 |
13 | const Label = React.forwardRef(({ className, ...props }, ref) => (
14 |
15 | ))
16 | Label.displayName = LabelPrimitive.Root.displayName
17 |
18 | export { Label }
19 |
--------------------------------------------------------------------------------
/src/components/ui/sheet.jsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import * as React from "react"
3 | import * as SheetPrimitive from "@radix-ui/react-dialog"
4 | import { Cross2Icon } from "@radix-ui/react-icons"
5 | import { cva } from "class-variance-authority";
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Sheet = SheetPrimitive.Root
10 |
11 | const SheetTrigger = SheetPrimitive.Trigger
12 |
13 | const SheetClose = SheetPrimitive.Close
14 |
15 | const SheetPortal = SheetPrimitive.Portal
16 |
17 | const SheetOverlay = React.forwardRef(({ className, ...props }, ref) => (
18 |
25 | ))
26 | SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
27 |
28 | const sheetVariants = cva(
29 | "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
30 | {
31 | variants: {
32 | side: {
33 | top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
34 | bottom:
35 | "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
36 | left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
37 | right:
38 | "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
39 | },
40 | },
41 | defaultVariants: {
42 | side: "right",
43 | },
44 | }
45 | )
46 |
47 | const SheetContent = React.forwardRef(({ side = "right", className, children, ...props }, ref) => (
48 |
49 |
50 |
51 | {children}
52 |
54 |
55 | Close
56 |
57 |
58 |
59 | ))
60 | SheetContent.displayName = SheetPrimitive.Content.displayName
61 |
62 | const SheetHeader = ({
63 | className,
64 | ...props
65 | }) => (
66 |
69 | )
70 | SheetHeader.displayName = "SheetHeader"
71 |
72 | const SheetFooter = ({
73 | className,
74 | ...props
75 | }) => (
76 |
79 | )
80 | SheetFooter.displayName = "SheetFooter"
81 |
82 | const SheetTitle = React.forwardRef(({ className, ...props }, ref) => (
83 |
87 | ))
88 | SheetTitle.displayName = SheetPrimitive.Title.displayName
89 |
90 | const SheetDescription = React.forwardRef(({ className, ...props }, ref) => (
91 |
95 | ))
96 | SheetDescription.displayName = SheetPrimitive.Description.displayName
97 |
98 | export {
99 | Sheet,
100 | SheetPortal,
101 | SheetOverlay,
102 | SheetTrigger,
103 | SheetClose,
104 | SheetContent,
105 | SheetHeader,
106 | SheetFooter,
107 | SheetTitle,
108 | SheetDescription,
109 | }
110 |
--------------------------------------------------------------------------------
/src/components/ui/table.jsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Table = React.forwardRef(({ className, ...props }, ref) => (
6 |
12 | ))
13 | Table.displayName = "Table"
14 |
15 | const TableHeader = React.forwardRef(({ className, ...props }, ref) => (
16 |
17 | ))
18 | TableHeader.displayName = "TableHeader"
19 |
20 | const TableBody = React.forwardRef(({ className, ...props }, ref) => (
21 |
25 | ))
26 | TableBody.displayName = "TableBody"
27 |
28 | const TableFooter = React.forwardRef(({ className, ...props }, ref) => (
29 | tr]:last:border-b-0", className)}
32 | {...props} />
33 | ))
34 | TableFooter.displayName = "TableFooter"
35 |
36 | const TableRow = React.forwardRef(({ className, ...props }, ref) => (
37 |
44 | ))
45 | TableRow.displayName = "TableRow"
46 |
47 | const TableHead = React.forwardRef(({ className, ...props }, ref) => (
48 | [role=checkbox]]:translate-y-[2px]",
52 | className
53 | )}
54 | {...props} />
55 | ))
56 | TableHead.displayName = "TableHead"
57 |
58 | const TableCell = React.forwardRef(({ className, ...props }, ref) => (
59 | | [role=checkbox]]:translate-y-[2px]",
63 | className
64 | )}
65 | {...props} />
66 | ))
67 | TableCell.displayName = "TableCell"
68 |
69 | const TableCaption = React.forwardRef(({ className, ...props }, ref) => (
70 |
74 | ))
75 | TableCaption.displayName = "TableCaption"
76 |
77 | export {
78 | Table,
79 | TableHeader,
80 | TableBody,
81 | TableFooter,
82 | TableHead,
83 | TableRow,
84 | TableCell,
85 | TableCaption,
86 | }
87 |
--------------------------------------------------------------------------------
/src/components/ui/textarea.jsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Textarea = React.forwardRef(({ className, ...props }, ref) => {
6 | return (
7 | ()
14 | );
15 | })
16 | Textarea.displayName = "Textarea"
17 |
18 | export { Textarea }
19 |
--------------------------------------------------------------------------------
/src/config/defaults.jsx:
--------------------------------------------------------------------------------
1 | export const DJANGO_BASE_URL=process.env.DJANGO_BASE_URL
2 | export const DJANGO_API_ENDPOINT=`${DJANGO_BASE_URL}/api`
--------------------------------------------------------------------------------
/src/lib/auth.jsx:
--------------------------------------------------------------------------------
1 | const { cookies } = require("next/headers")
2 |
3 | const TOKEN_AGE = 3600
4 | const TOKEN_NAME = "auth-token"
5 | const TOKEN_REFRESH_NAME = "auth-refresh-token"
6 |
7 | export function getToken(){
8 | // api requests
9 | const myAuthToken = cookies().get(TOKEN_NAME)
10 | return myAuthToken?.value
11 | }
12 |
13 |
14 | export function getRefreshToken(){
15 | // api requests
16 | const myAuthToken = cookies().get(TOKEN_REFRESH_NAME)
17 | return myAuthToken?.value
18 | }
19 |
20 | export function setToken(authToken){
21 | // login
22 | return cookies().set({
23 | name: TOKEN_NAME,
24 | value: authToken,
25 | httpOnly: true, // limit client-side js
26 | sameSite: 'strict',
27 | secure: process.env.NODE_ENV !== 'development',
28 | maxAge: TOKEN_AGE,
29 | })
30 | }
31 |
32 | export function setRefreshToken(authRefreshToken){
33 | // login
34 | return cookies().set({
35 | name: TOKEN_REFRESH_NAME,
36 | value: authRefreshToken,
37 | httpOnly: true, // limit client-side js
38 | sameSite: 'strict',
39 | secure: process.env.NODE_ENV !== 'development',
40 | maxAge: TOKEN_AGE,
41 | })
42 | }
43 |
44 | export function deleteTokens(){
45 | // logout
46 | cookies().delete(TOKEN_REFRESH_NAME)
47 | return cookies().delete(TOKEN_NAME)
48 | }
--------------------------------------------------------------------------------
/src/lib/fetcher.js:
--------------------------------------------------------------------------------
1 | const fetcher = async url => {
2 |
3 | const res = await fetch(url)
4 |
5 | // If the status code is not in the range 200-299,
6 | // we still try to parse and throw it.
7 | if (!res.ok) {
8 | const error = new Error('An error occurred while fetching the data.')
9 | // Attach extra info to the error object.
10 | error.info = await res.json()
11 | error.status = res.status
12 | throw error
13 | }
14 |
15 | return res.json()
16 | }
17 |
18 | export default fetcher
--------------------------------------------------------------------------------
/src/lib/utils.js:
--------------------------------------------------------------------------------
1 | import { clsx } from "clsx"
2 | import { twMerge } from "tailwind-merge"
3 |
4 | export function cn(...inputs) {
5 | return twMerge(clsx(inputs))
6 | }
7 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 |
3 | const { fontFamily } = require("tailwindcss/defaultTheme")
4 |
5 |
6 |
7 | module.exports = {
8 | darkMode: ["class"],
9 | content: [
10 | './pages/**/*.{js,jsx}',
11 | './components/**/*.{js,jsx}',
12 | './app/**/*.{js,jsx}',
13 | './src/**/*.{js,jsx}',
14 | ],
15 | prefix: "",
16 | theme: {
17 | container: {
18 | center: true,
19 | padding: "2rem",
20 | screens: {
21 | "2xl": "1400px",
22 | },
23 | },
24 | extend: {
25 | fontFamily: {
26 | sans: ["var(--font-sans)", ...fontFamily.sans],
27 | },
28 | colors: {
29 | border: "hsl(var(--border))",
30 | input: "hsl(var(--input))",
31 | ring: "hsl(var(--ring))",
32 | background: "hsl(var(--background))",
33 | foreground: "hsl(var(--foreground))",
34 | primary: {
35 | DEFAULT: "hsl(var(--primary))",
36 | foreground: "hsl(var(--primary-foreground))",
37 | },
38 | secondary: {
39 | DEFAULT: "hsl(var(--secondary))",
40 | foreground: "hsl(var(--secondary-foreground))",
41 | },
42 | destructive: {
43 | DEFAULT: "hsl(var(--destructive))",
44 | foreground: "hsl(var(--destructive-foreground))",
45 | },
46 | muted: {
47 | DEFAULT: "hsl(var(--muted))",
48 | foreground: "hsl(var(--muted-foreground))",
49 | },
50 | accent: {
51 | DEFAULT: "hsl(var(--accent))",
52 | foreground: "hsl(var(--accent-foreground))",
53 | },
54 | popover: {
55 | DEFAULT: "hsl(var(--popover))",
56 | foreground: "hsl(var(--popover-foreground))",
57 | },
58 | card: {
59 | DEFAULT: "hsl(var(--card))",
60 | foreground: "hsl(var(--card-foreground))",
61 | },
62 | },
63 | borderRadius: {
64 | lg: "var(--radius)",
65 | md: "calc(var(--radius) - 2px)",
66 | sm: "calc(var(--radius) - 4px)",
67 | },
68 | keyframes: {
69 | "accordion-down": {
70 | from: { height: "0" },
71 | to: { height: "var(--radix-accordion-content-height)" },
72 | },
73 | "accordion-up": {
74 | from: { height: "var(--radix-accordion-content-height)" },
75 | to: { height: "0" },
76 | },
77 | },
78 | animation: {
79 | "accordion-down": "accordion-down 0.2s ease-out",
80 | "accordion-up": "accordion-up 0.2s ease-out",
81 | },
82 | },
83 | },
84 | plugins: [require("tailwindcss-animate")],
85 | }
--------------------------------------------------------------------------------
|