styling fiexes and logout fixed
This commit is contained in:
@@ -9,6 +9,7 @@ from rest_framework.response import Response
|
|||||||
from rest_framework import status, permissions
|
from rest_framework import status, permissions
|
||||||
from drf_spectacular.utils import extend_schema
|
from drf_spectacular.utils import extend_schema
|
||||||
from igny8_core.api.response import success_response, error_response
|
from igny8_core.api.response import success_response, error_response
|
||||||
|
from django.conf import settings
|
||||||
from .views import (
|
from .views import (
|
||||||
GroupsViewSet, UsersViewSet, AccountsViewSet, SubscriptionsViewSet,
|
GroupsViewSet, UsersViewSet, AccountsViewSet, SubscriptionsViewSet,
|
||||||
SiteUserAccessViewSet, PlanViewSet, SiteViewSet, SectorViewSet,
|
SiteUserAccessViewSet, PlanViewSet, SiteViewSet, SectorViewSet,
|
||||||
@@ -285,6 +286,44 @@ class LoginView(APIView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@extend_schema(
|
||||||
|
tags=['Authentication'],
|
||||||
|
summary='User Logout',
|
||||||
|
description='Clear session and logout user'
|
||||||
|
)
|
||||||
|
class LogoutView(APIView):
|
||||||
|
"""Logout endpoint."""
|
||||||
|
permission_classes = [permissions.AllowAny]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
from django.contrib.auth import logout as django_logout
|
||||||
|
|
||||||
|
# Clear Django auth session
|
||||||
|
django_logout(request)
|
||||||
|
try:
|
||||||
|
request.session.flush()
|
||||||
|
except Exception:
|
||||||
|
# If session is unavailable or already cleared, ignore
|
||||||
|
pass
|
||||||
|
|
||||||
|
response = success_response(
|
||||||
|
message='Logged out successfully',
|
||||||
|
request=request
|
||||||
|
)
|
||||||
|
|
||||||
|
# Explicitly expire session cookie across domain/path
|
||||||
|
try:
|
||||||
|
response.delete_cookie(
|
||||||
|
settings.SESSION_COOKIE_NAME,
|
||||||
|
path=getattr(settings, 'SESSION_COOKIE_PATH', '/'),
|
||||||
|
domain=getattr(settings, 'SESSION_COOKIE_DOMAIN', None)
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
# If settings are misconfigured, still return success
|
||||||
|
pass
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
@extend_schema(
|
@extend_schema(
|
||||||
tags=['Authentication'],
|
tags=['Authentication'],
|
||||||
summary='Request Password Reset',
|
summary='Request Password Reset',
|
||||||
@@ -684,6 +723,7 @@ urlpatterns = [
|
|||||||
path('', include(router.urls)),
|
path('', include(router.urls)),
|
||||||
path('register/', csrf_exempt(RegisterView.as_view()), name='auth-register'),
|
path('register/', csrf_exempt(RegisterView.as_view()), name='auth-register'),
|
||||||
path('login/', csrf_exempt(LoginView.as_view()), name='auth-login'),
|
path('login/', csrf_exempt(LoginView.as_view()), name='auth-login'),
|
||||||
|
path('logout/', csrf_exempt(LogoutView.as_view()), name='auth-logout'),
|
||||||
path('refresh/', csrf_exempt(RefreshTokenView.as_view()), name='auth-refresh'),
|
path('refresh/', csrf_exempt(RefreshTokenView.as_view()), name='auth-refresh'),
|
||||||
path('change-password/', ChangePasswordView.as_view(), name='auth-change-password'),
|
path('change-password/', ChangePasswordView.as_view(), name='auth-change-password'),
|
||||||
path('password-reset/', csrf_exempt(PasswordResetRequestView.as_view()), name='auth-password-reset-request'),
|
path('password-reset/', csrf_exempt(PasswordResetRequestView.as_view()), name='auth-password-reset-request'),
|
||||||
|
|||||||
@@ -116,64 +116,6 @@ export default function SignInForm() {
|
|||||||
<div>
|
<div>
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Logout Reason Display */}
|
|
||||||
{logoutReason && (
|
|
||||||
<div className="p-4 bg-warning-50 border border-warning-200 rounded-lg dark:bg-warning-900/20 dark:border-warning-800">
|
|
||||||
<div className="flex items-start justify-between">
|
|
||||||
<div className="flex-1">
|
|
||||||
<div className="flex items-center gap-2 mb-2">
|
|
||||||
<svg className="w-5 h-5 text-warning-600 dark:text-warning-400" fill="currentColor" viewBox="0 0 20 20">
|
|
||||||
<path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
<h4 className="font-semibold text-warning-800 dark:text-warning-300">
|
|
||||||
Session Ended
|
|
||||||
</h4>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm text-warning-700 dark:text-warning-400 mb-2">
|
|
||||||
{logoutReason.message}
|
|
||||||
</p>
|
|
||||||
{logoutReason.path && logoutReason.path !== '/signin' && (
|
|
||||||
<p className="text-xs text-warning-600 dark:text-warning-500">
|
|
||||||
Original page: <span className="font-mono">{logoutReason.path}</span>
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setShowLogoutDetails(!showLogoutDetails)}
|
|
||||||
className="ml-2 p-1 text-warning-600 hover:text-warning-800 dark:text-warning-400 dark:hover:text-warning-300"
|
|
||||||
title="Toggle technical details"
|
|
||||||
>
|
|
||||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
|
||||||
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clipRule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Expandable Technical Details */}
|
|
||||||
{showLogoutDetails && (
|
|
||||||
<div className="mt-3 pt-3 border-t border-warning-300 dark:border-warning-700">
|
|
||||||
<p className="text-xs font-semibold text-warning-800 dark:text-warning-300 mb-2">
|
|
||||||
Technical Details:
|
|
||||||
</p>
|
|
||||||
<div className="space-y-1 text-xs font-mono text-warning-700 dark:text-warning-400">
|
|
||||||
<div><span className="font-bold">Code:</span> {logoutReason.code}</div>
|
|
||||||
<div><span className="font-bold">Source:</span> {logoutReason.source}</div>
|
|
||||||
<div><span className="font-bold">Time:</span> {formatDateTime(logoutReason.timestamp)}</div>
|
|
||||||
{logoutReason.context && Object.keys(logoutReason.context).length > 0 && (
|
|
||||||
<div className="mt-2">
|
|
||||||
<span className="font-bold">Context:</span>
|
|
||||||
<pre className="mt-1 p-2 bg-warning-100 dark:bg-warning-900/30 rounded text-xs overflow-x-auto">
|
|
||||||
{JSON.stringify(logoutReason.context, null, 2)}
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Session Conflict Alert */}
|
{/* Session Conflict Alert */}
|
||||||
{sessionConflict && (
|
{sessionConflict && (
|
||||||
<div className="p-4 bg-warning-50 border border-warning-200 rounded-lg dark:bg-warning-900/20 dark:border-warning-800">
|
<div className="p-4 bg-warning-50 border border-warning-200 rounded-lg dark:bg-warning-900/20 dark:border-warning-800">
|
||||||
|
|||||||
@@ -1,15 +1,22 @@
|
|||||||
import { useTheme } from "../../context/ThemeContext";
|
import { useTheme } from "../../context/ThemeContext";
|
||||||
|
import IconButton from "../ui/button/IconButton";
|
||||||
|
|
||||||
export const ThemeToggleButton: React.FC = () => {
|
export const ThemeToggleButton: React.FC = () => {
|
||||||
const { toggleTheme } = useTheme();
|
const { theme, toggleTheme } = useTheme();
|
||||||
|
const isDark = theme === "dark";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<IconButton
|
||||||
onClick={toggleTheme}
|
onClick={toggleTheme}
|
||||||
className="relative flex items-center justify-center text-gray-500 transition-colors bg-white border border-gray-200 rounded-full hover:text-dark-900 h-11 w-11 hover:bg-gray-100 hover:text-gray-700 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-white"
|
variant="outline"
|
||||||
>
|
tone="neutral"
|
||||||
|
size="md"
|
||||||
|
shape="circle"
|
||||||
|
aria-label={isDark ? "Switch to light mode" : "Switch to dark mode"}
|
||||||
|
title={isDark ? "Light mode" : "Dark mode"}
|
||||||
|
icon={
|
||||||
|
isDark ? (
|
||||||
<svg
|
<svg
|
||||||
className="hidden dark:block"
|
|
||||||
width="20"
|
width="20"
|
||||||
height="20"
|
height="20"
|
||||||
viewBox="0 0 20 20"
|
viewBox="0 0 20 20"
|
||||||
@@ -23,8 +30,8 @@ export const ThemeToggleButton: React.FC = () => {
|
|||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
|
) : (
|
||||||
<svg
|
<svg
|
||||||
className="dark:hidden"
|
|
||||||
width="20"
|
width="20"
|
||||||
height="20"
|
height="20"
|
||||||
viewBox="0 0 20 20"
|
viewBox="0 0 20 20"
|
||||||
@@ -36,6 +43,8 @@ export const ThemeToggleButton: React.FC = () => {
|
|||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -134,6 +134,7 @@ export default function NotificationDropdown() {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
tone="neutral"
|
tone="neutral"
|
||||||
size="md"
|
size="md"
|
||||||
|
shape="circle"
|
||||||
icon={
|
icon={
|
||||||
<svg
|
<svg
|
||||||
width="20"
|
width="20"
|
||||||
@@ -154,9 +155,9 @@ export default function NotificationDropdown() {
|
|||||||
/>
|
/>
|
||||||
{/* Notification badge */}
|
{/* Notification badge */}
|
||||||
{unreadCount > 0 && (
|
{unreadCount > 0 && (
|
||||||
<span className="absolute -right-0.5 -top-0.5 z-10 flex h-5 w-5 items-center justify-center rounded-full bg-warning-500 text-[10px] font-semibold text-white">
|
<span className="absolute -right-0.5 -top-0.5 z-10 flex h-5 w-5 items-center justify-center rounded-full bg-brand-500 text-[10px] font-semibold text-white">
|
||||||
{unreadCount > 9 ? '9+' : unreadCount}
|
{unreadCount > 9 ? '9+' : unreadCount}
|
||||||
<span className="absolute inline-flex w-full h-full bg-warning-400 rounded-full opacity-75 animate-ping"></span>
|
<span className="absolute inline-flex w-full h-full bg-brand-400 rounded-full opacity-75 animate-ping"></span>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { DropdownItem } from "../ui/dropdown/DropdownItem";
|
|||||||
import { Dropdown } from "../ui/dropdown/Dropdown";
|
import { Dropdown } from "../ui/dropdown/Dropdown";
|
||||||
import { useAuthStore } from "../../store/authStore";
|
import { useAuthStore } from "../../store/authStore";
|
||||||
import Button from "../ui/button/Button";
|
import Button from "../ui/button/Button";
|
||||||
|
import { ArrowRightIcon, DollarLineIcon, HelpCircleIcon, PieChartIcon, UserCircleIcon } from "../../icons";
|
||||||
|
|
||||||
export default function UserDropdown() {
|
export default function UserDropdown() {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
@@ -11,6 +12,14 @@ export default function UserDropdown() {
|
|||||||
const { user, logout } = useAuthStore();
|
const { user, logout } = useAuthStore();
|
||||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
|
const firstName = user?.first_name || (user as any)?.firstName || "";
|
||||||
|
const lastName = user?.last_name || (user as any)?.lastName || "";
|
||||||
|
const displayName = firstName || user?.username || user?.email?.split("@")[0] || "User";
|
||||||
|
const displayFullName = `${firstName} ${lastName}`.trim() || displayName;
|
||||||
|
const firstInitial = firstName?.trim()?.[0] || "";
|
||||||
|
const lastInitial = lastName?.trim()?.[0] || "";
|
||||||
|
const initials = (firstInitial + lastInitial).trim() || user?.email?.trim()?.[0] || "U";
|
||||||
|
|
||||||
function toggleDropdown() {
|
function toggleDropdown() {
|
||||||
setIsOpen(!isOpen);
|
setIsOpen(!isOpen);
|
||||||
}
|
}
|
||||||
@@ -34,14 +43,14 @@ export default function UserDropdown() {
|
|||||||
<span className="overflow-hidden rounded-full h-9 w-9 bg-brand-500 flex items-center justify-center flex-shrink-0">
|
<span className="overflow-hidden rounded-full h-9 w-9 bg-brand-500 flex items-center justify-center flex-shrink-0">
|
||||||
{user?.email ? (
|
{user?.email ? (
|
||||||
<span className="text-white font-semibold text-sm">
|
<span className="text-white font-semibold text-sm">
|
||||||
{user.email.charAt(0).toUpperCase()}
|
{initials.toUpperCase()}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<img src="/images/user/owner.jpg" alt="User" className="h-full w-full object-cover" />
|
<img src="/images/user/owner.jpg" alt="User" className="h-full w-full object-cover" />
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
<span className="font-medium text-sm whitespace-nowrap">
|
<span className="font-medium text-sm whitespace-nowrap">
|
||||||
{user?.username || user?.email?.split("@")[0] || "User"}
|
{displayName}
|
||||||
</span>
|
</span>
|
||||||
<svg
|
<svg
|
||||||
className={`w-4 h-4 text-gray-500 dark:text-gray-400 transition-transform duration-200 flex-shrink-0 ${
|
className={`w-4 h-4 text-gray-500 dark:text-gray-400 transition-transform duration-200 flex-shrink-0 ${
|
||||||
@@ -70,7 +79,7 @@ export default function UserDropdown() {
|
|||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<span className="block font-medium text-gray-700 text-theme-sm dark:text-gray-400">
|
<span className="block font-medium text-gray-700 text-theme-sm dark:text-gray-400">
|
||||||
{user?.username || "User"}
|
{displayFullName}
|
||||||
</span>
|
</span>
|
||||||
<span className="mt-0.5 block text-theme-xs text-gray-500 dark:text-gray-400">
|
<span className="mt-0.5 block text-theme-xs text-gray-500 dark:text-gray-400">
|
||||||
{user?.email || "No email"}
|
{user?.email || "No email"}
|
||||||
@@ -90,47 +99,30 @@ export default function UserDropdown() {
|
|||||||
to="/account/settings"
|
to="/account/settings"
|
||||||
className="flex items-center gap-3 px-3 py-2 font-medium text-gray-700 rounded-lg group text-theme-sm hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300"
|
className="flex items-center gap-3 px-3 py-2 font-medium text-gray-700 rounded-lg group text-theme-sm hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300"
|
||||||
>
|
>
|
||||||
<svg
|
<UserCircleIcon className="w-5 h-5 text-gray-500 group-hover:text-gray-700 dark:text-gray-400 dark:group-hover:text-gray-300" />
|
||||||
className="fill-gray-500 group-hover:fill-gray-700 dark:fill-gray-400 dark:group-hover:fill-gray-300"
|
Account Settings
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fillRule="evenodd"
|
|
||||||
clipRule="evenodd"
|
|
||||||
d="M12 3.5C7.30558 3.5 3.5 7.30558 3.5 12C3.5 14.1526 4.3002 16.1184 5.61936 17.616C6.17279 15.3096 8.24852 13.5955 10.7246 13.5955H13.2746C15.7509 13.5955 17.8268 15.31 18.38 17.6167C19.6996 16.119 20.5 14.153 20.5 12C20.5 7.30558 16.6944 3.5 12 3.5ZM17.0246 18.8566V18.8455C17.0246 16.7744 15.3457 15.0955 13.2746 15.0955H10.7246C8.65354 15.0955 6.97461 16.7744 6.97461 18.8455V18.856C8.38223 19.8895 10.1198 20.5 12 20.5C13.8798 20.5 15.6171 19.8898 17.0246 18.8566ZM2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12ZM11.9991 7.25C10.8847 7.25 9.98126 8.15342 9.98126 9.26784C9.98126 10.3823 10.8847 11.2857 11.9991 11.2857C13.1135 11.2857 14.0169 10.3823 14.0169 9.26784C14.0169 8.15342 13.1135 7.25 11.9991 7.25ZM8.48126 9.26784C8.48126 7.32499 10.0563 5.75 11.9991 5.75C13.9419 5.75 15.5169 7.32499 15.5169 9.26784C15.5169 11.2107 13.9419 12.7857 11.9991 12.7857C10.0563 12.7857 8.48126 11.2107 8.48126 9.26784Z"
|
|
||||||
fill=""
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
Edit profile
|
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
onItemClick={closeDropdown}
|
onItemClick={closeDropdown}
|
||||||
tag="a"
|
tag="a"
|
||||||
to="/account/settings"
|
to="/account/plans"
|
||||||
className="flex items-center gap-3 px-3 py-2 font-medium text-gray-700 rounded-lg group text-theme-sm hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300"
|
className="flex items-center gap-3 px-3 py-2 font-medium text-gray-700 rounded-lg group text-theme-sm hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300"
|
||||||
>
|
>
|
||||||
<svg
|
<DollarLineIcon className="w-5 h-5 text-gray-500 group-hover:text-gray-700 dark:text-gray-400 dark:group-hover:text-gray-300" />
|
||||||
className="fill-gray-500 group-hover:fill-gray-700 dark:fill-gray-400 dark:group-hover:fill-gray-300"
|
Billing
|
||||||
width="24"
|
</DropdownItem>
|
||||||
height="24"
|
</li>
|
||||||
viewBox="0 0 24 24"
|
<li>
|
||||||
fill="none"
|
<DropdownItem
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
onItemClick={closeDropdown}
|
||||||
|
tag="a"
|
||||||
|
to="/account/usage"
|
||||||
|
className="flex items-center gap-3 px-3 py-2 font-medium text-gray-700 rounded-lg group text-theme-sm hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300"
|
||||||
>
|
>
|
||||||
<path
|
<PieChartIcon className="w-5 h-5 text-gray-500 group-hover:text-gray-700 dark:text-gray-400 dark:group-hover:text-gray-300" />
|
||||||
fillRule="evenodd"
|
Usage
|
||||||
clipRule="evenodd"
|
|
||||||
d="M10.4858 3.5L13.5182 3.5C13.9233 3.5 14.2518 3.82851 14.2518 4.23377C14.2518 5.9529 16.1129 7.02795 17.602 6.1682C17.9528 5.96567 18.4014 6.08586 18.6039 6.43667L20.1203 9.0631C20.3229 9.41407 20.2027 9.86286 19.8517 10.0655C18.3625 10.9253 18.3625 13.0747 19.8517 13.9345C20.2026 14.1372 20.3229 14.5859 20.1203 14.9369L18.6039 17.5634C18.4013 17.9142 17.9528 18.0344 17.602 17.8318C16.1129 16.9721 14.2518 18.0471 14.2518 19.7663C14.2518 20.1715 13.9233 20.5 13.5182 20.5H10.4858C10.0804 20.5 9.75182 20.1714 9.75182 19.766C9.75182 18.0461 7.88983 16.9717 6.40067 17.8314C6.04945 18.0342 5.60037 17.9139 5.39767 17.5628L3.88167 14.937C3.67903 14.586 3.79928 14.1372 4.15026 13.9346C5.63949 13.0748 5.63946 10.9253 4.15025 10.0655C3.79926 9.86282 3.67901 9.41401 3.88165 9.06303L5.39764 6.43725C5.60034 6.08617 6.04943 5.96581 6.40065 6.16858C7.88982 7.02836 9.75182 5.9539 9.75182 4.23399C9.75182 3.82862 10.0804 3.5 10.4858 3.5ZM13.5182 2L10.4858 2C9.25201 2 8.25182 3.00019 8.25182 4.23399C8.25182 4.79884 7.64013 5.15215 7.15065 4.86955C6.08213 4.25263 4.71559 4.61859 4.0986 5.68725L2.58261 8.31303C1.96575 9.38146 2.33183 10.7477 3.40025 11.3645C3.88948 11.647 3.88947 12.3531 3.40026 12.6355C2.33184 13.2524 1.96578 14.6186 2.58263 15.687L4.09863 18.3128C4.71562 19.3814 6.08215 19.7474 7.15067 19.1305C7.64015 18.8479 8.25182 19.2012 8.25182 19.766C8.25182 20.9998 9.25201 22 10.4858 22H13.5182C14.7519 22 15.7518 20.9998 15.7518 19.7663C15.7518 19.2015 16.3632 18.8487 16.852 19.1309C17.9202 19.7476 19.2862 19.3816 19.9029 18.3134L21.4193 15.6869C22.0361 14.6185 21.6701 13.2523 20.6017 12.6355C20.1125 12.3531 20.1125 11.647 20.6017 11.3645C21.6701 10.7477 22.0362 9.38152 21.4193 8.3131L19.903 5.68667C19.2862 4.61842 17.9202 4.25241 16.852 4.86917C16.3632 5.15138 15.7518 4.79856 15.7518 4.23377C15.7518 3.00024 14.7519 2 13.5182 2ZM9.6659 11.9999C9.6659 10.7103 10.7113 9.66493 12.0009 9.66493C13.2905 9.66493 14.3359 10.7103 14.3359 11.9999C14.3359 13.2895 13.2905 14.3349 12.0009 14.3349C10.7113 14.3349 9.6659 13.2895 9.6659 11.9999ZM12.0009 8.16493C9.88289 8.16493 8.1659 9.88191 8.1659 11.9999C8.1659 14.1179 9.88289 15.8349 12.0009 15.8349C14.1189 15.8349 15.8359 14.1179 15.8359 11.9999C15.8359 9.88191 14.1189 8.16493 12.0009 8.16493Z"
|
|
||||||
fill=""
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
Account settings
|
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
@@ -140,46 +132,18 @@ export default function UserDropdown() {
|
|||||||
to="/help"
|
to="/help"
|
||||||
className="flex items-center gap-3 px-3 py-2 font-medium text-gray-700 rounded-lg group text-theme-sm hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300"
|
className="flex items-center gap-3 px-3 py-2 font-medium text-gray-700 rounded-lg group text-theme-sm hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300"
|
||||||
>
|
>
|
||||||
<svg
|
<HelpCircleIcon className="w-5 h-5 text-gray-500 group-hover:text-gray-700 dark:text-gray-400 dark:group-hover:text-gray-300" />
|
||||||
className="fill-gray-500 group-hover:fill-gray-700 dark:fill-gray-400 dark:group-hover:fill-gray-300"
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fillRule="evenodd"
|
|
||||||
clipRule="evenodd"
|
|
||||||
d="M3.5 12C3.5 7.30558 7.30558 3.5 12 3.5C16.6944 3.5 20.5 7.30558 20.5 12C20.5 16.6944 16.6944 20.5 12 20.5C7.30558 20.5 3.5 16.6944 3.5 12ZM12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2ZM11.0991 7.52507C11.0991 8.02213 11.5021 8.42507 11.9991 8.42507H12.0001C12.4972 8.42507 12.9001 8.02213 12.9001 7.52507C12.9001 7.02802 12.4972 6.62507 12.0001 6.62507H11.9991C11.5021 6.62507 11.0991 7.02802 11.0991 7.52507ZM12.0001 17.3714C11.5859 17.3714 11.2501 17.0356 11.2501 16.6214V10.9449C11.2501 10.5307 11.5859 10.1949 12.0001 10.1949C12.4143 10.1949 12.7501 10.5307 12.7501 10.9449V16.6214C12.7501 17.0356 12.4143 17.3714 12.0001 17.3714Z"
|
|
||||||
fill=""
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
Support
|
Support
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleLogout}
|
onClick={handleLogout}
|
||||||
variant="ghost"
|
variant="primary"
|
||||||
tone="neutral"
|
tone="brand"
|
||||||
className="flex items-center gap-3 px-3 py-2 mt-3 font-medium text-gray-700 rounded-lg group text-theme-sm hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300 w-full text-left"
|
endIcon={<ArrowRightIcon className="w-4 h-4" />}
|
||||||
|
className="mt-3 w-full justify-center bg-purple-600 hover:bg-purple-700 dark:bg-purple-500 dark:hover:bg-purple-600"
|
||||||
>
|
>
|
||||||
<svg
|
|
||||||
className="fill-gray-500 group-hover:fill-gray-700 dark:group-hover:fill-gray-300"
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fillRule="evenodd"
|
|
||||||
clipRule="evenodd"
|
|
||||||
d="M15.1007 19.247C14.6865 19.247 14.3507 18.9112 14.3507 18.497L14.3507 14.245H12.8507V18.497C12.8507 19.7396 13.8581 20.747 15.1007 20.747H18.5007C19.7434 20.747 20.7507 19.7396 20.7507 18.497L20.7507 5.49609C20.7507 4.25345 19.7433 3.24609 18.5007 3.24609H15.1007C13.8581 3.24609 12.8507 4.25345 12.8507 5.49609V9.74501L14.3507 9.74501V5.49609C14.3507 5.08188 14.6865 4.74609 15.1007 4.74609L18.5007 4.74609C18.9149 4.74609 19.2507 5.08188 19.2507 5.49609L19.2507 18.497C19.2507 18.9112 18.9149 19.247 18.5007 19.247H15.1007ZM3.25073 11.9984C3.25073 12.2144 3.34204 12.4091 3.48817 12.546L8.09483 17.1556C8.38763 17.4485 8.86251 17.4487 9.15549 17.1559C9.44848 16.8631 9.44863 16.3882 9.15583 16.0952L5.81116 12.7484L16.0007 12.7484C16.4149 12.7484 16.7507 12.4127 16.7507 11.9984C16.7507 11.5842 16.4149 11.2484 16.0007 11.2484L5.81528 11.2484L9.15585 7.90554C9.44864 7.61255 9.44847 7.13767 9.15547 6.84488C8.86248 6.55209 8.3876 6.55226 8.09481 6.84525L3.52309 11.4202C3.35673 11.5577 3.25073 11.7657 3.25073 11.9984Z"
|
|
||||||
fill=""
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
Sign out
|
Sign out
|
||||||
</Button>
|
</Button>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
|||||||
@@ -126,15 +126,16 @@ const AppHeader: React.FC = () => {
|
|||||||
<div className="hidden lg:flex items-center gap-3">
|
<div className="hidden lg:flex items-center gap-3">
|
||||||
{/* Sidebar Toggle Button - Always visible on desktop */}
|
{/* Sidebar Toggle Button - Always visible on desktop */}
|
||||||
<IconButton
|
<IconButton
|
||||||
variant="outline"
|
variant="ghost"
|
||||||
tone="neutral"
|
tone="neutral"
|
||||||
size="sm"
|
size="xs"
|
||||||
shape="circle"
|
shape="rounded"
|
||||||
onClick={toggleSidebar}
|
onClick={toggleSidebar}
|
||||||
aria-label={isExpanded ? "Collapse Sidebar" : "Expand Sidebar"}
|
aria-label={isExpanded ? "Collapse Sidebar" : "Expand Sidebar"}
|
||||||
|
className="ml-0 lg:-ml-8 rounded-md rounded-l-none border border-gray-300 border-l-0 dark:border-gray-700"
|
||||||
icon={
|
icon={
|
||||||
<svg
|
<svg
|
||||||
className={`w-3.5 h-3.5 transition-transform duration-300 ${isExpanded ? 'rotate-180' : ''}`}
|
className={`w-3 h-3 transition-transform duration-300 ${isExpanded ? 'rotate-180' : ''}`}
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
@@ -194,9 +195,10 @@ const AppHeader: React.FC = () => {
|
|||||||
|
|
||||||
{/* Search Icon */}
|
{/* Search Icon */}
|
||||||
<IconButton
|
<IconButton
|
||||||
variant="ghost"
|
variant="outline"
|
||||||
tone="neutral"
|
tone="neutral"
|
||||||
size="md"
|
size="md"
|
||||||
|
shape="circle"
|
||||||
onClick={() => setIsSearchOpen(true)}
|
onClick={() => setIsSearchOpen(true)}
|
||||||
title="Search (⌘K)"
|
title="Search (⌘K)"
|
||||||
aria-label="Search"
|
aria-label="Search"
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { Link, useLocation } from "react-router-dom";
|
import { Link, useLocation } from "react-router-dom";
|
||||||
import { Bell } from "lucide-react";
|
|
||||||
|
|
||||||
// Assume these icons are imported from an icon library
|
// Assume these icons are imported from an icon library
|
||||||
import {
|
import {
|
||||||
@@ -8,18 +7,12 @@ import {
|
|||||||
GridIcon,
|
GridIcon,
|
||||||
HorizontaLDots,
|
HorizontaLDots,
|
||||||
ListIcon,
|
ListIcon,
|
||||||
PieChartIcon,
|
|
||||||
PlugInIcon,
|
|
||||||
TaskIcon,
|
TaskIcon,
|
||||||
BoltIcon,
|
BoltIcon,
|
||||||
DocsIcon,
|
DocsIcon,
|
||||||
PageIcon,
|
|
||||||
DollarLineIcon,
|
|
||||||
FileIcon,
|
|
||||||
UserIcon,
|
|
||||||
UserCircleIcon,
|
|
||||||
ShootingStarIcon,
|
ShootingStarIcon,
|
||||||
CalendarIcon,
|
CalendarIcon,
|
||||||
|
TagIcon,
|
||||||
} from "../icons";
|
} from "../icons";
|
||||||
import { useSidebar } from "../context/SidebarContext";
|
import { useSidebar } from "../context/SidebarContext";
|
||||||
import { useAuthStore } from "../store/authStore";
|
import { useAuthStore } from "../store/authStore";
|
||||||
@@ -45,14 +38,12 @@ const AppSidebar: React.FC = () => {
|
|||||||
const { user, isAuthenticated } = useAuthStore();
|
const { user, isAuthenticated } = useAuthStore();
|
||||||
const { isModuleEnabled, settings: moduleSettings } = useModuleStore();
|
const { isModuleEnabled, settings: moduleSettings } = useModuleStore();
|
||||||
|
|
||||||
const [openSubmenu, setOpenSubmenu] = useState<{
|
const [openSubmenus, setOpenSubmenus] = useState<Set<string>>(new Set());
|
||||||
sectionIndex: number;
|
|
||||||
itemIndex: number;
|
|
||||||
} | null>(null);
|
|
||||||
const [subMenuHeight, setSubMenuHeight] = useState<Record<string, number>>(
|
const [subMenuHeight, setSubMenuHeight] = useState<Record<string, number>>(
|
||||||
{}
|
{}
|
||||||
);
|
);
|
||||||
const subMenuRefs = useRef<Record<string, HTMLDivElement | null>>({});
|
const subMenuRefs = useRef<Record<string, HTMLDivElement | null>>({});
|
||||||
|
const submenusInitialized = useRef(false);
|
||||||
|
|
||||||
// Check if a path is active - exact match only for menu items
|
// Check if a path is active - exact match only for menu items
|
||||||
// Prefix matching is only used for parent menus to determine if submenu should be open
|
// Prefix matching is only used for parent menus to determine if submenu should be open
|
||||||
@@ -89,7 +80,7 @@ const AppSidebar: React.FC = () => {
|
|||||||
|
|
||||||
// Setup Wizard at top - guides users through site setup
|
// Setup Wizard at top - guides users through site setup
|
||||||
setupItems.push({
|
setupItems.push({
|
||||||
icon: <ShootingStarIcon />,
|
icon: <BoltIcon />,
|
||||||
name: "Setup Wizard",
|
name: "Setup Wizard",
|
||||||
path: "/setup/wizard",
|
path: "/setup/wizard",
|
||||||
});
|
});
|
||||||
@@ -103,7 +94,7 @@ const AppSidebar: React.FC = () => {
|
|||||||
|
|
||||||
// Keywords Library - Browse and add curated keywords
|
// Keywords Library - Browse and add curated keywords
|
||||||
setupItems.push({
|
setupItems.push({
|
||||||
icon: <DocsIcon />,
|
icon: <TagIcon />,
|
||||||
name: "Keywords Library",
|
name: "Keywords Library",
|
||||||
path: "/keywords-library",
|
path: "/keywords-library",
|
||||||
});
|
});
|
||||||
@@ -166,7 +157,7 @@ const AppSidebar: React.FC = () => {
|
|||||||
// Add Automation if enabled (with dropdown)
|
// Add Automation if enabled (with dropdown)
|
||||||
if (isModuleEnabled('automation')) {
|
if (isModuleEnabled('automation')) {
|
||||||
workflowItems.push({
|
workflowItems.push({
|
||||||
icon: <BoltIcon />,
|
icon: <ShootingStarIcon />,
|
||||||
name: "Automation",
|
name: "Automation",
|
||||||
subItems: [
|
subItems: [
|
||||||
{ name: "Overview", path: "/automation/overview" },
|
{ name: "Overview", path: "/automation/overview" },
|
||||||
@@ -197,47 +188,6 @@ const AppSidebar: React.FC = () => {
|
|||||||
label: "WORKFLOW",
|
label: "WORKFLOW",
|
||||||
items: workflowItems,
|
items: workflowItems,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
label: "ACCOUNT",
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
icon: <UserCircleIcon />,
|
|
||||||
name: "Account Settings",
|
|
||||||
path: "/account/settings", // Single page, no sub-items
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: <DollarLineIcon />,
|
|
||||||
name: "Plans & Billing",
|
|
||||||
path: "/account/plans",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: <PieChartIcon />,
|
|
||||||
name: "Usage",
|
|
||||||
path: "/account/usage",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: <PlugInIcon />,
|
|
||||||
name: "AI Models",
|
|
||||||
path: "/settings/integration",
|
|
||||||
adminOnly: true, // Only visible to admin/staff users
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "HELP",
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
icon: <Bell className="w-5 h-5" />,
|
|
||||||
name: "Notifications",
|
|
||||||
path: "/account/notifications",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: <DocsIcon />,
|
|
||||||
name: "Help & Docs",
|
|
||||||
path: "/help",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
}, [isModuleEnabled, moduleSettings]); // Re-run when settings change
|
}, [isModuleEnabled, moduleSettings]); // Re-run when settings change
|
||||||
|
|
||||||
@@ -247,50 +197,46 @@ const AppSidebar: React.FC = () => {
|
|||||||
}, [menuSections]);
|
}, [menuSections]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const currentPath = location.pathname;
|
if (submenusInitialized.current) return;
|
||||||
let foundMatch = false;
|
|
||||||
|
|
||||||
// Find the matching submenu for the current path - use exact match only for subitems
|
const initialKeys = new Set<string>();
|
||||||
allSections.forEach((section, sectionIndex) => {
|
allSections.forEach((section, sectionIndex) => {
|
||||||
section.items.forEach((nav, itemIndex) => {
|
section.items.forEach((nav, itemIndex) => {
|
||||||
if (nav.subItems && !foundMatch) {
|
if (nav.subItems) {
|
||||||
// Only use exact match for submenu items to prevent multiple active states
|
initialKeys.add(`${sectionIndex}-${itemIndex}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
setOpenSubmenus(initialKeys);
|
||||||
|
submenusInitialized.current = true;
|
||||||
|
}, [allSections]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const currentPath = location.pathname;
|
||||||
|
|
||||||
|
allSections.forEach((section, sectionIndex) => {
|
||||||
|
section.items.forEach((nav, itemIndex) => {
|
||||||
|
if (nav.subItems) {
|
||||||
const shouldOpen = nav.subItems.some((subItem) => currentPath === subItem.path);
|
const shouldOpen = nav.subItems.some((subItem) => currentPath === subItem.path);
|
||||||
|
|
||||||
if (shouldOpen) {
|
if (shouldOpen) {
|
||||||
setOpenSubmenu((prev) => {
|
const key = `${sectionIndex}-${itemIndex}`;
|
||||||
// Only update if different to prevent infinite loops
|
setOpenSubmenus((prev) => new Set([...prev, key]));
|
||||||
if (prev?.sectionIndex === sectionIndex && prev?.itemIndex === itemIndex) {
|
|
||||||
return prev;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
sectionIndex,
|
|
||||||
itemIndex,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
foundMatch = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// If no match found and we're not on a submenu path, don't change the state
|
|
||||||
// This allows manual toggles to persist
|
|
||||||
}, [location.pathname, allSections]);
|
}, [location.pathname, allSections]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (openSubmenu !== null) {
|
|
||||||
const key = `${openSubmenu.sectionIndex}-${openSubmenu.itemIndex}`;
|
|
||||||
// Use requestAnimationFrame and setTimeout to ensure DOM is ready
|
|
||||||
const frameId = requestAnimationFrame(() => {
|
const frameId = requestAnimationFrame(() => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
openSubmenus.forEach((key) => {
|
||||||
const element = subMenuRefs.current[key];
|
const element = subMenuRefs.current[key];
|
||||||
if (element) {
|
if (element) {
|
||||||
// scrollHeight should work even when height is 0px due to overflow-hidden
|
|
||||||
const scrollHeight = element.scrollHeight;
|
const scrollHeight = element.scrollHeight;
|
||||||
if (scrollHeight > 0) {
|
if (scrollHeight > 0) {
|
||||||
setSubMenuHeight((prevHeights) => {
|
setSubMenuHeight((prevHeights) => {
|
||||||
// Only update if height changed to prevent infinite loops
|
|
||||||
if (prevHeights[key] === scrollHeight) {
|
if (prevHeights[key] === scrollHeight) {
|
||||||
return prevHeights;
|
return prevHeights;
|
||||||
}
|
}
|
||||||
@@ -301,22 +247,22 @@ const AppSidebar: React.FC = () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}, 50);
|
}, 50);
|
||||||
});
|
});
|
||||||
return () => cancelAnimationFrame(frameId);
|
return () => cancelAnimationFrame(frameId);
|
||||||
}
|
}, [openSubmenus]);
|
||||||
}, [openSubmenu]);
|
|
||||||
|
|
||||||
const handleSubmenuToggle = (sectionIndex: number, itemIndex: number) => {
|
const handleSubmenuToggle = (sectionIndex: number, itemIndex: number) => {
|
||||||
setOpenSubmenu((prevOpenSubmenu) => {
|
const key = `${sectionIndex}-${itemIndex}`;
|
||||||
if (
|
setOpenSubmenus((prev) => {
|
||||||
prevOpenSubmenu &&
|
const next = new Set(prev);
|
||||||
prevOpenSubmenu.sectionIndex === sectionIndex &&
|
if (next.has(key)) {
|
||||||
prevOpenSubmenu.itemIndex === itemIndex
|
next.delete(key);
|
||||||
) {
|
} else {
|
||||||
return null;
|
next.add(key);
|
||||||
}
|
}
|
||||||
return { sectionIndex, itemIndex };
|
return next;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -338,7 +284,7 @@ const AppSidebar: React.FC = () => {
|
|||||||
.map((nav, itemIndex) => {
|
.map((nav, itemIndex) => {
|
||||||
// Check if any subitem is active to determine parent active state (uses exact match for subitems)
|
// Check if any subitem is active to determine parent active state (uses exact match for subitems)
|
||||||
const hasActiveSubItem = nav.subItems?.some(subItem => isSubItemActive(subItem.path)) ?? false;
|
const hasActiveSubItem = nav.subItems?.some(subItem => isSubItemActive(subItem.path)) ?? false;
|
||||||
const isSubmenuOpen = openSubmenu?.sectionIndex === sectionIndex && openSubmenu?.itemIndex === itemIndex;
|
const isSubmenuOpen = openSubmenus.has(`${sectionIndex}-${itemIndex}`);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li key={`${sectionIndex}-${nav.name}-${location.pathname}`}>
|
<li key={`${sectionIndex}-${nav.name}-${location.pathname}`}>
|
||||||
@@ -476,7 +422,7 @@ const AppSidebar: React.FC = () => {
|
|||||||
onMouseLeave={() => setIsHovered(false)}
|
onMouseLeave={() => setIsHovered(false)}
|
||||||
>
|
>
|
||||||
<div className="py-4 flex justify-center items-center">
|
<div className="py-4 flex justify-center items-center">
|
||||||
<Link to="/" className="flex justify-center items-center">
|
<Link to="/" className="flex flex-col items-center">
|
||||||
{isExpanded || isHovered || isMobileOpen ? (
|
{isExpanded || isHovered || isMobileOpen ? (
|
||||||
<>
|
<>
|
||||||
<img
|
<img
|
||||||
@@ -493,6 +439,13 @@ const AppSidebar: React.FC = () => {
|
|||||||
width={113}
|
width={113}
|
||||||
height={30}
|
height={30}
|
||||||
/>
|
/>
|
||||||
|
<div
|
||||||
|
className="mt-2 h-6 w-[163px] rounded-full"
|
||||||
|
style={{
|
||||||
|
background: "radial-gradient(ellipse at center, rgba(59,130,246,0.35) 0%, rgba(59,130,246,0) 70%)",
|
||||||
|
filter: "blur(2px)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<img
|
<img
|
||||||
@@ -504,7 +457,7 @@ const AppSidebar: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col overflow-y-auto duration-300 ease-linear no-scrollbar mt-[50px]">
|
<div className="flex flex-col overflow-y-auto duration-300 ease-linear no-scrollbar mt-[25px]">
|
||||||
<nav>
|
<nav>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
{allSections.map((section, sectionIndex) => (
|
{allSections.map((section, sectionIndex) => (
|
||||||
|
|||||||
@@ -163,6 +163,19 @@ export const useAuthStore = create<AuthState>()(
|
|||||||
// Check if there's already a logout reason from automatic logout
|
// Check if there's already a logout reason from automatic logout
|
||||||
const existingReason = localStorage.getItem('logout_reason');
|
const existingReason = localStorage.getItem('logout_reason');
|
||||||
|
|
||||||
|
// Fire-and-forget backend logout to clear HttpOnly session cookie
|
||||||
|
try {
|
||||||
|
const API_BASE_URL = import.meta.env.VITE_BACKEND_URL || 'https://api.igny8.com/api';
|
||||||
|
fetch(`${API_BASE_URL}/v1/auth/logout/`, {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'include',
|
||||||
|
}).catch(() => {
|
||||||
|
// Ignore network errors - client logout still proceeds
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to call backend logout:', e);
|
||||||
|
}
|
||||||
|
|
||||||
if (!existingReason) {
|
if (!existingReason) {
|
||||||
// Only store manual logout reason if no automatic reason exists
|
// Only store manual logout reason if no automatic reason exists
|
||||||
const currentPath = typeof window !== 'undefined' ? window.location.pathname : 'unknown';
|
const currentPath = typeof window !== 'undefined' ? window.location.pathname : 'unknown';
|
||||||
|
|||||||
@@ -75,7 +75,7 @@
|
|||||||
--color-warning: #F59E0B;
|
--color-warning: #F59E0B;
|
||||||
--color-danger: #DC2626;
|
--color-danger: #DC2626;
|
||||||
--color-purple: #F63B82;
|
--color-purple: #F63B82;
|
||||||
--color-gray-base: #031D48;
|
--color-gray-base: #30425d;
|
||||||
|
|
||||||
|
|
||||||
/* ===================================================================
|
/* ===================================================================
|
||||||
@@ -161,14 +161,14 @@
|
|||||||
=================================================================== */
|
=================================================================== */
|
||||||
.dark {
|
.dark {
|
||||||
/* Backgrounds - derived from gray-base */
|
/* Backgrounds - derived from gray-base */
|
||||||
--color-surface: color-mix(in srgb, var(--color-gray-base) 20%, black);
|
--color-surface: color-mix(in srgb, var(--color-gray-base) 26%, black);
|
||||||
--color-panel: color-mix(in srgb, var(--color-gray-base) 30%, black);
|
--color-panel: color-mix(in srgb, var(--color-gray-base) 40%, black);
|
||||||
--color-panel-alt: color-mix(in srgb, var(--color-gray-base) 15%, black);
|
--color-panel-alt: color-mix(in srgb, var(--color-gray-base) 28%, black);
|
||||||
|
|
||||||
/* Text - derived from gray-base */
|
/* Text - derived from gray-base */
|
||||||
--color-text: color-mix(in srgb, var(--color-gray-base) 10%, white);
|
--color-text: color-mix(in srgb, var(--color-gray-base) 10%, white);
|
||||||
--color-text-dim: color-mix(in srgb, var(--color-gray-base) 40%, white);
|
--color-text-dim: color-mix(in srgb, var(--color-gray-base) 40%, white);
|
||||||
--color-stroke: color-mix(in srgb, var(--color-gray-base) 50%, black);
|
--color-stroke: color-mix(in srgb, var(--color-gray-base) 62%, black);
|
||||||
|
|
||||||
/* Shadows for dark mode */
|
/* Shadows for dark mode */
|
||||||
--shadow-xs: 0px 1px 2px 0px rgba(0, 0, 0, 0.3);
|
--shadow-xs: 0px 1px 2px 0px rgba(0, 0, 0, 0.3);
|
||||||
@@ -541,11 +541,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@utility menu-item-active {
|
@utility menu-item-active {
|
||||||
@apply bg-brand-50 text-brand-500 dark:bg-brand-500/[0.12] dark:text-brand-400;
|
@apply bg-brand-50 text-brand-600 dark:bg-brand-500/[0.18] dark:text-brand-300;
|
||||||
}
|
}
|
||||||
|
|
||||||
@utility menu-item-inactive {
|
@utility menu-item-inactive {
|
||||||
@apply text-gray-700 hover:bg-gray-100 group-hover:text-gray-700 dark:text-gray-300 dark:hover:bg-white/5 dark:hover:text-gray-300;
|
@apply text-gray-600 hover:bg-gray-100 group-hover:text-gray-800 dark:text-gray-300 dark:hover:bg-gray-800/70 dark:hover:text-white/90;
|
||||||
}
|
}
|
||||||
|
|
||||||
@utility menu-item-icon-size {
|
@utility menu-item-icon-size {
|
||||||
@@ -554,11 +554,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@utility menu-item-icon-active {
|
@utility menu-item-icon-active {
|
||||||
@apply text-brand-500;
|
@apply text-brand-600 dark:text-brand-300;
|
||||||
}
|
}
|
||||||
|
|
||||||
@utility menu-item-icon-inactive {
|
@utility menu-item-icon-inactive {
|
||||||
@apply text-gray-500 group-hover:text-gray-700;
|
@apply text-gray-400 group-hover:text-gray-700 dark:text-gray-300 dark:group-hover:text-white/90;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -----------------------------------------------------------------
|
/* -----------------------------------------------------------------
|
||||||
@@ -570,11 +570,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@utility menu-dropdown-item-active {
|
@utility menu-dropdown-item-active {
|
||||||
@apply text-brand-600 bg-brand-50;
|
@apply text-brand-600 bg-brand-50 dark:text-brand-300 dark:bg-brand-500/[0.14];
|
||||||
}
|
}
|
||||||
|
|
||||||
@utility menu-dropdown-item-inactive {
|
@utility menu-dropdown-item-inactive {
|
||||||
@apply text-gray-600 hover:text-gray-900 hover:bg-gray-50;
|
@apply text-gray-600 hover:text-gray-900 hover:bg-gray-50 dark:text-gray-300 dark:hover:text-white/90 dark:hover:bg-gray-800/70;
|
||||||
}
|
}
|
||||||
|
|
||||||
@utility menu-dropdown-badge {
|
@utility menu-dropdown-badge {
|
||||||
@@ -582,11 +582,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@utility menu-dropdown-badge-active {
|
@utility menu-dropdown-badge-active {
|
||||||
@apply bg-brand-100 text-brand-700;
|
@apply bg-brand-100 text-brand-700 dark:bg-brand-500/[0.18] dark:text-brand-300;
|
||||||
}
|
}
|
||||||
|
|
||||||
@utility menu-dropdown-badge-inactive {
|
@utility menu-dropdown-badge-inactive {
|
||||||
@apply bg-gray-100 text-gray-600;
|
@apply bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-300;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -600,15 +600,88 @@ select.igny8-select-styled {
|
|||||||
background-repeat: no-repeat !important;
|
background-repeat: no-repeat !important;
|
||||||
background-position: right 12px center !important;
|
background-position: right 12px center !important;
|
||||||
padding-right: 36px !important;
|
padding-right: 36px !important;
|
||||||
|
border: 1px solid var(--color-stroke) !important;
|
||||||
|
background-color: var(--color-panel) !important;
|
||||||
|
color: var(--color-text) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark select.igny8-select-styled {
|
.dark select.igny8-select-styled {
|
||||||
background-image: url("data:image/svg+xml,%3Csvg width='12' height='8' viewBox='0 0 12 8' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1L6 6L11 1' stroke='%2398A2B3' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") !important;
|
background-image: url("data:image/svg+xml,%3Csvg width='12' height='8' viewBox='0 0 12 8' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1L6 6L11 1' stroke='%2398A2B3' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") !important;
|
||||||
|
background-color: var(--color-panel) !important;
|
||||||
|
border-color: var(--color-stroke) !important;
|
||||||
|
color: var(--color-text) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* For button-based selects (SelectDropdown), icon is rendered by component */
|
/* For button-based selects (SelectDropdown), icon is rendered by component */
|
||||||
button.igny8-select-styled {
|
button.igny8-select-styled {
|
||||||
/* No background-image - ChevronDownIcon is rendered inside the component */
|
/* No background-image - ChevronDownIcon is rendered inside the component */
|
||||||
|
border: 1px solid var(--color-stroke);
|
||||||
|
background-color: var(--color-panel);
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark button.igny8-select-styled {
|
||||||
|
border-color: var(--color-stroke);
|
||||||
|
background-color: var(--color-panel);
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Global form surface refinement */
|
||||||
|
input:not([type="checkbox"]):not([type="radio"]),
|
||||||
|
textarea,
|
||||||
|
select {
|
||||||
|
border-color: var(--color-stroke);
|
||||||
|
background-color: var(--color-panel);
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark input:not([type="checkbox"]):not([type="radio"]),
|
||||||
|
.dark textarea,
|
||||||
|
.dark select {
|
||||||
|
border-color: var(--color-stroke);
|
||||||
|
background-color: var(--color-panel);
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus,
|
||||||
|
textarea:focus,
|
||||||
|
select:focus {
|
||||||
|
border-color: var(--color-primary);
|
||||||
|
box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary) 20%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode surface boosts for cards and panels */
|
||||||
|
.dark .dark\:bg-white\/\[0\.03\] {
|
||||||
|
background-color: color-mix(in srgb, var(--color-panel) 92%, transparent) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .dark\:bg-white\/\[0\.04\] {
|
||||||
|
background-color: color-mix(in srgb, var(--color-panel) 95%, transparent) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .dark\:bg-white\/\[0\.08\] {
|
||||||
|
background-color: color-mix(in srgb, var(--color-panel) 88%, transparent) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .dark\:bg-gray-800\/50,
|
||||||
|
.dark .dark\:bg-gray-800\/40,
|
||||||
|
.dark .dark\:bg-gray-800\/30,
|
||||||
|
.dark .dark\:bg-gray-900\/50 {
|
||||||
|
background-color: color-mix(in srgb, var(--color-panel) 75%, transparent) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .dark\:border-gray-800,
|
||||||
|
.dark .dark\:border-gray-700,
|
||||||
|
.dark .dark\:border-white\/10,
|
||||||
|
.dark .dark\:border-white\/\[0\.08\],
|
||||||
|
.dark .dark\:border-white\/\[0\.04\] {
|
||||||
|
border-color: var(--color-stroke) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar boundary clarity */
|
||||||
|
.dark aside.border-r {
|
||||||
|
border-right-color: var(--color-stroke) !important;
|
||||||
|
box-shadow: inset -1px 0 0 color-mix(in srgb, var(--color-gray-base) 55%, black);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Checkbox styling */
|
/* Checkbox styling */
|
||||||
@@ -655,6 +728,15 @@ button.igny8-select-styled {
|
|||||||
border-bottom-color: var(--color-stroke) !important;
|
border-bottom-color: var(--color-stroke) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Table row hover (subtle) */
|
||||||
|
.igny8-table-compact tbody tr:hover td {
|
||||||
|
background-color: color-mix(in srgb, var(--color-primary) 4%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .igny8-table-compact tbody tr:hover td {
|
||||||
|
background-color: color-mix(in srgb, var(--color-primary) 8%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
.igny8-table-compact th.text-center,
|
.igny8-table-compact th.text-center,
|
||||||
.igny8-table-compact td.text-center {
|
.igny8-table-compact td.text-center {
|
||||||
text-align: center !important;
|
text-align: center !important;
|
||||||
|
|||||||
Reference in New Issue
Block a user