Update various configuration files, components, and assets; enhance notification system and API endpoints; improve documentation and styles across the application.

This commit is contained in:
Haqeem Solehan
2025-10-16 16:05:39 +08:00
commit b124ff8092
336 changed files with 94392 additions and 0 deletions

View File

@@ -0,0 +1,284 @@
export default function () {
return [
{
name: "3024-day",
image:
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/3024-day.png",
},
{
name: "3024-night",
image:
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/3024-night.png",
},
{
name: "abcdef",
image:
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/abcdef.png",
},
{
name: "ambiance-mobile",
image:
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/ambiance-mobile.png",
},
{
name: "ambiance",
image:
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/ambiance.png",
},
{
name: "ayu-dark",
image:
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/ayu-dark.png",
},
{
name: "ayu-mirage",
image:
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/ayu-mirage.png",
},
{
name: "base16-dark",
image:
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/base16-dark.png",
},
{
name: "base16-light",
image:
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/base16-light.png",
},
{
name: "bespin",
image:
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/bespin.png",
},
{
name: "blackboard",
image:
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/blackboard.png",
},
{
name: "cobalt",
image:
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/cobalt.png",
},
{
name: "colorforth",
image:
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/colorforth.png",
},
{
name: "dracula",
image:
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/dracula.png",
},
{
name: "duotone-dark",
image:
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/duotone-dark.png",
},
{
name: "duotone-light",
image:
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/duotone-light.png",
},
{
name: "eclipse",
image:
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/eclipse.png",
},
{
name: "elegant",
image:
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/elegant.png",
},
{
name: "erlang-dark",
image:
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/erlang-dark.png",
},
{
name: "gruvbox-dark",
image:
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/gruvbox-dark.png",
},
{
name: "hopscotch",
image:
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/hopscotch.png",
},
{
name: "icecoder",
image:
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/icecoder.png",
},
{
name: "idea",
image:
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/idea.png",
},
{
name: "isotope",
image:
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/isotope.png",
},
{
name: "lesser-dark",
image:
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/lesser-dark.png",
},
{
name: "liquibyte",
image:
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/liquibyte.png",
},
{
name: "lucario",
image:
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/lucario.png",
},
{
name: "material",
image:
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/material.png",
},
{
name: "mbo",
image:
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/mbo.png",
},
{
name: "mdn-like",
image:
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/mdn-like.png",
},
{
name: "midnight",
image:
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/midnight.png",
},
{
name: "monokai",
image:
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/monokai.png",
},
{
name: "neat",
image:
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/neat.png",
},
{
name: "neo",
image:
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/neo.png",
},
{
name: "night",
image:
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/night.png",
},
{
name: "oceanic-next",
image:
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/oceanic-next.png",
},
{
name: "panda-syntax",
image:
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/panda-syntax.png",
},
{
name: "paraiso-dark",
image:
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/paraiso-dark.png",
},
{
name: "paraiso-light",
image:
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/paraiso-light.png",
},
{
name: "pastel-on-dark",
image:
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/pastel-on-dark.png",
},
{
name: "railscasts",
image:
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/railscasts.png",
},
{
name: "rubyblue",
image:
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/rubyblue.png",
},
{
name: "seti",
image:
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/seti.png",
},
{
name: "shadowfox",
image:
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/shadowfox.png",
},
{
name: "solarized",
image:
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/solarized.png",
},
{
name: "the-matrix",
image:
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/the-matrix.png",
},
{
name: "tomorrow-night-bright",
image:
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/tomorrow-night-bright.png",
},
{
name: "tomorrow-night-eighties",
image:
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/tomorrow-night-eighties.png",
},
{
name: "ttcn",
image:
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/ttcn.png",
},
{
name: "twilight",
image:
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/twilight.png",
},
{
name: "vibrant-ink",
image:
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/vibrant-ink.png",
},
{
name: "xq-dark",
image:
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/xq-dark.png",
},
{
name: "xq-light",
image:
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/xq-light.png",
},
{
name: "yeti",
image:
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/yeti.png",
},
{
name: "yonce",
image:
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/yonce.png",
},
{
name: "zenburn",
image:
"https://raw.githubusercontent.com/codemirror/CodeMirror/master/theme/zenburn.png",
},
];
}

View File

@@ -0,0 +1,20 @@
export default function () {
return [
{
name: "English",
value: "en",
flagCode: "GB",
default: true,
},
{
name: "Malay",
value: "ms",
flagCode: "MY",
},
{
name: "Chinese",
value: "cn",
flagCode: "CN",
}
];
}

55
composables/themeList.js Normal file
View File

@@ -0,0 +1,55 @@
export default function () {
return [
{
theme: "biasa",
colors: [
{
name: "primary",
value: "243, 88, 106",
},
{
name: "secondary",
value: "240, 122, 37",
},
{
name: "accent",
value: "243, 244, 246",
},
],
},
{
theme: "gelap",
colors: [
{
name: "primary",
value: "243, 88, 106",
},
{
name: "secondary",
value: "240, 122, 37",
},
{
name: "accent",
value: "15, 23, 42",
},
],
},
{
theme: "LZS",
colors: [
{
name: "primary",
value: "0, 90, 173", // #005AAD - Blue
},
{
name: "secondary",
value: "141, 199, 61", // #8DC73D - Green
},
{
name: "accent",
value: "255, 242, 0", // #FFF200 - Yellow
},
],
},
];
}

129
composables/themeList2.js Normal file
View File

