diff --git a/package-lock.json b/package-lock.json
index 3d00fe1..5e332ef 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -34,6 +34,7 @@
"bullmq": "^5.13.0",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
+ "framer-motion": "^11.9.0",
"ioredis": "^5.4.1",
"jest": "^29.7.0",
"lodash.debounce": "^4.0.8",
@@ -45,6 +46,7 @@
"react": "^18.3.1",
"react-dom": "^18",
"react-email": "^3.0.1",
+ "react-icons": "^5.3.0",
"react-hook-form": "^7.53.0",
"resend": "^4.0.0",
"sonner": "^1.5.0",
@@ -6467,6 +6469,30 @@
"node": ">=12.20.0"
}
},
+ "node_modules/framer-motion": {
+ "version": "11.9.0",
+ "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.9.0.tgz",
+ "integrity": "sha512-nCfGxvsQecVLjjYDu35G2F5ls+ArE3FBfhxV0RSiisMaUKqteq5DMBFNRKwMyVj+VqKTNhawt+BV480YCHKFlQ==",
+ "dependencies": {
+ "tslib": "^2.4.0"
+ },
+ "peerDependencies": {
+ "@emotion/is-prop-valid": "*",
+ "react": "^18.0.0",
+ "react-dom": "^18.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@emotion/is-prop-valid": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ },
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@@ -10405,6 +10431,14 @@
"semver": "bin/semver.js"
}
},
+
+ "node_modules/react-icons": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.3.0.tgz",
+ "integrity": "sha512-DnUk8aFbTyQPSkCfF8dbX6kQjXA9DktMeJqfjrg6cK9vwQVMxmcA3BfP4QoiztVmEHtwlTgLFsPuH2NskKT6eg==",
+ "peerDependencies": {
+ "react": "*"
+ },
"node_modules/react-hook-form": {
"version": "7.53.0",
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.53.0.tgz",
diff --git a/package.json b/package.json
index 7606a45..a43d313 100644
--- a/package.json
+++ b/package.json
@@ -37,6 +37,7 @@
"bullmq": "^5.13.0",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
+ "framer-motion": "^11.9.0",
"ioredis": "^5.4.1",
"jest": "^29.7.0",
"lodash.debounce": "^4.0.8",
@@ -48,6 +49,7 @@
"react": "^18.3.1",
"react-dom": "^18",
"react-email": "^3.0.1",
+ "react-icons": "^5.3.0",
"react-hook-form": "^7.53.0",
"resend": "^4.0.0",
"sonner": "^1.5.0",
diff --git a/src/app/actions/get-user-by-id.ts b/src/app/actions/get-user-by-id.ts
new file mode 100644
index 0000000..a79c5c9
--- /dev/null
+++ b/src/app/actions/get-user-by-id.ts
@@ -0,0 +1,13 @@
+import prisma from "@/server/db"; // Adjust the import based on your structure
+
+export async function getUserById(userId: string) {
+ const user = await prisma.user.findUnique({
+ where: { id: userId },
+ select: {
+ id: true,
+ role: true, // Include any other fields you need
+ },
+ });
+
+ return user;
+}
diff --git a/src/app/admin/globals.css b/src/app/admin/globals.css
new file mode 100644
index 0000000..f4447b4
--- /dev/null
+++ b/src/app/admin/globals.css
@@ -0,0 +1,88 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+:root {
+ --foreground-rgb: 0, 0, 0;
+ --background-start-rgb: 214, 219, 220;
+ --background-end-rgb: 255, 255, 255;
+}
+
+@media (prefers-color-scheme: dark) {
+ :root {
+ --foreground-rgb: 255, 255, 255;
+ --background-start-rgb: 0, 0, 0;
+ --background-end-rgb: 0, 0, 0;
+ }
+}
+
+@layer utilities {
+ .text-balance {
+ text-wrap: balance;
+ }
+}
+
+@layer base {
+ :root {
+ --background: 0 0% 100%;
+ --foreground: 0 0% 3.9%;
+ --card: 0 0% 100%;
+ --card-foreground: 0 0% 3.9%;
+ --popover: 0 0% 100%;
+ --popover-foreground: 0 0% 3.9%;
+ --primary: 0 0% 9%;
+ --primary-foreground: 0 0% 98%;
+ --secondary: 0 0% 96.1%;
+ --secondary-foreground: 0 0% 9%;
+ --muted: 0 0% 96.1%;
+ --muted-foreground: 0 0% 45.1%;
+ --accent: 0 0% 96.1%;
+ --accent-foreground: 0 0% 9%;
+ --destructive: 0 84.2% 60.2%;
+ --destructive-foreground: 0 0% 98%;
+ --border: 0 0% 89.8%;
+ --input: 0 0% 89.8%;
+ --ring: 0 0% 3.9%;
+ --chart-1: 12 76% 61%;
+ --chart-2: 173 58% 39%;
+ --chart-3: 197 37% 24%;
+ --chart-4: 43 74% 66%;
+ --chart-5: 27 87% 67%;
+ --radius: 0.5rem;
+ }
+ .dark {
+ --background: 0 0% 3.9%;
+ --foreground: 0 0% 98%;
+ --card: 0 0% 3.9%;
+ --card-foreground: 0 0% 98%;
+ --popover: 0 0% 3.9%;
+ --popover-foreground: 0 0% 98%;
+ --primary: 0 0% 98%;
+ --primary-foreground: 0 0% 9%;
+ --secondary: 0 0% 14.9%;
+ --secondary-foreground: 0 0% 98%;
+ --muted: 0 0% 14.9%;
+ --muted-foreground: 0 0% 63.9%;
+ --accent: 0 0% 14.9%;
+ --accent-foreground: 0 0% 98%;
+ --destructive: 0 62.8% 30.6%;
+ --destructive-foreground: 0 0% 98%;
+ --border: 0 0% 14.9%;
+ --input: 0 0% 14.9%;
+ --ring: 0 0% 83.1%;
+ --chart-1: 220 70% 50%;
+ --chart-2: 160 60% 45%;
+ --chart-3: 30 80% 55%;
+ --chart-4: 280 65% 60%;
+ --chart-5: 340 75% 55%;
+ }
+}
+
+@layer base {
+ * {
+ @apply border-border;
+ }
+ body {
+ @apply bg-background text-foreground;
+ }
+}
diff --git a/src/app/admin/layout.tsx b/src/app/admin/layout.tsx
new file mode 100644
index 0000000..eb9547e
--- /dev/null
+++ b/src/app/admin/layout.tsx
@@ -0,0 +1,27 @@
+"use client";
+import type { Metadata } from "next";
+import { Inter } from "next/font/google";
+import "./globals.css";
+import Providers from "@/components/Layout/Provider";
+import { AdminNavbar } from "@/components/Admin/Navbar/navbar";
+
+const inter = Inter({ subsets: ["latin"] });
+
+export default function RootLayout({
+ children,
+}: Readonly<{
+ children: React.ReactNode;
+}>) {
+ return (
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/Admin/Navbar/navbar.tsx b/src/components/Admin/Navbar/navbar.tsx
new file mode 100644
index 0000000..5b7cbce
--- /dev/null
+++ b/src/components/Admin/Navbar/navbar.tsx
@@ -0,0 +1,189 @@
+"use client";
+import React, { Dispatch, SetStateAction, useState } from "react";
+import { IconType } from "react-icons";
+import { FiChevronDown, FiChevronsRight, FiUser } from "react-icons/fi";
+import { RiCoupon3Line } from "react-icons/ri";
+import { motion } from "framer-motion";
+import Link from "next/link";
+
+export const AdminNavbar = () => {
+ return (
+
+
+
+
+ );
+};
+
+const Sidebar = () => {
+ const [open, setOpen] = useState(true);
+ const [selected, setSelected] = useState("Dashboard");
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+const Option = ({
+ Icon,
+ title,
+ selected,
+ setSelected,
+ open,
+ notifs,
+ href,
+}: {
+ Icon: IconType;
+ title: string;
+ selected: string;
+ setSelected: Dispatch>;
+ open: boolean;
+ notifs?: number;
+ href?: string;
+}) => {
+ return (
+
+ setSelected(title)}
+ className={`relative flex h-10 w-full items-center rounded-md transition-colors ${
+ selected === title ? "bg-indigo-100 text-indigo-800" : "text-slate-500 hover:bg-slate-100"
+ }`}
+ >
+
+
+
+ {open && (
+
+ {title}
+
+ )}
+
+ {notifs && open && (
+
+ {notifs}
+
+ )}
+
+
+ );
+};
+
+const TitleSection = ({ open }: { open: boolean }) => {
+ return (
+
+
+
+
+ {open && (
+
+ Tedxsjec
+ Admin Page
+
+ )}
+
+ {open &&
}
+
+
+ );
+};
+
+const Logo = () => {
+ // Temp logo from https://logoipsum.com/
+ return (
+
+
+
+ );
+};
+
+const ToggleClose = ({ open, setOpen }: { open: boolean; setOpen: Dispatch> }) => {
+ return (
+ setOpen((pv) => !pv)}
+ className="absolute bottom-0 left-0 right-0 border-t border-slate-300 transition-colors hover:bg-slate-100"
+ >
+
+
+
+
+ {open && (
+
+ Hide
+
+ )}
+
+
+ );
+};
+
+const NavbarContent = () => ;
diff --git a/src/lib/auth-options.ts b/src/lib/auth-options.ts
index 4209752..11e253a 100644
--- a/src/lib/auth-options.ts
+++ b/src/lib/auth-options.ts
@@ -1,74 +1,72 @@
-import {
- DefaultSession,
- type NextAuthOptions,
- type Session as NextAuthSession,
-} from "next-auth";
+import { DefaultSession, type NextAuthOptions, type Session as NextAuthSession } from "next-auth";
import NextAuth from "next-auth/next";
import GoogleProvider from "next-auth/providers/google";
import { PrismaAdapter } from "@next-auth/prisma-adapter";
import prisma from "@/server/db";
import { UserRoleType } from "@/types";
import { JWT } from "next-auth/jwt";
+import { getUserById } from "@/app/actions/get-user-by-id";
declare module "next-auth" {
- interface Session {
- user: {
- id: string;
- role: UserRoleType;
- } & DefaultSession["user"];
- }
+ interface Session {
+ user: {
+ id: string;
+ role: UserRoleType;
+ } & DefaultSession["user"];
+ }
}
declare module "next-auth/jwt" {
- interface JWT {
- role: UserRoleType;
- }
+ interface JWT {
+ role: UserRoleType;
+ }
}
export const authOptions: NextAuthOptions = {
- adapter: PrismaAdapter(prisma),
- providers: [
- GoogleProvider({
- clientId: process.env.GOOGLE_ID ?? "",
- clientSecret: process.env.GOOGLE_SECRET ?? "",
- }),
- ],
- callbacks: {
- async jwt({ token, user }: { token: JWT; user: any }): Promise {
- //add user role to token
- if (user) {
- return {
- ...token,
- id: user.id,
- role: user.role,
- };
- }
- return token;
- },
- async session({
- session,
- token,
- }: {
- session: NextAuthSession;
- token: JWT;
- }): Promise {
- //add role to session
- return {
- ...session,
- user: {
- ...session.user,
- id: token.id,
- role: token.role as UserRoleType,
+ adapter: PrismaAdapter(prisma),
+ providers: [
+ GoogleProvider({
+ clientId: process.env.GOOGLE_ID ?? "",
+ clientSecret: process.env.GOOGLE_SECRET ?? "",
+ }),
+ ],
+ callbacks: {
+ async jwt({ token, user }: { token: JWT; user: any }): Promise {
+ //add user role to token
+ if (user) {
+ return {
+ ...token,
+ id: user.id,
+ role: user.role,
+ };
+ }
+ if (token.id) {
+ const updatedUser = await getUserById(token.id);
+ return {
+ ...token,
+ role: updatedUser?.role,
+ };
+ }
+ return token;
+ },
+ async session({ session, token }: { session: NextAuthSession; token: JWT }): Promise {
+ //add role to session
+ return {
+ ...session,
+ user: {
+ ...session.user,
+ id: token.id,
+ role: token.role as UserRoleType,
+ },
+ };
},
- };
},
- },
- session: {
- strategy: "jwt",
- },
- secret: process.env.NEXTAUTH_SECRET,
- // debug: process.env.NODE_ENV === "development",
+ session: {
+ strategy: "jwt",
+ },
+ secret: process.env.NEXTAUTH_SECRET,
+ // debug: process.env.NODE_ENV === "development",
};
export const handlers = NextAuth(authOptions);
diff --git a/src/middleware.ts b/src/middleware.ts
index 8551210..5872c07 100644
--- a/src/middleware.ts
+++ b/src/middleware.ts
@@ -5,17 +5,17 @@ import { getToken } from "next-auth/jwt";
// This function can be marked `async` if using `await` inside
export async function middleware(request: NextRequest) {
- const token = await getToken({ req: request });
- const url = request.nextUrl;
+ const token = await getToken({ req: request });
+ const url = request.nextUrl;
- if (url.pathname.startsWith("/admin")) {
- if (token?.role !== "ADMIN") {
- return NextResponse.redirect(new URL("/", request.url));
+ if (url.pathname.startsWith("/admin")) {
+ if (token?.role !== "ADMIN") {
+ return NextResponse.redirect(new URL("/", request.url));
+ }
}
- }
}
// See "Matching Paths" below to learn more
export const config = {
- matcher: ["/:path*", "/"],
+ matcher: ["/:path*", "/"],
};