even more ui shit

This commit is contained in:
lovebird 2026-02-19 21:05:31 +01:00
parent f98396f652
commit 2cf5af613d
7 changed files with 239 additions and 16 deletions

View File

@ -128,8 +128,6 @@ const GlobalDragDrop = () => {
return () => window.removeEventListener('drop', handleDrop);
}, [navigate]);
console.log('isDragging', isDragging)
console.log('isLocalZoneActive', isLocalZoneActive)
if (!isDragging || isLocalZoneActive) return null;

View File

@ -0,0 +1,136 @@
import { useState, useEffect } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { Database } from "@/integrations/supabase/types";
import { T, translate } from "@/i18n";
import { toast } from "sonner";
import { Check, Trash2, Globe, Lock, Play } from "lucide-react";
type Layout = Database['public']['Tables']['layouts']['Row'];
interface LayoutPropertyPanelProps {
layout: Layout;
onRename: (newName: string) => Promise<void>;
onDelete: () => void;
onApply: () => void;
onToggleVisibility: () => void;
isLoading?: boolean;
}
export const LayoutPropertyPanel = ({
layout,
onRename,
onDelete,
onApply,
onToggleVisibility,
isLoading = false
}: LayoutPropertyPanelProps) => {
const [name, setName] = useState(layout.name);
const [isRenaming, setIsRenaming] = useState(false);
useEffect(() => {
setName(layout.name);
}, [layout.id, layout.name]);
const handleRename = async () => {
if (name.trim() === layout.name) return;
setIsRenaming(true);
try {
await onRename(name);
} finally {
setIsRenaming(false);
}
};
return (
<div className="flex flex-col h-full bg-background">
<div className="flex items-center justify-between px-4 py-2 border-b">
<h3 className="text-sm font-semibold"><T>Layout Properties</T></h3>
</div>
<div className="flex-1 overflow-y-auto p-4 space-y-6">
{/* Name Section */}
<div className="space-y-2">
<Label htmlFor="layout-name"><T>Name</T></Label>
<div className="flex gap-2">
<Input
id="layout-name"
value={name}
onChange={(e) => setName(e.target.value)}
onBlur={handleRename}
onKeyDown={(e) => e.key === 'Enter' && handleRename()}
disabled={isLoading || isRenaming}
/>
</div>
</div>
{/* Info Section */}
<div className="space-y-4 pt-4 border-t">
<div className="grid grid-cols-2 gap-2 text-sm">
<span className="text-muted-foreground"><T>Type</T></span>
<span className="font-medium">{layout.type}</span>
<span className="text-muted-foreground"><T>Created</T></span>
<span className="font-medium">
{new Date(layout.created_at).toLocaleDateString()}
</span>
</div>
</div>
{/* Visibility Section */}
<div className="space-y-4 pt-4 border-t">
<Label><T>Visibility</T></Label>
<div className="flex items-center justify-between p-3 border rounded-lg bg-muted/20">
<div className="flex items-center gap-2">
{layout.visibility === 'public' ? (
<Globe className="h-4 w-4 text-green-500" />
) : (
<Lock className="h-4 w-4 text-amber-500" />
)}
<span className="text-sm font-medium capitalize">
<T>{layout.visibility}</T>
</span>
</div>
<Button
variant="outline"
size="sm"
onClick={onToggleVisibility}
disabled={isLoading}
>
{layout.visibility === 'public' ? <T>Make Private</T> : <T>Make Public</T>}
</Button>
</div>
<p className="text-xs text-muted-foreground">
{layout.visibility === 'public'
? <T>Public layouts effectively become templates that are visible to everyone.</T>
: <T>Private layouts are only visible to you.</T>}
</p>
</div>
</div>
{/* Actions Footer */}
<div className="p-4 border-t bg-muted/10 space-y-3">
<Button
className="w-full gap-2"
onClick={onApply}
disabled={isLoading}
>
<Play className="h-4 w-4" />
<T>Apply Layout</T>
</Button>
<Button
variant="destructive"
className="w-full gap-2"
onClick={onDelete}
disabled={isLoading}
>
<Trash2 className="h-4 w-4" />
<T>Delete Layout</T>
</Button>
</div>
</div>
);
};

View File

