The dashboard uses a 12-column CSS Grid layout where cards can span 1-12 columns. Cards are self-contained components (Vue or React) that receive data and handle their own refresh logic. The dashboard supports both Vue.js 3 and React components, allowing developers to choose their preferred framework.
Each card consists of three main files. The component file extension depends on the framework you choose:
Vue Card Structure:
your-card/
├── metadata.js # Card configuration
├── component.vue # Vue component
└── index.js # Export file
React Card Structure:
your-card/
├── metadata.js # Card configuration
├── component.jsx # React component
└── index.js # Export file
metadata.jsexport default {
id: 'unique-card-id',
title: 'Card Title',
description: 'Card description',
width: 4, // 1-12 columns
language: 'vue', // 'vue' or 'react'
requires_capabilities: ['manage_options'], // Optional
};
component.vue (Vue Component)<script setup>
import { ref, computed, onMounted, watch } from 'vue';
const props = defineProps({
dateRange: {
type: Array,
required: true,
},
appData: {
type: Object,
required: true,
},
id: {
type: String,
required: true,
},
});
// Your component logic here
</script>
<template>
<!-- Your card template -->
</template>
component.jsx (React Component)import React, { useState, useEffect } from 'react';
/**
* Your React Card Component
*
* @param {Object} props - Component props
* @param {Array} props.dateRange - Array of two dates [startDate, endDate]
* @param {Object} props.appData - Vue app store instance
* @param {string} props.id - Card ID from metadata
*/
const YourCard = ({ dateRange, appData, id }) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
// Load data when component mounts
loadData();
}, []);
useEffect(() => {
// Reload data when dateRange changes
loadData();
}, [dateRange]);
const loadData = async () => {
setLoading(true);
try {
// Your API call here
// const response = await fetch(...);
// setData(response);
} catch (error) {
console.error('Error loading data:', error);
} finally {
setLoading(false);
}
};
return (
<div className="bg-zinc-50 dark:bg-zinc-800/20 border border-zinc-200/40 dark:border-zinc-800/60 rounded-3xl p-6">
{/* Your card content */}
</div>
);
};
export default YourCard;
index.jsFor Vue Components:
import metadata from './metadata.js';
import component from './component.vue';
export default {
metadata,
component,
};
For React Components:
import metadata from './metadata.js';
import component from './component.jsx';
export default {
metadata,
component,
};
| Property | Type | Description | Default |
|---|---|---|---|
id | string | Unique identifier for the card | Required |
title | string | Display title for the card | Required |
description | string | Brief description of the card | Required |
width | number | Number of columns (1-12) the card spans | Required |
category | string | Dashboard category (e.g., 'site', 'analytics') | Required |
language | string | Component language ('vue' or 'react') | 'vue' |
mobileWidth | number | Number of columns on mobile (1-12) | 12 |
| Property | Type | Description |
|---|---|---|
requires_capabilities | array | WordPress capabilities required to view the card |
Vue Card Example:
export default {
id: 'user-analytics',
title: 'User Analytics',
description: 'Track user engagement and activity',
width: 6,
mobileWidth: 12,
language: 'vue',
category: 'site',
requires_capabilities: ['manage_options', 'edit_users'],
};
React Card Example:
export default {
id: 'react-analytics',
title: 'React Analytics',
description: 'Analytics built with React',
width: 6,
mobileWidth: 12,
language: 'react', // Important: Set to 'react' for React components
category: 'analytics',
requires_capabilities: ['manage_options'],
};
Your component receives three props:
dateRange (Array)An array containing two Date objects:
[
Date, // Start date of selected range
Date // End date of selected range
]
Vue Example:
const startDate = props.dateRange[0].toISOString();
const endDate = props.dateRange[1].toISOString();
React Example:
const startDate = dateRange[0].toISOString();
const endDate = dateRange[1].toISOString();
appData (Object)The Vue app store instance containing:
{
state: {
adminUrl: string, // WordPress admin URL
siteURL: string, // Site URL
currentUser: object, // Current user data
uixpress_settings: object, // Plugin settings
// ... other app state
}
}
id (String)The card ID from metadata, useful for unique identifiers or debugging.
Note for React Components: Veaury automatically converts Vue's kebab-case props (date-range, app-data) to React's camelCase (dateRange, appData). However, it's recommended to handle both formats for maximum compatibility:
const dateRange = props.dateRange || props['date-range'];
const appData = props.appData || props['app-data'];
<script setup>
import { ref, computed, onMounted, watch } from 'vue';
const props = defineProps({
dateRange: { type: Object, required: true },
appData: { type: Object, required: true },
});
// Component state
const data = ref([]);
const loading = ref(false);
const error = ref(null);
// Load data function
const loadData = async () => {
loading.value = true;
try {
// Your API call here
const response = await fetch('/wp-json/wp/v2/posts');
data.value = await response.json();
} catch (err) {
error.value = err.message;
} finally {
loading.value = false;
}
};
// Watch for date range changes
watch(
() => props.dateRange,
() => {
loadData();
},
{ deep: true }
);
// Watch for app data changes
watch(
() => props.appData,
() => {
// React to app data changes
},
{ deep: true }
);
// Load data on mount
onMounted(() => {
loadData();
});
</script>
For cards within the UIXpress plugin:
// In app/src/pages/dashboard/src/cards/index.js
import { addFilter } from '@/assets/js/functions/HooksSystem.js';
import myCard from './my-card/index.js';
addFilter('uixpress/dashboard/cards/register', (widgets) => {
return [...widgets, myCard];
});
For external plugins, listen for the uixpress/dashboard/ready event and use the global window.uixpress API:
// In your external plugin
document.addEventListener("uixpress/dashboard/ready", () => {
const { addFilter } = window.uixpress;
// Add a new card
addFilter("uixpress/dashboard/cards/register", (widgets) => {
return [...widgets, myExternalCard];
});
});
You can filter out existing cards by returning a filtered array:
document.addEventListener("uixpress/dashboard/ready", () => {
const { addFilter } = window.uixpress;
// Remove a specific card by ID
addFilter("uixpress/dashboard/cards/register", (widgets) => {
return widgets.filter((widget) => {
return !(widget.metadata && widget.metadata.id === "server-health");
});
});
});
Dashboard cards are organized into categories. You can manipulate the categories using the uixpress/dashboard/categories/register filter.
document.addEventListener("uixpress/dashboard/ready", () => {
const { addFilter } = window.uixpress;
// Sort categories (e.g., put Analytics first)
addFilter("uixpress/dashboard/categories/register", (categories) => {
return categories.sort((a, b) =>
a.value === "analytics" ? -1 : b.value === "analytics" ? 1 : 0
);
});
});
document.addEventListener("uixpress/dashboard/ready", () => {
const { addFilter } = window.uixpress;
addFilter("uixpress/dashboard/categories/register", (categories) => {
return [
...categories,
{
label: "My Custom Category",
value: "my-category",
},
];
});
});
document.addEventListener("uixpress/dashboard/ready", () => {
const { addFilter } = window.uixpress;
addFilter("uixpress/dashboard/categories/register", (categories) => {
return categories.filter((category) => category.value !== "analytics");
});
});
Here's a complete example showing how to manipulate both categories and cards:
document.addEventListener("uixpress/dashboard/ready", () => {
const { addFilter } = window.uixpress;
// Reorder categories - put Analytics first
addFilter("uixpress/dashboard/categories/register", (categories) => {
return categories.sort((a, b) =>
a.value === "analytics" ? -1 : b.value === "analytics" ? 1 : 0
);
});
// Filter out specific cards
addFilter("uixpress/dashboard/cards/register", (widgets) => {
return widgets.filter((widget) => {
return !(widget.metadata && widget.metadata.id === "server-health");
});
});
});
Groups allow you to organize related cards:
const analyticsGroup = {
metadata: {
id: 'analytics-group',
title: 'Analytics',
width: 6,
columns: 2, // Number of columns in the group
},
isGroup: true,
children: [card1, card2, card3],
};
// Register the group
addFilter('uixpress/dashboard/cards/register', (widgets) => {
return [...widgets, analyticsGroup];
});
Groups can contain other groups:
const mainGroup = {
metadata: {
id: 'main-group',
title: 'Main Dashboard',
width: 8,
columns: 4,
},
isGroup: true,
children: [analyticsGroup, anotherGroup, standaloneCard],
};
Control card visibility based on user permissions:
export default {
id: 'admin-only-card',
title: 'Admin Only',
description: 'Only visible to administrators',
width: 4,
language: 'vue',
requires_capabilities: ['manage_options'],
};
export default {
id: 'editor-card',
title: 'Editor Card',
description: 'Visible to editors and admins',
width: 4,
language: 'vue',
requires_capabilities: ['edit_posts', 'manage_options'],
};
Here's a complete example of a React dashboard card:
metadata.js:
export default {
id: 'react-test-card',
title: 'React Test Card',
description: 'A test card built with React',
width: 4,
mobileWidth: 12,
language: 'react', // Important: Set to 'react'
category: 'site',
};
component.jsx:
import React, { useState, useEffect, useMemo } from 'react';
/**
* React Test Card Component
*
* @param {Object} props - Component props
* @param {Array} props.dateRange - Array of two dates [startDate, endDate]
* @param {Object} props.appData - Vue app store instance
* @param {string} props.id - Card ID from metadata
*/
const ReactTestCard = (props) => {
// Handle both camelCase and kebab-case prop names (veaury compatibility)
const dateRange = props.dateRange || props['date-range'];
const appData = props.appData || props['app-data'];
const id = props.id;
const [count, setCount] = useState(0);
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
/**
* Normalizes a date value to a Date object
*/
const normalizeDate = (date) => {
if (!date) return null;
if (date instanceof Date) return date;
if (typeof date === 'string' || typeof date === 'number') {
const d = new Date(date);
return isNaN(d.getTime()) ? null : d;
}
return null;
};
/**
* Calculates days between two dates
*/
const getDaysBetween = (start, end) => {
const startDate = normalizeDate(start);
const endDate = normalizeDate(end);
if (!startDate || !endDate) return 0;
const diffTime = Math.abs(endDate.getTime() - startDate.getTime());
return Math.ceil(diffTime / (1000 * 60 * 60 * 24));
};
// Normalize dateRange and calculate days
const normalizedDateRange = useMemo(() => {
if (!dateRange || !Array.isArray(dateRange) || dateRange.length !== 2) {
return null;
}
const start = normalizeDate(dateRange[0]);
const end = normalizeDate(dateRange[1]);
if (!start || !end) return null;
return [start, end];
}, [dateRange]);
const daysInRange = normalizedDateRange
? getDaysBetween(normalizedDateRange[0], normalizedDateRange[1])
: 0;
useEffect(() => {
// Load data when component mounts or dateRange changes
loadData();
}, [dateRange]);
const loadData = async () => {
setLoading(true);
try {
// Your API call here
// Example:
// const response = await fetch('/wp-json/wp/v2/posts');
// setData(await response.json());
} catch (error) {
console.error('Error loading data:', error);
} finally {
setLoading(false);
}
};
return (
<div className="bg-zinc-50 dark:bg-zinc-800/20 border border-zinc-200/40 dark:border-zinc-800/60 rounded-3xl p-6 flex flex-col">
<div className="flex items-center justify-between mb-6">
<div>
<h3 className="text-lg font-semibold text-zinc-900 dark:text-zinc-100">
React Test Card
</h3>
<p className="text-sm text-zinc-500 dark:text-zinc-400">
Built with React
</p>
</div>
</div>
{loading ? (
<div className="text-center py-4">Loading...</div>
) : (
<div className="space-y-4">
<div className="text-center">
<div className="text-2xl font-bold text-zinc-900 dark:text-zinc-100">
{daysInRange}
</div>
<div className="text-xs text-zinc-500 dark:text-zinc-400">
Days Selected
</div>
</div>
</div>
)}
</div>
);
};
export default ReactTestCard;
index.js:
import metadata from './metadata.js';
import component from './component.jsx';
export default {
metadata,
component,
};
useMemo for expensive calculations based on propsuseEffect to watch for prop changes and reload data// metadata.js
export default {
id: 'simple-stats',
title: 'Simple Stats',
description: 'Display basic statistics',
width: 3,
language: 'vue',
};
<!-- component.vue -->
<script setup>
import { ref, onMounted } from 'vue';
const props = defineProps({
dateRange: { type: Object, required: true },
appData: { type: Object, required: true },
});
const stats = ref({
total: 0,
active: 0,
inactive: 0,
});
const loadStats = async () => {
// Your API call here
stats.value = {
total: 100,
active: 85,
inactive: 15,
};
};
onMounted(() => {
loadStats();
});
</script>
<template>
<div class="p-4">
<h3 class="text-lg font-semibold mb-4">Simple Stats</h3>
<div class="grid grid-cols-3 gap-4">
<div class="text-center">
<div class="text-2xl font-bold">{{ stats.total }}</div>
<div class="text-sm text-gray-500">Total</div>
</div>
<div class="text-center">
<div class="text-2xl font-bold text-green-600">{{ stats.active }}</div>
<div class="text-sm text-gray-500">Active</div>
</div>
<div class="text-center">
<div class="text-2xl font-bold text-red-600">{{ stats.inactive }}</div>
<div class="text-sm text-gray-500">Inactive</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, watch } from 'vue';
import { lmnFetch } from '@/assets/js/functions/lmnFetch.js';
const props = defineProps({
dateRange: { type: Object, required: true },
appData: { type: Object, required: true },
});
const posts = ref([]);
const loading = ref(false);
const loadPosts = async () => {
loading.value = true;
try {
const response = await lmnFetch('/wp/v2/posts', {
method: 'GET',
params: {
per_page: 5,
after: props.dateRange.startDate.toISOString(),
before: props.dateRange.endDate.toISOString(),
},
});
posts.value = response;
} catch (error) {
console.error('Failed to load posts:', error);
} finally {
loading.value = false;
}
};
watch(() => props.dateRange, loadPosts, { deep: true });
onMounted(loadPosts);
</script>
<template>
<div class="p-4">
<h3 class="text-lg font-semibold mb-4">Recent Posts</h3>
<div v-if="loading" class="text-center py-4">Loading...</div>
<div v-else-if="posts.length === 0" class="text-center py-4 text-gray-500">
No posts found
</div>
<div v-else class="space-y-3">
<div
v-for="post in posts"
:key="post.id"
class="p-3 bg-gray-50 rounded-lg"
>
<h4 class="font-medium">{{ post.title.rendered }}</h4>
<p class="text-sm text-gray-500">{{ post.date }}</p>
</div>
</div>
</div>
</template>
Vue Component Example:
// In your external plugin
(function () {
'use strict';
// Wait for UIXpress dashboard to be ready
document.addEventListener('uixpress/dashboard/ready', function () {
const { addFilter } = window.uixpress;
// Define your Vue card
const myExternalCard = {
metadata: {
id: 'my-plugin-card',
title: 'My Plugin Data',
description: 'Data from my plugin',
width: 4,
language: 'vue',
category: 'site',
requires_capabilities: ['manage_options'],
},
component: {
// Your Vue component definition
template: `
<div class="p-4">
<h3>My Plugin Card</h3>
<p>This card is from an external plugin!</p>
</div>
`,
props: ['dateRange', 'appData', 'id'],
},
};
// Register the card
addFilter('uixpress/dashboard/cards/register', function (widgets) {
return [...widgets, myExternalCard];
});
});
})();
React Component Example:
// In your external plugin
(function () {
'use strict';
// Wait for UIXpress dashboard to be ready
document.addEventListener('uixpress/dashboard/ready', function () {
const { addFilter } = window.uixpress;
// Define your React card component
const MyReactCard = ({ dateRange, appData, id }) => {
return (
<div className="p-4">
<h3>My React Plugin Card</h3>
<p>This React card is from an external plugin!</p>
</div>
);
};
// Define your React card
const myExternalReactCard = {
metadata: {
id: 'my-react-plugin-card',
title: 'My React Plugin Data',
description: 'React component from my plugin',
width: 4,
language: 'react', // Important: Set to 'react'
category: 'site',
requires_capabilities: ['manage_options'],
},
component: MyReactCard,
};
// Register the card
addFilter('uixpress/dashboard/cards/register', function (widgets) {
return [...widgets, myExternalReactCard];
});
});
})();
// Add debugging to your component
console.log('Date range changed:', props.dateRange);
console.log('App data:', props.appData);
For more information, visit the UIXpress Documentation or contact support.