added shortlinks on frontend

This commit is contained in:
2023-05-08 19:28:07 +10:00
parent b4cf05ad44
commit 729fc3fd39
10 changed files with 712 additions and 1 deletions

View File

@@ -120,3 +120,19 @@ export interface LogsDiscordResponse {
error: string;
};
}
export interface Shortlink {
id: number;
code: string;
url: string;
used: number;
}
export interface ShortlinkCollection {
shortlinks: Array<Shortlink>;
total: number;
}
export interface ShortlinkResponse {
shortlink: Shortlink;
}

View File

@@ -345,6 +345,41 @@ export const routes = [
},
],
},
{
path: "shortlinks",
children: [
{
path: "",
name: "dashboard-shortlink-list",
meta: {
title: "Shortlink",
middleware: "authenticated",
},
component: () =>
import("@/views/dashboard/ShortlinkList.vue"),
},
{
path: "create",
name: "dashboard-shortlink-create",
meta: {
title: "Create Shortlink",
middleware: "authenticated",
},
component: () =>
import("@/views/dashboard/ShortlinkEdit.vue"),
},
{
path: ":id",
name: "dashboard-shortlink-edit",
meta: {
title: "Edit Shortlink",
middleware: "authenticated",
},
component: () =>
import("@/views/dashboard/ShortlinkEdit.vue"),
},
],
},
{
path: "discord-bot-logs",
name: "dashboard-discord-bot-logs",

View File

@@ -56,6 +56,13 @@
<img src="/img/minecraft-grass-block.png" />
<h3>Minecraft</h3>
</router-link> -->
<router-link
v-if="userStore.permissions.includes('logs/discord')"
:to="{ name: 'dashboard-shortlink-list' }"
class="admin-card discord">
<ion-icon name="link-outline" />
<h3>Shortlinks</h3>
</router-link>
<router-link
v-if="userStore.permissions.includes('logs/discord')"
:to="{ name: 'dashboard-discord-bot-logs' }"

View File

@@ -0,0 +1,183 @@
<template>
<SMMastHead
:title="pageHeading"
:back-link="
route.params.id || isCreating
? { name: 'dashboard-shortlink-list' }
: { name: 'dashboard' }
"
:back-title="
route.params.id || isCreating
? 'Back to Shortlinks'
: 'Back to Dashboard'
" />
<SMContainer>
<SMForm :model-value="form" @submit="handleSubmit">
<SMRow>
<SMColumn><SMInput control="code" /></SMColumn>
<SMColumn
><SMInput type="static" v-model="used" label="Times used"
/></SMColumn>
</SMRow>
<SMRow>
<SMColumn><SMInput control="url" /></SMColumn>
</SMRow>
<SMRow>
<SMColumn>
<SMButtonRow>
<template #right>
<SMButton type="submit" :label="saveButtonLabel" />
</template>
</SMButtonRow>
</SMColumn>
</SMRow>
</SMForm>
</SMContainer>
</template>
<script setup lang="ts">
import { computed, reactive, ref } from "vue";
import { useRoute, useRouter } from "vue-router";
import SMButton from "../../components/SMButton.vue";
import SMForm from "../../components/SMForm.vue";
import SMInput from "../../components/SMInput.vue";
import { api } from "../../helpers/api";
import { ShortlinkResponse } from "../../helpers/api.types";
import { Form, FormControl } from "../../helpers/form";
import { And, Length, Max, Min, Required } from "../../helpers/validate";
import SMMastHead from "../../components/SMMastHead.vue";
import { useToastStore } from "../../store/ToastStore";
import SMButtonRow from "../../components/SMButtonRow.vue";
const route = useRoute();
const router = useRouter();
const isCreating = route.path.endsWith("/create");
let form = reactive(
Form({
code: FormControl("", And([Required(), Length(4)])),
url: FormControl("", And([Required(), Min(4), Max(255)])),
})
);
const used = ref(0);
/**
* Load the page data.
*/
const loadData = async () => {
if (route.params.id) {
try {
form.loading(true);
const result = await api.get({
url: "/shortlinks/{id}",
params: {
id: route.params.id,
},
});
const data = result.data as ShortlinkResponse;
if (data && data.shortlink) {
form.controls.code.value = data.shortlink.code;
form.controls.url.value = data.shortlink.url;
used.value = data.shortlink.used;
}
} catch (err) {
form.apiErrors(err);
} finally {
form.loading(false);
}
} else {
let foundCode = false;
while (foundCode == false) {
const randomCode = Math.random()
.toString(36)
.substring(2, 6)
.toLowerCase();
try {
await api.get({
url: "/shortlinks",
params: {
code: randomCode,
},
});
} catch (err) {
foundCode = true;
if (err.status == 404) {
form.controls.code.value = randomCode;
}
}
}
}
};
/**
* Handle the user submitting the form.
*/
const handleSubmit = async () => {
try {
form.loading(true);
if (isCreating == false) {
await api.put({
url: "/shortlinks/{id}",
params: {
id: route.params.id,
},
body: {
code: form.controls.code.value,
url: form.controls.url.value,
},
});
useToastStore().addToast({
title: "Shortlink Updated",
content: "The shortlink has been updated.",
type: "success",
});
} else {
await api.post({
url: "/shortlinks",
body: {
code: form.controls.code.value,
url: form.controls.url.value,
},
});
useToastStore().addToast({
title: "Shortlink Created",
content: "The shortlink has been created.",
type: "success",
});
}
router.push({ name: "dashboard" });
} catch (err) {
form.apiErrors(err);
} finally {
form.loading(false);
}
};
const pageHeading = computed(() => {
return route.params.id == null ? "Create Shortlink" : "Edit Shortlink";
});
const saveButtonLabel = computed(() => {
return route.params.id == null ? "Create" : "Update";
});
loadData();
</script>
<style lang="scss">
.page-dashboard-account-details {
h3 {
margin-top: 0;
margin-bottom: 16px;
}
}
</style>

View File

@@ -0,0 +1,218 @@
<template>
<SMPage permission="admin/users">
<SMMastHead
title="Shortlinks"
:back-link="{ name: 'dashboard' }"
back-title="Return to Dashboard" />
<SMContainer class="flex-grow-1">
<SMToolbar>
<SMButton
:to="{ name: 'dashboard-shortlink-create' }"
type="primary"
label="Create Link" />
<SMInput
v-model="itemSearch"
label="Search"
class="toolbar-search"
@keyup.enter="handleSearch">
<template #append>
<SMButton
type="primary"
label="Search"
icon="search-outline"
@click="handleSearch" />
</template>
</SMInput>
</SMToolbar>
<SMLoading large v-if="itemsLoading" />
<template v-else>
<SMPagination
v-if="items.length < itemsTotal"
v-model="itemsPage"
:total="itemsTotal"
:per-page="itemsPerPage" />
<SMNoItems v-if="items.length == 0" text="No Links Found" />
<SMTable
v-if="items.length > 0"
:headers="headers"
:items="items"
@row-click="handleEdit">
<template #item-actions="item">
<SMButton
label="Edit"
:dropdown="{
delete: 'Delete',
}"
size="medium"
@click="handleActionButton(item, $event)" />
</template>
</SMTable>
</template>
</SMContainer>
</SMPage>
</template>
<script setup lang="ts">
import { ref, watch } from "vue";
import { useRoute, useRouter } from "vue-router";
import { openDialog } from "../../components/SMDialog";
import DialogConfirm from "../../components/dialogs/SMDialogConfirm.vue";
import { api, getApiResultData } from "../../helpers/api";
import SMTable from "../../components/SMTable.vue";
import SMMastHead from "../../components/SMMastHead.vue";
import { useToastStore } from "../../store/ToastStore";
import SMNoItems from "../../components/SMNoItems.vue";
import SMButton from "../../components/SMButton.vue";
import SMInput from "../../components/SMInput.vue";
import SMToolbar from "../../components/SMToolbar.vue";
import { updateRouterParams } from "../../helpers/url";
import { Shortlink, ShortlinkCollection } from "../../helpers/api.types";
import SMLoading from "../../components/SMLoading.vue";
import SMPagination from "../../components/SMPagination.vue";
const route = useRoute();
const router = useRouter();
const items = ref([]);
const itemsLoading = ref(false);
const itemSearch = ref((route.query.search as string) || "");
const itemsTotal = ref(0);
const itemsPerPage = 25;
const itemsPage = ref(parseInt((route.query.page as string) || "1"));
const headers = [
{ text: "Code", value: "code", sortable: true },
{ text: "URL", value: "url", sortable: true },
{ text: "Used", value: "used", sortable: true },
{ text: "Actions", value: "actions" },
];
/**
* Watch if page number changes.
*/
watch(itemsPage, () => {
handleLoad();
});
/**
* Handle searching for item.
*/
const handleSearch = () => {
itemsPage.value = 1;
handleLoad();
};
/**
* Handle user selecting option in action button.
*
* @param {Shortlink} item The item.
* @param option
*/
const handleActionButton = (item: Shortlink, option: string): void => {
if (option.length == 0) {
handleEdit(item);
} else if (option.toLowerCase() == "delete") {
handleDelete(item);
}
};
/**
* Handle loading the page and list
*/
const handleLoad = async () => {
itemsLoading.value = true;
items.value = [];
itemsTotal.value = 0;
updateRouterParams(router, {
search: itemSearch.value,
page: itemsPage.value == 1 ? "" : itemsPage.value.toString(),
});
try {
let params = {
page: itemsPage.value,
limit: itemsPerPage,
};
if (itemSearch.value.length > 0) {
params[
"filter"
] = `code:${itemSearch.value},OR,url:${itemSearch.value}`;
}
let result = await api.get({
url: "/shortlinks",
params: params,
});
const collection = getApiResultData<ShortlinkCollection>(result);
items.value = collection.shortlinks;
itemsTotal.value = collection.total;
} catch (err) {
/* empty */
}
itemsLoading.value = false;
};
const handleEdit = (shortlink: Shortlink) => {
router.push({
name: "dashboard-shortlink-edit",
params: { id: shortlink.id },
});
};
const handleDelete = async (shortlink: Shortlink) => {
let result = await openDialog(DialogConfirm, {
title: "Delete User?",
text: `Are you sure you want to delete the user <strong>${shortlink.code}</strong>?`,
cancel: {
type: "secondary",
label: "Cancel",
},
confirm: {
type: "danger",
label: "Delete User",
},
});
if (result == true) {
try {
await api.delete(`shortlinks${shortlink.id}`);
handleLoad();
useToastStore().addToast({
title: "Shortlink Deleted",
content: "Shortlink deleted successfully.",
type: "success",
});
} catch (err) {
useToastStore().addToast({
title: "Server Error",
content:
"Shortlink could not be deleted because an error occurred.",
type: "danger",
});
}
}
};
handleLoad();
</script>
<style lang="scss">
.page-dashboard-shortlink-list {
.toolbar-search {
max-width: 350px;
}
}
@media only screen and (max-width: 768px) {
.page-dashboard-shortlink-list {
.toolbar-search {
max-width: none;
}
}
}
</style>