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:
226
pages/devtool/content-editor/code/index.vue
Normal file
226
pages/devtool/content-editor/code/index.vue
Normal 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>
|
||||
247
pages/devtool/content-editor/index.vue
Normal file
247
pages/devtool/content-editor/index.vue
Normal 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>
|
||||
127
pages/devtool/content-editor/template/index.vue
Normal file
127
pages/devtool/content-editor/template/index.vue
Normal 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>
|
||||
33
pages/devtool/content-editor/template/view/[id]/index.vue
Normal file
33
pages/devtool/content-editor/template/view/[id]/index.vue
Normal 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>
|
||||
Reference in New Issue
Block a user