Adds in dashboard page showing basic Mantis statistics
This commit is contained in:
parent
7564937faa
commit
92230f8a07
13 changed files with 595 additions and 19 deletions
8
src/boot/apexcharts.js
Normal file
8
src/boot/apexcharts.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
import { defineBoot } from '#q-app/wrappers';
|
||||
|
||||
import VueApexCharts from 'vue3-apexcharts';
|
||||
|
||||
export default defineBoot(async({app}) =>
|
||||
{
|
||||
app.use(VueApexCharts);
|
||||
});
|
|
@ -1,14 +1,30 @@
|
|||
import { boot } from 'quasar/wrappers';
|
||||
import axios from 'axios';
|
||||
|
||||
// Be careful when using SSR for cross-request state pollution
|
||||
// due to creating a Singleton instance here;
|
||||
// If any client changes this (global) instance, it might be a
|
||||
// good idea to move this instance creation inside of the
|
||||
// "export default () => {}" function below (which runs individually
|
||||
// for each client)
|
||||
import SuperJSON from 'superjson';
|
||||
|
||||
axios.defaults.withCredentials = true; // Enable sending cookies with requests
|
||||
|
||||
// Export the API instance so you can import it easily elsewhere, e.g. stores
|
||||
axios.interceptors.response.use(
|
||||
(response) =>
|
||||
{
|
||||
//If the response content type is application/json, we want to parse it with SuperJSON
|
||||
if (response.headers['content-type'] && response.headers['content-type'].includes('application/json'))
|
||||
{
|
||||
try
|
||||
{
|
||||
response.data = SuperJSON.parse(JSON.stringify(response.data));
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
console.error('Error deserializing response data:', error);
|
||||
}
|
||||
}
|
||||
return response;
|
||||
},
|
||||
(error) =>
|
||||
{
|
||||
// Handle errors here if needed
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
export default axios;
|
|
@ -21,4 +21,8 @@ body {
|
|||
.bg-blurred {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.drop-shadow {
|
||||
filter: drop-shadow(0 0 25px rgba(0, 0, 0, 0.5));
|
||||
}
|
366
src/pages/DashboardPage.vue
Normal file
366
src/pages/DashboardPage.vue
Normal file
|
@ -0,0 +1,366 @@
|
|||
<template>
|
||||
<q-page padding>
|
||||
<div class="q-pa-md">
|
||||
<div class="text-h4 q-mb-md">
|
||||
Dashboard
|
||||
</div>
|
||||
|
||||
<div class="row q-col-gutter-md">
|
||||
<!-- Mantis Issues Chart -->
|
||||
<div class="col-12 col-md-6">
|
||||
<q-card class="dashboard-card">
|
||||
<q-card-section>
|
||||
<div class="text-h6">
|
||||
New Mantis Issues
|
||||
</div>
|
||||
<div class="text-subtitle2">
|
||||
Last 7 days
|
||||
</div>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section>
|
||||
<div
|
||||
v-if="loadingIssues"
|
||||
class="flex flex-center q-pa-xl"
|
||||
>
|
||||
<q-spinner
|
||||
color="primary"
|
||||
size="3em"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="issueChartData.series[0].data.length === 0"
|
||||
class="text-center q-pa-md"
|
||||
>
|
||||
<q-icon
|
||||
name="info"
|
||||
color="grey"
|
||||
size="3em"
|
||||
/>
|
||||
<div class="text-grey q-mt-md">
|
||||
No data available for this period
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<apexchart
|
||||
type="bar"
|
||||
height="300"
|
||||
:options="issueChartOptions"
|
||||
:series="issueChartData.series"
|
||||
/>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
<!-- Mantis Comments Chart -->
|
||||
<div class="col-12 col-md-6">
|
||||
<q-card class="dashboard-card">
|
||||
<q-card-section>
|
||||
<div class="text-h6">
|
||||
New Mantis Comments
|
||||
</div>
|
||||
<div class="text-subtitle2">
|
||||
Last 7 days
|
||||
</div>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section>
|
||||
<div
|
||||
v-if="loadingComments"
|
||||
class="flex flex-center q-pa-xl"
|
||||
>
|
||||
<q-spinner
|
||||
color="primary"
|
||||
size="3em"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="commentChartData.series[0].data.length === 0"
|
||||
class="text-center q-pa-md"
|
||||
>
|
||||
<q-icon
|
||||
name="info"
|
||||
color="grey"
|
||||
size="3em"
|
||||
/>
|
||||
<div class="text-grey q-mt-md">
|
||||
No data available for this period
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<apexchart
|
||||
type="bar"
|
||||
height="300"
|
||||
:options="commentChartOptions"
|
||||
:series="commentChartData.series"
|
||||
/>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent, ref, computed, onMounted, watch } from 'vue';
|
||||
import { useQuasar } from 'quasar';
|
||||
import { date } from 'quasar';
|
||||
import axios from 'boot/axios';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'DashboardPage',
|
||||
|
||||
setup()
|
||||
{
|
||||
const $q = useQuasar();
|
||||
|
||||
const loadingIssues = ref(true);
|
||||
const loadingComments = ref(true);
|
||||
const issuesData = ref([]);
|
||||
const commentsData = ref([]);
|
||||
|
||||
// Format date as "Monday 25th"
|
||||
const formatDate = (dateStr) =>
|
||||
{
|
||||
const dateObj = new Date(dateStr);
|
||||
const day = date.formatDate(dateObj, 'dddd');
|
||||
const dayNum = dateObj.getDate();
|
||||
|
||||
// Add ordinal suffix (st, nd, rd, th)
|
||||
let suffix = 'th';
|
||||
if (dayNum % 10 === 1 && dayNum !== 11) suffix = 'st';
|
||||
else if (dayNum % 10 === 2 && dayNum !== 12) suffix = 'nd';
|
||||
else if (dayNum % 10 === 3 && dayNum !== 13) suffix = 'rd';
|
||||
|
||||
return `${day} ${dayNum}${suffix}`;
|
||||
};
|
||||
|
||||
// Generate an array of dates for the last 7 days
|
||||
const getLast7Days = () =>
|
||||
{
|
||||
const result = [];
|
||||
for (let i = 6; i >= 0; i--)
|
||||
{
|
||||
const d = new Date();
|
||||
d.setDate(d.getDate() - i);
|
||||
result.push(date.formatDate(d, 'YYYY-MM-DD'));
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
// Prepare data with 0 counts for days with no data
|
||||
const prepareChartData = (data) =>
|
||||
{
|
||||
const last7Days = getLast7Days();
|
||||
const countMap = {};
|
||||
|
||||
// Initialize with 0 counts
|
||||
last7Days.forEach(day =>
|
||||
{
|
||||
countMap[day] = 0;
|
||||
});
|
||||
|
||||
// Fill in actual counts
|
||||
data.forEach(item =>
|
||||
{
|
||||
console.log(item);
|
||||
const dateStr = item.date.toISOString().split('T')[0];
|
||||
if (countMap[dateStr] !== undefined)
|
||||
{
|
||||
countMap[dateStr] = Number(item.count);
|
||||
}
|
||||
});
|
||||
|
||||
// Convert to arrays for chart
|
||||
const categories = Object.keys(countMap).map(formatDate);
|
||||
const counts = Object.values(countMap);
|
||||
|
||||
return {
|
||||
categories,
|
||||
series: [{
|
||||
name: 'Count',
|
||||
data: counts
|
||||
}]
|
||||
};
|
||||
};
|
||||
|
||||
// Computed properties for chart data
|
||||
const issueChartData = computed(() =>
|
||||
{
|
||||
return prepareChartData(issuesData.value);
|
||||
});
|
||||
|
||||
const commentChartData = computed(() =>
|
||||
{
|
||||
return prepareChartData(commentsData.value);
|
||||
});
|
||||
|
||||
// Chart options
|
||||
const getChartOptions = (title, isDark) =>
|
||||
{
|
||||
return {
|
||||
chart: {
|
||||
type: 'bar',
|
||||
height: 300,
|
||||
toolbar: {
|
||||
show: false
|
||||
},
|
||||
foreColor: isDark ? '#ccc' : '#333'
|
||||
},
|
||||
plotOptions: {
|
||||
bar: {
|
||||
borderRadius: 6,
|
||||
columnWidth: '50%',
|
||||
dataLabels: {
|
||||
position: 'top'
|
||||
}
|
||||
}
|
||||
},
|
||||
colors: [isDark ? '#3498db' : '#1976D2'],
|
||||
dataLabels: {
|
||||
enabled: true,
|
||||
offsetY: -20,
|
||||
style: {
|
||||
fontSize: '12px',
|
||||
colors: [isDark ? '#fff' : '#000']
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
borderColor: isDark ? '#555' : '#e0e0e0'
|
||||
},
|
||||
xaxis: {
|
||||
categories: issueChartData.value.categories,
|
||||
position: 'bottom',
|
||||
axisBorder: {
|
||||
show: false
|
||||
},
|
||||
axisTicks: {
|
||||
show: false
|
||||
}
|
||||
},
|
||||
yaxis: {
|
||||
axisBorder: {
|
||||
show: false
|
||||
},
|
||||
axisTicks: {
|
||||
show: false
|
||||
},
|
||||
labels: {
|
||||
show: true
|
||||
},
|
||||
min: 0,
|
||||
forceNiceScale: true
|
||||
},
|
||||
title: {
|
||||
text: title,
|
||||
align: 'center',
|
||||
style: {
|
||||
fontSize: '16px',
|
||||
color: isDark ? '#fff' : '#333'
|
||||
},
|
||||
floating: false
|
||||
},
|
||||
theme: {
|
||||
mode: isDark ? 'dark' : 'light'
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// Computed options that change with dark mode
|
||||
const issueChartOptions = computed(() =>
|
||||
{
|
||||
return getChartOptions('New Mantis Issues', $q.dark.isActive);
|
||||
});
|
||||
|
||||
const commentChartOptions = computed(() =>
|
||||
{
|
||||
return getChartOptions('New Mantis Comments', $q.dark.isActive);
|
||||
});
|
||||
|
||||
// Fetch data from API
|
||||
const fetchIssuesData = async() =>
|
||||
{
|
||||
loadingIssues.value = true;
|
||||
try
|
||||
{
|
||||
const response = await axios.get('/api/mantis/stats/issues');
|
||||
issuesData.value = response.data;
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
console.error('Error fetching issues data:', error);
|
||||
$q.notify({
|
||||
color: 'negative',
|
||||
message: 'Failed to load issues data',
|
||||
icon: 'warning'
|
||||
});
|
||||
}
|
||||
finally
|
||||
{
|
||||
loadingIssues.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const fetchCommentsData = async() =>
|
||||
{
|
||||
loadingComments.value = true;
|
||||
try
|
||||
{
|
||||
const response = await axios.get('/api/mantis/stats/comments');
|
||||
console.log(response.data);
|
||||
commentsData.value = response.data;
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
console.error('Error fetching comments data:', error);
|
||||
$q.notify({
|
||||
color: 'negative',
|
||||
message: 'Failed to load comments data',
|
||||
icon: 'warning'
|
||||
});
|
||||
}
|
||||
finally
|
||||
{
|
||||
loadingComments.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Watch for dark mode changes
|
||||
const refreshCharts = () =>
|
||||
{
|
||||
issueChartOptions.value = getChartOptions('New Mantis Issues', $q.dark.isActive);
|
||||
commentChartOptions.value = getChartOptions('New Mantis Comments', $q.dark.isActive);
|
||||
};
|
||||
|
||||
watch(
|
||||
() => $q.dark.isActive,
|
||||
refreshCharts
|
||||
);
|
||||
|
||||
// Fetch data on component mount
|
||||
onMounted(() =>
|
||||
{
|
||||
fetchIssuesData();
|
||||
fetchCommentsData();
|
||||
});
|
||||
|
||||
return {
|
||||
loadingIssues,
|
||||
loadingComments,
|
||||
issueChartData,
|
||||
commentChartData,
|
||||
issueChartOptions,
|
||||
commentChartOptions
|
||||
};
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dashboard-card {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
|
@ -4,7 +4,7 @@
|
|||
<img
|
||||
src="/stylepoint.png"
|
||||
alt="StylePoint Logo"
|
||||
class="logo q-mb-md"
|
||||
class="drop-shadow q-mb-md"
|
||||
style="max-width: 300px; width: 100%;"
|
||||
>
|
||||
<h1 class="text-h3 text-weight-bold text-yellow q-mb-sm">
|
||||
|
@ -63,10 +63,6 @@ const features = ref([
|
|||
text-shadow: 0 0 10px rgba(0, 0, 0, 0.75);
|
||||
}
|
||||
|
||||
.logo {
|
||||
filter: drop-shadow(0 0 25px rgba(0, 0, 0, 0.5));
|
||||
}
|
||||
|
||||
.features {
|
||||
background-color: rgba(0, 0, 0, 0.25);
|
||||
border-radius: 10px;
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<img
|
||||
src="/stylepoint.png"
|
||||
alt="StylePoint Logo"
|
||||
class="logo q-mb-md absolute"
|
||||
class="drop-shadow q-mb-md absolute"
|
||||
style="max-width: 300px; width: 100%; top: 75px;"
|
||||
>
|
||||
<q-card
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<img
|
||||
src="/stylepoint.png"
|
||||
alt="StylePoint Logo"
|
||||
class="logo q-mb-md absolute"
|
||||
class="drop-shadow q-mb-md absolute"
|
||||
style="max-width: 300px; width: 100%; top: 75px;"
|
||||
>
|
||||
<q-card
|
||||
|
|
|
@ -33,7 +33,18 @@ const routes = [
|
|||
caption: 'Create an account'
|
||||
}
|
||||
},
|
||||
// Add a new route specifically for managing passkeys when logged in
|
||||
{
|
||||
path: '/dashboard',
|
||||
name: 'dashboard',
|
||||
component: () => import('pages/DashboardPage.vue'),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
navGroup: 'auth',
|
||||
icon: 'dashboard',
|
||||
title: 'Dashboard',
|
||||
caption: 'Overview and stats'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/passkeys',
|
||||
name: 'passkeys',
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue