feat: location.pathname based bar navigation + admin layout + separate
auth routes
This commit is contained in:
@ -1,26 +1,22 @@
|
||||
import type { FC } from "react";
|
||||
import { useBarItems } from "../tabs";
|
||||
import { Link } from "react-router";
|
||||
|
||||
export interface ISidebarProps {
|
||||
activeTab: string;
|
||||
onChangeTab: (tab: string) => void;
|
||||
}
|
||||
|
||||
const Sidebar: FC<ISidebarProps> = ({ activeTab, onChangeTab }) => {
|
||||
const barItems = useBarItems();
|
||||
const Sidebar: FC = () => {
|
||||
const [barItems, isActive] = useBarItems();
|
||||
|
||||
return (
|
||||
<div className="hidden sm:flex flex-col gap-2 items-stretch min-w-80 w-80 p-5 pt-18 min-h-screen select-none bg-white/65 dark:bg-black/65 shadow-lg shadow-gray-300 dark:shadow-gray-700">
|
||||
{barItems.map((item) => (
|
||||
<div
|
||||
key={item.tab}
|
||||
onClick={() => onChangeTab(item.tab)}
|
||||
className={`dark:text-gray-200 transition-colors text-sm cursor-pointer p-4 rounded-lg flex flex-row items-center gap-3${
|
||||
item.tab === activeTab ? " bg-gray-200 dark:bg-gray-900" : ""
|
||||
}`}
|
||||
>
|
||||
{item.icon} {item.title}
|
||||
</div>
|
||||
<Link to={item.pathname} key={item.tab}>
|
||||
<div
|
||||
className={`dark:text-gray-200 transition-colors text-sm cursor-pointer p-4 rounded-lg flex flex-row items-center gap-3${
|
||||
isActive(item) ? " bg-gray-200 dark:bg-gray-900" : ""
|
||||
}`}
|
||||
>
|
||||
{item.icon} {item.title}
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
@ -1,104 +0,0 @@
|
||||
import type { FC } from "react";
|
||||
import { Link } from "react-router";
|
||||
|
||||
const services = [
|
||||
{
|
||||
id: "1",
|
||||
name: "User Service",
|
||||
clientId: "user-svc-001",
|
||||
isActive: true,
|
||||
createdAt: "2024-09-15T10:20:30Z",
|
||||
updatedAt: "2025-01-10T12:00:00Z",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "Billing Service",
|
||||
clientId: "billing-svc-009",
|
||||
isActive: false,
|
||||
createdAt: "2024-10-01T08:45:10Z",
|
||||
updatedAt: "2025-03-22T14:30:00Z",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
name: "Analytics Service",
|
||||
clientId: "analytics-svc-777",
|
||||
isActive: true,
|
||||
createdAt: "2024-11-25T16:00:00Z",
|
||||
updatedAt: "2025-02-05T10:15:45Z",
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
name: "Email Service",
|
||||
clientId: "email-svc-333",
|
||||
isActive: false,
|
||||
createdAt: "2023-07-10T13:00:00Z",
|
||||
updatedAt: "2024-12-31T09:25:00Z",
|
||||
},
|
||||
];
|
||||
|
||||
const ApiServices: FC = () => {
|
||||
return (
|
||||
<div className="overflow-x-auto rounded shadow-md dark:shadow-gray-800">
|
||||
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<thead className="bg-gray-50 dark:bg-gray-800">
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left text-sm font-semibold text-gray-700 dark:text-gray-200">
|
||||
Name
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-sm font-semibold text-gray-700 dark:text-gray-200">
|
||||
Client ID
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-sm font-semibold text-gray-700 dark:text-gray-200">
|
||||
Is Active
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-sm font-semibold text-gray-700 dark:text-gray-200">
|
||||
Created At
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-sm font-semibold text-gray-700 dark:text-gray-200">
|
||||
Updated At
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{services.map((service) => (
|
||||
<tr
|
||||
key={service.id}
|
||||
className="hover:bg-gray-50 dark:hover:bg-gray-800"
|
||||
>
|
||||
<td className="px-6 py-4 text-sm font-medium text-blue-600">
|
||||
<Link
|
||||
to={`/services/${service.id}`}
|
||||
className="hover:underline hover:text-gray-600 dark:hover:text-gray-300 transition-colors"
|
||||
>
|
||||
{service.name}
|
||||
</Link>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm text-gray-700 dark:text-gray-300">
|
||||
{service.clientId}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm">
|
||||
<span
|
||||
className={`inline-block px-2 py-1 text-xs rounded-full font-semibold ${
|
||||
service.isActive
|
||||
? "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300"
|
||||
: "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300"
|
||||
}`}
|
||||
>
|
||||
{service.isActive ? "Yes" : "No"}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">
|
||||
{new Date(service.createdAt).toLocaleString()}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">
|
||||
{new Date(service.updatedAt).toLocaleString()}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ApiServices;
|
@ -1,30 +0,0 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import Avatar from "@/feature/Avatar";
|
||||
import { useAuth } from "@/store/auth";
|
||||
import { type FC } from "react";
|
||||
|
||||
const Home: FC = () => {
|
||||
const profile = useAuth((state) => state.profile);
|
||||
const signOut = useAuth((state) => state.signOut);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-2 p-7">
|
||||
<div className="w-24 h-24 sm:w-36 sm:h-36 overflow-hidden rounded-full flex items-center justify-center bg-gray-300">
|
||||
<Avatar iconSize={64} />
|
||||
</div>
|
||||
<h1 className="dark:text-gray-200 text-gray-800 text-2xl select-none">
|
||||
Welcome, {profile?.full_name}
|
||||
</h1>
|
||||
<p className="text-gray-600 dark:text-gray-500 select-none text-center text-sm/normal sm:text-lg">
|
||||
Manage your info, private and security to make Home Guard work better
|
||||
for you.
|
||||
</p>
|
||||
|
||||
<Button className="mt-10" onClick={signOut}>
|
||||
Sign Out
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Home;
|
@ -1,68 +0,0 @@
|
||||
import Avatar from "@/feature/Avatar";
|
||||
import { useAuth } from "@/store/auth";
|
||||
import { ChevronRight } from "lucide-react";
|
||||
import { type FC } from "react";
|
||||
|
||||
const PersonalInfo: FC = () => {
|
||||
const profile = useAuth((state) => state.profile);
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1 className="dark:text-gray-200 text-gray-800 text-2xl">
|
||||
Your profile info in Home services
|
||||
</h1>
|
||||
|
||||
<p className="text-gray-500 text-sm mt-2 sm:text-lg">
|
||||
Personal info and options to manage it. You can make some of this info,
|
||||
like your contact details, visible to others so they can reach you
|
||||
easily. You can also see a summary of your profiles.
|
||||
</p>
|
||||
|
||||
<div className="border dark:border-gray-800 border-gray-300 p-4 rounded mt-4">
|
||||
<h3 className="dark:text-gray-300 text-gray-800">Basic info</h3>
|
||||
<p className="text-gray-500 text-sm mt-2 mb-4">
|
||||
Some info may be visible to other services and tools using Home Guard.{" "}
|
||||
<a href="#" className="text-blue-500">
|
||||
Learn more
|
||||
</a>
|
||||
</p>
|
||||
|
||||
{/* Profile Picture */}
|
||||
<div className="flex flex-row items-center justify-between px-2 p-4 border-b gap-2 dark:border-b-gray-800 border-b-gray-100">
|
||||
<div className="flex flex-col items-start gap-2">
|
||||
<p className="text-sm dark:text-gray-400 font-medium text-gray-600">
|
||||
Profile picture
|
||||
</p>
|
||||
<p className="text-sm dark:text-gray-500 text-gray-600">
|
||||
Add a profile picture to personalize your account
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<div className="w-16 h-16 overflow-hidden rounded-full dark:bg-gray-400 bg-gray-700">
|
||||
<Avatar iconSize={12} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Name */}
|
||||
<div className="flex flex-row items-center justify-between px-2 p-4 border-b dark:border-b-gray-800 border-b-gray-100">
|
||||
<div className="flex flex-col items-start gap-2">
|
||||
<p className="text-sm dark:text-gray-400 font-medium text-gray-600">
|
||||
Name
|
||||
</p>
|
||||
<p className="text dark:text-gray-200 text-gray-800">
|
||||
{profile?.full_name}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-gray-500">
|
||||
<ChevronRight size={26} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default PersonalInfo;
|
@ -1,28 +1,24 @@
|
||||
import { type FC } from "react";
|
||||
import { useBarItems } from "../tabs";
|
||||
import { Link } from "react-router";
|
||||
|
||||
export interface ITopBarProps {
|
||||
activeTab: string;
|
||||
onChangeTab: (tab: string) => void;
|
||||
}
|
||||
|
||||
const TopBar: FC<ITopBarProps> = ({ activeTab, onChangeTab }) => {
|
||||
const barItems = useBarItems();
|
||||
const TopBar: FC = () => {
|
||||
const [barItems, isActive] = useBarItems();
|
||||
|
||||
return (
|
||||
<div className="sm:hidden flex w-full overflow-x-auto sm:overflow-x-visible max-w-full min-w-full sm:justify-center sm:space-x-4 no-scrollbar shadow-md shadow-gray-300 dark:shadow-gray-700 dark:bg-black/70 bg-white/70 pt-14">
|
||||
{barItems.map((item) => (
|
||||
<div
|
||||
key={item.tab}
|
||||
onClick={() => onChangeTab(item.tab)}
|
||||
className={`flex-shrink-0 transition-all border-b-4 px-4 py-2 min-w-[120px] sm:min-w-0 sm:flex-1 flex items-center justify-center cursor-pointer select-none whitespace-nowrap text-sm font-medium ${
|
||||
item.tab === activeTab
|
||||
? " border-b-4 border-b-blue-500 text-blue-500"
|
||||
: " border-b-transparent text-gray-500"
|
||||
}`}
|
||||
>
|
||||
{item.title}
|
||||
</div>
|
||||
<Link to={item.pathname} key={item.tab}>
|
||||
<div
|
||||
className={`flex-shrink-0 transition-all border-b-4 px-4 py-2 min-w-[120px] sm:min-w-0 sm:flex-1 flex items-center justify-center cursor-pointer select-none whitespace-nowrap text-sm font-medium ${
|
||||
isActive(item)
|
||||
? " border-b-4 border-b-blue-500 text-blue-500"
|
||||
: " border-b-transparent text-gray-500"
|
||||
}`}
|
||||
>
|
||||
{item.title}
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
@ -1,37 +1,61 @@
|
||||
import { useAuth } from "@/store/auth";
|
||||
import { Blocks, Home, Settings2, User } from "lucide-react";
|
||||
import { useCallback, type ReactNode } from "react";
|
||||
import { useLocation } from "react-router";
|
||||
|
||||
export const useBarItems = () => {
|
||||
export interface BarItem {
|
||||
icon: ReactNode;
|
||||
title: string;
|
||||
tab: string;
|
||||
pathname: string;
|
||||
}
|
||||
|
||||
export const useBarItems = (): [BarItem[], (item: BarItem) => boolean] => {
|
||||
const profile = useAuth((state) => state.profile);
|
||||
const location = useLocation();
|
||||
|
||||
const isActive = useCallback(
|
||||
(item: BarItem) => {
|
||||
return location.pathname === item.pathname;
|
||||
},
|
||||
[location.pathname],
|
||||
);
|
||||
|
||||
if (!profile) {
|
||||
return [];
|
||||
return [[], isActive];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
icon: <Home />,
|
||||
title: "Home",
|
||||
tab: "home",
|
||||
},
|
||||
{
|
||||
icon: <User />,
|
||||
title: "Personal Info",
|
||||
tab: "personal-info",
|
||||
},
|
||||
{
|
||||
icon: <Settings2 />,
|
||||
title: "Data & Personalization",
|
||||
tab: "data-personalization",
|
||||
},
|
||||
...(profile.isAdmin
|
||||
? [
|
||||
{
|
||||
icon: <Blocks />,
|
||||
title: "API Services",
|
||||
tab: "api-services",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
[
|
||||
{
|
||||
icon: <Home />,
|
||||
title: "Home",
|
||||
tab: "home",
|
||||
pathname: "/",
|
||||
},
|
||||
{
|
||||
icon: <User />,
|
||||
title: "Personal Info",
|
||||
tab: "personal-info",
|
||||
pathname: "/personal-info",
|
||||
},
|
||||
{
|
||||
icon: <Settings2 />,
|
||||
title: "Data & Personalization",
|
||||
tab: "data-personalization",
|
||||
pathname: "/data-personalize",
|
||||
},
|
||||
...(profile.isAdmin
|
||||
? [
|
||||
{
|
||||
icon: <Blocks />,
|
||||
title: "API Services",
|
||||
tab: "api-services",
|
||||
pathname: "/admin/api-services",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
isActive,
|
||||
];
|
||||
};
|
||||
|
Reference in New Issue
Block a user