@ -4,10 +4,8 @@ import { fetchWithDeduplication } from "@/lib/db";
export const fetchUserPage = async (userId: string, slug: string) => {
const key = `user-page-${userId}-${slug}`;
return fetchWithDeduplication(key, async () => {
console.log('Fetching user page for user:', userId, 'slug:', slug);
const { data: sessionData } = await defaultSupabase.auth.getSession();
const token = sessionData.session?.access_token;
const headers: HeadersInit = {};
if (token) headers['Authorization'] = `Bearer ${token}`;

View File

@ -5,6 +5,7 @@ import { Page } from "../types";
const WidgetPropertyPanel = lazy(() => import("@/components/widgets/WidgetPropertyPanel").then(module => ({ default: module.WidgetPropertyPanel })));
const ContainerPropertyPanel = lazy(() => import("@/components/containers/ContainerPropertyPanel").then(module => ({ default: module.ContainerPropertyPanel })));
const UserPageTypeFields = lazy(() => import("./UserPageTypeFields").then(module => ({ default: module.UserPageTypeFields })));
import { LayoutPropertyPanel } from "@/components/layouts/LayoutPropertyPanel";
interface EditorRightPanelProps {
selectedWidgetId: string | null;
@ -16,6 +17,11 @@ interface EditorRightPanelProps {
onPageUpdate: (updatedPage: Page) => void;
contextVariables?: Record<string, any>;
setSelectedWidgetId: (id: string | null) => void;
selectedLayout: any | null;
onRenameLayout: (template: any, newName: string) => Promise<void>;
onDeleteLayout: (template: any) => void;
onApplyLayout: (template: any) => void;
onToggleLayoutVisibility: (template: any) => void;
}
export const EditorRightPanel = ({
@ -28,15 +34,28 @@ export const EditorRightPanel = ({
onPageUpdate,
contextVariables,
setSelectedWidgetId,
selectedLayout,
onRenameLayout,
onDeleteLayout,
onApplyLayout,
onToggleLayoutVisibility
}: EditorRightPanelProps) => {
if (!selectedWidgetId && !selectedContainerId && !showTypeFields) return null;
if (!selectedWidgetId && !selectedContainerId && !showTypeFields && !selectedLayout) return null;
return (
<>
<ResizableHandle withHandle />
<ResizablePanel defaultSize={25} minSize={20} maxSize={50} order={2} id="user-page-props">
<div className="h-full flex flex-col shrink-0 transition-all duration-300 overflow-hidden bg-background">
{selectedWidgetId ? (
{selectedLayout ? (
<LayoutPropertyPanel
layout={selectedLayout}
onRename={(name) => onRenameLayout(selectedLayout, name)}
onDelete={() => onDeleteLayout(selectedLayout)}
onApply={() => onApplyLayout(selectedLayout)}
onToggleVisibility={() => onToggleLayoutVisibility(selectedLayout)}
/>
) : selectedWidgetId ? (
<Suspense fallback={<div className="h-full flex items-center justify-center text-muted-foreground">Loading settings...</div>}>
<WidgetPropertyPanel
pageId={selectedPageId || `page-${page.id}`}

View File

@ -110,6 +110,7 @@ const UserPageEditInner = ({
const [settingsDialogOpen, setSettingsDialogOpen] = useState(false);
const [settingsWidgetId, setSettingsWidgetId] = useState<string | null>(null);
const [settingsLayoutId, setSettingsLayoutId] = useState<string | null>(null);
const [selectedLayoutId, setSelectedLayoutId] = useState<string | null>(null);
// Confirmation dialog state
const [confirmationState, setConfirmationState] = useState<{
@ -148,6 +149,7 @@ const UserPageEditInner = ({
getLoadedPageLayout,
importPageLayout: useLayout().importPageLayout,
setConfirmation: setConfirmationState,
currentLayout: loadedPages.get(pageId) || null,
});
const emailActions = useEmailActions({
@ -224,6 +226,14 @@ const UserPageEditInner = ({
}
};
const handleSelectTemplate = (template: any) => {
setSelectedWidgetId(null);
setSelectedContainerId(null);
setShowTypeFields(false);
setSelectedLayoutId(template.id);
setIsSidebarCollapsed(false);
};
// --- Widget handlers ---
const handleWidgetClick = async (widgetId: string, initialProps?: Record<string, any>) => {
await handleAddWidget(widgetId, initialProps);
@ -439,8 +449,9 @@ const UserPageEditInner = ({
onDelete={handleDeletePage}
onMetaUpdated={() => { }}
templates={templateManager.templates}
onLoadTemplate={templateManager.handleLoadTemplate}
onSelectTemplate={handleSelectTemplate}
onDeleteTemplate={templateManager.handleDeleteTemplate}
onToggleVisibility={templateManager.handleToggleVisibility}
onToggleWidget={handleWidgetClick}
onAddContainer={handleAddContainer}
onUndo={undo}
@ -450,6 +461,7 @@ const UserPageEditInner = ({
onExportLayout={layoutIO.handleExportLayout}
onImportLayout={layoutIO.handleImportLayout}
activeTemplateId={templateManager.activeTemplateId || undefined}
selectedLayoutId={selectedLayoutId}
onSaveToTemplate={templateManager.handleSaveToTemplate}
onSaveAsNewTemplate={templateManager.handleSaveAsNewTemplate}
onNewLayout={templateManager.handleNewLayout}
@ -613,6 +625,11 @@ const UserPageEditInner = ({
onPageUpdate={onPageUpdate}
contextVariables={contextVariables}
setSelectedWidgetId={setSelectedWidgetId}
selectedLayout={templateManager.templates?.find(t => t.id === selectedLayoutId) || null}
onRenameLayout={templateManager.handleRenameTemplate}
onDeleteLayout={templateManager.handleDeleteTemplate}
onApplyLayout={templateManager.handleLoadTemplate}
onToggleLayoutVisibility={templateManager.handleToggleVisibility}
/>
</ResizablePanelGroup>
</div>

View File

@ -13,14 +13,15 @@ interface UseTemplateManagerParams {
isOwner: boolean;
getLoadedPageLayout: ReturnType<typeof useLayout>['getLoadedPageLayout'];
importPageLayout: ReturnType<typeof useLayout>['importPageLayout'];
setConfirmation: (state: {
setConfirmation: (state: React.SetStateAction<{
open: boolean;
title: string;
description: string;
onConfirm: () => void;
confirmLabel?: string;
variant?: "default" | "destructive";
}) => void;
}>) => void;
currentLayout: any | null;
}
export function useTemplateManager({
@ -29,6 +30,7 @@ export function useTemplateManager({
getLoadedPageLayout,
importPageLayout,
setConfirmation,
currentLayout,
}: UseTemplateManagerParams) {
const [templates, setTemplates] = useState<Layout[]>([]);
const [activeTemplateId, setActiveTemplateId] = useState<string | null>(null);
@ -41,6 +43,13 @@ export function useTemplateManager({
}
}, [isOwner]);
// Sync activeTemplateId with layout state (supports Undo/Redo)
useEffect(() => {
if (currentLayout?.rootTemplate) {
setActiveTemplateId(currentLayout.rootTemplate);
}
}, [currentLayout?.rootTemplate]);
const loadTemplates = async () => {
const { data } = await getLayouts({ type: 'canvas' });
if (data) {
@ -51,8 +60,16 @@ export function useTemplateManager({
const handleLoadTemplate = useCallback(async (template: Layout) => {
if (!pageId) return;
try {
const layoutJsonString = JSON.stringify(template.layout_json);
// Inject rootTemplate ID into the layout JSON so it persists in history
const layoutJson = template.layout_json as any;
const layoutWithTemplateId = {
...(typeof layoutJson === 'object' ? layoutJson : {}),
rootTemplate: template.id
};
const layoutJsonString = JSON.stringify(layoutWithTemplateId);
await importPageLayout(pageId, layoutJsonString);
// Local state update is redundant due to useEffect but keeps UI snappy
setActiveTemplateId(template.id);
toast.success(`Loaded layout: ${template.name}`);
} catch (e) {
@ -127,6 +144,35 @@ export function useTemplateManager({
});
}, [activeTemplateId, setConfirmation]);
const handleToggleVisibility = useCallback(async (template: Layout) => {
try {
const newVisibility = template.visibility === 'public' ? 'private' : 'public';
await updateLayout(template.id, {
visibility: newVisibility,
updated_at: new Date().toISOString()
});
toast.success(translate(newVisibility === 'public' ? "Template is now public" : "Template is now private"));
loadTemplates();
} catch (e) {
console.error("Failed to update template visibility", e);
toast.error(translate("Failed to update visibility"));
}
}, []);
const handleRenameTemplate = useCallback(async (template: Layout, newName: string) => {
try {
await updateLayout(template.id, {
name: newName,
updated_at: new Date().toISOString()
});
toast.success(translate("Template renamed"));
loadTemplates();
} catch (e) {
console.error("Failed to rename template", e);
toast.error(translate("Failed to rename template"));
}
}, []);
const handleNewLayout = useCallback(() => {
if (!pageId) return;
setConfirmation({
@ -168,5 +214,7 @@ export function useTemplateManager({
onSaveTemplate,
handleDeleteTemplate,
handleNewLayout,
handleToggleVisibility,
handleRenameTemplate,
};
}

View File

@ -34,7 +34,9 @@ import {
MoreHorizontal,
Plus,
BookmarkCheck,
Sparkles
Sparkles,
Globe,
Lock
} from "lucide-react";
import {
AlertDialog,
@ -143,6 +145,9 @@ interface PageRibbonBarProps {
onPaste?: () => void;
hasClipboard?: boolean;
onAIAssistant?: () => void;
onToggleVisibility?: (template: Layout) => void;
onSelectTemplate?: (template: Layout) => void;
selectedLayoutId?: string | null;
}
// Ribbon UI Components
@ -347,7 +352,10 @@ export const PageRibbonBar = ({
onCopy,
onPaste,
hasClipboard = false,
onAIAssistant
onAIAssistant,
onToggleVisibility,
onSelectTemplate,
selectedLayoutId
}: PageRibbonBarProps) => {
const { executeCommand, loadPageLayout, clearHistory, getLoadedPageLayout } = useLayout();
const navigate = useNavigate();
@ -949,10 +957,9 @@ export const PageRibbonBar = ({
id: t.id,
icon: LayoutTemplate,
label: t.name,
onClick: () => onLoadTemplate?.(t),
active: activeTemplateId === t.id,
iconColor: activeTemplateId === t.id ? "text-green-600" : "text-gray-500",
onDelete: onDeleteTemplate ? () => onDeleteTemplate(t) : undefined
onClick: () => onSelectTemplate?.(t),
active: selectedLayoutId === t.id,
iconColor: activeTemplateId === t.id ? "text-green-600" : "text-gray-500"
})) || []}
/>
{(!templates || templates.length === 0) && (