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,244 @@
<script setup>
// import pinia store
import { useThemeStore } from "~/stores/theme";
definePageMeta({
title: "API 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 themeStore = useThemeStore();
const editorTheme = ref({
label: themeStore.codeTheme,
value: themeStore.codeTheme,
});
const dropdownThemes = ref([]);
const linterError = ref(false);
const linterErrorText = ref("");
const linterErrorColumn = ref(0);
const linterErrorLine = ref(0);
// Add new ref for loading state
const isLinterChecking = ref(false);
// Get all themes
const themes = codemirrorThemes();
// map the themes to the dropdown
dropdownThemes.value = themes.map((theme) => {
return {
label: theme.name,
value: theme.name,
};
});
// watch for changes in the theme
watch(editorTheme, (theme) => {
themeStore.setCodeTheme(theme.value);
forceRerender();
});
// Call API to get the code
const { data } = await useFetch("/api/devtool/api/file-code", {
initialCache: false,
method: "GET",
query: {
path: route.query?.path,
},
});
if (data.value.statusCode === 200) {
fileCode.value = data.value.data;
fileCodeConstant.value = data.value.data;
} else {
$swal
.fire({
title: "Error",
text: "The API you are trying to edit is not found. Please choose a API to edit.",
icon: "error",
confirmButtonText: "Ok",
})
.then(async (result) => {
if (result.isConfirmed) {
await $router.push("/devtool/api-editor");
}
});
}
async function formatCode() {
// Call API to get the code
const { data } = await useFetch("/api/devtool/api/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/api/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/api/save", {
initialCache: false,
method: "POST",
body: {
path: route.query?.path,
code: fileCode.value,
type: "update",
},
});
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" class="mb-4" variant="danger">{{
error
}}</rs-alert>
<rs-card>
<rs-tab fill>
<rs-tab-item title="Editor">
<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 API" }}
</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>
<rs-code-mirror
:key="componentKey"
v-model="fileCode"
mode="javascript"
/>
</rs-tab-item>
<rs-tab-item title="API Tester">
<rs-api-tester :url="route.query?.path" />
</rs-tab-item>
</rs-tab>
</rs-card>
</div>
</template>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,327 @@
<script setup>
definePageMeta({
title: "API Editor",
middleware: ["auth"],
requiresAuth: true,
});
const nuxtApp = useNuxtApp();
const searchText = ref("");
const showModalAdd = ref(false);
const showModalAddForm = ref({
apiURL: "",
});
const showModalEdit = ref(false);
const showModalEditForm = ref({
apiURL: "",
oldApiURL: "",
});
const openModalAdd = () => {
showModalAddForm.value = {
apiURL: "",
method: "all",
};
showModalAdd.value = true;
};
const openModalEdit = (url, method = "all") => {
const apiURL = url.replace("/api/", "");
showModalEditForm.value = {
apiURL: apiURL,
oldApiURL: apiURL,
method: method,
};
showModalEdit.value = true;
};
const { data: apiList, refresh } = await useFetch("/api/devtool/api/list");
const searchApi = () => {
if (!apiList.value || !apiList.value.data) return [];
return apiList.value.data.filter((api) => {
return (
api.name.toLowerCase().includes(searchText.value.toLowerCase()) ||
api.url.toLowerCase().includes(searchText.value.toLowerCase())
);
});
};
const kebabCasetoTitleCase = (str) => {
return str
.split("-")
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(" ");
};
const redirectToApiCode = (api) => {
window.location.href = `/devtool/api-editor/code?path=${api}`;
};
const saveAddAPI = async () => {
const { data } = await useFetch("/api/devtool/api/save", {
initialCache: false,
method: "POST",
body: {
path: "/api/" + showModalAddForm.value.apiURL,
type: "add",
},
});
if (data.value.statusCode === 200) {
nuxtApp.$swal.fire({
title: "Success",
text: "The code has been saved successfully.",
icon: "success",
confirmButtonText: "Ok",
timer: 1000,
});
// Close modal and refresh list
showModalAdd.value = false;
refresh();
}
};
const saveEditAPI = async () => {
const { data } = await useFetch("/api/devtool/api/save", {
initialCache: false,
method: "POST",
body: {
path: "/api/" + showModalEditForm.value.apiURL,
oldPath: "/api/" + showModalEditForm.value.oldApiURL,
type: "edit",
},
});
if (data.value.statusCode === 200) {
nuxtApp.$swal.fire({
title: "Success",
text: "The code has been saved successfully.",
icon: "success",
confirmButtonText: "Ok",
timer: 1000,
});
// Close modal and refresh list
showModalEdit.value = false;
refresh();
}
};
const deleteAPI = async (apiURL) => {
nuxtApp.$swal
.fire({
title: "Are you sure to delete this API?",
text: "You won't be able to revert this!",
icon: "warning",
showCancelButton: true,
confirmButtonColor: "#3085d6",
cancelButtonColor: "#d33",
confirmButtonText: "Yes, delete it!",
})
.then(async (result) => {
if (result.isConfirmed) {
const { data } = await useFetch("/api/devtool/api/save", {
initialCache: false,
method: "POST",
body: {
path: apiURL,
type: "delete",
},
});
if (data.value.statusCode === 200) {
nuxtApp.$swal.fire({
title: "Success",
text: "The code has been saved successfully.",
icon: "success",
confirmButtonText: "Ok",
timer: 1000,
});
// Refresh list after deletion
refresh();
}
}
});
};
</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 api for the server side. You can edit
the api by choosing the api to edit from the card list below.
</p>
</template>
</rs-card>
<rs-card>
<div class="p-4">
<div class="flex justify-end items-center mb-4">
<rs-button @click="openModalAdd">
<Icon name="material-symbols:add" class="mr-1"></Icon>
Add API
</rs-button>
</div>
<!-- Search Button -->
<FormKit
v-model="searchText"
placeholder="Search Title..."
type="search"
class="mb-4"
/>
<div v-auto-animate>
<div
class="shadow-md shadow-black/5 ring-1 ring-slate-700/10 rounded-lg mb-4"
v-for="api in searchApi()"
>
<div class="relative p-4 border-l-8 border-primary rounded-lg">
<div class="flex justify-between items-center">
<div class="block">
<span class="font-semibold text-lg">{{
kebabCasetoTitleCase(api.name)
}}</span>
<br />
<span class=""> {{ api.url }}</span>
</div>
<div class="flex gap-4">
<rs-button
variant="primary-outline"
@click="redirectToApiCode(api.url)"
>
<Icon
name="material-symbols:code-blocks-outline-rounded"
class="mr-2"
/>
Code Editor
</rs-button>
<div class="flex gap-2">
<rs-button @click="openModalEdit(api.url)">
<Icon name="material-symbols:edit-outline-rounded" />
</rs-button>
<rs-button @click="deleteAPI(api.url)">
<Icon name="carbon:trash-can" />
</rs-button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</rs-card>
<rs-modal title="Add API" v-model="showModalAdd" :overlay-close="false">
<template #body>
<FormKit type="form" :actions="false" @submit="saveAddAPI">
<FormKit
type="text"
label="URL"
:validation="[['required'], ['matches', '/^[a-z0-9/-]+$/']]"
:validation-messages="{
required: 'URL is required',
matches:
'URL contains invalid characters. Only letters, numbers, dashes, and forward slashes are allowed.',
}"
v-model="showModalAddForm.apiURL"
>
<template #prefix>
<div
class="bg-slate-100 dark:bg-slate-700 h-full rounded-l-md p-3"
>
/api/
</div>
</template>
</FormKit>
<!-- <FormKit
type="select"
label="Request Method"
:options="requestMethods"
validation="required"
placeholder="Select a method"
v-model="showModalAddForm.method"
/> -->
<div class="flex justify-end gap-2">
<rs-button variant="outline" @click="showModalAdd = false">
Cancel
</rs-button>
<rs-button btnType="submit">
<Icon
name="material-symbols:save-outline"
class="mr-2 !w-4 !h-4"
/>
Save
</rs-button>
</div>
</FormKit>
</template>
<template #footer></template>
</rs-modal>
<rs-modal title="Edit API" v-model="showModalEdit" :overlay-close="false">
<template #body>
<FormKit type="form" :actions="false" @submit="saveEditAPI">
<FormKit
type="text"
label="URL"
:validation="[['required'], ['matches', '/^[a-z0-9/-]+$/']]"
:validation-messages="{
required: 'URL is required',
matches:
'URL contains invalid characters. Only letters, numbers, dashes, and forward slashes are allowed.',
}"
v-model="showModalEditForm.apiURL"
>
<template #prefix>
<div
class="bg-slate-100 dark:bg-slate-700 h-full rounded-l-md p-3"
>
/api/
</div>
</template>
</FormKit>
<!-- <FormKit
type="select"
label="Request Method"
:options="requestMethods"
validation="required"
placeholder="Select a method"
v-model="showModalEditForm.method"
/> -->
<div class="flex justify-end gap-2">
<rs-button variant="outline" @click="showModalEdit = false">
Cancel
</rs-button>
<rs-button btnType="submit">
<Icon
name="material-symbols:save-outline"
class="mr-2 !w-4 !h-4"
/>
Save
</rs-button>
</div>
</FormKit>
</template>
<template #footer></template>
</rs-modal>
</div>
</template>