├── .eslintrc.json ├── next.config.js ├── postcss.config.js ├── app ├── favicon.ico ├── layout.tsx ├── globals.css └── page.tsx ├── .github └── dependabot.yml ├── lib └── utils.ts ├── .env.example ├── utils └── providers.tsx ├── firebase ├── auth │ ├── logout.js │ ├── githubLogin.js │ ├── googleLogin.js │ ├── emailPasswordLogin.js │ ├── emailPasswordRegistration.js │ └── emailVerificationLink.js └── config.ts ├── components.json ├── .env ├── .gitignore ├── tailwind.config.ts ├── public ├── vercel.svg └── next.svg ├── tsconfig.json ├── components ├── ui │ ├── label.tsx │ ├── input.tsx │ ├── toaster.tsx │ ├── popover.tsx │ ├── avatar.tsx │ ├── button.tsx │ ├── card.tsx │ ├── use-toast.ts │ ├── form.tsx │ └── toast.tsx └── site-header.tsx ├── package.json ├── context └── auth-context.js ├── README.md └── tailwind.config.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {} 3 | 4 | module.exports = nextConfig 5 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sahandmohammadrehzaii/next-shadcn-firebase-auth-boilerplate/HEAD/app/favicon.ico -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # npm 4 | - package-ecosystem: "npm" 5 | directory: "/" 6 | schedule: 7 | interval: "weekly" 8 | -------------------------------------------------------------------------------- /lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_FIREBASE_API_KEY="xxx" 2 | NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN="xxx" 3 | NEXT_PUBLIC_FIREBASE_PROJECT_ID="xxx" 4 | NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET="xxx" 5 | NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID="xxx" 6 | NEXT_PUBLIC_FIREBASE_APP_ID="xxx" 7 | NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID="xxx" -------------------------------------------------------------------------------- /utils/providers.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | import { AuthContextProvider } from "@/context/auth-context" 3 | function Providers({ children }: React.PropsWithChildren) { 4 | 5 | return ( 6 | 7 | {children} 8 | 9 | ) 10 | } 11 | 12 | export default Providers -------------------------------------------------------------------------------- /firebase/auth/logout.js: -------------------------------------------------------------------------------- 1 | import { auth } from "@/firebase/config" 2 | import { signOut } from "firebase/auth" 3 | 4 | export const useLogout = () => { 5 | const logout = async () => { 6 | try { 7 | await signOut(auth) 8 | console.log("user logged out") 9 | } catch (error) { 10 | console.log(error.message) 11 | } 12 | } 13 | 14 | return { logout } 15 | } 16 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "app/globals.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true 11 | }, 12 | "aliases": { 13 | "components": "@/components", 14 | "utils": "@/lib/utils" 15 | } 16 | } -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_FIREBASE_API_KEY="AIzaSyBVj49pS1e30DKxT3uF4OcDKNWS1QLdoQQ" 2 | NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN="next-shadcn-firebase-auth.firebaseapp.com" 3 | NEXT_PUBLIC_FIREBASE_PROJECT_ID="next-shadcn-firebase-auth" 4 | NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET="next-shadcn-firebase-auth.appspot.com" 5 | NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID="702587753052" 6 | NEXT_PUBLIC_FIREBASE_APP_ID="1:702587753052:web:b59f65ed73b9cf54c00c99" 7 | NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID="G-VFNFD23DZ3" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'tailwindcss' 2 | 3 | const config: Config = { 4 | content: [ 5 | './pages/**/*.{js,ts,jsx,tsx,mdx}', 6 | './components/**/*.{js,ts,jsx,tsx,mdx}', 7 | './app/**/*.{js,ts,jsx,tsx,mdx}', 8 | ], 9 | theme: { 10 | extend: { 11 | backgroundImage: { 12 | 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', 13 | 'gradient-conic': 14 | 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))', 15 | }, 16 | }, 17 | }, 18 | plugins: [], 19 | } 20 | export default config 21 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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 | "@/*": ["./*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as LabelPrimitive from "@radix-ui/react-label" 5 | import { cva, type VariantProps } 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< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef & 16 | VariantProps 17 | >(({ className, ...props }, ref) => ( 18 | 23 | )) 24 | Label.displayName = LabelPrimitive.Root.displayName 25 | 26 | export { Label } 27 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Inter } from "next/font/google"; 3 | import { Toaster } from "@/components/ui/toaster"; 4 | import { SiteHeader } from "@/components/site-header"; 5 | import Providers from "@/utils/providers"; 6 | 7 | import "./globals.css"; 8 | 9 | const inter = Inter({ subsets: ["latin"] }); 10 | 11 | export const metadata: Metadata = { 12 | title: "Create Next App", 13 | description: "Generated by create next app", 14 | }; 15 | 16 | export default function RootLayout({ 17 | children, 18 | }: { 19 | children: React.ReactNode; 20 | }) { 21 | return ( 22 | 23 | 24 | 25 | 26 | 27 | {children} 28 | 29 | 30 | 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /firebase/config.ts: -------------------------------------------------------------------------------- 1 | // Import the functions you need from the SDKs you need 2 | 3 | import { getApps, initializeApp } from "firebase/app"; 4 | import { getAuth } from "firebase/auth"; 5 | 6 | const firebaseConfig = { 7 | apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY, 8 | authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN, 9 | projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID, 10 | storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET, 11 | messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID, 12 | appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID, 13 | measurementId: process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID, 14 | }; 15 | 16 | // Initialize Firebase 17 | let firebase_app = 18 | getApps().length === 0 ? initializeApp(firebaseConfig) : getApps()[0]; 19 | 20 | export default firebase_app; 21 | 22 | export const auth = getAuth(firebase_app); 23 | -------------------------------------------------------------------------------- /components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | export interface InputProps 6 | extends React.InputHTMLAttributes {} 7 | 8 | const Input = React.forwardRef( 9 | ({ className, type, ...props }, ref) => { 10 | return ( 11 | 20 | ) 21 | } 22 | ) 23 | Input.displayName = "Input" 24 | 25 | export { Input } 26 | -------------------------------------------------------------------------------- /firebase/auth/githubLogin.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { auth } from "@/firebase/config"; 3 | import { GithubAuthProvider, signInWithPopup, signOut } from "firebase/auth"; 4 | 5 | export const useGithubLogin = () => { 6 | const [errorGithubLogin, setErrorGithubLogin] = useState(false); 7 | const [isPendingGithubLogin, setIsPendingGithubLogin] = useState(false); 8 | const provider = new GithubAuthProvider(); 9 | 10 | const githubLogin = async () => { 11 | setErrorGithubLogin(null); 12 | setIsPendingGithubLogin(true); 13 | 14 | try { 15 | const res = await signInWithPopup(auth, provider); 16 | if (!res) { 17 | return; 18 | } 19 | } catch (error) { 20 | setErrorGithubLogin(error.code); 21 | await signOut(auth); 22 | } finally { 23 | setIsPendingGithubLogin(false); 24 | } 25 | }; 26 | 27 | return { githubLogin, errorGithubLogin, isPendingGithubLogin }; 28 | }; 29 | -------------------------------------------------------------------------------- /firebase/auth/googleLogin.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { auth } from "@/firebase/config"; 3 | import { GoogleAuthProvider, signInWithPopup, signOut } from "firebase/auth"; 4 | 5 | export const useGoogleLogin = () => { 6 | const [errorGoogleLogin, setErrorGoogleLogin] = useState(false); 7 | const [isPendingGoogleLogin, setIsPendingGoogleLogin] = useState(false); 8 | const provider = new GoogleAuthProvider(); 9 | 10 | const googleLogin = async () => { 11 | setErrorGoogleLogin(null); 12 | setIsPendingGoogleLogin(true); 13 | 14 | try { 15 | const res = await signInWithPopup(auth, provider); 16 | if (!res) { 17 | return; 18 | } 19 | } catch (error) { 20 | setErrorGoogleLogin(error.code); 21 | await signOut(auth); 22 | } finally { 23 | setIsPendingGoogleLogin(false); 24 | } 25 | }; 26 | 27 | return { googleLogin, errorGoogleLogin, isPendingGoogleLogin }; 28 | }; 29 | -------------------------------------------------------------------------------- /components/ui/toaster.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { 4 | Toast, 5 | ToastClose, 6 | ToastDescription, 7 | ToastProvider, 8 | ToastTitle, 9 | ToastViewport, 10 | } from "@/components/ui/toast" 11 | import { useToast } from "@/components/ui/use-toast" 12 | 13 | export function Toaster() { 14 | const { toasts } = useToast() 15 | 16 | return ( 17 | 18 | {toasts.map(function ({ id, title, description, action, ...props }) { 19 | return ( 20 | 21 |
22 | {title && {title}} 23 | {description && ( 24 | {description} 25 | )} 26 |
27 | {action} 28 | 29 |
30 | ) 31 | })} 32 | 33 |
34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /firebase/auth/emailPasswordLogin.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { auth } from "@/firebase/config"; 3 | import { signInWithEmailAndPassword, signOut } from "firebase/auth"; 4 | 5 | export const useEmailPasswordLogin = () => { 6 | const [errorEmailPasswordLogin, setErrorEmailPasswordLogin] = useState(null); 7 | const [isPendingEmailPasswordLogin, setIsPendingEmailPasswordLogin] = 8 | useState(false); 9 | 10 | const emailPasswordLogin = async (email, password) => { 11 | setErrorEmailPasswordLogin(null); 12 | setIsPendingEmailPasswordLogin(true); 13 | 14 | try { 15 | const res = await signInWithEmailAndPassword(auth, email, password); 16 | if (!res.user) { 17 | return; 18 | } 19 | } catch (error) { 20 | setErrorEmailPasswordLogin(error.code); 21 | await signOut(auth); 22 | } finally { 23 | setIsPendingEmailPasswordLogin(false); 24 | } 25 | }; 26 | 27 | return { 28 | emailPasswordLogin, 29 | errorEmailPasswordLogin, 30 | isPendingEmailPasswordLogin, 31 | }; 32 | }; 33 | -------------------------------------------------------------------------------- /firebase/auth/emailPasswordRegistration.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { auth } from "@/firebase/config"; 3 | import { createUserWithEmailAndPassword, signOut } from "firebase/auth"; 4 | 5 | export const useEmailPasswordRegistration = () => { 6 | const [errorEmailPasswordRegistration, setErrorEmailPasswordRegistration] = 7 | useState(null); 8 | const [isPendingEmailPasswordRegistration, setIsPendingEmailRegistration] = 9 | useState(false); 10 | 11 | const emailPasswordRegistration = async (email, password) => { 12 | setErrorEmailPasswordRegistration(null); 13 | setIsPendingEmailRegistration(true); 14 | 15 | try { 16 | const res = await createUserWithEmailAndPassword(auth, email, password); 17 | if (!res.user) { 18 | return; 19 | } 20 | } catch (error) { 21 | setErrorEmailPasswordRegistration(error.code); 22 | await signOut(auth); 23 | } finally { 24 | setIsPendingEmailRegistration(false); 25 | } 26 | }; 27 | 28 | return { 29 | emailPasswordRegistration, 30 | errorEmailPasswordRegistration, 31 | isPendingEmailPasswordRegistration, 32 | }; 33 | }; 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-shadcn-firebase-auth-boilerplate", 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 --fix" 10 | }, 11 | "dependencies": { 12 | "@hookform/resolvers": "^3.3.4", 13 | "@radix-ui/react-avatar": "^1.0.4", 14 | "@radix-ui/react-label": "^2.0.2", 15 | "@radix-ui/react-popover": "^1.0.7", 16 | "@radix-ui/react-slot": "^1.0.2", 17 | "@radix-ui/react-toast": "^1.1.5", 18 | "class-variance-authority": "^0.7.0", 19 | "clsx": "^2.0.0", 20 | "firebase": "^10.8.1", 21 | "lucide-react": "^0.288.0", 22 | "next": "13.5.6", 23 | "react": "^18", 24 | "react-dom": "^18", 25 | "react-hook-form": "^7.47.0", 26 | "tailwind-merge": "^1.14.0", 27 | "tailwindcss-animate": "^1.0.7", 28 | "zod": "^3.22.4" 29 | }, 30 | "devDependencies": { 31 | "@types/node": "^20", 32 | "@types/react": "^18", 33 | "@types/react-dom": "^18", 34 | "autoprefixer": "^10", 35 | "eslint": "^8", 36 | "eslint-config-next": "14.1.1", 37 | "postcss": "^8", 38 | "tailwindcss": "^3", 39 | "typescript": "^5" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /firebase/auth/emailVerificationLink.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { auth } from "@/firebase/config"; 3 | import { sendEmailVerification } from "firebase/auth"; 4 | 5 | export const useEmailVerification = () => { 6 | const [isEmailVerificationSent, setIsEmailVerificationSent] = useState(false); 7 | const [isEmailVerificationPending, setIsEmailVerificationPending] = 8 | useState(false); 9 | const [errorVerificationLink, setErrorVerificationLink] = useState(null); 10 | 11 | const sendEmailVerificationLink = async () => { 12 | setIsEmailVerificationPending(true); 13 | 14 | try { 15 | const user = auth.currentUser; 16 | 17 | if (user) { 18 | await sendEmailVerification(user); 19 | setIsEmailVerificationSent(true); 20 | } else { 21 | setErrorVerificationLink("No user is currently logged in."); 22 | } 23 | } catch (error) { 24 | setErrorVerificationLink( 25 | "Error sending verification email : " + error.message 26 | ); 27 | } finally { 28 | setIsEmailVerificationPending(false); 29 | } 30 | }; 31 | 32 | return { 33 | isEmailVerificationSent, 34 | isEmailVerificationPending, 35 | errorVerificationLink, 36 | sendEmailVerificationLink, 37 | }; 38 | }; 39 | -------------------------------------------------------------------------------- /context/auth-context.js: -------------------------------------------------------------------------------- 1 | import { createContext, useContext, useEffect, useState } from "react"; 2 | import firebase_app from "@/firebase/config"; 3 | import { getAuth, onAuthStateChanged } from "firebase/auth"; 4 | import { Shell } from "lucide-react"; 5 | 6 | const auth = getAuth(firebase_app); 7 | 8 | export const AuthContext = createContext({ 9 | user: { 10 | email: "", 11 | emailVerified: "", 12 | displayName: "", 13 | }, 14 | }); 15 | 16 | export const useAuthContext = () => useContext(AuthContext); 17 | 18 | export const AuthContextProvider = ({ children }) => { 19 | const [user, setUser] = useState(null); 20 | const [loading, setLoading] = useState(true); 21 | 22 | useEffect(() => { 23 | const unsubscribe = onAuthStateChanged(auth, (user) => { 24 | if (user) { 25 | //console.log("user", user); 26 | setUser(user); 27 | } else { 28 | setUser(null); 29 | } 30 | setLoading(false); 31 | }); 32 | 33 | return () => unsubscribe(); 34 | }, []); 35 | 36 | return ( 37 | 38 | {loading ? ( 39 |
40 | 41 |
42 | ) : ( 43 | <>{children} 44 | )} 45 |
46 | ); 47 | }; 48 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/ui/popover.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as PopoverPrimitive from "@radix-ui/react-popover" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Popover = PopoverPrimitive.Root 9 | 10 | const PopoverTrigger = PopoverPrimitive.Trigger 11 | 12 | const PopoverContent = React.forwardRef< 13 | React.ElementRef, 14 | React.ComponentPropsWithoutRef 15 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( 16 | 17 | 27 | 28 | )) 29 | PopoverContent.displayName = PopoverPrimitive.Content.displayName 30 | 31 | export { Popover, PopoverTrigger, PopoverContent } 32 | -------------------------------------------------------------------------------- /components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as AvatarPrimitive from "@radix-ui/react-avatar" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Avatar = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 20 | )) 21 | Avatar.displayName = AvatarPrimitive.Root.displayName 22 | 23 | const AvatarImage = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, ...props }, ref) => ( 27 | 32 | )) 33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName 34 | 35 | const AvatarFallback = React.forwardRef< 36 | React.ElementRef, 37 | React.ComponentPropsWithoutRef 38 | >(({ className, ...props }, ref) => ( 39 | 47 | )) 48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName 49 | 50 | export { Avatar, AvatarImage, AvatarFallback } 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Next Shadcn Firebase Auth Boilerplate 2 | 3 | This is a Boilerplate that uses ```NEXTJS``` - ```SHADCNUI``` - ```REACT HOOK FORM``` - ```ZOD``` - ```FIREBASE``` to create an authentication system. 4 | 5 | You must therefore create your firebase project at home and fill in this information in your ```.env``` file 6 | 7 | ``` 8 | NEXT_PUBLIC_FIREBASE_API_KEY="xxx" 9 | NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN="xxx" 10 | NEXT_PUBLIC_FIREBASE_PROJECT_ID="xxx" 11 | NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET="xxx" 12 | NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID="xxx" 13 | NEXT_PUBLIC_FIREBASE_APP_ID="xxx" 14 | NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID="xxx" 15 | ``` 16 | ![FIREBASE CONFIG](https://github.com/ln-dev7/next-shadcn-firebase-auth-boilerplate/assets/62269693/bcb549fe-e46a-45b4-9b0b-2ba5d5286057) 17 | 18 | ## Authentication methods 19 | 20 | Currently there are 3 authentication methods: 21 | 22 | - Google 23 | - Email/Password 24 | - GitHub 25 | 26 | To add one of these methods, you must enable it in your firebase console 27 | 28 | ### GOOGLE 29 | ![FIREBASE GOOGLE AUTH SETTING](https://github.com/ln-dev7/next-shadcn-firebase-auth-boilerplate/assets/62269693/ed0787d2-9eb2-4bef-a977-71ae7a16820d) 30 | 31 | ### EMAIL / PASSWORD 32 | ![FIREBASE EMAIL/PASSWORD AUTH SETTING](https://github.com/ln-dev7/next-shadcn-firebase-auth-boilerplate/assets/62269693/e2f13346-9cec-4454-932d-ae40e6a1614c) 33 | 34 | ### GITHUB 35 | You must create your application on your github to have this information https://github.com/settings/developers 36 | 37 | ![FIREBASE GITHUB AUTH SETTING](https://github.com/ln-dev7/next-shadcn-firebase-auth-boilerplate/assets/62269693/1fe2520c-3b8b-4149-9929-a7bc042931c0) 38 | 39 | 40 | -------------------------------------------------------------------------------- /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: 240 10% 3.9%; 9 | 10 | --card: 0 0% 100%; 11 | --card-foreground: 240 10% 3.9%; 12 | 13 | --popover: 0 0% 100%; 14 | --popover-foreground: 240 10% 3.9%; 15 | 16 | --primary: 240 5.9% 10%; 17 | --primary-foreground: 0 0% 98%; 18 | 19 | --secondary: 240 4.8% 95.9%; 20 | --secondary-foreground: 240 5.9% 10%; 21 | 22 | --muted: 240 4.8% 95.9%; 23 | --muted-foreground: 240 3.8% 46.1%; 24 | 25 | --accent: 240 4.8% 95.9%; 26 | --accent-foreground: 240 5.9% 10%; 27 | 28 | --destructive: 0 84.2% 60.2%; 29 | --destructive-foreground: 0 0% 98%; 30 | 31 | --border: 240 5.9% 90%; 32 | --input: 240 5.9% 90%; 33 | --ring: 240 10% 3.9%; 34 | 35 | --radius: 0.5rem; 36 | } 37 | 38 | .dark { 39 | --background: 240 10% 3.9%; 40 | --foreground: 0 0% 98%; 41 | 42 | --card: 240 10% 3.9%; 43 | --card-foreground: 0 0% 98%; 44 | 45 | --popover: 240 10% 3.9%; 46 | --popover-foreground: 0 0% 98%; 47 | 48 | --primary: 0 0% 98%; 49 | --primary-foreground: 240 5.9% 10%; 50 | 51 | --secondary: 240 3.7% 15.9%; 52 | --secondary-foreground: 0 0% 98%; 53 | 54 | --muted: 240 3.7% 15.9%; 55 | --muted-foreground: 240 5% 64.9%; 56 | 57 | --accent: 240 3.7% 15.9%; 58 | --accent-foreground: 0 0% 98%; 59 | 60 | --destructive: 0 62.8% 30.6%; 61 | --destructive-foreground: 0 0% 98%; 62 | 63 | --border: 240 3.7% 15.9%; 64 | --input: 240 3.7% 15.9%; 65 | --ring: 240 4.9% 83.9%; 66 | } 67 | } 68 | 69 | @layer base { 70 | * { 71 | @apply border-border; 72 | } 73 | body { 74 | @apply bg-background text-foreground; 75 | } 76 | } 77 | 78 | .loading-container{ 79 | height: 100vh; 80 | width: 100%; 81 | display: flex; 82 | justify-content: center; 83 | align-items: center; 84 | } -------------------------------------------------------------------------------- /components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 15 | outline: 16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground", 17 | secondary: 18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 19 | ghost: "hover:bg-accent hover:text-accent-foreground", 20 | link: "text-primary underline-offset-4 hover:underline", 21 | }, 22 | size: { 23 | default: "h-10 px-4 py-2", 24 | sm: "h-9 rounded-md px-3", 25 | lg: "h-11 rounded-md px-8", 26 | icon: "h-10 w-10", 27 | }, 28 | }, 29 | defaultVariants: { 30 | variant: "default", 31 | size: "default", 32 | }, 33 | } 34 | ) 35 | 36 | export interface ButtonProps 37 | extends React.ButtonHTMLAttributes, 38 | VariantProps { 39 | asChild?: boolean 40 | } 41 | 42 | const Button = React.forwardRef( 43 | ({ className, variant, size, asChild = false, ...props }, ref) => { 44 | const Comp = asChild ? Slot : "button" 45 | return ( 46 | 51 | ) 52 | } 53 | ) 54 | Button.displayName = "Button" 55 | 56 | export { Button, buttonVariants } 57 | -------------------------------------------------------------------------------- /components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
17 | )) 18 | Card.displayName = "Card" 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 |
29 | )) 30 | CardHeader.displayName = "CardHeader" 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLParagraphElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |

