Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
585 changes: 585 additions & 0 deletions Lb-web/package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Lb-web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"preview": "vite preview"
},
"dependencies": {
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-slot": "^1.2.4",
"@tailwindcss/vite": "^4.1.18",
Expand Down
122 changes: 122 additions & 0 deletions Lb-web/src/components/ui/dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
"use client"

import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { X } from "lucide-react"

import { cn } from "@/lib/utils"

const Dialog = DialogPrimitive.Root

const DialogTrigger = DialogPrimitive.Trigger

const DialogPortal = DialogPrimitive.Portal

const DialogClose = DialogPrimitive.Close

const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80",
className
)}
{...props}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName

const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 sm:rounded-lg",
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName

const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className
)}
{...props}
/>
)
DialogHeader.displayName = "DialogHeader"

const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
DialogFooter.displayName = "DialogFooter"

const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
DialogTitle.displayName = DialogPrimitive.Title.displayName

const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
DialogDescription.displayName = DialogPrimitive.Description.displayName

export {
Dialog,
DialogPortal,
DialogOverlay,
DialogClose,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
}
48 changes: 48 additions & 0 deletions Lb-web/src/features/admin-setting/components/SettingsSidebar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { Users } from "lucide-react";
import { Link, useLocation } from "react-router";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";

const sidebarItems = [
{
title: "User Management",
icon: Users,
href: "/settings/users",
variant: "ghost"
},
// Add more items here in the future
];

export function SettingsSidebar() {
const location = useLocation();

return (
<aside className="w-64 bg-white/50 backdrop-blur-md border-r border-sky-100 h-[calc(100vh-65px)] flex flex-col pt-4">
<nav className="flex-1 px-4 space-y-2">
<div className="mb-4">
<h3 className="px-2 text-xs font-semibold text-sky-500 uppercase tracking-wider">
Admin Settings
</h3>
</div>
{sidebarItems.map((item) => (
<Button
key={item.href}
variant="ghost"
asChild
className={cn(
"w-full justify-start",
location.pathname === item.href
? "bg-sky-100 text-sky-900"
: "text-sky-700 hover:text-sky-900 hover:bg-sky-50"
)}
>
<Link to={item.href}>
<item.icon className="mr-2 h-4 w-4" />
{item.title}
</Link>
</Button>
))}
</nav>
</aside>
);
}
71 changes: 71 additions & 0 deletions Lb-web/src/features/admin-setting/layout/SettingsLayout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { Outlet, Link } from "react-router";
import { SettingsSidebar } from "../components/SettingsSidebar";
import { LogOut, User } from "lucide-react";
import { Button } from "@/components/ui/button";
import { useAuth } from "@/features/Login/context/AuthContext";
import { useState } from "react";

export default function SettingsLayout() {
const { logout } = useAuth();
const [isProfileOpen, setIsProfileOpen] = useState(false);

const handleLogout = async () => {
await logout();
};

return (
<div className="flex flex-col h-screen bg-slate-50 overflow-hidden">
{/* Subtle Grid & Gradient Background (Reused from Home) */}
<div className="absolute inset-0 bg-[linear-gradient(to_right,#e0f2fe_1px,transparent_1px),linear-gradient(to_bottom,#e0f2fe_1px,transparent_1px)] bg-size-[24px_24px] mask-[radial-gradient(ellipse_60%_50%_at_50%_0%,#000_70%,transparent_100%)] opacity-90 pointer-events-none z-0"></div>

{/* Navbar (Reused from Home but simplified or modularized ideally) */}
<header className="flex items-center justify-between px-6 py-4 bg-white/50 backdrop-blur-md border-b border-sky-100 z-20 relative">
<div className="flex items-center space-x-3">
<Link to="/" className="flex items-center space-x-3">
<img src="/Localbase Logo.svg" alt="Localbase Logo" className="h-8 w-auto" />
<h1 className="text-xl font-bold text-sky-900 tracking-tight">Localbase</h1>
</Link>
</div>
<div className="flex items-center space-x-2">
<div className="relative isolate z-50">
<Button
variant="ghost"
size="icon"
className={`text-sky-700 hover:text-sky-900 hover:bg-sky-100 ${isProfileOpen ? 'bg-sky-100 text-sky-900' : ''}`}
onClick={() => setIsProfileOpen(!isProfileOpen)}
>
<User className="h-5 w-5" />
<span className="sr-only">Profile</span>
</Button>

{isProfileOpen && (
<>
<div className="fixed inset-0 z-40" onClick={() => setIsProfileOpen(false)}></div>
<div className="absolute right-0 mt-2 w-48 bg-white border border-sky-100 rounded-lg shadow-lg transition-all duration-200 z-50 animate-in fade-in zoom-in-95">
<div className="p-1">
<Button
variant="ghost"
className="w-full justify-start text-red-600 hover:text-red-700 hover:bg-red-50 cursor-pointer"
onClick={handleLogout}
>
<LogOut className="mr-2 h-4 w-4" />
Logout
</Button>
</div>
</div>
</>
)}
</div>
</div>
</header>

{/* Main Content Area with Sidebar */}
<div className="flex flex-1 overflow-hidden z-10 relative">
<SettingsSidebar />
<main className="flex-1 overflow-y-auto p-0">
<Outlet />
</main>
</div>
</div>
);
}
81 changes: 81 additions & 0 deletions Lb-web/src/features/admin-setting/pages/UserManagement.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { Button } from "@/components/ui/button";
import { Plus } from "lucide-react";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { useState } from "react";

