feat: location.pathname based bar navigation + admin layout + separate

auth routes
This commit is contained in:
2025-05-29 23:42:17 +02:00
parent 78e84567c7
commit db2cb36f54
14 changed files with 219 additions and 143 deletions

View File

@ -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>
);

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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>
);

View File

@ -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,
];
};