44 | )) 45 | CardTitle.displayName = "CardTitle" 46 | 47 | const CardDescription = React.forwardRef< 48 | HTMLParagraphElement, 49 | React.HTMLAttributes 50 | >(({ className, ...props }, ref) => ( 51 |

56 | )) 57 | CardDescription.displayName = "CardDescription" 58 | 59 | const CardContent = React.forwardRef< 60 | HTMLDivElement, 61 | React.HTMLAttributes 62 | >(({ className, ...props }, ref) => ( 63 |

64 | )) 65 | CardContent.displayName = "CardContent" 66 | 67 | const CardFooter = React.forwardRef< 68 | HTMLDivElement, 69 | React.HTMLAttributes 70 | >(({ className, ...props }, ref) => ( 71 |
76 | )) 77 | CardFooter.displayName = "CardFooter" 78 | 79 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } 80 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | darkMode: ["class"], 4 | content: [ 5 | './pages/**/*.{ts,tsx}', 6 | './components/**/*.{ts,tsx}', 7 | './app/**/*.{ts,tsx}', 8 | './src/**/*.{ts,tsx}', 9 | ], 10 | theme: { 11 | container: { 12 | center: true, 13 | padding: "2rem", 14 | screens: { 15 | "2xl": "1400px", 16 | }, 17 | }, 18 | extend: { 19 | colors: { 20 | border: "hsl(var(--border))", 21 | input: "hsl(var(--input))", 22 | ring: "hsl(var(--ring))", 23 | background: "hsl(var(--background))", 24 | foreground: "hsl(var(--foreground))", 25 | primary: { 26 | DEFAULT: "hsl(var(--primary))", 27 | foreground: "hsl(var(--primary-foreground))", 28 | }, 29 | secondary: { 30 | DEFAULT: "hsl(var(--secondary))", 31 | foreground: "hsl(var(--secondary-foreground))", 32 | }, 33 | destructive: { 34 | DEFAULT: "hsl(var(--destructive))", 35 | foreground: "hsl(var(--destructive-foreground))", 36 | }, 37 | muted: { 38 | DEFAULT: "hsl(var(--muted))", 39 | foreground: "hsl(var(--muted-foreground))", 40 | }, 41 | accent: { 42 | DEFAULT: "hsl(var(--accent))", 43 | foreground: "hsl(var(--accent-foreground))", 44 | }, 45 | popover: { 46 | DEFAULT: "hsl(var(--popover))", 47 | foreground: "hsl(var(--popover-foreground))", 48 | }, 49 | card: { 50 | DEFAULT: "hsl(var(--card))", 51 | foreground: "hsl(var(--card-foreground))", 52 | }, 53 | }, 54 | borderRadius: { 55 | lg: "var(--radius)", 56 | md: "calc(var(--radius) - 2px)", 57 | sm: "calc(var(--radius) - 4px)", 58 | }, 59 | keyframes: { 60 | "accordion-down": { 61 | from: { height: 0 }, 62 | to: { height: "var(--radix-accordion-content-height)" }, 63 | }, 64 | "accordion-up": { 65 | from: { height: "var(--radix-accordion-content-height)" }, 66 | to: { height: 0 }, 67 | }, 68 | }, 69 | animation: { 70 | "accordion-down": "accordion-down 0.2s ease-out", 71 | "accordion-up": "accordion-up 0.2s ease-out", 72 | }, 73 | }, 74 | }, 75 | plugins: [require("tailwindcss-animate")], 76 | } -------------------------------------------------------------------------------- /components/site-header.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Link from "next/link"; 4 | import { Button } from "@/components/ui/button"; 5 | import { Github, LogOut, Mail, Shell, Twitter } from "lucide-react"; 6 | import { 7 | Popover, 8 | PopoverContent, 9 | PopoverTrigger, 10 | } from "@/components/ui/popover"; 11 | import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; 12 | import { useToast } from "./ui/use-toast"; 13 | import { useAuthContext } from "@/context/auth-context"; 14 | import { useLogout } from "@/firebase/auth/logout"; 15 | 16 | export function SiteHeader() { 17 | const { toast } = useToast(); 18 | const { logout } = useLogout(); 19 | const { user } = useAuthContext(); 20 | return ( 21 |
22 |
23 |
24 | 28 | LN 29 | 30 | 44 |
45 | 46 | {user ? ( 47 |
48 |
49 | 58 | 59 | 60 | 61 | 62 | {user.email?.charAt(0)} 63 | {user.email?.charAt(1)} 64 | 65 | 66 | 67 | 68 | 83 | 84 | 85 |
86 |
87 | ) : null} 88 |
89 |
90 | ); 91 | } 92 | -------------------------------------------------------------------------------- /components/ui/use-toast.ts: -------------------------------------------------------------------------------- 1 | // Inspired by react-hot-toast library 2 | import * as React from "react" 3 | 4 | import type { 5 | ToastActionElement, 6 | ToastProps, 7 | } from "@/components/ui/toast" 8 | 9 | const TOAST_LIMIT = 1 10 | const TOAST_REMOVE_DELAY = 1000000 11 | 12 | type ToasterToast = ToastProps & { 13 | id: string 14 | title?: React.ReactNode 15 | description?: React.ReactNode 16 | action?: ToastActionElement 17 | } 18 | 19 | const actionTypes = { 20 | ADD_TOAST: "ADD_TOAST", 21 | UPDATE_TOAST: "UPDATE_TOAST", 22 | DISMISS_TOAST: "DISMISS_TOAST", 23 | REMOVE_TOAST: "REMOVE_TOAST", 24 | } as const 25 | 26 | let count = 0 27 | 28 | function genId() { 29 | count = (count + 1) % Number.MAX_VALUE 30 | return count.toString() 31 | } 32 | 33 | type ActionType = typeof actionTypes 34 | 35 | type Action = 36 | | { 37 | type: ActionType["ADD_TOAST"] 38 | toast: ToasterToast 39 | } 40 | | { 41 | type: ActionType["UPDATE_TOAST"] 42 | toast: Partial 43 | } 44 | | { 45 | type: ActionType["DISMISS_TOAST"] 46 | toastId?: ToasterToast["id"] 47 | } 48 | | { 49 | type: ActionType["REMOVE_TOAST"] 50 | toastId?: ToasterToast["id"] 51 | } 52 | 53 | interface State { 54 | toasts: ToasterToast[] 55 | } 56 | 57 | const toastTimeouts = new Map>() 58 | 59 | const addToRemoveQueue = (toastId: string) => { 60 | if (toastTimeouts.has(toastId)) { 61 | return 62 | } 63 | 64 | const timeout = setTimeout(() => { 65 | toastTimeouts.delete(toastId) 66 | dispatch({ 67 | type: "REMOVE_TOAST", 68 | toastId: toastId, 69 | }) 70 | }, TOAST_REMOVE_DELAY) 71 | 72 | toastTimeouts.set(toastId, timeout) 73 | } 74 | 75 | export const reducer = (state: State, action: Action): State => { 76 | switch (action.type) { 77 | case "ADD_TOAST": 78 | return { 79 | ...state, 80 | toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT), 81 | } 82 | 83 | case "UPDATE_TOAST": 84 | return { 85 | ...state, 86 | toasts: state.toasts.map((t) => 87 | t.id === action.toast.id ? { ...t, ...action.toast } : t 88 | ), 89 | } 90 | 91 | case "DISMISS_TOAST": { 92 | const { toastId } = action 93 | 94 | // ! Side effects ! - This could be extracted into a dismissToast() action, 95 | // but I'll keep it here for simplicity 96 | if (toastId) { 97 | addToRemoveQueue(toastId) 98 | } else { 99 | state.toasts.forEach((toast) => { 100 | addToRemoveQueue(toast.id) 101 | }) 102 | } 103 | 104 | return { 105 | ...state, 106 | toasts: state.toasts.map((t) => 107 | t.id === toastId || toastId === undefined 108 | ? { 109 | ...t, 110 | open: false, 111 | } 112 | : t 113 | ), 114 | } 115 | } 116 | case "REMOVE_TOAST": 117 | if (action.toastId === undefined) { 118 | return { 119 | ...state, 120 | toasts: [], 121 | } 122 | } 123 | return { 124 | ...state, 125 | toasts: state.toasts.filter((t) => t.id !== action.toastId), 126 | } 127 | } 128 | } 129 | 130 | const listeners: Array<(state: State) => void> = [] 131 | 132 | let memoryState: State = { toasts: [] } 133 | 134 | function dispatch(action: Action) { 135 | memoryState = reducer(memoryState, action) 136 | listeners.forEach((listener) => { 137 | listener(memoryState) 138 | }) 139 | } 140 | 141 | type Toast = Omit 142 | 143 | function toast({ ...props }: Toast) { 144 | const id = genId() 145 | 146 | const update = (props: ToasterToast) => 147 | dispatch({ 148 | type: "UPDATE_TOAST", 149 | toast: { ...props, id }, 150 | }) 151 | const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id }) 152 | 153 | dispatch({ 154 | type: "ADD_TOAST", 155 | toast: { 156 | ...props, 157 | id, 158 | open: true, 159 | onOpenChange: (open) => { 160 | if (!open) dismiss() 161 | }, 162 | }, 163 | }) 164 | 165 | return { 166 | id: id, 167 | dismiss, 168 | update, 169 | } 170 | } 171 | 172 | function useToast() { 173 | const [state, setState] = React.useState(memoryState) 174 | 175 | React.useEffect(() => { 176 | listeners.push(setState) 177 | return () => { 178 | const index = listeners.indexOf(setState) 179 | if (index > -1) { 180 | listeners.splice(index, 1) 181 | } 182 | } 183 | }, [state]) 184 | 185 | return { 186 | ...state, 187 | toast, 188 | dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }), 189 | } 190 | } 191 | 192 | export { useToast, toast } 193 | -------------------------------------------------------------------------------- /components/ui/form.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as LabelPrimitive from "@radix-ui/react-label" 3 | import { Slot } from "@radix-ui/react-slot" 4 | import { 5 | Controller, 6 | ControllerProps, 7 | FieldPath, 8 | FieldValues, 9 | FormProvider, 10 | useFormContext, 11 | } from "react-hook-form" 12 | 13 | import { cn } from "@/lib/utils" 14 | import { Label } from "@/components/ui/label" 15 | 16 | const Form = FormProvider 17 | 18 | type FormFieldContextValue< 19 | TFieldValues extends FieldValues = FieldValues, 20 | TName extends FieldPath = FieldPath 21 | > = { 22 | name: TName 23 | } 24 | 25 | const FormFieldContext = React.createContext( 26 | {} as FormFieldContextValue 27 | ) 28 | 29 | const FormField = < 30 | TFieldValues extends FieldValues = FieldValues, 31 | TName extends FieldPath = FieldPath 32 | >({ 33 | ...props 34 | }: ControllerProps) => { 35 | return ( 36 | 37 | 38 | 39 | ) 40 | } 41 | 42 | const useFormField = () => { 43 | const fieldContext = React.useContext(FormFieldContext) 44 | const itemContext = React.useContext(FormItemContext) 45 | const { getFieldState, formState } = useFormContext() 46 | 47 | const fieldState = getFieldState(fieldContext.name, formState) 48 | 49 | if (!fieldContext) { 50 | throw new Error("useFormField should be used within ") 51 | } 52 | 53 | const { id } = itemContext 54 | 55 | return { 56 | id, 57 | name: fieldContext.name, 58 | formItemId: `${id}-form-item`, 59 | formDescriptionId: `${id}-form-item-description`, 60 | formMessageId: `${id}-form-item-message`, 61 | ...fieldState, 62 | } 63 | } 64 | 65 | type FormItemContextValue = { 66 | id: string 67 | } 68 | 69 | const FormItemContext = React.createContext( 70 | {} as FormItemContextValue 71 | ) 72 | 73 | const FormItem = React.forwardRef< 74 | HTMLDivElement, 75 | React.HTMLAttributes 76 | >(({ className, ...props }, ref) => { 77 | const id = React.useId() 78 | 79 | return ( 80 | 81 |
82 | 83 | ) 84 | }) 85 | FormItem.displayName = "FormItem" 86 | 87 | const FormLabel = React.forwardRef< 88 | React.ElementRef, 89 | React.ComponentPropsWithoutRef 90 | >(({ className, ...props }, ref) => { 91 | const { error, formItemId } = useFormField() 92 | 93 | return ( 94 |