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,226 @@
<script setup>
definePageMeta({
title: "Code Editor",
middleware: ["auth"],
requiresAuth: true,
});
const { $swal, $router } = useNuxtApp();
const route = useRoute();
const router = useRouter();
const fileCode = ref("");
const fileCodeConstant = ref("");
const componentKey = ref(0);
const hasError = ref(false);
const error = ref("");
const linterError = ref(false);
const linterErrorText = ref("");
const linterErrorColumn = ref(0);
const linterErrorLine = ref(0);
const isLinterChecking = ref(false);
const page = router.getRoutes().find((page) => {
return page.name === route.query?.page;
});
if (!route.query.page || !page) {
$swal
.fire({
title: "Error",
text: "The page you are trying to edit is not found. Please choose a page to edit.",
icon: "error",
confirmButtonText: "Ok",
})
.then(async (result) => {
if (result.isConfirmed) {
await $router.push("/devtool/content-editor");
}
});
}
if (page?.path)
page.path = page.path.replace(/:(\w+)/g, "[$1]").replace(/\(\)/g, "");
// Call API to get the code
const { data } = await useFetch("/api/devtool/content/code/file-code", {
initialCache: false,
method: "GET",
query: {
path: page.path,
},
});
// console.log(data.value);
if (data.value.statusCode === 200) {
fileCode.value = data.value.data;
fileCodeConstant.value = data.value.data;
// If its index append the path
if (data.value?.mode == "index") page.path = page.path + "/index";
} else {
$swal.fire({
title: "Error",
text: "The page you are trying to edit is not found. Please choose a page to edit. You will be redirected to the content editor page.",
icon: "error",
confirmButtonText: "Ok",
timer: 3000,
});
setTimeout(() => {
$router.push("/devtool/content-editor");
}, 3000);
}
async function formatCode() {
// Call API to get the code
const { data } = await useFetch("/api/devtool/content/code/prettier-format", {
initialCache: false,
method: "POST",
body: JSON.stringify({
code: fileCode.value,
}),
});
forceRerender();
if (data.value.statusCode === 200) {
fileCode.value = data.value.data;
}
}
async function checkLinterVue() {
isLinterChecking.value = true;
try {
// Call API to get the code
const { data } = await useFetch("/api/devtool/content/code/linter", {
initialCache: false,
method: "POST",
body: JSON.stringify({
code: fileCode.value,
}),
});
if (data.value.statusCode === 200) {
linterError.value = false;
linterErrorText.value = "";
linterErrorColumn.value = 0;
linterErrorLine.value = 0;
} else if (data.value.statusCode === 400) {
linterError.value = true;
linterErrorText.value = data.value.data.message;
linterErrorColumn.value = data.value.data.column;
linterErrorLine.value = data.value.data.line;
}
} finally {
isLinterChecking.value = false;
}
}
const forceRerender = () => {
componentKey.value += 1;
};
const keyPress = (key) => {
// console.log(key);
const event = new KeyboardEvent("keydown", {
key: key,
ctrlKey: true,
});
// console.log(event);
document.dispatchEvent(event);
};
const saveCode = async () => {
// Check Linter Vue
await checkLinterVue();
if (linterError.value) {
$swal.fire({
title: "Error",
text: "There is an error in your code. Please fix it before saving.",
icon: "error",
confirmButtonText: "Ok",
});
return;
}
const { data } = await useFetch("/api/devtool/content/code/save", {
initialCache: false,
method: "POST",
body: {
path: page.path,
code: fileCode.value,
},
});
if (data.value.statusCode === 200) {
$swal.fire({
title: "Success",
text: "The code has been saved successfully.",
icon: "success",
confirmButtonText: "Ok",
timer: 1000,
});
setTimeout(() => {
$router.go();
}, 1000);
}
};
</script>
<template>
<div>
<LayoutsBreadcrumb />
<rs-alert v-if="hasError" variant="danger" class="mb-4">{{
error
}}</rs-alert>
<rs-card class="mb-0">
<div class="p-4">
<div class="flex justify-end gap-2 mb-4">
<rs-button
class="!p-2"
@click="saveCode"
:disabled="isLinterChecking"
>
<div class="flex items-center">
<Icon
v-if="!isLinterChecking"
name="material-symbols:save-outline-rounded"
size="20px"
class="mr-1"
/>
<Icon
v-else
name="eos-icons:loading"
size="20px"
class="mr-1 animate-spin"
/>
{{ isLinterChecking ? "Checking..." : "Save Code" }}
</div>
</rs-button>
</div>
<Transition>
<rs-alert v-if="linterError" variant="danger" class="mb-4">
<div class="flex gap-2">
<Icon name="material-symbols:error-outline-rounded" size="20px" />
<div>
<div class="font-bold">ESLint Error</div>
<div class="text-sm">
{{ linterErrorText }}
</div>
<div class="text-xs mt-2">
Line: {{ linterErrorLine }} Column: {{ linterErrorColumn }}
</div>
</div>
</div>
</rs-alert>
</Transition>
</div>
<rs-code-mirror :key="componentKey" v-model="fileCode" />
</rs-card>
</div>
</template>