@@ -0,0 +1,129 @@
export default function () {
return [
{
theme: "biru",
colors: [
{
name: "primary",
value: "0, 102, 204", // Strong blue
},
{
name: "secondary",
value: "51, 153, 255", // Lighter blue
},
{
name: "accent",
value: "255, 204, 0", // Gold
},
{
name: "background",
value: "240, 248, 255", // Alice blue
},
{
name: "text",
value: "0, 0, 0", // Black
},
],
},
{
theme: "merah",
colors: [
{
name: "primary",
value: "204, 0, 0", // Strong red
},
{
name: "secondary",
value: "255, 102, 102", // Lighter red
},
{
name: "accent",
value: "255, 255, 153", // Light yellow
},
{
name: "background",
value: "255, 240, 240", // Very light pink
},
{
name: "text",
value: "0, 0, 0", // Black
},
],
},
{
theme: "ungu",
colors: [
{
name: "primary",
value: "75, 0, 130", // Indigo
},
{
name: "secondary",
value: "138, 43, 226", // Blue violet
},
{
name: "accent",
value: "255, 215, 0", // Gold
},
{
name: "background",
value: "240, 248, 255", // Alice blue
},
{
name: "text",
value: "0, 0, 0", // Black
},
],
},
{
theme: "oren",
colors: [
{
name: "primary",
value: "255, 103, 0", // Dark orange
},
{
name: "secondary",
value: "255, 159, 64", // Lighter orange
},
{
name: "accent",
value: "0, 128, 128", // Teal
},
{
name: "background",
value: "255, 250, 240", // Floral white
},
{
name: "text",
value: "0, 0, 0", // Black
},
],
},
{
theme: "LZS",
colors: [
{
name: "primary",
value: "0, 90, 173", // #005AAD - Blue
},
{
name: "secondary",
value: "141, 199, 61", // #8DC73D - Green
},
{
name: "accent",
value: "255, 242, 0", // #FFF200 - Yellow
},
{
name: "background",
value: "245, 250, 255", // Very light blue background
},
{
name: "text",
value: "0, 0, 0", // Black
},
],
},
];
}

View File

@@ -0,0 +1,87 @@
import { ref, onUnmounted } from 'vue';
/**
* Creates a debounced version of a function
* @param {Function} fn - The function to debounce
* @param {number} delay - The delay in milliseconds
* @returns {Function} - The debounced function
*/
export const useDebounceFn = (fn, delay = 300) => {
if (typeof fn !== 'function') {
throw new Error('First argument must be a function');
}
// Store the timeout ID for cleanup
const timeoutId = ref(null);
// Clean up any pending timeouts when the component is unmounted
onUnmounted(() => {
if (timeoutId.value) {
clearTimeout(timeoutId.value);
}
});
// Return the debounced function
return (...args) => {
// Clear previous timeout if exists
if (timeoutId.value) {
clearTimeout(timeoutId.value);
}
// Set a new timeout
timeoutId.value = setTimeout(() => {
fn(...args);
timeoutId.value = null;
}, delay);
};
};
/**
* Creates a throttled version of a function
* @param {Function} fn - The function to throttle
* @param {number} delay - The delay in milliseconds
* @returns {Function} - The throttled function
*/
export const useThrottleFn = (fn, delay = 300) => {
if (typeof fn !== 'function') {
throw new Error('First argument must be a function');
}
// Store the last execution time
const lastExecution = ref(0);
// Store the timeout ID for cleanup
const timeoutId = ref(null);
// Clean up any pending timeouts when the component is unmounted
onUnmounted(() => {
if (timeoutId.value) {
clearTimeout(timeoutId.value);
}
});
// Return the throttled function
return (...args) => {
const now = Date.now();
const elapsed = now - lastExecution.value;
// Clear any existing timeout
if (timeoutId.value) {
clearTimeout(timeoutId.value);
}
// If enough time has passed, execute immediately
if (elapsed >= delay) {
lastExecution.value = now;
fn(...args);
} else {
// Otherwise, schedule execution for when the delay has passed
timeoutId.value = setTimeout(() => {
lastExecution.value = Date.now();
fn(...args);
timeoutId.value = null;
}, delay - elapsed);
}
};
};
export default useDebounceFn;

View File

