This commit is contained in:
IGNY8 VPS (Salman)
2025-11-29 08:59:31 +00:00
parent 4bea79a76d
commit 4237c203b4
18 changed files with 507 additions and 52 deletions

View File

@@ -8,8 +8,8 @@ import clsx from "clsx";
type BadgeVariant = "solid" | "soft" | "outline" | "light";
type BadgeSize = "xs" | "sm" | "md";
type BadgeTone = "brand" | "success" | "warning" | "danger" | "info" | "neutral";
type BadgeColor = "success" | "warning" | "error" | "danger" | "info" | "primary" | "brand" | "light" | "neutral";
type BadgeTone = "brand" | "success" | "warning" | "danger" | "info" | "neutral" | "purple" | "indigo" | "pink" | "teal" | "cyan" | "blue";
type BadgeColor = "success" | "warning" | "error" | "danger" | "info" | "primary" | "brand" | "light" | "neutral" | "purple" | "indigo" | "pink" | "teal" | "cyan" | "blue";
interface BadgeProps {
variant?: BadgeVariant; // Visual treatment (light maps to soft)
@@ -32,46 +32,82 @@ const toneStyles: Record<
> = {
brand: {
solid: "bg-brand-500 text-white",
soft: "bg-brand-50 text-brand-600 dark:bg-brand-500/15 dark:text-brand-300",
soft: "bg-brand-50 text-brand-700 dark:bg-brand-500/15 dark:text-brand-400",
outline:
"text-brand-600 ring-1 ring-brand-200 dark:ring-brand-500/30 dark:text-brand-300",
"text-brand-700 ring-1 ring-brand-200 dark:ring-brand-500/30 dark:text-brand-400",
},
success: {
solid: "bg-success-500 text-white",
soft: "bg-success-50 text-success-600 dark:bg-success-500/15 dark:text-success-300",
soft: "bg-success-50 text-success-700 dark:bg-success-500/15 dark:text-success-400",
outline:
"text-success-600 ring-1 ring-success-200 dark:ring-success-500/30 dark:text-success-300",
"text-success-700 ring-1 ring-success-200 dark:ring-success-500/30 dark:text-success-400",
},
warning: {
solid: "bg-warning-500 text-white",
soft: "bg-warning-50 text-warning-600 dark:bg-warning-500/15 dark:text-warning-300",
soft: "bg-warning-50 text-warning-700 dark:bg-warning-500/15 dark:text-warning-400",
outline:
"text-warning-600 ring-1 ring-warning-200 dark:ring-warning-500/30 dark:text-warning-300",
"text-warning-700 ring-1 ring-warning-200 dark:ring-warning-500/30 dark:text-warning-400",
},
danger: {
solid: "bg-error-500 text-white",
soft: "bg-error-50 text-error-600 dark:bg-error-500/15 dark:text-error-300",
soft: "bg-error-50 text-error-700 dark:bg-error-500/15 dark:text-error-400",
outline:
"text-error-600 ring-1 ring-error-200 dark:ring-error-500/30 dark:text-error-300",
"text-error-700 ring-1 ring-error-200 dark:ring-error-500/30 dark:text-error-400",
},
info: {
solid: "bg-blue-light-500 text-white",
soft: "bg-blue-light-50 text-blue-light-600 dark:bg-blue-light-500/15 dark:text-blue-light-300",
soft: "bg-blue-light-50 text-blue-light-700 dark:bg-blue-light-500/15 dark:text-blue-light-400",
outline:
"text-blue-light-600 ring-1 ring-blue-light-200 dark:ring-blue-light-500/30 dark:text-blue-light-300",
"text-blue-light-700 ring-1 ring-blue-light-200 dark:ring-blue-light-500/30 dark:text-blue-light-400",
},
neutral: {
solid: "bg-gray-800 text-white",
soft: "bg-gray-100 text-gray-700 dark:bg-white/5 dark:text-white/80",
soft: "bg-gray-100 text-gray-800 dark:bg-white/5 dark:text-white/90",
outline:
"text-gray-700 ring-1 ring-gray-300 dark:ring-white/[0.08] dark:text-white/80",
"text-gray-800 ring-1 ring-gray-300 dark:ring-white/[0.08] dark:text-white/90",
},
purple: {
solid: "bg-purple-600 text-white",
soft: "bg-purple-50 text-purple-700 dark:bg-purple-500/15 dark:text-purple-400",
outline:
"text-purple-700 ring-1 ring-purple-200 dark:ring-purple-500/30 dark:text-purple-400",
},
indigo: {
solid: "bg-indigo-600 text-white",
soft: "bg-indigo-50 text-indigo-700 dark:bg-indigo-500/15 dark:text-indigo-400",
outline:
"text-indigo-700 ring-1 ring-indigo-200 dark:ring-indigo-500/30 dark:text-indigo-400",
},
pink: {
solid: "bg-pink-600 text-white",
soft: "bg-pink-50 text-pink-700 dark:bg-pink-500/15 dark:text-pink-400",
outline:
"text-pink-700 ring-1 ring-pink-200 dark:ring-pink-500/30 dark:text-pink-400",
},
teal: {
solid: "bg-teal-600 text-white",
soft: "bg-teal-50 text-teal-700 dark:bg-teal-500/15 dark:text-teal-400",
outline:
"text-teal-700 ring-1 ring-teal-200 dark:ring-teal-500/30 dark:text-teal-400",
},
cyan: {
solid: "bg-cyan-600 text-white",
soft: "bg-cyan-50 text-cyan-700 dark:bg-cyan-500/15 dark:text-cyan-400",
outline:
"text-cyan-700 ring-1 ring-cyan-200 dark:ring-cyan-500/30 dark:text-cyan-400",
},
blue: {
solid: "bg-blue-600 text-white",
soft: "bg-blue-50 text-blue-700 dark:bg-blue-500/15 dark:text-blue-400",
outline:
"text-blue-700 ring-1 ring-blue-200 dark:ring-blue-500/30 dark:text-blue-400",
},
};
const sizeClasses: Record<BadgeSize, string> = {
xs: "h-5 px-2 text-[11px]",
sm: "h-6 px-2.5 text-xs",
md: "h-7 px-3 text-sm",
xs: "h-5 px-2 text-[11px] leading-tight",
sm: "h-6 px-2.5 text-xs leading-tight",
md: "h-7 px-3 text-sm leading-tight",
};
const Badge: React.FC<BadgeProps> = ({
@@ -98,6 +134,12 @@ const Badge: React.FC<BadgeProps> = ({
"brand": "brand",
"light": "neutral",
"neutral": "neutral",
"purple": "purple",
"indigo": "indigo",
"pink": "pink",
"teal": "teal",
"cyan": "cyan",
"blue": "blue",
};
return colorToToneMap[color] || "brand";
})();
@@ -110,7 +152,7 @@ const Badge: React.FC<BadgeProps> = ({
return (
<span
className={clsx(
"inline-flex items-center justify-center gap-1 rounded-full font-medium uppercase tracking-wide",
"inline-flex items-center justify-center gap-1 rounded-full font-normal",
sizeClasses[size],
toneClass,
className,

View File

@@ -107,8 +107,8 @@ export const createClustersPageConfig = (
sortField: 'name',
render: (value: string, row: Cluster) => (
<Link
to={`/clusters/${row.id}`}
className="font-medium text-brand-600 hover:text-brand-700 dark:text-brand-400 dark:hover:text-brand-300"
to={`/planner/clusters/${row.id}`}
className="text-base font-light text-brand-600 hover:text-brand-700 dark:text-brand-400 dark:hover:text-brand-300"
>
{value}
</Link>

View File

@@ -104,12 +104,12 @@ export const createContentPageConfig = (
{handlers.onRowClick ? (
<button
onClick={() => handlers.onRowClick!(row)}
className="font-medium text-blue-500 hover:text-blue-600 hover:underline text-left transition-colors"
className="text-base font-light text-blue-500 hover:text-blue-600 hover:underline text-left transition-colors"
>
{row.title || `Content #${row.id}`}
</button>
) : (
<div className="font-medium text-gray-900 dark:text-white">
<div className="text-base font-light text-gray-900 dark:text-white">
{row.title || `Content #${row.id}`}
</div>
)}
@@ -178,24 +178,48 @@ export const createContentPageConfig = (
},
},
{
key: 'taxonomy_terms',
key: 'tags',
label: 'Tags',
sortable: false,
width: '150px',
render: (_value: any, row: Content) => {
const taxonomyTerms = row.taxonomy_terms;
if (!taxonomyTerms || taxonomyTerms.length === 0) {
const tags = row.tags || [];
if (!tags || tags.length === 0) {
return <span className="text-gray-400 dark:text-gray-500 text-[11px]">-</span>;
}
return (
<div className="flex flex-wrap gap-1">
{taxonomyTerms.slice(0, 2).map((term) => (
<Badge key={term.id} color="pink" size="xs" variant="soft">
<span className="text-[11px] font-normal">{term.name}</span>
{tags.slice(0, 2).map((tag, index) => (
<Badge key={`${tag}-${index}`} color="pink" size="xs" variant="soft">
<span className="text-[11px] font-normal">{tag}</span>
</Badge>
))}
{taxonomyTerms.length > 2 && (
<span className="text-[11px] text-gray-500">+{taxonomyTerms.length - 2}</span>
{tags.length > 2 && (
<span className="text-[11px] text-gray-500">+{tags.length - 2}</span>
)}
</div>
);
},
},
{
key: 'categories',
label: 'Categories',
sortable: false,
width: '150px',
render: (_value: any, row: Content) => {
const categories = row.categories || [];
if (!categories || categories.length === 0) {
return <span className="text-gray-400 dark:text-gray-500 text-[11px]">-</span>;
}
return (
<div className="flex flex-wrap gap-1">
{categories.slice(0, 2).map((category, index) => (
<Badge key={`${category}-${index}`} color="blue" size="xs" variant="soft">
<span className="text-[11px] font-normal">{category}</span>
</Badge>
))}
{categories.length > 2 && (
<span className="text-[11px] text-gray-500">+{categories.length - 2}</span>
)}
</div>
);

View File

@@ -101,7 +101,7 @@ export const createIdeasPageConfig = (
toggleContentKey: 'description', // Use description field for toggle content
toggleContentLabel: 'Content Outline', // Label for expanded content
render: (value: string) => (
<span className="text-gray-800 dark:text-white font-medium">{value}</span>
<span className="text-gray-800 dark:text-white text-base font-light">{value}</span>
),
},
// Sector column - only show when viewing all sectors

View File

@@ -69,7 +69,7 @@ export const createImagesPageConfig = (
<div>
<a
href={`/writer/content/${row.content_id}`}
className="font-medium text-brand-500 hover:text-brand-600 dark:text-brand-400"
className="text-base font-light text-brand-500 hover:text-brand-600 dark:text-brand-400"
>
{row.content_title}
</a>

View File

@@ -54,6 +54,7 @@ export function createPublishedPageConfig(params: {
setPublishStatusFilter: (value: string) => void;
setCurrentPage: (page: number) => void;
activeSector: { id: number; name: string } | null;
onRowClick?: (row: Content) => void;
}): PublishedPageConfig {
const showSectorColumn = !params.activeSector;
@@ -63,14 +64,20 @@ export function createPublishedPageConfig(params: {
label: 'Title',
sortable: true,
sortField: 'title',
toggleable: true,
toggleContentKey: 'content_html',
toggleContentLabel: 'Generated Content',
render: (value: string, row: Content) => (
<div className="flex items-center gap-2">
<span className="font-medium text-gray-900 dark:text-white">
{value || `Content #${row.id}`}
</span>
{params.onRowClick ? (
<button
onClick={() => params.onRowClick!(row)}
className="text-base font-light text-blue-500 hover:text-blue-600 hover:underline text-left transition-colors"
>
{value || `Content #${row.id}`}
</button>
) : (
<span className="text-base font-light text-gray-900 dark:text-white">
{value || `Content #${row.id}`}
</span>
)}
{row.external_url && (
<a
href={row.external_url}

View File

@@ -67,12 +67,12 @@ export function createReviewPageConfig(params: {
{params.onRowClick ? (
<button
onClick={() => params.onRowClick!(row)}
className="font-medium text-blue-500 hover:text-blue-600 hover:underline text-left transition-colors"
className="text-base font-light text-blue-500 hover:text-blue-600 hover:underline text-left transition-colors"
>
{value || `Content #${row.id}`}
</button>
) : (
<span className="font-medium text-gray-900 dark:text-white">
<span className="text-base font-light text-gray-900 dark:text-white">
{value || `Content #${row.id}`}
</span>
)}

View File

@@ -112,7 +112,7 @@ export const createTasksPageConfig = (
return (
<div className="flex items-center gap-2">
<span className="font-medium text-gray-900 dark:text-white">
<span className="text-base font-light text-gray-900 dark:text-white">
{displayTitle}
</span>
{isSiteBuilder && (

View File

@@ -256,8 +256,11 @@ export default function Published() {
setPublishStatusFilter,
setCurrentPage,
activeSector,
onRowClick: (row: Content) => {
navigate(`/writer/content/${row.id}`);
},
});
}, [searchTerm, statusFilter, publishStatusFilter, activeSector]);
}, [searchTerm, statusFilter, publishStatusFilter, activeSector, navigate]);
// Calculate header metrics
const headerMetrics = useMemo(() => {

View File

@@ -2028,6 +2028,8 @@ export interface Content {
name: string;
taxonomy_type: string;
}>;
tags?: string[];
categories?: string[];
// WordPress integration
external_id?: string | null;
external_url?: string | null;

View File

@@ -2,9 +2,11 @@
* Column Visibility Store (Zustand)
* Manages column visibility settings per page with localStorage persistence
* Uses the same pattern as siteStore and sectorStore
* Stores preferences per user for multi-user support
*/
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { persist, createJSONStorage, StateStorage } from 'zustand/middleware';
import { useAuthStore } from './authStore';
interface ColumnVisibilityState {
// Map of page pathname to Set of visible column keys
@@ -17,6 +19,29 @@ interface ColumnVisibilityState {
resetPageColumns: (pathname: string) => void;
}
// Custom storage that uses user-specific keys
const userSpecificStorage: StateStorage = {
getItem: (name: string) => {
const user = useAuthStore.getState().user;
const userId = user?.id || 'anonymous';
const key = `igny8-column-visibility-user-${userId}`;
const value = localStorage.getItem(key);
return value;
},
setItem: (name: string, value: string) => {
const user = useAuthStore.getState().user;
const userId = user?.id || 'anonymous';
const key = `igny8-column-visibility-user-${userId}`;
localStorage.setItem(key, value);
},
removeItem: (name: string) => {
const user = useAuthStore.getState().user;
const userId = user?.id || 'anonymous';
const key = `igny8-column-visibility-user-${userId}`;
localStorage.removeItem(key);
},
};
export const useColumnVisibilityStore = create<ColumnVisibilityState>()(
persist<ColumnVisibilityState>(
(set, get) => ({
@@ -61,6 +86,7 @@ export const useColumnVisibilityStore = create<ColumnVisibilityState>()(
}),
{
name: 'igny8-column-visibility',
storage: createJSONStorage(() => userSpecificStorage),
partialize: (state) => ({
pageColumns: state.pageColumns,
}),