Skip to content

Commit

Permalink
Implement page for challenge categories (#127)
Browse files Browse the repository at this point in the history
  • Loading branch information
Lauritz-Tieste authored May 23, 2024
1 parent 8f641a6 commit abbf877
Show file tree
Hide file tree
Showing 10 changed files with 528 additions and 34 deletions.
2 changes: 2 additions & 0 deletions components/PageTitle.vue
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ export default defineComponent({
return "Headings.ReportedTasks";
case "dashboard-reported-tasks-id":
return "Headings.ManageReport";
case "dashboard-challenges":
return "Headings.Challenges";
default:
return routeName;
Expand Down
138 changes: 138 additions & 0 deletions components/challenges/Table.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
<template>
<Table :data="data" :loading="isLoading" :headers="headers">
<template #title="{ item }">
<div class="flex gap-2 md:gap-4 items-center max-w-[350px]">
<p class="clamp line-1 text-body-2">
{{ item?.title ?? "" }}
</p>
</div>
</template>

<template #description="{ item }">
<p class="clamp line-2 max-w-xl w-full">
{{ item?.description ?? "" }}
</p>
</template>

<template #actions="{ item }">
<div class="flex gap-3 justify-center">
<Icon @click="onclickDeleteItem(item)" class="cursor-pointer" rounded sm :icon="TrashIcon" />
<Icon @click="onclickEditItem(item)" class="cursor-pointer" rounded sm :icon="PencilIcon" />
</div>
</template>
</Table>
</template>

<script lang="ts">
import type { Ref, PropType } from "vue";
import { useI18n } from "vue-i18n";
import {
ArrowTopRightOnSquareIcon,
XMarkIcon,
TrashIcon,
PencilIcon,
} from "@heroicons/vue/24/outline/index.js";
export default {
props: {
data: { type: Array as PropType<any[]>, default: [] },
loading: { type: Boolean, default: true },
},
components: {
ArrowTopRightOnSquareIcon,
XMarkIcon,
TrashIcon,
PencilIcon,
},
setup(props) {
const { t } = useI18n();
const isLoading = computed(() => {
return props.loading && props.data.length <= 0;
});
const headers = computed(() => {
let arrHeaders = [
{
label: "Headings.Title",
key: "title",
},
{
label: "Headings.Description",
key: "description",
},
{
label: "Headings.Actions",
key: "actions",
class: "text-center",
},
];
return arrHeaders;
});
async function onclickDeleteItem(item: any) {
openDialog(
"warning",
"Dialogs.DeleteChallengeHeading",
"Dialogs.DeleteChallengeBody",
false,
{
label: "Dialogs.DeleteChallengeButton",
onclick: async () => {
setLoading(true);
const [success, error] = await deleteChallengeCategory(item.id);
setLoading(false);
success
? openSnackbar(
"success",
"Dialogs.DeleteChallengeConfirmation"
)
: openSnackbar("error", error?.detail ?? "");
},
},
{
label: "Buttons.Cancel",
onclick: () => { },
}
);
}
const router = useRouter();
const challenge = useCodingChallenge();
function onclickEditItem(item: any) {
if (Boolean(!item) || Boolean(!item.id)) return;
challenge.value = item;
router.push(`/dashboard/challenges/${item.id}`);
}
return {
isLoading,
headers,
XMarkIcon,
TrashIcon,
PencilIcon,
onclickDeleteItem,
onclickEditItem,
};
},
};
</script>

<style scoped>
a {
@apply block w-fit pl-4 cursor-pointer;
}
a.pl-extra {
@apply pl-6;
}
.icon {
@apply h-6 w-6 md:h-7 md:w-7;
}
</style>
125 changes: 125 additions & 0 deletions components/form/ChallengeCategory.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
<template>
<form class="grid grid-cols-1 md:grid-cols-2 gap-x-card gap-y-2" :class="{ 'form-submitting': form.submitting }"
@submit.prevent="onclickSubmitForm()" ref="refForm">

<h2 class="text-heading-3 md:col-span-2 mb-card flex gap-card items-center">
<span class="inline-block flex-shrink-0">{{ t("Headings.ChallengeCategoryInformation") }}</span>
<hr class="w-full" />
</h2>

<Input label="Headings.Title" v-model="form.title.value" @valid="form.title.valid = $event"
:rules="form.title.rules" />

<Input label="Headings.Description" v-model="form.description.value" @valid="form.description.valid = $event"
:rules="form.description.rules" />

<InputBtn :loading="form.submitting" class="md:col-span-2 self-end justify-self-end" @click="onclickSubmitForm()"
mt>
{{ t(!!data ? 'Headings.EditChallengeCategory' : 'Headings.CreateChallengeCategory') }}
</InputBtn>
</form>
</template>

<script lang="ts">
import { defineComponent, ref } from 'vue';
import type { PropType, Ref } from 'vue';
import { useI18n } from 'vue-i18n';
import type { IForm } from '~/types/form';
export default defineComponent({
props: { data: { type: Object as PropType<any>, default: null } },
setup(props) {
const { t } = useI18n();
// ============================================================= refs
const refForm = ref<HTMLFormElement | null>(null);
// ============================================================= reactive
const form = reactive<IForm>({
title: {
valid: false,
value: '',
rules: [
(v: string) => Boolean(v) || 'Error.InputEmpty_Inputs.CompanyName',
(v: string) => !v || v.length >= 3 || 'Error.InputMinLength_3',
(v: string) => v.length <= 255 || 'Error.InputMaxLength_255',
],
},
description: {
valid: true,
value: '',
rules: [(v: string) => v.length <= 255 || 'Error.InputMaxLength_255'],
},
submitting: false,
validate: () => {
let isValid = true;
for (const key in form) {
if (
key != 'validate' &&
key != 'body' &&
key != 'submitting' &&
!form[key].valid
) {
isValid = false;
}
}
if (refForm.value) refForm.value.reportValidity();
return isValid;
},
body: () => {
let obj: any = {};
for (const key in form) {
if (key != 'validate' && key != 'body' && key != 'submitting')
obj[key] = form[key].value;
}
return obj;
},
});
// ============================================================= Pre-set Input fields
watch(
() => props.data,
(newValue, _) => {
if (!newValue) return;
form.title.value = newValue?.title ?? '';
form.description.value = newValue?.description ?? '';
},
{ immediate: true, deep: true }
);
// ============================================================= functions
async function onclickSubmitForm() {
if (form.validate()) {
form.submitting = true;
const [success, error] = (props.data)
? await editChallengeCategory(props.data.id, form.body())
: await createChallengeCategory(form.body());
form.submitting = false;
success ? successHandler(success) : errorHandler(error);
} else {
openSnackbar('error', 'Error.InvalidForm');
}
}
const router = useRouter();
function successHandler(res: any) {
router.push("/dashboard/challenges");
}
function errorHandler(res: any) {
openSnackbar('error', res?.detail ?? '');
}
return {
form,
onclickSubmitForm,
refForm,
t,
};
},
});
</script>

<style scoped></style>
48 changes: 18 additions & 30 deletions components/navbar/Links.vue
Original file line number Diff line number Diff line change
@@ -1,40 +1,20 @@
<template>
<aside
class="h-full w-72 lg:w-full bg-tertiary card shadow-2xl lg:shadow-none grid grid-rows-[auto_1fr_auto]"
>
<aside class="h-full w-72 lg:w-full bg-tertiary card shadow-2xl lg:shadow-none grid grid-rows-[auto_1fr_auto]">
<NuxtLink to="/" class="flex gap-box items-center">
<img
src="/images/logo-text.png"
alt="bootstrap academy logo"
class="object-contain w-36 cursor-pointer"
/>
<h6
class="w-fit mt-2 italic px-2 py-0.5 bg-info rounded text-white font-heading text-heading-5"
>
<img src="/images/logo-text.png" alt="bootstrap academy logo" class="object-contain w-36 cursor-pointer" />
<h6 class="w-fit mt-2 italic px-2 py-0.5 bg-info rounded text-white font-heading text-heading-5">
Admin
</h6>
</NuxtLink>

<nav class="mt-12 flex flex-col gap-8">
<NuxtLink
v-for="({ name, icon, label, pathname }, i) of links"
:key="i"
:to="pathname"
class="h-fit px-4 py-3 rounded"
@click.prevent="emit('closeMenu', true)"
:class="{
<NuxtLink v-for="({ name, icon, label, pathname }, i) of links" :key="i" :to="pathname"
class="h-fit px-4 py-3 rounded" @click.prevent="emit('closeMenu', true)" :class="{
'active-link': activePathName == name,
}"
>
<IconText
lg
:icon="icon"
:fill="activePathName == name ? 'fill-white' : 'fill-accent'"
:iconColor="activePathName == name ? 'text-white' : 'text-accent'"
:labelColor="
activePathName == name ? 'text-white' : 'text-subheading'
"
>
}">
<IconText lg :icon="icon" :fill="activePathName == name ? 'fill-white' : 'fill-accent'"
:iconColor="activePathName == name ? 'text-white' : 'text-accent'" :labelColor="activePathName == name ? 'text-white' : 'text-subheading'
">
{{ t(label) }}
</IconText>
</NuxtLink>
Expand All @@ -53,6 +33,7 @@ import {
BriefcaseIcon,
Bars3Icon,
BookOpenIcon,
TrophyIcon
} from "@heroicons/vue/24/solid/index.js";
import IconSkillTree from "~/components/icon/SkillTree.vue";
Expand Down Expand Up @@ -105,6 +86,12 @@ export default {
label: "Links.ReportedTasks",
pathname: "/dashboard/reported-tasks",
},
{
name: "dashboard-challenges",
icon: TrophyIcon,
label: "Links.Challenges",
pathname: "/dashboard/challenges",
},
]);
const route = useRoute();
Expand All @@ -121,7 +108,8 @@ export default {
.active-link {
background-color: var(--color-accent);
}
.active-link > * {
.active-link>* {
color: var(--color-white) !important;
fill: var(--color-white) !important;
}
Expand Down
Loading

0 comments on commit abbf877

Please sign in to comment.