@@ -0,0 +1,411 @@
import { ref } from "vue";
export const useNotificationDelivery = () => {
const isLoading = ref(false);
const error = ref(null);
const deliveryStats = ref(null);
const emailConfig = ref(null);
const pushConfig = ref(null);
const smsConfig = ref(null);
const deliverySettings = ref(null);
const deliveryProviders = ref([]);
const deliveryMetrics = ref({
sent: 0,
delivered: 0,
failed: 0,
pending: 0
});
// Reset error state
const resetError = () => {
error.value = null;
};
// Fetch delivery statistics
const fetchDeliveryStats = async () => {
isLoading.value = true;
error.value = null;
try {
const response = await $fetch("/api/notifications/delivery/stats");
if (response.success) {
deliveryStats.value = response.data;
// Update metrics from stats
deliveryMetrics.value = {
sent: response.data.totalSent || 0,
delivered: response.data.totalDelivered || 0,
failed: response.data.totalFailed || 0,
pending: response.data.totalPending || 0
};
return response.data;
} else {
throw new Error(response.message || "Failed to fetch delivery stats");
}
} catch (err) {
error.value = err.data?.message || err.message || "Failed to fetch delivery stats";
throw error.value;
} finally {
isLoading.value = false;
}
};
// Fetch email configuration
const fetchEmailConfig = async () => {
isLoading.value = true;
error.value = null;
try {
const response = await $fetch("/api/notifications/delivery/email-config");
if (response.success) {
emailConfig.value = response.data;
return response.data;
} else {
throw new Error(response.message || "Failed to fetch email configuration");
}
} catch (err) {
error.value = err.data?.message || err.message || "Failed to fetch email configuration";
throw error.value;
} finally {
isLoading.value = false;
}
};
// Fetch push notification configuration
const fetchPushConfig = async () => {
isLoading.value = true;
error.value = null;
try {
const response = await $fetch("/api/notifications/delivery/push-config");
if (response.success) {
pushConfig.value = response.data;
return response.data;
} else {
throw new Error(response.message || "Failed to fetch push configuration");
}
} catch (err) {
error.value = err.data?.message || err.message || "Failed to fetch push configuration";
throw error.value;
} finally {
isLoading.value = false;
}
};
// Fetch SMS configuration
const fetchSmsConfig = async () => {
isLoading.value = true;
error.value = null;
try {
const response = await $fetch("/api/notifications/delivery/sms-config");
if (response.success) {
smsConfig.value = response.data;
return response.data;
} else {
throw new Error(response.message || "Failed to fetch SMS configuration");
}
} catch (err) {
error.value = err.data?.message || err.message || "Failed to fetch SMS configuration";
throw error.value;
} finally {
isLoading.value = false;
}
};
// Fetch delivery settings
const fetchDeliverySettings = async () => {
isLoading.value = true;
error.value = null;
try {
const response = await $fetch("/api/notifications/delivery/settings");
if (response.success) {
deliverySettings.value = response.data;
return response.data;
} else {
throw new Error(response.message || "Failed to fetch delivery settings");
}
} catch (err) {
error.value = err.data?.message || err.message || "Failed to fetch delivery settings";
throw error.value;
} finally {
isLoading.value = false;
}
};
// Fetch all delivery providers
const fetchDeliveryProviders = async () => {
isLoading.value = true;
error.value = null;
try {
const response = await $fetch("/api/notifications/delivery/providers");
if (response.success) {
deliveryProviders.value = response.data;
return response.data;
} else {
throw new Error(response.message || "Failed to fetch delivery providers");
}
} catch (err) {
error.value = err.data?.message || err.message || "Failed to fetch delivery providers";
throw error.value;
} finally {
isLoading.value = false;
}
};
// Fetch delivery performance metrics
const fetchDeliveryPerformance = async (period = 'last_30_days') => {
isLoading.value = true;
error.value = null;
try {
const response = await $fetch(`/api/notifications/delivery/performance?period=${period}`);
if (response.success) {
return response.data;
} else {
throw new Error(response.message || "Failed to fetch delivery performance");
}
} catch (err) {
error.value = err.data?.message || err.message || "Failed to fetch delivery performance";
throw error.value;
} finally {
isLoading.value = false;
}
};
// Test delivery configuration
const testDeliveryConfig = async (channel, config) => {
isLoading.value = true;
error.value = null;
try {
const response = await $fetch("/api/notifications/delivery/test", {
method: "POST",
body: {
channel,
config
}
});
if (response.success) {
return {
success: true,
message: response.message || "Test successful"
};
} else {
throw new Error(response.message || `Failed to test ${channel} configuration`);
}
} catch (err) {
error.value = err.data?.message || err.message || `Failed to test ${channel} configuration`;
throw error.value;
} finally {
isLoading.value = false;
}
};
// Update email configuration
const updateEmailConfig = async (config) => {
isLoading.value = true;
error.value = null;
try {
const response = await $fetch("/api/notifications/delivery/email-config", {
method: "PUT",
body: config,
});
if (response.success) {
// Update local state
emailConfig.value = response.data;
return response.data;
} else {
throw new Error(response.message || "Failed to update email configuration");
}
} catch (err) {
error.value = err.data?.message || err.message || "Failed to update email configuration";
throw error.value;
} finally {
isLoading.value = false;
}
};
// Update push notification configuration
const updatePushConfig = async (config) => {
isLoading.value = true;
error.value = null;
try {
const response = await $fetch("/api/notifications/delivery/push-config", {
method: "PUT",
body: config,
});
if (response.success) {
// Update local state
pushConfig.value = response.data;
return response.data;
} else {
throw new Error(response.message || "Failed to update push configuration");
}
} catch (err) {
error.value = err.data?.message || err.message || "Failed to update push configuration";
throw error.value;
} finally {
isLoading.value = false;
}
};
// Update SMS configuration
const updateSmsConfig = async (config) => {
isLoading.value = true;
error.value = null;
try {
const response = await $fetch("/api/notifications/delivery/sms-config", {
method: "PUT",
body: config,
});
if (response.success) {
// Update local state
smsConfig.value = response.data;
return response.data;
} else {
throw new Error(response.message || "Failed to update SMS configuration");
}
} catch (err) {
error.value = err.data?.message || err.message || "Failed to update SMS configuration";
throw error.value;
} finally {
isLoading.value = false;
}
};
// Update delivery settings
const updateDeliverySettings = async (settings) => {
isLoading.value = true;
error.value = null;
try {
const response = await $fetch("/api/notifications/delivery/settings", {
method: "PUT",
body: settings,
});
if (response.success) {
// Update local state
deliverySettings.value = response.data;
return response.data;
} else {
throw new Error(response.message || "Failed to update delivery settings");
}
} catch (err) {
error.value = err.data?.message || err.message || "Failed to update delivery settings";
throw error.value;
} finally {
isLoading.value = false;
}
};
// Retry failed deliveries
const retryFailedDeliveries = async (notificationId = null) => {
isLoading.value = true;
error.value = null;
try {
const endpoint = notificationId
? `/api/notifications/delivery/retry/${notificationId}`
: "/api/notifications/delivery/retry";
const response = await $fetch(endpoint, {
method: "POST"
});
if (response.success) {
return {
success: true,
message: response.message || "Retry initiated successfully",
data: response.data
};
} else {
throw new Error(response.message || "Failed to retry deliveries");
}
} catch (err) {
error.value = err.data?.message || err.message || "Failed to retry deliveries";
throw error.value;
} finally {
isLoading.value = false;
}
};
// Fetch delivery logs
const fetchDeliveryLogs = async (params = {}) => {
isLoading.value = true;
error.value = null;
try {
const queryString = new URLSearchParams();
if (params.page) queryString.append("page", params.page);
if (params.limit) queryString.append("limit", params.limit);
if (params.status) queryString.append("status", params.status);
if (params.channel) queryString.append("channel", params.channel);
if (params.notificationId) queryString.append("notificationId", params.notificationId);
if (params.startDate) queryString.append("startDate", params.startDate);
if (params.endDate) queryString.append("endDate", params.endDate);
const response = await $fetch(`/api/notifications/delivery/logs?${queryString.toString()}`);
if (response.success) {
return response.data;
} else {
throw new Error(response.message || "Failed to fetch delivery logs");
}
} catch (err) {
error.value = err.data?.message || err.message || "Failed to fetch delivery logs";
throw error.value;
} finally {
isLoading.value = false;
}
};
return {
// State
isLoading,
error,
deliveryStats,
emailConfig,
pushConfig,
smsConfig,
deliverySettings,
deliveryProviders,
deliveryMetrics,
// Methods
resetError,
fetchDeliveryStats,
fetchEmailConfig,
fetchPushConfig,
fetchSmsConfig,
fetchDeliverySettings,
fetchDeliveryProviders,
fetchDeliveryPerformance,
fetchDeliveryLogs,
testDeliveryConfig,
updateEmailConfig,
updatePushConfig,
updateSmsConfig,
updateDeliverySettings,
retryFailedDeliveries
};
};

