87 | >(({ className, ...props }, ref) => (
88 | [role=checkbox]]:translate-y-[2px]",
92 | className
93 | )}
94 | {...props}
95 | />
96 | ))
97 | TableCell.displayName = "TableCell"
98 |
99 | const TableCaption = React.forwardRef<
100 | HTMLTableCaptionElement,
101 | React.HTMLAttributes
102 | >(({ className, ...props }, ref) => (
103 |
108 | ))
109 | TableCaption.displayName = "TableCaption"
110 |
111 | export {
112 | Table,
113 | TableHeader,
114 | TableBody,
115 | TableFooter,
116 | TableHead,
117 | TableRow,
118 | TableCell,
119 | TableCaption,
120 | }
121 |
--------------------------------------------------------------------------------
/src/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Textarea = React.forwardRef<
6 | HTMLTextAreaElement,
7 | React.ComponentProps<"textarea">
8 | >(({ className, ...props }, ref) => {
9 | return (
10 |
18 | )
19 | })
20 | Textarea.displayName = "Textarea"
21 |
22 | export { Textarea }
23 |
--------------------------------------------------------------------------------
/src/components/ui/toaster.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useToast } from "@/hooks/use-toast"
4 | import {
5 | Toast,
6 | ToastClose,
7 | ToastDescription,
8 | ToastProvider,
9 | ToastTitle,
10 | ToastViewport,
11 | } from "@/components/ui/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 |
--------------------------------------------------------------------------------
/src/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as TooltipPrimitive from "@radix-ui/react-tooltip"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const TooltipProvider = TooltipPrimitive.Provider
9 |
10 | const Tooltip = TooltipPrimitive.Root
11 |
12 | const TooltipTrigger = TooltipPrimitive.Trigger
13 |
14 | const TooltipContent = React.forwardRef<
15 | React.ElementRef,
16 | React.ComponentPropsWithoutRef
17 | >(({ className, sideOffset = 4, ...props }, ref) => (
18 |
19 |
28 |
29 | ))
30 | TooltipContent.displayName = TooltipPrimitive.Content.displayName
31 |
32 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
33 |
--------------------------------------------------------------------------------
/src/components/userdashboard.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React from "react";
4 | import {redirect } from "next/navigation";
5 | import { useActionState } from "react";
6 | import { FaHome, FaPlus, FaCog, FaLock, FaUserCircle, FaSignOutAlt } from "react-icons/fa";
7 | import { Button } from "@/components/ui/button";
8 | import {
9 | Sheet,
10 | SheetContent,
11 | SheetHeader,
12 | SheetTitle,
13 | SheetTrigger,
14 | } from "@/components/ui/sheet";
15 | import Link from "next/link";
16 | import { LuList } from "react-icons/lu";
17 | import logout from "@/actions/auth/logout"; // Import the logout function
18 |
19 | export default function UserDashboard() {
20 | // use the useActionState hook to get the state of the logout action
21 | const [state, action, isPending] = useActionState(logout, undefined, null);
22 |
23 |
24 | return (
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | Products Categories
34 |
35 |
36 |
37 |
38 |
39 | Dashboard
40 |
41 |
42 |
43 |
44 |
45 | Add Products
46 |
47 |
48 |
49 |
50 |
51 | Manage Products
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 | Profile
60 |
61 |
62 |
63 |
64 |
74 |
75 |
76 |
77 |
78 | );
79 | }
80 |
--------------------------------------------------------------------------------
/src/context/userContext.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { createContext, useContext, useEffect, useState } from "react";
3 | import { createClient } from "@/utils/supabase/client";
4 | import { redirect } from "next/navigation";
5 |
6 | const supabase = createClient();
7 |
8 | const UserContext = createContext(null);
9 |
10 | export function UserProvider({ children }: { children: React.ReactNode }) {
11 | const [user, setUser] = useState(null);
12 |
13 | useEffect(() => {
14 | const getUserProfile = async () => {
15 | try {
16 | const { data: authUser, error: authError } = await supabase.auth.getUser();
17 | if (authError) {
18 | console.error("Error getting user from auth:", authError);
19 | redirect("/login")
20 | return;
21 | }
22 | console.log("Auth User:", authUser);
23 |
24 | if (!authUser?.user) {
25 | console.log("No authenticated user found.");
26 | return;
27 | }
28 |
29 | const { data: profile, error } = await supabase
30 | .from("user_profile")
31 | .select("*")
32 | .eq("user_id", authUser.user.id)
33 | .single();
34 |
35 | if (error) {
36 | console.error("Error fetching profile:", error);
37 | return;
38 | }
39 |
40 | console.log("Fetched profile:", profile);
41 | setUser(profile);
42 | } catch (error) {
43 | console.error("Unexpected error:", error);
44 | }
45 | };
46 |
47 | getUserProfile();
48 | }, []);
49 |
50 | return {children} ;
51 | }
52 |
53 | export function useUser() {
54 | return useContext(UserContext);
55 | }
--------------------------------------------------------------------------------
/src/hooks/use-mobile.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | const MOBILE_BREAKPOINT = 768
4 |
5 | export function useIsMobile() {
6 | const [isMobile, setIsMobile] = React.useState(undefined)
7 |
8 | React.useEffect(() => {
9 | const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
10 | const onChange = () => {
11 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
12 | }
13 | mql.addEventListener("change", onChange)
14 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
15 | return () => mql.removeEventListener("change", onChange)
16 | }, [])
17 |
18 | return !!isMobile
19 | }
20 |
--------------------------------------------------------------------------------
/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { clsx, type ClassValue } from "clsx"
2 | import { twMerge } from "tailwind-merge"
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs))
6 | }
7 |
--------------------------------------------------------------------------------
/src/middleware.ts:
--------------------------------------------------------------------------------
1 | import { createServerClient } from '@supabase/ssr';
2 | import { NextResponse, type NextRequest } from 'next/server';
3 | import { updateSession } from './utils/supabaseSession'; // Adjust the path as necessary
4 |
5 | export async function middleware(request: NextRequest) {
6 | // Refresh session and handle cookies
7 | const response = await updateSession(request);
8 |
9 | // Initialize Supabase client
10 | const supabase = createServerClient(
11 | process.env.NEXT_PUBLIC_SUPABASE_URL!,
12 | process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
13 | {
14 | cookies: {
15 | getAll: () => request.cookies.getAll(),
16 | setAll: (cookiesToSet) => {
17 | cookiesToSet.forEach(({ name, value, options }) =>
18 | response.cookies.set(name, value, options)
19 | );
20 | },
21 | },
22 | }
23 | );
24 |
25 | // Get the authenticated user
26 | const {
27 | data: { user },
28 | error,
29 | } = await supabase.auth.getUser();
30 |
31 | if (error || !user) {
32 | // Redirect unauthenticated users to login
33 | return NextResponse.redirect(new URL('/login', request.url));
34 | }
35 |
36 | // Fetch the user's role from the user_profile table
37 | const { data: userProfile, error: profileError } = await supabase
38 | .from('user_profile')
39 | .select('role')
40 | .eq('id', user.id)
41 | .single();
42 |
43 | // Redirect users without admin privileges
44 | if (profileError || userProfile?.role !== 'admin') {
45 | return NextResponse.redirect(new URL('/unauthorized', request.url));
46 | }
47 |
48 | // Allow access for authenticated admins
49 | return response;
50 | }
51 |
52 | // Apply middleware only to the `/admin` path
53 | export const config = {
54 | matcher: '/admin/:path*',
55 | };
56 |
--------------------------------------------------------------------------------
/src/utils/supabase-db.ts:
--------------------------------------------------------------------------------
1 |
2 |
3 | import { createClient } from '@supabase/supabase-js'
4 |
5 | const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL
6 | const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
7 |
8 | // Create a single supabase client for interacting with your database
9 | const supabaseDb = createClient(supabaseUrl!, supabaseKey!)
10 |
11 | export default supabaseDb
--------------------------------------------------------------------------------
/src/utils/supabase-server.ts:
--------------------------------------------------------------------------------
1 | // src/utils/supabase/server.ts
2 | import { createServerClient } from '@supabase/ssr';
3 | import { cookies } from 'next/headers';
4 |
5 | export async function createServerClient() {
6 | const cookieStore = await cookies();
7 | // Add your Supabase setup code here
8 | const supabase = createServerClient({ cookieStore });
9 | return supabase;
10 | }
11 |
--------------------------------------------------------------------------------
/src/utils/supabase/client.ts:
--------------------------------------------------------------------------------
1 | import { createBrowserClient } from '@supabase/ssr'
2 |
3 | export function createClient() {
4 | return createBrowserClient(
5 | process.env.NEXT_PUBLIC_SUPABASE_URL!,
6 | process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
7 | )
8 | }
--------------------------------------------------------------------------------
/src/utils/supabase/server.ts:
--------------------------------------------------------------------------------
1 | import { createServerClient } from '@supabase/ssr'
2 | import { cookies } from 'next/headers'
3 |
4 | export async function createClient() {
5 | const cookieStore = await cookies()
6 |
7 | return createServerClient(
8 | process.env.NEXT_PUBLIC_SUPABASE_URL!,
9 | process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
10 | {
11 | cookies: {
12 | getAll() {
13 | return cookieStore.getAll()
14 | },
15 | setAll(cookiesToSet) {
16 | try {
17 | cookiesToSet.forEach(({ name, value, options }) =>
18 | cookieStore.set(name, value, options)
19 | )
20 | } catch {
21 | // The `setAll` method was called from a Server Component.
22 | // This can be ignored if you have middleware refreshing
23 | // user sessions.
24 | }
25 | },
26 | },
27 | }
28 | )
29 | }
--------------------------------------------------------------------------------
/src/utils/supabaseSession.ts:
--------------------------------------------------------------------------------
1 | import { createServerClient } from '@supabase/ssr'
2 | import { NextResponse, type NextRequest } from 'next/server'
3 |
4 | export async function updateSession(request: NextRequest) {
5 | let supabaseResponse = NextResponse.next({
6 | request,
7 | })
8 |
9 | const supabase = createServerClient(
10 | process.env.NEXT_PUBLIC_SUPABASE_URL!,
11 | process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
12 | {
13 | cookies: {
14 | getAll() {
15 | return request.cookies.getAll()
16 | },
17 | setAll(cookiesToSet) {
18 | cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value))
19 | supabaseResponse = NextResponse.next({
20 | request,
21 | })
22 | cookiesToSet.forEach(({ name, value, options: _ }) =>
23 | supabaseResponse.cookies.set(name, value, _)
24 | )
25 | },
26 | },
27 | }
28 | )
29 |
30 | // refreshing the auth token
31 | await supabase.auth.getUser()
32 |
33 | return supabaseResponse
34 | }
35 |
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss";
2 |
3 | export default {
4 | darkMode: ["class"],
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 | colors: {
13 | background: 'hsl(var(--background))',
14 | foreground: 'hsl(var(--foreground))',
15 | card: {
16 | DEFAULT: 'hsl(var(--card))',
17 | foreground: 'hsl(var(--card-foreground))'
18 | },
19 | popover: {
20 | DEFAULT: 'hsl(var(--popover))',
21 | foreground: 'hsl(var(--popover-foreground))'
22 | },
23 | primary: {
24 | DEFAULT: 'hsl(var(--primary))',
25 | foreground: 'hsl(var(--primary-foreground))'
26 | },
27 | secondary: {
28 | DEFAULT: 'hsl(var(--secondary))',
29 | foreground: 'hsl(var(--secondary-foreground))'
30 | },
31 | muted: {
32 | DEFAULT: 'hsl(var(--muted))',
33 | foreground: 'hsl(var(--muted-foreground))'
34 | },
35 | accent: {
36 | DEFAULT: 'hsl(var(--accent))',
37 | foreground: 'hsl(var(--accent-foreground))'
38 | },
39 | destructive: {
40 | DEFAULT: 'hsl(var(--destructive))',
41 | foreground: 'hsl(var(--destructive-foreground))'
42 | },
43 | border: 'hsl(var(--border))',
44 | input: 'hsl(var(--input))',
45 | ring: 'hsl(var(--ring))',
46 | chart: {
47 | '1': 'hsl(var(--chart-1))',
48 | '2': 'hsl(var(--chart-2))',
49 | '3': 'hsl(var(--chart-3))',
50 | '4': 'hsl(var(--chart-4))',
51 | '5': 'hsl(var(--chart-5))'
52 | },
53 | sidebar: {
54 | DEFAULT: 'hsl(var(--sidebar-background))',
55 | foreground: 'hsl(var(--sidebar-foreground))',
56 | primary: 'hsl(var(--sidebar-primary))',
57 | 'primary-foreground': 'hsl(var(--sidebar-primary-foreground))',
58 | accent: 'hsl(var(--sidebar-accent))',
59 | 'accent-foreground': 'hsl(var(--sidebar-accent-foreground))',
60 | border: 'hsl(var(--sidebar-border))',
61 | ring: 'hsl(var(--sidebar-ring))'
62 | }
63 | },
64 | borderRadius: {
65 | lg: 'var(--radius)',
66 | md: 'calc(var(--radius) - 2px)',
67 | sm: 'calc(var(--radius) - 4px)'
68 | }
69 | }
70 | },
71 | plugins: [require("tailwindcss-animate")],
72 | } satisfies Config;
73 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2017",
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 |
--------------------------------------------------------------------------------