Replaces TSX/Babel compilation pipeline with a JSON element model:
- New _do_parse_v2(): 1 LLM call/layout (vs 2) classifies OXML geometry
elements into placeholder types → JSON stored in layout_code
- SlideRenderer.tsx: renders JSON element model as %-positioned divs,
no Babel compilation or runtime errors
- parseLayoutSchema.ts: isJsonLayoutCode() / parseLayoutSchema() /
mergeElementsWithContent() — full JSON schema parsing layer
- useCustomTemplates.ts: transparent dual-format support (JSON + TSX)
via parsedLayoutToCompiled() adapter
Template management improvements:
- PresentationLayoutCodeModel: +is_enabled (bool) +thumbnail_path (str)
- Migration 005: adds both columns to presentation_layout_codes
- DELETE /master-decks/{id}: hard delete (files + TemplateModel +
PresentationLayoutCodeModel rows + MasterDeckModel)
- PATCH /template-management/layouts/{db_id}/toggle-enabled: new endpoint
- LayoutData response: +db_id, +is_enabled, +thumbnail_path
- _register_as_template(): stores thumbnail_path + is_enabled per layout
Admin UI:
- /admin/templates/ — list all custom templates with delete
- /admin/templates/[id]/ — layout grid with screenshots + enable/disable
- AdminSidebar: Templates nav item
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
96 lines
3.5 KiB
TypeScript
96 lines
3.5 KiB
TypeScript
'use client';
|
|
|
|
import Link from 'next/link';
|
|
import { usePathname, useRouter } from 'next/navigation';
|
|
import { useSelector, useDispatch } from 'react-redux';
|
|
import { RootState, AppDispatch } from '@/store/store';
|
|
import { logoutUser } from '@/store/slices/authSlice';
|
|
import { Separator } from '@/components/ui/separator';
|
|
import {
|
|
Users,
|
|
Building2,
|
|
HardDrive,
|
|
FileText,
|
|
BarChart3,
|
|
Settings,
|
|
ArrowLeft,
|
|
LogOut,
|
|
LayoutTemplate,
|
|
} from 'lucide-react';
|
|
|
|
interface NavItem {
|
|
label: string;
|
|
href: string;
|
|
icon: React.ReactNode;
|
|
roles: string[];
|
|
}
|
|
|
|
const NAV_ITEMS: NavItem[] = [
|
|
{ label: 'Users', href: '/admin/users', icon: <Users className="w-4 h-4" />, roles: ['super_admin'] },
|
|
{ label: 'Clients', href: '/admin/clients', icon: <Building2 className="w-4 h-4" />, roles: ['super_admin', 'client_admin'] },
|
|
{ label: 'Templates', href: '/admin/templates', icon: <LayoutTemplate className="w-4 h-4" />, roles: ['super_admin', 'client_admin'] },
|
|
{ label: 'Storage', href: '/admin/storage', icon: <HardDrive className="w-4 h-4" />, roles: ['super_admin', 'client_admin'] },
|
|
{ label: 'Audit Log', href: '/admin/audit', icon: <FileText className="w-4 h-4" />, roles: ['super_admin', 'client_admin'] },
|
|
{ label: 'Analytics', href: '/admin/analytics', icon: <BarChart3 className="w-4 h-4" />, roles: ['super_admin', 'client_admin'] },
|
|
{ label: 'Settings', href: '/admin/settings', icon: <Settings className="w-4 h-4" />, roles: ['super_admin'] },
|
|
];
|
|
|
|
export default function AdminSidebar() {
|
|
const pathname = usePathname();
|
|
const router = useRouter();
|
|
const dispatch = useDispatch<AppDispatch>();
|
|
const user = useSelector((state: RootState) => state.auth.user);
|
|
const role = user?.role || 'user';
|
|
|
|
const handleLogout = async () => {
|
|
await dispatch(logoutUser());
|
|
router.push('/login');
|
|
};
|
|
|
|
const visibleItems = NAV_ITEMS.filter((item) => item.roles.includes(role));
|
|
|
|
return (
|
|
<aside className="w-60 min-h-screen bg-white border-r border-gray-200 flex flex-col">
|
|
<div className="p-4">
|
|
<Link href="/dashboard" className="flex items-center gap-2 text-sm text-gray-500 hover:text-gray-700 mb-4">
|
|
<ArrowLeft className="w-4 h-4" />
|
|
Back to Dashboard
|
|
</Link>
|
|
<h2 className="text-lg font-semibold text-gray-900 font-inter">Admin Panel</h2>
|
|
<p className="text-xs text-gray-500 mt-1">Manage your organization</p>
|
|
</div>
|
|
<Separator />
|
|
<nav className="flex-1 p-2">
|
|
{visibleItems.map((item) => {
|
|
const isActive = pathname === item.href || pathname.startsWith(item.href + '/');
|
|
return (
|
|
<Link
|
|
key={item.href + item.label}
|
|
href={item.href}
|
|
className={`flex items-center gap-3 px-3 py-2 rounded-md text-sm font-medium transition-colors ${
|
|
isActive
|
|
? 'bg-[#5146E5]/10 text-[#5146E5]'
|
|
: 'text-gray-700 hover:bg-gray-100'
|
|
}`}
|
|
>
|
|
{item.icon}
|
|
{item.label}
|
|
</Link>
|
|
);
|
|
})}
|
|
</nav>
|
|
<div className="p-4 border-t border-gray-200">
|
|
<div className="text-xs text-gray-400 mb-2">
|
|
Signed in as <span className="font-medium text-gray-600">{user?.email}</span>
|
|
</div>
|
|
<button
|
|
onClick={handleLogout}
|
|
className="flex items-center gap-2 text-xs text-gray-500 hover:text-red-600 transition-colors"
|
|
>
|
|
<LogOut className="w-3.5 h-3.5" />
|
|
Sign out
|
|
</button>
|
|
</div>
|
|
</aside>
|
|
);
|
|
}
|