View File

@@ -0,0 +1,301 @@
import { ref } from 'vue'
export const useNotificationLogs = () => {
// Use Nuxt's useFetch instead of $fetch
const config = useRuntimeConfig()
// Logs data state
const logs = ref([])
const loading = ref(false)
const error = ref(null)
const pagination = ref({
page: 1,
limit: 10,
total: 0,
pages: 1
})
const summaryStats = ref({
totalLogs: 0,
failedDeliveries: 0,
successfulDeliveries: 0,
successRate: 0
})
// Filters state
const filters = ref({
startDate: null,
endDate: null,
action: null,
channel: null,
status: null,
actor: '',
keyword: ''
})
// Analytics state
const analyticsData = ref(null)
const analyticsLoading = ref(false)
const analyticsError = ref(null)
// Monitoring state
const monitoringData = ref(null)
const monitoringLoading = ref(false)
const monitoringError = ref(null)
// Fetch logs with filters and pagination
const fetchLogs = async () => {
try {
loading.value = true
error.value = null
// Build query parameters
const queryParams = new URLSearchParams()
queryParams.append('page', pagination.value.page)
queryParams.append('limit', pagination.value.limit)
// Add filters if they exist
if (filters.value.startDate) queryParams.append('startDate', filters.value.startDate)
if (filters.value.endDate) queryParams.append('endDate', filters.value.endDate)
if (filters.value.action) queryParams.append('action', filters.value.action)
if (filters.value.channel) queryParams.append('channel', filters.value.channel)
if (filters.value.status) queryParams.append('status', filters.value.status)
if (filters.value.actor) queryParams.append('actor', filters.value.actor)
if (filters.value.keyword) queryParams.append('keyword', filters.value.keyword)
// Make API call with useFetch
const { data, error: fetchError } = await useFetch(`/api/notifications/logs`, {
method: 'GET',
params: {
page: pagination.value.page,
limit: pagination.value.limit,
...filters.value
}
})
if (fetchError.value) {
throw new Error(fetchError.value.message || 'Failed to fetch logs');
}
if (data.value && data.value.body && data.value.body.success) {
logs.value = data.value.body.data.logs || [];
pagination.value = data.value.body.data.pagination || pagination.value;
// Ensure summary stats are properly assigned
if (data.value.body.data.summary) {
summaryStats.value = {
totalLogs: data.value.body.data.summary.totalLogs || 0,
failedDeliveries: data.value.body.data.summary.failedDeliveries || 0,
successfulDeliveries: data.value.body.data.summary.successfulDeliveries || 0,
successRate: data.value.body.data.summary.successRate || 0
};
}
console.log('Logs fetched successfully:', {
logsCount: logs.value.length,
pagination: pagination.value,
summaryStats: summaryStats.value
});
} else {
throw new Error('Failed to fetch logs: ' + (data.value?.body?.message || 'Unknown error'))
}
} catch (err) {
error.value = err.message || 'An error occurred'
console.error('Error fetching logs:', err)
} finally {
loading.value = false
}
}
// Get a specific log by ID
const getLogById = async (id) => {
try {
loading.value = true
error.value = null
// Make API call
const { data } = await useFetch(`/api/notifications/logs/${id}`, {
method: 'GET'
})
if (data.value && data.value.body && data.value.body.success) {
return data.value.body.data
} else {
throw new Error('Failed to fetch log details')
}
} catch (err) {
error.value = err.message || 'An error occurred'
console.error('Error fetching log details:', err)
return null
} finally {
loading.value = false
}
}
// Update filters and fetch logs
const applyFilters = async (newFilters) => {
filters.value = { ...filters.value, ...newFilters }
pagination.value.page = 1 // Reset to first page when filters change
await fetchLogs()
}
// Clear all filters
const clearFilters = async () => {
filters.value = {
startDate: null,
endDate: null,
action: null,
channel: null,
status: null,
actor: '',
keyword: ''
}
pagination.value.page = 1
await fetchLogs()
}
// Change page
const changePage = async (page) => {
pagination.value.page = page
await fetchLogs()
}
// Fetch analytics data
const fetchAnalytics = async (period = '7d', channel = 'all') => {
try {
analyticsLoading.value = true
analyticsError.value = null
// Make API call
const { data } = await useFetch(`/api/notifications/logs/analytics`, {
method: 'GET',
params: {
period,
channel
}
})
if (data.value && data.value.body && data.value.body.success) {
analyticsData.value = data.value.body.data
} else {
throw new Error('Failed to fetch analytics data')
}
} catch (err) {
analyticsError.value = err.message || 'An error occurred'
console.error('Error fetching analytics data:', err)
} finally {
analyticsLoading.value = false
}
}
// Fetch monitoring data
const fetchMonitoringData = async () => {
try {
monitoringLoading.value = true
monitoringError.value = null
// Make API call
const { data } = await useFetch('/api/notifications/logs/monitoring', {
method: 'GET'
})
if (data.value && data.value.body && data.value.body.success) {
monitoringData.value = data.value.body.data
} else {
throw new Error('Failed to fetch monitoring data')
}
} catch (err) {
monitoringError.value = err.message || 'An error occurred'
console.error('Error fetching monitoring data:', err)
} finally {
monitoringLoading.value = false
}
}
// Format date for display
const formatDate = (date, includeTime = false) => {
if (!date) return ''
const options = { year: 'numeric', month: 'short', day: 'numeric' }
if (includeTime) {
options.hour = '2-digit'
options.minute = '2-digit'
}
return new Date(date).toLocaleDateString(undefined, options)
}
// Format time ago (e.g., "2 minutes ago")
const formatTimeAgo = (timestamp) => {
const now = new Date()
const time = new Date(timestamp)
const diffInMinutes = Math.floor((now - time) / (1000 * 60))
if (diffInMinutes < 1) return "Just now"
if (diffInMinutes < 60) return `${diffInMinutes} minutes ago`
if (diffInMinutes < 1440) return `${Math.floor(diffInMinutes / 60)} hours ago`
return `${Math.floor(diffInMinutes / 1440)} days ago`
}
// Get available actions for filters
const availableActions = [
{ label: 'All Actions', value: null },
{ label: 'Notification Created', value: 'Notification Created' },
{ label: 'Notification Sent', value: 'Notification Sent' },
{ label: 'Delivery Attempted', value: 'Delivery Attempted' },
{ label: 'Delivery Failed', value: 'Delivery Failed' },
{ label: 'Notification Opened', value: 'Notification Opened' },
{ label: 'Template Updated', value: 'Template Updated' },
{ label: 'Notification Queued', value: 'Notification Queued' },
]
// Get available channels for filters
const availableChannels = [
{ label: 'All Channels', value: null },
{ label: 'Email', value: 'Email' },
{ label: 'SMS', value: 'SMS' },
{ label: 'Push Notification', value: 'Push Notification' },
{ label: 'Webhook', value: 'Webhook' }
]
// Get available statuses for filters
const availableStatuses = [
{ label: 'All Statuses', value: null },
{ label: 'Sent', value: 'Sent' },
{ label: 'Failed', value: 'Failed' },
{ label: 'Bounced', value: 'Bounced' },
{ label: 'Opened', value: 'Opened' },
{ label: 'Queued', value: 'Queued' },
{ label: 'Created', value: 'Created' },
{ label: 'Updated', value: 'Updated' }
]
return {
// State
logs,
loading,
error,
pagination,
summaryStats,
filters,
analyticsData,
analyticsLoading,
analyticsError,
monitoringData,
monitoringLoading,
monitoringError,
// Methods
fetchLogs,
getLogById,
applyFilters,
clearFilters,
changePage,
fetchAnalytics,
fetchMonitoringData,
formatDate,
formatTimeAgo,
// Filter options
availableActions,
availableChannels,
availableStatuses
}
}