View File

@@ -0,0 +1,247 @@
<script setup>
definePageMeta({
title: "Content Editor",
middleware: ["auth"],
requiresAuth: true,
});
const { $swal, $router } = useNuxtApp();
const router = useRouter();
const getPages = router.getRoutes();
const pages = getPages.filter((page) => {
// Filter out pages in the devtool path
if (page.path.includes("/devtool")) {
return false;
}
// Use page.name if page.meta.title doesn't exist
const pageTitle = page.meta?.title || page.name;
return pageTitle && pageTitle !== "Home" && page.name;
});
const searchText = ref("");
const showModal = ref(false);
const modalData = ref({
name: "",
path: "",
});
const searchPages = () => {
return pages.filter((page) => {
const pageTitle = page.meta?.title || page.name;
return pageTitle.toLowerCase().includes(searchText.value.toLowerCase());
});
};
const capitalizeSentence = (sentence) => {
return sentence
.split(" ")
.map((word) => {
return word[0].toUpperCase() + word.slice(1);
})
.join(" ");
};
const templateOptions = ref([{ label: "Select Template", value: "" }]);
const selectTemplate = ref("");
const { data: templates } = await useFetch(
"/api/devtool/content/template/list",
{
method: "GET",
}
);
templateOptions.value.push(
...templates?.value.data.map((template) => {
return {
label: `${template.title} - ${template.id}`,
value: template.id,
};
})
);
const importTemplate = (pageName) => {
showModal.value = true;
modalData.value.name = pageName;
modalData.value.path = router.getRoutes().find((page) => {
return page.name === pageName;
}).path;
};
const confirmModal = async () => {
$swal
.fire({
title: "Are you sure you want to import this template?",
text: "This action cannot be undone.",
icon: "warning",
showCancelButton: true,
confirmButtonColor: "#3085d6",
cancelButtonColor: "#d33",
confirmButtonText: "Yes",
})
.then(async (result) => {
if (result.isConfirmed) {
const { data: res } = await useFetch(
"/api/devtool/content/template/import",
{
initialCache: false,
method: "GET",
query: {
path: modalData.value.path + "/index",
templateId: selectTemplate.value,
},
}
);
if (res.value.statusCode == 200) {
$swal.fire({
title: "Success",
text: res.value.message,
icon: "success",
confirmButtonText: "Ok",
timer: 1000,
});
setTimeout(() => {
$router.go();
}, 1000);
}
}
})
.finally(() => {
showModal.value = false;
});
};
</script>
<template>
<div>
<LayoutsBreadcrumb />
<rs-card>
<template #header>
<div class="flex">
<Icon class="mr-2 flex justify-center" name="ic:outline-info"></Icon
>Info
</div>
</template>
<template #body>
<p class="mb-4">
This page is used to edit the content of a page. You can edit the
content of the page by choosing the page to edit from the card list
below.
</p>
</template>
</rs-card>
<rs-card>
<div class="p-4">
<!-- Search Button -->
<FormKit
v-model="searchText"
placeholder="Search Title..."
type="search"
/>
<div
class="page-wrapper grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5"
v-auto-animate
>
<!-- <div
class="page border-2 border-gray-400 border-dashed rounded-lg"
style="min-height: 250px"
>
Add New Page
</div> -->
<div
v-for="page in searchPages()"
:key="page.path"
class="page shadow-md shadow-black/5 p-5 ring-1 ring-slate-700/10 rounded-lg"
>
<div class="pb-4">
<h4 class="font-semibold">
{{ capitalizeSentence(page.meta?.title || page.name) }}
</h4>
<nuxt-link :to="page.path">
<div
class="flex items-center text-primary hover:text-primary/70"
>
<Icon
class="mr-2"
name="ic:outline-link"
style="font-size: 1.2rem"
></Icon>
<p class="text-sm">{{ page.path }}</p>
</div>
</nuxt-link>
</div>
<div
class="button-list flex justify-between border-t pt-4 border-gray-300"
>
<div class="flex gap-x-2">
<!-- <nuxt-link
:to="`/devtool/content-editor/canvas?page=${page.name}`"
>
<rs-button variant="primary" class="!py-2 !px-3">
<Icon name="ph:paint-brush-broad"></Icon>
</rs-button>
</nuxt-link> -->
<nuxt-link
:to="`/devtool/content-editor/code?page=${page.name}`"
>
<rs-button variant="primary" class="!py-2 !px-3">
<Icon
name="material-symbols:code-blocks-outline-rounded"
></Icon>
</rs-button>
</nuxt-link>
</div>
<rs-button
variant="primary-text"
class="!py-2 !px-3"
@click="importTemplate(page.name)"
>
<Icon name="mdi:import"></Icon>
</rs-button>
</div>
</div>
</div>
</div>
</rs-card>
<rs-modal :title="`Import (${modalData.name})`" v-model="showModal">
<FormKit
v-model="selectTemplate"
type="select"
label="Content Template"
:options="templateOptions"
validation="required"
validation-visibility="dirty"
help="Please choose carefully the template that you want to import. This action cannot be undone."
/>
<template #footer>
<rs-button @click="showModal = false" variant="primary-text">
Cancel
</rs-button>
<rs-button @click="confirmModal" :disabled="!selectTemplate"
>Confirm</rs-button
>
</template>
</rs-modal>
</div>
</template>
<style lang="scss" scoped>
.thumbnail::before {
content: "";
display: block;
height: 100%;
position: absolute;
top: 0;
left: 0;
width: 100%;
background-color: rgba(255, 255, 255, 0.8);
}
</style>

