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:
284
composables/codemirrorThemes.js
Normal file
284
composables/codemirrorThemes.js
Normal 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",
|
||||
},
|
||||
];
|
||||
}
|
||||
20
composables/languageList.js
Normal file
20
composables/languageList.js
Normal 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
55
composables/themeList.js
Normal 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
129
composables/themeList2.js
Normal 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
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
87
composables/useDebounceFn.js
Normal file
87
composables/useDebounceFn.js
Normal 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;
|
||||
411
composables/useNotificationDelivery.js
Normal file
411
composables/useNotificationDelivery.js
Normal 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
|
||||
};
|
||||
};
|
||||
301
composables/useNotificationLogs.js
Normal file
301
composables/useNotificationLogs.js
Normal 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
|
||||
}
|
||||
}
|
||||
200
composables/useNotifications.js
Normal file
200
composables/useNotifications.js
Normal 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,
|
||||
};
|
||||
};
|
||||
363
composables/useSiteSettings.js
Normal file
363
composables/useSiteSettings.js
Normal 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
61
composables/useToast.js
Normal 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;
|
||||
};
|
||||
55
composables/useVoiceReader.js
Normal file
55
composables/useVoiceReader.js
Normal 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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user