View File

@@ -0,0 +1,200 @@
import { ref } from "vue";
export const useNotifications = () => {
const isLoading = ref(false);
const notifications = ref([]);
const pagination = ref(null);
const error = ref(null);
// Fetch notifications list
const fetchNotifications = async (options = {}) => {
isLoading.value = true;
error.value = null;
try {
const queryParams = new URLSearchParams();
console.log("Query Params:", queryParams);
// Add query parameters
if (options.page) queryParams.append("page", options.page);
if (options.limit) queryParams.append("limit", options.limit);
if (options.status) queryParams.append("status", options.status);
if (options.priority) queryParams.append("priority", options.priority);
if (options.category) queryParams.append("category", options.category);
if (options.search) queryParams.append("search", options.search);
// Convert camelCase to snake_case for sort fields
if (options.sortBy) {
// Convert camelCase to snake_case (e.g., createdAt -> created_at)
const snakeCaseField = options.sortBy.replace(/([A-Z])/g, "_$1").toLowerCase();
queryParams.append("sortBy", snakeCaseField);
}
if (options.sortOrder) queryParams.append("sortOrder", options.sortOrder);
const url = `/api/notifications/list${
queryParams.toString() ? "?" + queryParams.toString() : ""
}`;
const { data } = await $fetch(url);
notifications.value = data.notifications;
pagination.value = data.pagination;
return data;
} catch (err) {
error.value =
err.data?.message || err.message || "Failed to fetch notifications";
throw err;
} finally {
isLoading.value = false;
}
};
// Delete notification
const deleteNotification = async (notificationId) => {
try {
const response = await $fetch(`/api/notifications/${notificationId}`, {
method: "DELETE",
});
// Remove from local state
notifications.value = notifications.value.filter(
(n) => n.id !== notificationId
);
return response;
} catch (err) {
error.value =
err.data?.message || err.message || "Failed to delete notification";
throw err;
}
};
// Get notification by ID
const getNotificationById = async (notificationId) => {
isLoading.value = true;
error.value = null;
console.log("Notification ID:", notificationId);
try {
const response = await $fetch(`/api/notifications/${notificationId}`);
return response.data;
} catch (err) {
error.value =
err.data?.message || err.message || "Failed to fetch notification";
throw err;
} finally {
isLoading.value = false;
}
};
// Create notification
const createNotification = async (notificationData) => {
isLoading.value = true;
error.value = null;
try {
const response = await $fetch("/api/notifications", {
method: "POST",
body: notificationData,
});
return response;
} catch (err) {
error.value =
err.data?.message || err.message || "Failed to create notification";
throw err;
} finally {
isLoading.value = false;
}
};
// Update notification
const updateNotification = async (notificationId, notificationData) => {
isLoading.value = true;
error.value = null;
try {
const response = await $fetch(`/api/notifications/${notificationId}`, {
method: "PUT",
body: notificationData,
});
return response;
} catch (err) {
error.value =
err.data?.message || err.message || "Failed to update notification";
throw err;
} finally {
isLoading.value = false;
}
};
// Save draft
const saveDraft = async (draftData) => {
try {
const response = await $fetch("/api/notifications/draft", {
method: "POST",
body: draftData,
});
return response;
} catch (err) {
error.value = err.data?.message || err.message || "Failed to save draft";
throw err;
}
};
// Test send notification
const testSendNotification = async (testData) => {
try {
const response = await $fetch("/api/notifications/test-send", {
method: "POST",
body: testData,
});
return response;
} catch (err) {
error.value =
err.data?.message || err.message || "Failed to send test notification";
throw err;
}
};
// Get audience preview
const getAudiencePreview = async (audienceData) => {
try {
const response = await $fetch("/api/notifications/audience-preview", {
method: "POST",
body: audienceData,
});
return response;
} catch (err) {
error.value =
err.data?.message || err.message || "Failed to get audience preview";
throw err;
}
};
return {
// State
isLoading,
notifications,
pagination,
error,
// Actions
fetchNotifications,
deleteNotification,
getNotificationById,
createNotification,
updateNotification,
saveDraft,
testSendNotification,
getAudiencePreview,
};
};

