├── .env.example
├── .eslintrc.json
├── .gitignore
├── .prettierrc.json
├── README.md
├── next.config.js
├── package-lock.json
├── package.json
├── postcss.config.js
├── public
├── next.svg
└── vercel.svg
├── src
├── app
│ ├── (authenticated)
│ │ ├── dashboard
│ │ │ └── page.tsx
│ │ └── layout.tsx
│ ├── (guest)
│ │ ├── forgot-password
│ │ │ └── page.tsx
│ │ ├── login
│ │ │ └── page.tsx
│ │ ├── password-reset
│ │ │ └── [token]
│ │ │ │ └── page.tsx
│ │ ├── register
│ │ │ └── page.tsx
│ │ └── verify-email
│ │ │ └── page.tsx
│ ├── favicon.ico
│ ├── globals.css
│ ├── layout.tsx
│ ├── not-found.tsx
│ └── page.tsx
├── components
│ ├── ApplicationLogo.tsx
│ ├── AuthCard.tsx
│ ├── AuthSessionStatus.tsx
│ ├── Dropdown.tsx
│ ├── DropdownLink.tsx
│ ├── Layouts
│ │ └── Navigation.tsx
│ ├── NavLink.tsx
│ └── ResponsiveNavLink.tsx
├── hooks
│ └── auth.ts
├── lib
│ └── axios.ts
└── types
│ └── User.ts
├── tailwind.config.ts
└── tsconfig.json
/.env.example:
--------------------------------------------------------------------------------
1 | NEXT_PUBLIC_BACKEND_URL=
2 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/.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 | .yarn/install-state.gz
8 |
9 | # testing
10 | /coverage
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | # local env files
29 | .env*.local
30 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 | next-env.d.ts
37 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "arrowParens": "avoid",
3 | "jsxBracketSameLine": true,
4 | "semi": false,
5 | "singleQuote": true,
6 | "tabWidth": 2,
7 | "trailingComma": "all"
8 | }
9 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Laravel Breeze - Next.js v14 Edition with TypeScript 🏝️
2 | ## Introduction
3 |
4 | ---
5 | **This repository is a refactor of [breeze-next](https://github.com/laravel/breeze-next) by changing programing language from [JavaScript](https://www.javascript.com/) to [TypeScript](https://www.typescriptlang.org/)**
6 |
7 | **This used NextJS Version 14**
8 |
9 | ### Little extras
10 | * Add Formik Validation
11 | * Next Features inside app folder
12 | * Add Route Group
13 |
14 | ---
15 |
16 | This repository is an implementation of the [Laravel Breeze](https://laravel.com/docs/starter-kits) application / authentication starter kit frontend in [Next.js](https://nextjs.org). All of the authentication boilerplate is already written for you - powered by [Laravel Sanctum](https://laravel.com/docs/sanctum), allowing you to quickly begin pairing your beautiful Next.js frontend with a powerful Laravel backend.
17 |
18 | ## Official Documentation
19 |
20 | ### Installation
21 |
22 | First, create a Next.js compatible Laravel backend by installing Laravel Breeze into a [fresh Laravel application](https://laravel.com/docs/installation) and installing Breeze's API scaffolding:
23 |
24 | ```bash
25 | # Create the Laravel application...
26 | laravel new next-backend
27 |
28 | cd next-backend
29 |
30 | # Install Breeze and dependencies...
31 | composer require laravel/breeze --dev
32 |
33 | php artisan breeze:install api
34 | ```
35 |
36 | Next, ensure that your application's `APP_URL` and `FRONTEND_URL` environment variables are set to `http://localhost:8000` and `http://localhost:3000`, respectively.
37 |
38 | After defining the appropriate environment variables, you may serve the Laravel application using the `serve` Artisan command:
39 |
40 | ```bash
41 | # Serve the application...
42 | php artisan serve
43 | ```
44 |
45 | Next, clone this repository and install its dependencies with `yarn install` or `npm install`. Then, copy the `.env.example` file to `.env.local` and supply the URL of your backend:
46 |
47 | ```
48 | NEXT_PUBLIC_BACKEND_URL=http://localhost:8000
49 | ```
50 |
51 | Finally, run the application via `npm run dev`. The application will be available at `http://localhost:3000`:
52 |
53 | ```
54 | npm run dev
55 | ```
56 |
57 | ## License
58 |
59 | Laravel Breeze Next.js v14 is open-sourced software licensed under the [MIT license](LICENSE.md).
60 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {}
3 |
4 | module.exports = nextConfig
5 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "laravel-breeze-next-typescript",
3 | "version": "0.1.0",
4 | "private": true,
5 | "keywords": [
6 | "laravel",
7 | "breeze",
8 | "laravel-breeze",
9 | "next",
10 | "typescript",
11 | "next-v14"
12 | ],
13 | "scripts": {
14 | "dev": "next dev",
15 | "build": "next build",
16 | "start": "next start",
17 | "lint": "next lint"
18 | },
19 | "dependencies": {
20 | "@headlessui/react": "^1.7.17",
21 | "@tailwindcss/forms": "^0.5.7",
22 | "axios": "^1.6.2",
23 | "formik": "^2.4.5",
24 | "next": "14.0.3",
25 | "react": "^18",
26 | "react-dom": "^18",
27 | "swr": "^2.2.4",
28 | "yup": "^1.3.2"
29 | },
30 | "devDependencies": {
31 | "@types/node": "^20",
32 | "@types/react": "^18",
33 | "@types/react-dom": "^18",
34 | "autoprefixer": "^10.0.1",
35 | "eslint": "^8",
36 | "eslint-config-next": "14.0.3",
37 | "postcss": "^8",
38 | "tailwindcss": "^3.3.0",
39 | "typescript": "^5"
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/(authenticated)/dashboard/page.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const DashboardPage = () => {
4 | return (
5 |
6 |
7 |
8 |
9 | {`You're logged in!`}
10 |
11 |
12 |
13 |
14 | )
15 | }
16 |
17 | export default DashboardPage
18 |
--------------------------------------------------------------------------------
/src/app/(authenticated)/layout.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import { ReactNode } from 'react'
3 | import { useAuth } from '@/hooks/auth'
4 | import Navigation from '@/components/Layouts/Navigation'
5 |
6 | const AppLayout = ({ children }: { children: ReactNode }) => {
7 | const { user } = useAuth({ middleware: 'auth' })
8 |
9 | return (
10 |
11 |
12 |
13 | {/* Page Content */}
14 | {children}
15 |
16 | )
17 | }
18 |
19 | export default AppLayout
20 |
--------------------------------------------------------------------------------
/src/app/(guest)/forgot-password/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import React, { useState } from 'react'
3 | import * as Yup from 'yup'
4 | import Link from 'next/link'
5 | import axios, { AxiosError } from 'axios'
6 | import { ErrorMessage, Field, Form, Formik, FormikHelpers } from 'formik'
7 |
8 | import { useAuth } from '@/hooks/auth'
9 | import AuthCard from '@/components/AuthCard'
10 | import ApplicationLogo from '@/components/ApplicationLogo'
11 | import AuthSessionStatus from '@/components/AuthSessionStatus'
12 |
13 | interface FormValues {
14 | email: string
15 | }
16 |
17 | const ForgotPasswordPage = () => {
18 | const [status, setStatus] = useState('')
19 |
20 | const { forgotPassword } = useAuth({
21 | middleware: 'guest',
22 | redirectIfAuthenticated: '/dashboard',
23 | })
24 |
25 | const ForgotPasswordSchema = Yup.object().shape({
26 | email: Yup.string()
27 | .email('Invalid email')
28 | .required('The email field is required.'),
29 | })
30 |
31 | const submitForm = async (
32 | values: FormValues,
33 | { setSubmitting, setErrors }: FormikHelpers,
34 | ): Promise => {
35 | try {
36 | const response = await forgotPassword(values)
37 |
38 | setStatus(response.data.status)
39 | } catch (error: Error | AxiosError | any) {
40 | setStatus('')
41 | if (axios.isAxiosError(error) && error.response?.status === 422) {
42 | setErrors(error.response?.data?.errors)
43 | }
44 | } finally {
45 | setSubmitting(false)
46 | }
47 | }
48 |
49 | return (
50 |
53 |
54 |
55 | }>
56 |
57 | Forgot your password? No problem. Just let us know your email address
58 | and we will email you a password reset link that will allow you to
59 | choose a new one.
60 |
61 |
62 |
63 |
64 |
68 |
98 |
99 |
100 | )
101 | }
102 |
103 | export default ForgotPasswordPage
104 |
--------------------------------------------------------------------------------
/src/app/(guest)/login/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import Link from 'next/link'
3 | import * as Yup from 'yup'
4 | import { useSearchParams } from 'next/navigation'
5 | import axios, { AxiosError } from 'axios'
6 | import { ErrorMessage, Field, Form, Formik, FormikHelpers } from 'formik'
7 |
8 | import { useAuth } from '@/hooks/auth'
9 | import ApplicationLogo from '@/components/ApplicationLogo'
10 | import AuthCard from '@/components/AuthCard'
11 | import { useEffect, useState } from 'react'
12 | import AuthSessionStatus from '@/components/AuthSessionStatus'
13 |
14 | interface Values {
15 | email: string
16 | password: string
17 | remember: boolean
18 | }
19 |
20 | const LoginPage = () => {
21 | const searchParams = useSearchParams()
22 | const [status, setStatus] = useState('')
23 |
24 | const { login } = useAuth({
25 | middleware: 'guest',
26 | redirectIfAuthenticated: '/dashboard',
27 | })
28 |
29 | useEffect(() => {
30 | const resetToken = searchParams.get('reset')
31 | setStatus(resetToken ? atob(resetToken) : '')
32 | }, [searchParams])
33 |
34 | const submitForm = async (
35 | values: Values,
36 | { setSubmitting, setErrors }: FormikHelpers,
37 | ): Promise => {
38 | try {
39 | await login(values)
40 | } catch (error: Error | AxiosError | any) {
41 | if (axios.isAxiosError(error) && error.response?.status === 422) {
42 | setErrors(error.response?.data?.errors)
43 | }
44 | } finally {
45 | setSubmitting(false)
46 | setStatus('')
47 | }
48 | }
49 |
50 | const LoginSchema = Yup.object().shape({
51 | email: Yup.string()
52 | .email('Invalid email')
53 | .required('The email field is required.'),
54 | password: Yup.string().required('The password field is required.'),
55 | })
56 |
57 | return (
58 |
61 |
62 |
63 | }>
64 |
65 |
66 |
70 |
141 |
142 |
143 | )
144 | }
145 |
146 | export default LoginPage
147 |
--------------------------------------------------------------------------------
/src/app/(guest)/password-reset/[token]/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import Link from 'next/link'
3 | import * as Yup from 'yup'
4 | import axios, { AxiosError } from 'axios'
5 | import { useSearchParams } from 'next/navigation'
6 | import { ErrorMessage, Field, Form, Formik, FormikHelpers } from 'formik'
7 |
8 | import { useAuth } from '@/hooks/auth'
9 | import AuthCard from '@/components/AuthCard'
10 | import ApplicationLogo from '@/components/ApplicationLogo'
11 |
12 | interface Values {
13 | email: string
14 | password: string
15 | password_confirmation: string
16 | }
17 |
18 | const PasswordResetPage = () => {
19 | const query = useSearchParams()
20 | const { resetPassword } = useAuth({ middleware: 'guest' })
21 |
22 | const submitForm = async (
23 | values: Values,
24 | { setSubmitting, setErrors }: FormikHelpers,
25 | ): Promise => {
26 | try {
27 | await resetPassword(values)
28 | } catch (error: Error | AxiosError | any) {
29 | if (axios.isAxiosError(error) && error.response?.status === 422) {
30 | setErrors(error.response?.data?.errors)
31 | }
32 | } finally {
33 | setSubmitting(false)
34 | }
35 | }
36 |
37 | const ForgotPasswordSchema = Yup.object().shape({
38 | email: Yup.string()
39 | .email('Invalid email')
40 | .required('The email field is required.'),
41 | password: Yup.string().required('The password field is required.'),
42 | password_confirmation: Yup.string()
43 | .required('Please confirm password.')
44 | .oneOf([Yup.ref('password')], 'Your passwords do not match.'),
45 | })
46 |
47 | return (
48 |
51 |
52 |
53 | }>
54 |
62 |
135 |
136 |
137 | )
138 | }
139 |
140 | export default PasswordResetPage
141 |
--------------------------------------------------------------------------------
/src/app/(guest)/register/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import Link from 'next/link'
3 | import * as Yup from 'yup'
4 | import axios, { AxiosError } from 'axios'
5 | import { ErrorMessage, Field, Form, Formik, FormikHelpers } from 'formik'
6 |
7 | import { useAuth } from '@/hooks/auth'
8 | import ApplicationLogo from '@/components/ApplicationLogo'
9 | import AuthCard from '@/components/AuthCard'
10 |
11 | interface Values {
12 | name: string
13 | email: string
14 | password: string
15 | password_confirmation: string
16 | }
17 |
18 | const RegisterPage = () => {
19 | const { register } = useAuth({
20 | middleware: 'guest',
21 | redirectIfAuthenticated: '/dashboard',
22 | })
23 |
24 | const submitForm = async (
25 | values: Values,
26 | { setSubmitting, setErrors }: FormikHelpers,
27 | ): Promise => {
28 | try {
29 | await register(values)
30 | } catch (error: Error | AxiosError | any) {
31 | if (axios.isAxiosError(error) && error.response?.status === 422) {
32 | setErrors(error.response?.data?.errors)
33 | }
34 | } finally {
35 | setSubmitting(false)
36 | }
37 | }
38 |
39 | const RegisterSchema = Yup.object().shape({
40 | name: Yup.string().required('The name field is required.'),
41 | email: Yup.string()
42 | .email('Invalid email')
43 | .required('The email field is required.'),
44 | password: Yup.string().required('The password field is required.'),
45 | password_confirmation: Yup.string()
46 | .required('Please confirm password.')
47 | .oneOf([Yup.ref('password')], 'Your passwords do not match.'),
48 | })
49 |
50 | return (
51 |
54 |
55 |
56 | }>
57 |
66 |
164 |
165 |
166 | )
167 | }
168 |
169 | export default RegisterPage
170 |
--------------------------------------------------------------------------------
/src/app/(guest)/verify-email/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import Link from 'next/link'
3 | import React, { useState } from 'react'
4 |
5 | import { useAuth } from '@/hooks/auth'
6 | import AuthCard from '@/components/AuthCard'
7 | import ApplicationLogo from '@/components/ApplicationLogo'
8 |
9 | const VerifyEmailPage = () => {
10 | const [status, setStatus] = useState('')
11 |
12 | const { logout, resendEmailVerification } = useAuth({
13 | middleware: 'auth',
14 | redirectIfAuthenticated: '/dashboard',
15 | })
16 |
17 | const onClickResend = () => {
18 | resendEmailVerification().then(response => setStatus(response.data.status))
19 | }
20 |
21 | return (
22 |
25 |
26 |
27 | }>
28 |
29 | Thanks for signing up! Before getting started, could you verify your
30 | email address by clicking on the link we just emailed to you? If you
31 | didn't receive the email, we will gladly send you another.
32 |
33 |
34 | {status === 'verification-link-sent' && (
35 |
36 | A new verification link has been sent to the email address you
37 | provided during registration.
38 |
39 | )}
40 |
41 |
42 |
45 | Resend Verification Email
46 |
47 |
48 |
52 | Logout
53 |
54 |
55 |
56 | )
57 | }
58 |
59 | export default VerifyEmailPage
60 |
--------------------------------------------------------------------------------
/src/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Byandev/laravel-breeze-next-typescript/b4315642ddc83d66890e0ee8535efd5acbf15ab4/src/app/favicon.ico
--------------------------------------------------------------------------------
/src/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
--------------------------------------------------------------------------------
/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from 'next'
2 | import { Nunito } from 'next/font/google'
3 | import './globals.css'
4 |
5 | const nunito = Nunito({ subsets: ['latin'] })
6 |
7 | export const metadata: Metadata = {
8 | title: 'Create Next App',
9 | description: 'Generated by create next app',
10 | }
11 |
12 | export default function RootLayout({
13 | children,
14 | }: {
15 | children: React.ReactNode
16 | }) {
17 | return (
18 |
19 |
20 | {children}
21 |
22 |
23 | )
24 | }
25 |
--------------------------------------------------------------------------------
/src/app/not-found.tsx:
--------------------------------------------------------------------------------
1 | export default function NotFound() {
2 | return (
3 |
4 |
5 |
6 |
7 | 404
8 |
9 |
10 |
11 | Not Found
12 |
13 |
14 |
15 |
16 | )
17 | }
18 |
--------------------------------------------------------------------------------
/src/app/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import Head from 'next/head'
4 | import Link from 'next/link'
5 | import { useAuth } from '@/hooks/auth'
6 |
7 | export default function Home() {
8 | const { user } = useAuth({ middleware: 'guest' })
9 |
10 | return (
11 | <>
12 |
13 | Laravel
14 |
15 |
16 |
17 |
18 | {user ? (
19 |
22 | Dashboard
23 |
24 | ) : (
25 | <>
26 |
27 | Login
28 |
29 |
30 |
33 | Register
34 |
35 | >
36 | )}
37 |
38 |
39 |
40 |
41 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
64 |
65 |
66 |
67 |
74 |
75 |
76 |
77 |
78 | Laravel has wonderful, thorough documentation covering every
79 | aspect of the framework. Whether you are new to the
80 | framework or have previous experience with Laravel, we
81 | recommend reading all of the documentation from beginning to
82 | end.
83 |
84 |
85 |
86 |
87 |
88 |
89 |
97 |
98 |
99 |
100 |
101 |
108 |
109 |
110 |
111 |
112 | Laracasts offers thousands of video tutorials on Laravel,
113 | PHP, and JavaScript development. Check them out, see for
114 | yourself, and massively level up your development skills in
115 | the process.
116 |
117 |
118 |
119 |
120 |
121 |
122 |
130 |
131 |
132 |
133 |
140 |
141 |
142 |
143 |
144 | Laravel News is a community driven portal and newsletter
145 | aggregating all of the latest and most important news in the
146 | Laravel ecosystem, including new package releases and
147 | tutorials.
148 |
149 |
150 |
151 |
152 |
153 |
154 |
162 |
163 |
164 |
165 |
166 | Vibrant Ecosystem
167 |
168 |
169 |
170 |
171 |
172 | Laravel is robust library of first-party tools and
173 | libraries, such as{' '}
174 |
175 | Forge
176 |
177 | ,{' '}
178 |
179 | Vapor
180 |
181 | ,{' '}
182 |
183 | Nova
184 |
185 | , and{' '}
186 |
187 | Envoyer
188 | {' '}
189 | help you take your projects to the next level. Pair them
190 | with powerful open source libraries like{' '}
191 |
194 | Cashier
195 |
196 | ,{' '}
197 |
200 | Dusk
201 |
202 | ,{' '}
203 |
206 | Echo
207 |
208 | ,{' '}
209 |
212 | Horizon
213 |
214 | ,{' '}
215 |
218 | Sanctum
219 |
220 | ,{' '}
221 |
224 | Telescope
225 |
226 | , and more.
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
271 |
272 |
273 | Laravel Breeze + Next.js template
274 |
275 |
276 |
277 |
278 | >
279 | )
280 | }
281 |
--------------------------------------------------------------------------------
/src/components/ApplicationLogo.tsx:
--------------------------------------------------------------------------------
1 | const ApplicationLogo = ({ ...props }) => (
2 |
3 |
4 |
5 | )
6 |
7 | export default ApplicationLogo
8 |
--------------------------------------------------------------------------------
/src/components/AuthCard.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode } from 'react'
2 |
3 | type Props = {
4 | logo: ReactNode
5 | children: ReactNode
6 | }
7 |
8 | const AuthCard = ({ logo, children }: Props) => {
9 | return (
10 |
11 |
{logo}
12 |
13 |
14 | {children}
15 |
16 |
17 | )
18 | }
19 |
20 | export default AuthCard
21 |
--------------------------------------------------------------------------------
/src/components/AuthSessionStatus.tsx:
--------------------------------------------------------------------------------
1 | import React, { ComponentProps } from 'react'
2 |
3 | interface AuthSessionStatusProps extends ComponentProps<'div'> {
4 | status: string
5 | }
6 |
7 | const AuthSessionStatus = ({
8 | status,
9 | className,
10 | ...props
11 | }: AuthSessionStatusProps) => {
12 | return (
13 | <>
14 | {status && (
15 |
18 | {status}
19 |
20 | )}
21 | >
22 | )
23 | }
24 |
25 | export default AuthSessionStatus
26 |
--------------------------------------------------------------------------------
/src/components/Dropdown.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode } from 'react'
2 | import { Menu, Transition } from '@headlessui/react'
3 |
4 | type DropdownProps = {
5 | width?: number
6 | trigger: ReactNode
7 | children: ReactNode
8 | contentClasses?: string
9 | align?: 'right' | 'left' | 'top'
10 | }
11 |
12 | const Dropdown = ({
13 | align = 'right',
14 | width = 48,
15 | contentClasses = 'py-1 bg-white',
16 | trigger,
17 | children,
18 | }: DropdownProps) => {
19 | let alignmentClasses: string
20 |
21 | switch (align) {
22 | case 'left':
23 | alignmentClasses = 'origin-top-left left-0'
24 | break
25 | case 'top':
26 | alignmentClasses = 'origin-top'
27 | break
28 | case 'right':
29 | default:
30 | alignmentClasses = 'origin-top-right right-0'
31 | break
32 | }
33 |
34 | return (
35 |
36 | {({ open }) => (
37 | <>
38 | {trigger}
39 |
40 |
48 |
50 |
53 | {children}
54 |
55 |
56 |
57 | >
58 | )}
59 |
60 | )
61 | }
62 |
63 | export default Dropdown
64 |
--------------------------------------------------------------------------------
/src/components/DropdownLink.tsx:
--------------------------------------------------------------------------------
1 | import { Menu } from '@headlessui/react'
2 | import Link, { LinkProps } from 'next/link'
3 | import { ComponentProps, ReactNode } from 'react'
4 |
5 | interface DropdownLinkProps extends LinkProps {
6 | children: ReactNode
7 | }
8 |
9 | interface DropdownButtonProps extends ComponentProps<'button'> {
10 | children: ReactNode
11 | }
12 |
13 | const DropdownLink = ({ children, ...props }: DropdownLinkProps) => (
14 |
15 | {({ active }) => (
16 |
21 | {children}
22 |
23 | )}
24 |
25 | )
26 |
27 | export const DropdownButton = ({ children, ...props }: DropdownButtonProps) => (
28 |
29 | {({ active }) => (
30 |
35 | {children}
36 |
37 | )}
38 |
39 | )
40 |
41 | export default DropdownLink
42 |
--------------------------------------------------------------------------------
/src/components/Layouts/Navigation.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link'
2 | import { useState } from 'react'
3 | import { usePathname } from 'next/navigation'
4 |
5 | import NavLink from '@/components/NavLink'
6 | import Dropdown from '@/components/Dropdown'
7 | import ResponsiveNavLink, {
8 | ResponsiveNavButton,
9 | } from '@/components/ResponsiveNavLink'
10 | import { DropdownButton } from '@/components/DropdownLink'
11 | import ApplicationLogo from '@/components/ApplicationLogo'
12 |
13 | import { UserType } from '@/types/User'
14 | import { useAuth } from '@/hooks/auth'
15 |
16 | const Navigation = ({ user }: { user: UserType }) => {
17 | const pathname = usePathname()
18 |
19 | const { logout } = useAuth({})
20 | const [open, setOpen] = useState(false)
21 |
22 | return (
23 |
24 | {/* Primary Navigation Menu */}
25 |
26 |
27 |
28 | {/* Logo */}
29 |
34 |
35 | {/* Navigation Links */}
36 |
37 |
38 | Dashboard
39 |
40 |
41 |
42 |
43 | {/* Settings Dropdown */}
44 |
45 |
50 | {user?.name}
51 |
52 |
64 |
65 | }>
66 | {/* Authentication */}
67 | Logout
68 |
69 |
70 |
71 | {/* Hamburger */}
72 |
73 |
setOpen(open => !open)}
75 | className="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 focus:text-gray-500 transition duration-150 ease-in-out">
76 |
81 | {open ? (
82 |
89 | ) : (
90 |
97 | )}
98 |
99 |
100 |
101 |
102 |
103 |
104 | {/* Responsive Navigation Menu */}
105 | {open && (
106 |
107 |
108 |
111 | Dashboard
112 |
113 |
114 |
115 | {/* Responsive Settings Options */}
116 |
117 |
118 |
133 |
134 |
135 |
136 | {user?.name}
137 |
138 |
139 | {user?.email}
140 |
141 |
142 |
143 |
144 |
145 | {/* Authentication */}
146 | Logout
147 |
148 |
149 |
150 | )}
151 |
152 | )
153 | }
154 |
155 | export default Navigation
156 |
--------------------------------------------------------------------------------
/src/components/NavLink.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode } from 'react'
2 | import Link, { LinkProps } from 'next/link'
3 |
4 | interface NavLinkProps extends LinkProps {
5 | active?: boolean
6 | children: ReactNode
7 | }
8 |
9 | const NavLink = ({
10 | active = false,
11 | href,
12 | children,
13 | ...props
14 | }: NavLinkProps) => (
15 |
24 | {children}
25 |
26 | )
27 |
28 | export default NavLink
29 |
--------------------------------------------------------------------------------
/src/components/ResponsiveNavLink.tsx:
--------------------------------------------------------------------------------
1 | import Link, { LinkProps } from 'next/link'
2 | import { ComponentProps, ReactNode } from 'react'
3 |
4 | interface ResponsiveNavLinkProps extends LinkProps {
5 | active?: boolean
6 | children: ReactNode
7 | }
8 |
9 | const ResponsiveNavLink = ({
10 | active = false,
11 | children,
12 | ...props
13 | }: ResponsiveNavLinkProps) => (
14 |
21 | {children}
22 |
23 | )
24 |
25 | export const ResponsiveNavButton = (props: ComponentProps<'button'>) => (
26 |
30 | )
31 |
32 | export default ResponsiveNavLink
33 |
--------------------------------------------------------------------------------
/src/hooks/auth.ts:
--------------------------------------------------------------------------------
1 | import useSWR from 'swr'
2 | import axios from '@/lib/axios'
3 | import { useEffect } from 'react'
4 | import { AxiosResponse } from 'axios'
5 | import { useRouter, useParams } from 'next/navigation'
6 |
7 | export const useAuth = ({
8 | middleware,
9 | redirectIfAuthenticated,
10 | }: {
11 | middleware?: string
12 | redirectIfAuthenticated?: string
13 | }) => {
14 | const router = useRouter()
15 | const params = useParams()
16 |
17 | const {
18 | data: user,
19 | error,
20 | mutate,
21 | } = useSWR('/api/user', () =>
22 | axios
23 | .get('/api/user')
24 | .then(res => res.data)
25 | .catch(error => {
26 | if (error.response.status !== 409) throw error
27 |
28 | router.push('/verify-email')
29 | }),
30 | )
31 |
32 | const csrf = () => axios.get('/sanctum/csrf-cookie')
33 |
34 | const register = async (data: {
35 | name: string
36 | email: string
37 | password: string
38 | password_confirmation: string
39 | }) => {
40 | try {
41 | await csrf()
42 |
43 | await axios.post('/register', data)
44 | mutate()
45 | } catch (error) {
46 | throw error
47 | }
48 | }
49 |
50 | const login = async (data: {
51 | email: string
52 | password: string
53 | remember: boolean
54 | }) => {
55 | try {
56 | await csrf()
57 | await axios.post('/login', data)
58 | mutate()
59 | } catch (error) {
60 | throw error
61 | }
62 | }
63 |
64 | const forgotPassword = async (data: {
65 | email: string
66 | }): Promise => {
67 | try {
68 | await csrf()
69 | return await axios.post('/forgot-password', data)
70 | } catch (error) {
71 | throw error
72 | }
73 | }
74 |
75 | const resetPassword = async (data: {
76 | email: string
77 | password: string
78 | password_confirmation: string
79 | }) => {
80 | try {
81 | await csrf()
82 |
83 | const response = await axios.post('/reset-password', {
84 | ...data,
85 | token: params.token,
86 | })
87 |
88 | router.push('/login?reset=' + btoa(response.data.status))
89 | } catch (error) {
90 | throw error
91 | }
92 | }
93 |
94 | const resendEmailVerification = async () => {
95 | try {
96 | return await axios.post('/email/verification-notification')
97 | } catch (error) {
98 | throw error
99 | }
100 | }
101 |
102 | const logout = async () => {
103 | if (!error) {
104 | await axios.post('/logout').then(() => mutate())
105 | }
106 |
107 | window.location.pathname = '/login'
108 | }
109 |
110 | useEffect(() => {
111 | if (middleware === 'guest' && redirectIfAuthenticated && user) {
112 | router.push(redirectIfAuthenticated)
113 | }
114 |
115 | if (
116 | window.location.pathname === '/verify-email' &&
117 | user?.email_verified_at &&
118 | redirectIfAuthenticated
119 | ) {
120 | router.push(redirectIfAuthenticated)
121 | }
122 | if (middleware === 'auth' && error) logout()
123 | }, [user, error, middleware, redirectIfAuthenticated])
124 |
125 | return {
126 | user,
127 | register,
128 | login,
129 | forgotPassword,
130 | resetPassword,
131 | resendEmailVerification,
132 | logout,
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/src/lib/axios.ts:
--------------------------------------------------------------------------------
1 | import Axios, { AxiosInstance } from 'axios'
2 |
3 | const axios: AxiosInstance = Axios.create({
4 | baseURL: process.env.NEXT_PUBLIC_BACKEND_URL,
5 | headers: {
6 | 'X-Requested-With': 'XMLHttpRequest',
7 | },
8 | withCredentials: true,
9 | withXSRFToken: true,
10 | })
11 |
12 | export default axios
13 |
--------------------------------------------------------------------------------
/src/types/User.ts:
--------------------------------------------------------------------------------
1 | export interface UserType {
2 | id: number
3 | email: string
4 | name: string
5 | email_verified_at?: Date
6 | created_at: Date
7 | updated_at: Date
8 | }
9 |
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from 'tailwindcss'
2 | import tailwindcssForm from '@tailwindcss/forms'
3 |
4 | const config: Config = {
5 | content: [
6 | './src/pages/**/*.{js,ts,jsx,tsx,mdx}',
7 | './src/components/**/*.{js,ts,jsx,tsx,mdx}',
8 | './src/app/**/*.{js,ts,jsx,tsx,mdx}',
9 | ],
10 | theme: {
11 | extend: {
12 | backgroundImage: {
13 | 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
14 | 'gradient-conic':
15 | 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
16 | },
17 | },
18 | },
19 | plugins: [tailwindcssForm],
20 | }
21 | export default config
22 |
23 | // const defaultTheme = require('tailwindcss/defaultTheme')
24 |
25 | // module.exports = {
26 | // content: ['./src/**/*.js'],
27 | // darkMode: 'media',
28 | // theme: {
29 | // extend: {
30 | // fontFamily: {
31 | // sans: ['Nunito', ...defaultTheme.fontFamily.sans],
32 | // },
33 | // },
34 | // },
35 | // variants: {
36 | // extend: {
37 | // opacity: ['disabled'],
38 | // },
39 | // },
40 | // plugins: [require('@tailwindcss/forms')],
41 | // }
42 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "noEmit": true,
9 | "esModuleInterop": true,
10 | "module": "esnext",
11 | "moduleResolution": "bundler",
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "jsx": "preserve",
15 | "incremental": true,
16 | "plugins": [
17 | {
18 | "name": "next"
19 | }
20 | ],
21 | "paths": {
22 | "@/*": ["./src/*"]
23 | }
24 | },
25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
26 | "exclude": ["node_modules"]
27 | }
28 |
--------------------------------------------------------------------------------