Section 2 Part 3
This commit is contained in:
@@ -3,6 +3,7 @@ interface ComponentCardProps {
|
||||
children: React.ReactNode;
|
||||
className?: string; // Additional custom classes for styling
|
||||
desc?: string | React.ReactNode; // Description text
|
||||
headerContent?: React.ReactNode; // Additional content to display in header (e.g., actions, navigation)
|
||||
}
|
||||
|
||||
const ComponentCard: React.FC<ComponentCardProps> = ({
|
||||
@@ -10,21 +11,29 @@ const ComponentCard: React.FC<ComponentCardProps> = ({
|
||||
children,
|
||||
className = "",
|
||||
desc = "",
|
||||
headerContent,
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={`rounded-2xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-white/[0.03] overflow-visible ${className}`}
|
||||
>
|
||||
{/* Card Header (render only when title or desc provided) */}
|
||||
{(title || desc) && (
|
||||
<div className="px-6 py-5 relative z-0">
|
||||
<h3 className="text-base font-medium text-gray-800 dark:text-white/90">
|
||||
{title}
|
||||
</h3>
|
||||
{desc && (
|
||||
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
{desc}
|
||||
</p>
|
||||
{(title || desc || headerContent) && (
|
||||
<div className="px-6 py-5 relative z-0 flex items-start justify-between gap-4">
|
||||
<div className="flex-1">
|
||||
<h3 className="text-base font-medium text-gray-800 dark:text-white/90">
|
||||
{title}
|
||||
</h3>
|
||||
{desc && (
|
||||
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
{desc}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{headerContent && (
|
||||
<div className="flex-shrink-0">
|
||||
{headerContent}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -56,13 +56,21 @@ export default function SingleSiteSelector() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleSiteSelect = async (siteId: number) => {
|
||||
const handleSiteSelect = async (newSiteId: number) => {
|
||||
try {
|
||||
await apiSetActiveSite(siteId);
|
||||
const selectedSite = sites.find(s => s.id === siteId);
|
||||
await apiSetActiveSite(newSiteId);
|
||||
const selectedSite = sites.find(s => s.id === newSiteId);
|
||||
if (selectedSite) {
|
||||
setActiveSite(selectedSite);
|
||||
toast.success(`Switched to "${selectedSite.name}"`);
|
||||
|
||||
// If we're on a site-specific page (/sites/:id/...), navigate to same subpage for new site
|
||||
const path = window.location.pathname;
|
||||
const sitePageMatch = path.match(/^\/sites\/(\d+)(\/.*)?$/);
|
||||
if (sitePageMatch) {
|
||||
const subPath = sitePageMatch[2] || ''; // e.g., '/settings', '/content', ''
|
||||
navigate(`/sites/${newSiteId}${subPath}`);
|
||||
}
|
||||
}
|
||||
setSitesOpen(false);
|
||||
} catch (error: any) {
|
||||
|
||||
133
frontend/src/components/common/SiteInfoBar.tsx
Normal file
133
frontend/src/components/common/SiteInfoBar.tsx
Normal file
@@ -0,0 +1,133 @@
|
||||
/**
|
||||
* SiteInfoBar - Reusable site info bar for site-specific pages
|
||||
* Shows site name, URL, badges, and action buttons in a single row
|
||||
*/
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import Button from '../ui/button/Button';
|
||||
import {
|
||||
GridIcon,
|
||||
FileIcon,
|
||||
GlobeIcon,
|
||||
PlusIcon,
|
||||
} from '../../icons';
|
||||
|
||||
interface SiteInfoBarProps {
|
||||
site: {
|
||||
id: number;
|
||||
name: string;
|
||||
domain?: string;
|
||||
url?: string;
|
||||
site_type?: string;
|
||||
hosting_type?: string;
|
||||
is_active?: boolean;
|
||||
} | null;
|
||||
/** Current page - determines which buttons to show */
|
||||
currentPage: 'dashboard' | 'settings' | 'content';
|
||||
/** Optional: total items count for content page */
|
||||
itemsCount?: number;
|
||||
/** Optional: show New Post button */
|
||||
showNewPostButton?: boolean;
|
||||
}
|
||||
|
||||
export default function SiteInfoBar({
|
||||
site,
|
||||
currentPage,
|
||||
itemsCount,
|
||||
showNewPostButton = false,
|
||||
}: SiteInfoBarProps) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
if (!site) return null;
|
||||
|
||||
const siteUrl = site.domain || site.url;
|
||||
|
||||
return (
|
||||
<div className="mb-6 px-4 py-3 rounded-xl bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 shadow-sm">
|
||||
<div className="flex items-center justify-between gap-4 flex-wrap">
|
||||
{/* Left: Badges */}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="inline-flex items-center px-2.5 py-1 rounded-md text-xs font-medium bg-brand-50 text-brand-700 dark:bg-brand-900/30 dark:text-brand-300 border border-brand-200 dark:border-brand-800">
|
||||
{site.site_type || 'marketing'}
|
||||
</span>
|
||||
<span className="inline-flex items-center px-2.5 py-1 rounded-md text-xs font-medium bg-purple-50 text-purple-700 dark:bg-purple-900/30 dark:text-purple-300 border border-purple-200 dark:border-purple-800">
|
||||
{site.hosting_type || 'igny8_sites'}
|
||||
</span>
|
||||
<span className={`inline-flex items-center px-2.5 py-1 rounded-md text-xs font-medium ${
|
||||
site.is_active !== false
|
||||
? 'bg-success-50 text-success-700 dark:bg-success-900/30 dark:text-success-300 border border-success-200 dark:border-success-800'
|
||||
: 'bg-gray-50 text-gray-700 dark:bg-gray-800 dark:text-gray-300 border border-gray-200 dark:border-gray-700'
|
||||
}`}>
|
||||
{site.is_active !== false ? '● Active' : '○ Inactive'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Center: Site Name and URL */}
|
||||
<div className="flex-1 text-center min-w-0">
|
||||
<h2 className="text-base font-semibold text-gray-900 dark:text-white truncate">{site.name}</h2>
|
||||
{siteUrl && (
|
||||
<a
|
||||
href={siteUrl.startsWith('http') ? siteUrl : `https://${siteUrl}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-brand-600 hover:text-brand-700 dark:text-brand-400 text-sm transition truncate inline-block max-w-full"
|
||||
>
|
||||
{siteUrl}
|
||||
</a>
|
||||
)}
|
||||
{itemsCount !== undefined && (
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400 ml-2">
|
||||
({itemsCount} items)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Right: Action Buttons */}
|
||||
<div className="flex items-center gap-2">
|
||||
{currentPage !== 'dashboard' && (
|
||||
<Button
|
||||
onClick={() => navigate(`/sites/${site.id}`)}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="border-brand-300 text-brand-700 hover:bg-brand-50 dark:border-brand-600 dark:text-brand-400 dark:hover:bg-brand-900/20"
|
||||
startIcon={<GlobeIcon className="w-4 h-4" />}
|
||||
>
|
||||
Dashboard
|
||||
</Button>
|
||||
)}
|
||||
{currentPage !== 'settings' && (
|
||||
<Button
|
||||
onClick={() => navigate(`/sites/${site.id}/settings`)}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="border-purple-300 text-purple-700 hover:bg-purple-50 dark:border-purple-600 dark:text-purple-400 dark:hover:bg-purple-900/20"
|
||||
startIcon={<GridIcon className="w-4 h-4" />}
|
||||
>
|
||||
Settings
|
||||
</Button>
|
||||
)}
|
||||
{currentPage !== 'content' && (
|
||||
<Button
|
||||
onClick={() => navigate(`/sites/${site.id}/content`)}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="border-success-300 text-success-700 hover:bg-success-50 dark:border-success-600 dark:text-success-400 dark:hover:bg-success-900/20"
|
||||
startIcon={<FileIcon className="w-4 h-4" />}
|
||||
>
|
||||
Content
|
||||
</Button>
|
||||
)}
|
||||
{showNewPostButton && (
|
||||
<Button
|
||||
onClick={() => navigate(`/sites/${site.id}/posts/new`)}
|
||||
variant="primary"
|
||||
size="sm"
|
||||
startIcon={<PlusIcon className="w-4 h-4" />}
|
||||
>
|
||||
New Post
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user