View File

@@ -0,0 +1,363 @@
export const useSiteSettings = () => {
// Global site settings state
const siteSettings = useState('siteSettings', () => ({
siteName: 'corradAF',
siteDescription: 'corradAF Base Project',
siteLogo: '',
siteLoginLogo: '',
siteLoadingLogo: '',
siteFavicon: '',
showSiteNameInHeader: true,
siteNameFontSize: 18,
customCSS: '',
selectedTheme: 'biasa', // Use existing theme system
customThemeFile: '',
currentFont: '',
fontSource: '',
// SEO fields
seoTitle: '',
seoDescription: '',
seoKeywords: '',
seoAuthor: '',
seoOgImage: '',
seoTwitterCard: 'summary_large_image',
seoCanonicalUrl: '',
seoRobots: 'index, follow',
seoGoogleAnalytics: '',
seoGoogleTagManager: '',
seoFacebookPixel: ''
}));
// Loading state
const loading = useState('siteSettingsLoading', () => false);
// Load site settings from API
const loadSiteSettings = async () => {
loading.value = true;
try {
const response = await $fetch("/api/devtool/config/site-settings", {
method: "GET",
});
if (response && response.data) {
siteSettings.value = { ...siteSettings.value, ...response.data };
applyThemeSettings();
updateGlobalMeta();
}
} catch (error) {
console.error("Error loading site settings:", error);
} finally {
loading.value = false;
}
};
// Update global meta tags and SEO
const updateGlobalMeta = () => {
if (process.client) {
// Update page title - use SEO title if available
const title = siteSettings.value.seoTitle || siteSettings.value.siteName;
if (title) {
document.title = title;
// Update meta description - use SEO description if available
let metaDescription = document.querySelector('meta[name="description"]');
if (!metaDescription) {
metaDescription = document.createElement('meta');
metaDescription.name = 'description';
document.head.appendChild(metaDescription);
}
metaDescription.content = siteSettings.value.seoDescription || siteSettings.value.siteDescription || title;
// Update keywords meta tag
if (siteSettings.value.seoKeywords) {
let keywordsMeta = document.querySelector('meta[name="keywords"]');
if (!keywordsMeta) {
keywordsMeta = document.createElement('meta');
keywordsMeta.name = 'keywords';
document.head.appendChild(keywordsMeta);
}
keywordsMeta.content = siteSettings.value.seoKeywords;
}
// Update author meta tag
if (siteSettings.value.seoAuthor) {
let authorMeta = document.querySelector('meta[name="author"]');
if (!authorMeta) {
authorMeta = document.createElement('meta');
authorMeta.name = 'author';
document.head.appendChild(authorMeta);
}
authorMeta.content = siteSettings.value.seoAuthor;
}
// Update robots meta tag
let robotsMeta = document.querySelector('meta[name="robots"]');
if (!robotsMeta) {
robotsMeta = document.createElement('meta');
robotsMeta.name = 'robots';
document.head.appendChild(robotsMeta);
}
robotsMeta.content = siteSettings.value.seoRobots;
// Update Open Graph tags
let ogTitle = document.querySelector('meta[property="og:title"]');
if (!ogTitle) {
ogTitle = document.createElement('meta');
ogTitle.setAttribute('property', 'og:title');
document.head.appendChild(ogTitle);
}
ogTitle.content = title;
let ogDescription = document.querySelector('meta[property="og:description"]');
if (!ogDescription) {
ogDescription = document.createElement('meta');
ogDescription.setAttribute('property', 'og:description');
document.head.appendChild(ogDescription);
}
ogDescription.content = siteSettings.value.seoDescription || siteSettings.value.siteDescription || title;
// Update OG image
if (siteSettings.value.seoOgImage) {
let ogImage = document.querySelector('meta[property="og:image"]');
if (!ogImage) {
ogImage = document.createElement('meta');
ogImage.setAttribute('property', 'og:image');
document.head.appendChild(ogImage);
}
ogImage.content = siteSettings.value.seoOgImage;
}
// Update Twitter Card tags
let twitterCard = document.querySelector('meta[name="twitter:card"]');
if (!twitterCard) {
twitterCard = document.createElement('meta');
twitterCard.name = 'twitter:card';
document.head.appendChild(twitterCard);
}
twitterCard.content = siteSettings.value.seoTwitterCard;
let twitterTitle = document.querySelector('meta[name="twitter:title"]');
if (!twitterTitle) {
twitterTitle = document.createElement('meta');
twitterTitle.name = 'twitter:title';
document.head.appendChild(twitterTitle);
}
twitterTitle.content = title;
let twitterDescription = document.querySelector('meta[name="twitter:description"]');
if (!twitterDescription) {
twitterDescription = document.createElement('meta');
twitterDescription.name = 'twitter:description';
document.head.appendChild(twitterDescription);
}
twitterDescription.content = siteSettings.value.seoDescription || siteSettings.value.siteDescription || title;
// Update canonical URL
if (siteSettings.value.seoCanonicalUrl) {
let canonicalLink = document.querySelector('link[rel="canonical"]');
if (!canonicalLink) {
canonicalLink = document.createElement('link');
canonicalLink.rel = 'canonical';
document.head.appendChild(canonicalLink);
}
canonicalLink.href = siteSettings.value.seoCanonicalUrl;
}
}
// Update favicon
if (siteSettings.value.siteFavicon) {
let faviconLink = document.querySelector("link[rel*='icon']");
if (!faviconLink) {
faviconLink = document.createElement('link');
faviconLink.rel = 'icon';
document.head.appendChild(faviconLink);
}
faviconLink.href = siteSettings.value.siteFavicon;
// Update apple touch icon
let appleTouchIcon = document.querySelector("link[rel='apple-touch-icon']");
if (!appleTouchIcon) {
appleTouchIcon = document.createElement('link');
appleTouchIcon.rel = 'apple-touch-icon';
document.head.appendChild(appleTouchIcon);
}
appleTouchIcon.href = siteSettings.value.siteFavicon;
}
// Apply analytics scripts
if (siteSettings.value.seoGoogleAnalytics) {
// Add Google Analytics
const script = document.createElement('script');
script.async = true;
script.src = `https://www.googletagmanager.com/gtag/js?id=${siteSettings.value.seoGoogleAnalytics}`;
document.head.appendChild(script);
const gtag = document.createElement('script');
gtag.textContent = `
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '${siteSettings.value.seoGoogleAnalytics}');
`;
document.head.appendChild(gtag);
}
if (siteSettings.value.seoGoogleTagManager) {
// Add Google Tag Manager
const gtmScript = document.createElement('script');
gtmScript.textContent = `
(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','${siteSettings.value.seoGoogleTagManager}');
`;
document.head.appendChild(gtmScript);
}
if (siteSettings.value.seoFacebookPixel) {
// Add Facebook Pixel
const fbScript = document.createElement('script');
fbScript.textContent = `
!function(f,b,e,v,n,t,s)
{if(f.fbq)return;n=f.fbq=function(){n.callMethod?
n.callMethod.apply(n,arguments):n.queue.push(arguments)};
if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';
n.queue=[];t=b.createElement(e);t.async=!0;
t.src=v;s=b.getElementsByTagName(e)[0];
s.parentNode.insertBefore(t,s)}(window, document,'script',
'https://connect.facebook.net/en_US/fbevents.js');
fbq('init', '${siteSettings.value.seoFacebookPixel}');
fbq('track', 'PageView');
`;
document.head.appendChild(fbScript);
}
}
};
// Apply theme settings to the document
const applyThemeSettings = () => {
if (process.client) {
// Apply selected theme using existing theme system
if (siteSettings.value.selectedTheme) {
document.documentElement.setAttribute("data-theme", siteSettings.value.selectedTheme);
localStorage.setItem("theme", siteSettings.value.selectedTheme);
}
// Apply custom theme file if exists (append to theme.css)
if (siteSettings.value.customThemeFile) {
let customThemeElement = document.getElementById('custom-theme-file');
if (!customThemeElement) {
customThemeElement = document.createElement('link');
customThemeElement.id = 'custom-theme-file';
customThemeElement.rel = 'stylesheet';
customThemeElement.type = 'text/css';
document.head.appendChild(customThemeElement);
}
customThemeElement.href = siteSettings.value.customThemeFile;
} else {
// Remove custom theme file if it exists
const existingThemeElement = document.getElementById('custom-theme-file');
if (existingThemeElement) {
existingThemeElement.remove();
}
}
// Apply custom CSS
let customStyleElement = document.getElementById('custom-site-styles');
if (!customStyleElement) {
customStyleElement = document.createElement('style');
customStyleElement.id = 'custom-site-styles';
document.head.appendChild(customStyleElement);
}
customStyleElement.textContent = siteSettings.value.customCSS || '';
}
};
// Set theme (integrate with existing theme system)
const setTheme = (theme) => {
siteSettings.value.selectedTheme = theme;
applyThemeSettings();
// Optionally save to server
updateSiteSettings(siteSettings.value);
};
// Get current theme
const getCurrentTheme = () => {
return siteSettings.value.selectedTheme || 'biasa';
};
// Update site settings
const updateSiteSettings = async (newSettings) => {
console.log("[useSiteSettings] updateSiteSettings called with:", JSON.parse(JSON.stringify(newSettings)));
try {
const response = await $fetch("/api/devtool/config/site-settings", {
method: "POST",
body: newSettings,
});
console.log("[useSiteSettings] API response received:", JSON.parse(JSON.stringify(response)));
if (response && response.data) {
siteSettings.value = { ...siteSettings.value, ...response.data };
applyThemeSettings();
updateGlobalMeta();
console.log("[useSiteSettings] Returning success from updateSiteSettings.");
return { success: true, data: response.data };
}
let errorMessage = "Update operation failed: No data returned from server.";
if (response && typeof response === 'object' && response !== null && 'message' in response) {
errorMessage = response.message;
} else if (response) {
errorMessage = "Update operation failed: Unexpected server response format on success.";
} else if (response === undefined) {
errorMessage = "Update failed: Server returned no content (e.g. 204). Treating as failure as data is expected for settings.";
console.log("[useSiteSettings] Returning failure (204 or undefined response) from updateSiteSettings.");
return { success: false, error: { message: errorMessage, details: response } };
}
console.log("[useSiteSettings] Returning failure (general case) from updateSiteSettings:", errorMessage);
return { success: false, error: { message: errorMessage, details: response } };
} catch (error) {
console.error("[useSiteSettings] Error in updateSiteSettings catch block:", error);
let detailedMessage = "An unexpected error occurred during update.";
if (error.data && error.data.message) {
detailedMessage = error.data.message;
} else if (error.message) {
detailedMessage = error.message;
}
console.log("[useSiteSettings] Returning failure (catch block) from updateSiteSettings:", detailedMessage);
return { success: false, error: { message: detailedMessage, details: error } };
}
};
// Add custom theme to theme.css file
const addCustomThemeToFile = async (themeName, themeCSS) => {
try {
const response = await $fetch("/api/devtool/config/add-custom-theme", {
method: "POST",
body: {
themeName,
themeCSS
}
});
return response;
} catch (error) {
console.error("Error adding custom theme:", error);
return { success: false, error };
}
};
return {
siteSettings,
loading: readonly(loading),
loadSiteSettings,
updateSiteSettings,
applyThemeSettings,
updateGlobalMeta,
getCurrentTheme,
setTheme,
addCustomThemeToFile
};
};

