diff --git a/.gitignore b/.gitignore index 00bba9b..f4139fd 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,5 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + + diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..b4bfed3 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,3 @@ +{ + "plugins": ["prettier-plugin-tailwindcss"] +} diff --git a/package-lock.json b/package-lock.json index 997e377..cb31468 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,9 @@ "next": "14.2.6", "react": "^18", "react-dom": "^18", + "react-firebase-hooks": "^5.1.1", + "react-hot-toast": "^2.4.1", + "react-icons": "^5.3.0", "tailwind-merge": "^2.5.2", "tailwindcss-animate": "^1.0.7" }, @@ -25,6 +28,8 @@ "eslint": "^8", "eslint-config-next": "14.2.6", "postcss": "^8", + "prettier": "^3.3.3", + "prettier-plugin-tailwindcss": "^0.6.8", "tailwindcss": "^3.4.1", "typescript": "^5" } @@ -1791,8 +1796,7 @@ "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, "node_modules/damerau-levenshtein": { "version": "1.0.8", @@ -3013,6 +3017,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/goober": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.16.tgz", + "integrity": "sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g==", + "license": "MIT", + "peerDependencies": { + "csstype": "^3.0.10" + } + }, "node_modules/gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", @@ -4425,6 +4438,101 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-plugin-tailwindcss": { + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.8.tgz", + "integrity": "sha512-dGu3kdm7SXPkiW4nzeWKCl3uoImdd5CTZEJGxyypEPL37Wj0HT2pLqjrvSei1nTeuQfO4PUfjeW5cTUNRLZ4sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.21.3" + }, + "peerDependencies": { + "@ianvs/prettier-plugin-sort-imports": "*", + "@prettier/plugin-pug": "*", + "@shopify/prettier-plugin-liquid": "*", + "@trivago/prettier-plugin-sort-imports": "*", + "@zackad/prettier-plugin-twig-melody": "*", + "prettier": "^3.0", + "prettier-plugin-astro": "*", + "prettier-plugin-css-order": "*", + "prettier-plugin-import-sort": "*", + "prettier-plugin-jsdoc": "*", + "prettier-plugin-marko": "*", + "prettier-plugin-multiline-arrays": "*", + "prettier-plugin-organize-attributes": "*", + "prettier-plugin-organize-imports": "*", + "prettier-plugin-sort-imports": "*", + "prettier-plugin-style-order": "*", + "prettier-plugin-svelte": "*" + }, + "peerDependenciesMeta": { + "@ianvs/prettier-plugin-sort-imports": { + "optional": true + }, + "@prettier/plugin-pug": { + "optional": true + }, + "@shopify/prettier-plugin-liquid": { + "optional": true + }, + "@trivago/prettier-plugin-sort-imports": { + "optional": true + }, + "@zackad/prettier-plugin-twig-melody": { + "optional": true + }, + "prettier-plugin-astro": { + "optional": true + }, + "prettier-plugin-css-order": { + "optional": true + }, + "prettier-plugin-import-sort": { + "optional": true + }, + "prettier-plugin-jsdoc": { + "optional": true + }, + "prettier-plugin-marko": { + "optional": true + }, + "prettier-plugin-multiline-arrays": { + "optional": true + }, + "prettier-plugin-organize-attributes": { + "optional": true + }, + "prettier-plugin-organize-imports": { + "optional": true + }, + "prettier-plugin-sort-imports": { + "optional": true + }, + "prettier-plugin-style-order": { + "optional": true + }, + "prettier-plugin-svelte": { + "optional": true + } + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -4510,6 +4618,41 @@ "react": "^18.3.1" } }, + "node_modules/react-firebase-hooks": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/react-firebase-hooks/-/react-firebase-hooks-5.1.1.tgz", + "integrity": "sha512-y2UpWs82xs+39q5Rc/wq316ca52QsC0n8m801V+yM4IC4hbfOL4yQPVSh7w+ydstdvjN9F+lvs1WrO2VYxpmdA==", + "license": "Apache-2.0", + "peerDependencies": { + "firebase": ">= 9.0.0", + "react": ">= 16.8.0" + } + }, + "node_modules/react-hot-toast": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.4.1.tgz", + "integrity": "sha512-j8z+cQbWIM5LY37pR6uZR6D4LfseplqnuAO4co4u8917hBUvXlEqyP1ZzqVLcqoyUesZZv/ImreoCeHVDpE5pQ==", + "license": "MIT", + "dependencies": { + "goober": "^2.1.10" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" + } + }, + "node_modules/react-icons": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.3.0.tgz", + "integrity": "sha512-DnUk8aFbTyQPSkCfF8dbX6kQjXA9DktMeJqfjrg6cK9vwQVMxmcA3BfP4QoiztVmEHtwlTgLFsPuH2NskKT6eg==", + "license": "MIT", + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", diff --git a/package.json b/package.json index 14062ff..d9f3958 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,9 @@ "next": "14.2.6", "react": "^18", "react-dom": "^18", + "react-firebase-hooks": "^5.1.1", + "react-hot-toast": "^2.4.1", + "react-icons": "^5.3.0", "tailwind-merge": "^2.5.2", "tailwindcss-animate": "^1.0.7" }, @@ -26,6 +29,8 @@ "eslint": "^8", "eslint-config-next": "14.2.6", "postcss": "^8", + "prettier": "^3.3.3", + "prettier-plugin-tailwindcss": "^0.6.8", "tailwindcss": "^3.4.1", "typescript": "^5" } diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx new file mode 100644 index 0000000..e337ac0 --- /dev/null +++ b/src/app/dashboard/page.tsx @@ -0,0 +1,55 @@ +"use client"; + +import { FaCalendarAlt, FaBoxOpen } from "react-icons/fa"; +import { BsPcDisplay } from "react-icons/bs"; +import Link from "next/link"; +import ProtectedRoute from "@/components/auth/ProtectedRoute"; + +export default function Dashboard() { + return ( + +
+

Dashboard

+
+ +
+
+ ); +} diff --git a/src/app/globals.css b/src/app/globals.css index 99a7b0c..218c415 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -2,6 +2,8 @@ @tailwind components; @tailwind utilities; + + @layer base { :root { --background: 0 0% 100%; @@ -66,4 +68,5 @@ body { @apply bg-background text-foreground; } -} \ No newline at end of file +} + diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 522abfd..287663b 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -2,6 +2,7 @@ import type { Metadata } from "next"; import { Inter } from "next/font/google"; import "./globals.css"; import Navbar from "@/components/Navbar/Navbar"; +import { Toaster } from 'react-hot-toast'; const inter = Inter({ subsets: ["latin"] }); @@ -19,6 +20,7 @@ export default function RootLayout({ + {children} diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx new file mode 100644 index 0000000..2282cb1 --- /dev/null +++ b/src/app/login/page.tsx @@ -0,0 +1,107 @@ +"use client"; +import { useEffect, useState } from "react"; +import { useRouter } from "next/navigation"; +import Link from "next/link"; +import { auth } from "@/lib/firebase/firebase"; +import { + useAuthState, + useSignInWithEmailAndPassword, +} from "react-firebase-hooks/auth"; +import toast from "react-hot-toast"; + +type Credentials = { + email: string; + password: string; +}; + +export default function Login() { + const router = useRouter(); + const [user, authLoading] = useAuthState(auth); + + useEffect(() => { + if (user && !authLoading) { + router.push("/dashboard"); + } + }, [user, authLoading, router]); + + const [credentials, setCredentials] = useState({ + email: "", + password: "", + }); + + const handleCrendentialsChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setCredentials((prev) => ({ ...prev, [name]: value })); + }; + + const [signInWithEmailAndPassword, _, loading, error] = + useSignInWithEmailAndPassword(auth); + + const handleLogin = async (e: React.FormEvent) => { + e.preventDefault(); + if (!credentials.email || !credentials.password) { + return toast.error("Missing Email or Password"); + } + + const result = await signInWithEmailAndPassword( + credentials.email, + credentials.password, + ); + + if (result) { + router.push("/dashboard"); + toast.success("Successfully Logged In"); + } else { + toast.error("Invalid password or email."); + } + }; + + return ( +
+
+
+

Admin Login

+

+ Use the admin email and password to login. +

+
+
+
+ + +
+
+ + +
+ +
+ + Lost? Click to return to safety + +
+
+ ); +} diff --git a/src/components/Navbar/Navbar.tsx b/src/components/Navbar/Navbar.tsx index 52c657b..2b5e76f 100644 --- a/src/components/Navbar/Navbar.tsx +++ b/src/components/Navbar/Navbar.tsx @@ -1,5 +1,5 @@ 'use client'; - +import Link from "next/link"; import { useState } from 'react'; export default function Navbar() { @@ -12,7 +12,7 @@ export default function Navbar() { return ( diff --git a/src/components/auth/ProtectedRoute.tsx b/src/components/auth/ProtectedRoute.tsx new file mode 100644 index 0000000..6a95d96 --- /dev/null +++ b/src/components/auth/ProtectedRoute.tsx @@ -0,0 +1,40 @@ +"use client"; + +import { useEffect } from "react"; +import { auth } from "@/lib/firebase/firebase"; +import { useAuthState } from "react-firebase-hooks/auth"; +import { useRouter } from "next/navigation"; +import toast from "react-hot-toast"; +import { LuLoader2 } from "react-icons/lu"; + +export default function ProtectedRoute({ + children, +}: { + children: React.ReactNode; +}) { + const router = useRouter(); + const [user, loading, error] = useAuthState(auth); + + useEffect(() => { + if (!loading && (!user || error)) { + toast.error("Please login to access this page."); + router.push("/login"); + } + }, [user, loading, error, router]); + + if (loading) { + return ( +
+ + + +
+ ); + } + + if (!user || error) { + return null; + } + + return children; +} diff --git a/src/lib/firebase/firebase.js b/src/lib/firebase/firebase.js index f012c34..545d8c7 100644 --- a/src/lib/firebase/firebase.js +++ b/src/lib/firebase/firebase.js @@ -1,7 +1,9 @@ // Import the functions you need from the SDKs you need import { initializeApp } from "firebase/app"; import { getFirestore } from "firebase/firestore"; +import { getAuth, Auth } from "firebase/auth"; import { getStorage } from "firebase/storage"; + // TODO: Add SDKs for Firebase products that you want to use // https://firebase.google.com/docs/web/setup#available-libraries @@ -19,7 +21,8 @@ const firebaseConfig = { // Initialize Firebase const app = initializeApp(firebaseConfig); +const auth = getAuth(app); const db = getFirestore(app); const storage = getStorage(app); -export { db, storage}; \ No newline at end of file +export { db, auth, app, storage }; \ No newline at end of file