View File

@@ -0,0 +1,127 @@
<script setup>
definePageMeta({
title: "Template Editor",
middleware: ["auth"],
requiresAuth: true,
});
const searchText = ref("");
const { data: templateList } = await useFetch(
"/api/devtool/content/template/list",
{
method: "GET",
}
);
// Search function that can search the template by title and tags if tags is available after data is fetched
const searchTemplate = () => {
return templateList?.value.data.filter((template) => {
return template.title
.toLowerCase()
.includes(searchText.value.toLowerCase());
});
};
</script>
<template>
<div>
<LayoutsBreadcrumb />
<rs-card>
<template #header>
<div class="flex">
<Icon class="mr-2 flex justify-center" name="ic:outline-info"></Icon
>Info
</div>
</template>
<template #body>
<p class="mb-4">
This webpage serves as a platform for template management, enabling
users to select and utilize templates for rendering pages according to
their chosen design.
</p>
</template>
</rs-card>
<rs-card>
<div class="p-4">
<!-- Search Button -->
<FormKit
v-model="searchText"
placeholder="Search Title..."
type="search"
/>
<div
class="page-wrapper grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5"
v-auto-animate
>
<!-- <div
class="page border-2 border-gray-400 border-dashed rounded-lg"
style="min-height: 250px"
>
Add New Page
</div> -->
<div
v-for="val in searchTemplate()"
class="page shadow-md shadow-black/5 ring-1 ring-slate-700/10 rounded-lg"
style="min-height: 250px"
>
<div class="thumbnail-wrapper relative">
<div class="button-list absolute bottom-3 right-3 flex z-10">
<nuxt-link :to="val.img" target="_blank">
<rs-button class="!py-2 !px-3 rounded-r-none">
<Icon name="material-symbols:fullscreen-rounded"></Icon>
</rs-button>
</nuxt-link>
<nuxt-link
:to="`/devtool/content-editor/template/view/${val.id}`"
>
<rs-button class="!py-2 !px-3 rounded-l-none">
<Icon name="material-symbols:preview"></Icon>
</rs-button>
</nuxt-link>
</div>
<img
class="thumbnail rounded-tl-lg rounded-tr-lg bg-[#F3F4F6]"
style="height: 250px; width: 100%; object-fit: contain"
:src="val.img"
alt=""
/>
<div
class="overlay-img opacity-10 bg-black text-black before:content-['Hello_World'] absolute top-0 left-0 w-full h-full rounded-tl-lg rounded-tr-lg"
></div>
</div>
<div class="p-4">
<h4 class="font-semibold">{{ val.title }} ({{ val.id }})</h4>
<div class="flex items-center mb-4">
<p class="text-sm">{{ val.description }}</p>
</div>
<div
class="tag h-10 flex justify-start items-center overflow-x-auto gap-x-2"
>
<rs-badge v-for="val2 in val.tag">
{{ val2 }}
</rs-badge>
</div>
</div>
</div>
</div>
</div>
</rs-card>
</div>
</template>
<style lang="scss" scoped>
.thumbnail::before {
content: "";
display: block;
height: 100%;
position: absolute;
top: 0;
left: 0;
width: 100%;
background-color: rgba(255, 255, 255, 0.8);
}
</style>

View File

@@ -0,0 +1,33 @@
<script setup>
definePageMeta({
title: "Template Viewer",
middleware: ["auth"],
requiresAuth: true,
});
const id = useRoute().params.id;
const { data: template } = await useFetch(
`/api/devtool/content/template/get-list`,
{
method: "GET",
params: {
id: id,
},
}
);
console.log(template.value.data);
const templateComponent = defineAsyncComponent(
() => import(`../../../../../templates/${template.value.data.filename}.vue`)
);
</script>
<template>
<div>
<LayoutsBreadcrumb />
<component :is="templateComponent" />
</div>
</template>