61
composables/useToast.js Normal file
View File

@@ -0,0 +1,61 @@
import { useNuxtApp } from "#app";
export const useToast = () => {
const { $swal } = useNuxtApp();
const toast = {
success(message) {
$swal.fire({
icon: "success",
title: "Success",
text: message,
toast: true,
position: "top-end",
showConfirmButton: false,
timer: 3000,
timerProgressBar: true,
});
},
error(message) {
$swal.fire({
icon: "error",
title: "Error",
text: message,
toast: true,
position: "top-end",
showConfirmButton: false,
timer: 5000,
timerProgressBar: true,
});
},
warning(message) {
$swal.fire({
icon: "warning",
title: "Warning",
text: message,
toast: true,
position: "top-end",
showConfirmButton: false,
timer: 4000,
timerProgressBar: true,
});
},
info(message) {
$swal.fire({
icon: "info",
title: "Info",
text: message,
toast: true,
position: "top-end",
showConfirmButton: false,
timer: 3000,
timerProgressBar: true,
});
},
};
return toast;
};

View File

@@ -0,0 +1,55 @@
export function useVoiceReader() {
const isReading = ref(false);
const announceElement = ref(null);
let speechSynthesis;
let speechUtterance;
onMounted(() => {
speechSynthesis = window.speechSynthesis;
speechUtterance = new SpeechSynthesisUtterance();
window.addEventListener("keydown", handleKeydown);
});
onUnmounted(() => {
if (speechSynthesis) {
speechSynthesis.cancel();
}
window.removeEventListener("keydown", handleKeydown);
});
const toggleReading = () => {
if (!speechSynthesis) return;
if (isReading.value) {
speechSynthesis.pause();
isReading.value = false;
announce("Reading paused");
} else {
const textToRead = document.body.innerText;
speechUtterance.text = textToRead;
speechSynthesis.speak(speechUtterance);
isReading.value = true;
announce("Reading started");
}
};
const handleKeydown = (event) => {
if (event.ctrlKey && event.key === "r") {
event.preventDefault();
toggleReading();
}
};
const announce = (message) => {
if (announceElement.value) {
announceElement.value.textContent = message;
}
};
return {
isReading,
toggleReading,
announceElement,
};
}