export default function UserManagement() {
const [email, setEmail] = useState("");
const [open, setOpen] = useState(false);

const handleInvite = () => {
// Logic to invite user will go here
console.log("Inviting user:", email);
setOpen(false);
setEmail("");
};

return (
<div className="p-8 space-y-8">
<div className="flex items-center justify-between">
<div>
<h2 className="text-2xl font-bold text-sky-900 tracking-tight">User Management</h2>
<p className="text-sky-600">Manage system users and their roles.</p>
</div>
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button className="bg-sky-600 hover:bg-sky-700 text-white shadow-sm">
<Plus className="mr-2 h-4 w-4" />
Add User
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle className="text-sky-900">Add New User</DialogTitle>
<DialogDescription>
Invite a new user to the system by entering their email address.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<Input
id="email"
placeholder="user@example.com"
className="w-full border-sky-200 focus-visible:ring-sky-400"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
<DialogFooter>
<Button
type="submit"
className="bg-sky-600 hover:bg-sky-700 text-white"
onClick={handleInvite}
>
Send Invite
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>

<div className="bg-white rounded-xl border border-sky-100 shadow-sm p-6 flex flex-col items-center justify-center min-h-[400px] text-center space-y-4">
<div className="p-4 bg-sky-50 rounded-full">
<Plus className="h-8 w-8 text-sky-400" />
</div>
<h3 className="text-lg font-medium text-sky-900">No users found</h3>
<p className="text-sky-500 max-w-sm">
You haven't added any users yet. Click the button above to create a new user account.
</p>
{/* This is a placeholder for the actual table */}
</div>
</div>
);
}
19 changes: 18 additions & 1 deletion Lb-web/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@ import './index.css'
import Login from './features/Login/login.tsx'
import Register from './features/Login/register.tsx'
import Home from './pages/Home.tsx'
import { createBrowserRouter, RouterProvider } from 'react-router'
import { createBrowserRouter, RouterProvider, Navigate } from 'react-router'
import { AuthProvider } from './features/Login/context/AuthContext.tsx'
import { ProtectedRoute, PublicRoute } from './features/Login/components/AuthWrappers.tsx'

import SettingsLayout from './features/admin-setting/layout/SettingsLayout.tsx'
import UserManagement from './features/admin-setting/pages/UserManagement.tsx'

const router = createBrowserRouter([
{
element: <AuthProvider><PublicRoute /></AuthProvider>,
Expand All @@ -29,6 +32,20 @@ const router = createBrowserRouter([
{
index: true,
element: <Home />
},
{
path: 'settings',
element: <SettingsLayout />,
children: [
{
path: 'users',
element: <UserManagement />
},
{
index: true,
element: <Navigate to="users" replace />
}
]
}
]
}
Expand Down
11 changes: 7 additions & 4 deletions Lb-web/src/pages/Home.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Button } from "@/components/ui/button";
import { User, Settings, Plus, LogOut } from "lucide-react";
import { Link } from "react-router";
import { useState } from "react";
import { useAuth } from "@/features/Login/context/AuthContext";

Expand Down Expand Up @@ -58,10 +59,12 @@ export default function Home() {
</>
)}
</div>
<Button variant="ghost" size="icon" className="text-sky-700 hover:text-sky-900 hover:bg-sky-100">
<Settings className="h-5 w-5" />
<span className="sr-only">Settings</span>
</Button>
<Link to="/settings">
<Button variant="ghost" size="icon" className="text-sky-700 hover:text-sky-900 hover:bg-sky-100">
<Settings className="h-5 w-5" />
<span className="sr-only">Settings</span>
</Button>
</Link>
</div>
</header>

Expand Down
Loading