From 5320f2301718e3e3b8f69f979c5dccefffe7689d Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Sun, 8 Jan 2023 14:17:56 +0900 Subject: [PATCH] enhance(client): improve user activity page --- packages/frontend/src/components/MkFolder.vue | 10 +- packages/frontend/src/components/MkSignin.vue | 4 +- .../src/pages/user/activity.following.vue | 174 ++++++++++++++++++ .../src/pages/user/activity.notes.vue | 174 ++++++++++++++++++ .../frontend/src/pages/user/activity.pv.vue | 78 +++----- packages/frontend/src/pages/user/activity.vue | 14 +- 6 files changed, 386 insertions(+), 68 deletions(-) create mode 100644 packages/frontend/src/pages/user/activity.following.vue create mode 100644 packages/frontend/src/pages/user/activity.notes.vue diff --git a/packages/frontend/src/components/MkFolder.vue b/packages/frontend/src/components/MkFolder.vue index dc10c7d3f3..aa2d9aac25 100644 --- a/packages/frontend/src/components/MkFolder.vue +++ b/packages/frontend/src/components/MkFolder.vue @@ -1,7 +1,7 @@ <template> <div class="ssazuxis"> <header class="_button" :style="{ background: bg }" @click="showBody = !showBody"> - <div class="title"><slot name="header"></slot></div> + <div class="title"><div><slot name="header"></slot></div></div> <div class="divider"></div> <button class="_button"> <template v-if="showBody"><i class="ti ti-chevron-up"></i></template> @@ -127,14 +127,6 @@ export default defineComponent({ place-content: center; margin: 0; padding: 12px 16px 12px 0; - - > i { - margin-right: 6px; - } - - &:empty { - display: none; - } } > .divider { diff --git a/packages/frontend/src/components/MkSignin.vue b/packages/frontend/src/components/MkSignin.vue index 56e6f938f8..d70ae13ffd 100644 --- a/packages/frontend/src/components/MkSignin.vue +++ b/packages/frontend/src/components/MkSignin.vue @@ -14,7 +14,7 @@ <template #prefix><i class="ti ti-lock"></i></template> <template #caption><button class="_textButton" type="button" @click="resetPassword">{{ i18n.ts.forgotPassword }}</button></template> </MkInput> - <MkButton type="submit" primary :disabled="signing" style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton> + <MkButton type="submit" large primary rounded :disabled="signing" style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton> </div> <div v-if="totpLogin" class="2fa-signin" :class="{ securityKeys: user && user.securityKeys }"> <div v-if="user && user.securityKeys" class="twofa-group tap-group"> @@ -36,7 +36,7 @@ <template #label>{{ i18n.ts.token }}</template> <template #prefix><i class="ti ti-123"></i></template> </MkInput> - <MkButton type="submit" :disabled="signing" primary style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton> + <MkButton type="submit" :disabled="signing" large primary rounded style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton> </div> </div> </div> diff --git a/packages/frontend/src/pages/user/activity.following.vue b/packages/frontend/src/pages/user/activity.following.vue new file mode 100644 index 0000000000..b7a51d5b69 --- /dev/null +++ b/packages/frontend/src/pages/user/activity.following.vue @@ -0,0 +1,174 @@ +<template> +<div> + <MkLoading v-if="fetching"/> + <div v-show="!fetching" :class="$style.root" class="_panel"> + <canvas ref="chartEl"></canvas> + <MkChartLegend ref="legendEl" style="margin-top: 8px;"/> + </div> +</div> +</template> + +<script lang="ts" setup> +import { markRaw, version as vueVersion, onMounted, onBeforeUnmount, nextTick } from 'vue'; +import { Chart, ChartDataset } from 'chart.js'; +import tinycolor from 'tinycolor2'; +import * as misskey from 'misskey-js'; +import gradient from 'chartjs-plugin-gradient'; +import { satisfies } from 'compare-versions'; +import * as os from '@/os'; +import { defaultStore } from '@/store'; +import { useChartTooltip } from '@/scripts/use-chart-tooltip'; +import { chartVLine } from '@/scripts/chart-vline'; +import { alpha } from '@/scripts/color'; +import { initChart } from '@/scripts/init-chart'; +import { chartLegend } from '@/scripts/chart-legend'; +import MkChartLegend from '@/components/MkChartLegend.vue'; + +initChart(); + +const props = defineProps<{ + user: misskey.entities.User; +}>(); + +const chartEl = $shallowRef<HTMLCanvasElement>(null); +let legendEl = $shallowRef<InstanceType<typeof MkChartLegend>>(); +const now = new Date(); +let chartInstance: Chart = null; +const chartLimit = 50; +let fetching = $ref(true); + +const { handler: externalTooltipHandler } = useChartTooltip(); + +async function renderChart() { + if (chartInstance) { + chartInstance.destroy(); + } + + const getDate = (ago: number) => { + const y = now.getFullYear(); + const m = now.getMonth(); + const d = now.getDate(); + + return new Date(y, m, d - ago); + }; + + const format = (arr) => { + return arr.map((v, i) => ({ + x: getDate(i).getTime(), + y: v, + })); + }; + + const raw = await os.api('charts/user/following', { userId: props.user.id, limit: chartLimit, span: 'day' }); + + const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'; + + const colorFollowLocal = '#008FFB'; + const colorFollowRemote = '#008FFB88'; + const colorFollowedLocal = '#2ecc71'; + const colorFollowedRemote = '#2ecc7188'; + + function makeDataset(label: string, data: ChartDataset['data'], extra: Partial<ChartDataset> = {}): ChartDataset { + return Object.assign({ + label: label, + data: data, + parsing: false, + pointRadius: 0, + borderWidth: 0, + borderJoinStyle: 'round', + borderRadius: 4, + barPercentage: 0.9, + fill: true, + } satisfies ChartDataset, extra); + } + + chartInstance = new Chart(chartEl, { + type: 'bar', + data: { + datasets: [ + makeDataset('Follow (local)', format(raw.local.followings.inc).slice().reverse(), { backgroundColor: colorFollowLocal }), + makeDataset('Follow (remote)', format(raw.remote.followings.inc).slice().reverse(), { backgroundColor: colorFollowRemote }), + makeDataset('Followed (local)', format(raw.local.followers.inc).slice().reverse(), { backgroundColor: colorFollowedLocal }), + makeDataset('Followed (remote)', format(raw.remote.followers.inc).slice().reverse(), { backgroundColor: colorFollowedRemote }), + ], + }, + options: { + aspectRatio: 3, + layout: { + padding: { + left: 0, + right: 8, + top: 0, + bottom: 0, + }, + }, + scales: { + x: { + type: 'time', + offset: true, + stacked: true, + time: { + stepSize: 1, + unit: 'day', + displayFormats: { + day: 'M/d', + month: 'Y/M', + }, + }, + grid: { + display: false, + }, + ticks: { + display: true, + maxRotation: 0, + autoSkipPadding: 8, + }, + }, + y: { + position: 'left', + stacked: true, + suggestedMax: 10, + grid: { + display: true, + }, + ticks: { + display: true, + //mirror: true, + }, + }, + }, + interaction: { + intersect: false, + mode: 'index', + }, + plugins: { + legend: { + display: false, + }, + tooltip: { + enabled: false, + mode: 'index', + animation: { + duration: 0, + }, + external: externalTooltipHandler, + }, + gradient, + }, + }, + plugins: [chartVLine(vLineColor), chartLegend(legendEl)], + }); + + fetching = false; +} + +onMounted(async () => { + renderChart(); +}); +</script> + +<style lang="scss" module> +.root { + padding: 20px; +} +</style> diff --git a/packages/frontend/src/pages/user/activity.notes.vue b/packages/frontend/src/pages/user/activity.notes.vue new file mode 100644 index 0000000000..f2103b152c --- /dev/null +++ b/packages/frontend/src/pages/user/activity.notes.vue @@ -0,0 +1,174 @@ +<template> +<div> + <MkLoading v-if="fetching"/> + <div v-show="!fetching" :class="$style.root" class="_panel"> + <canvas ref="chartEl"></canvas> + <MkChartLegend ref="legendEl" style="margin-top: 8px;"/> + </div> +</div> +</template> + +<script lang="ts" setup> +import { markRaw, version as vueVersion, onMounted, onBeforeUnmount, nextTick } from 'vue'; +import { Chart, ChartDataset } from 'chart.js'; +import tinycolor from 'tinycolor2'; +import * as misskey from 'misskey-js'; +import gradient from 'chartjs-plugin-gradient'; +import { satisfies } from 'compare-versions'; +import * as os from '@/os'; +import { defaultStore } from '@/store'; +import { useChartTooltip } from '@/scripts/use-chart-tooltip'; +import { chartVLine } from '@/scripts/chart-vline'; +import { alpha } from '@/scripts/color'; +import { initChart } from '@/scripts/init-chart'; +import { chartLegend } from '@/scripts/chart-legend'; +import MkChartLegend from '@/components/MkChartLegend.vue'; + +initChart(); + +const props = defineProps<{ + user: misskey.entities.User; +}>(); + +const chartEl = $shallowRef<HTMLCanvasElement>(null); +let legendEl = $shallowRef<InstanceType<typeof MkChartLegend>>(); +const now = new Date(); +let chartInstance: Chart = null; +const chartLimit = 50; +let fetching = $ref(true); + +const { handler: externalTooltipHandler } = useChartTooltip(); + +async function renderChart() { + if (chartInstance) { + chartInstance.destroy(); + } + + const getDate = (ago: number) => { + const y = now.getFullYear(); + const m = now.getMonth(); + const d = now.getDate(); + + return new Date(y, m, d - ago); + }; + + const format = (arr) => { + return arr.map((v, i) => ({ + x: getDate(i).getTime(), + y: v, + })); + }; + + const raw = await os.api('charts/user/notes', { userId: props.user.id, limit: chartLimit, span: 'day' }); + + const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'; + + const colorNormal = '#008FFB'; + const colorReply = '#FEB019'; + const colorRenote = '#00E396'; + const colorFile = '#e300db'; + + function makeDataset(label: string, data: ChartDataset['data'], extra: Partial<ChartDataset> = {}): ChartDataset { + return Object.assign({ + label: label, + data: data, + parsing: false, + pointRadius: 0, + borderWidth: 0, + borderJoinStyle: 'round', + borderRadius: 4, + barPercentage: 0.9, + fill: true, + } satisfies ChartDataset, extra); + } + + chartInstance = new Chart(chartEl, { + type: 'bar', + data: { + datasets: [ + makeDataset('Normal', format(raw.diffs.normal).slice().reverse(), { backgroundColor: colorNormal }), + makeDataset('Reply', format(raw.diffs.reply).slice().reverse(), { backgroundColor: colorReply }), + makeDataset('Renote', format(raw.diffs.renote).slice().reverse(), { backgroundColor: colorRenote }), + makeDataset('File', format(raw.diffs.withFile).slice().reverse(), { backgroundColor: colorFile }), + ], + }, + options: { + aspectRatio: 3, + layout: { + padding: { + left: 0, + right: 8, + top: 0, + bottom: 0, + }, + }, + scales: { + x: { + type: 'time', + offset: true, + stacked: true, + time: { + stepSize: 1, + unit: 'day', + displayFormats: { + day: 'M/d', + month: 'Y/M', + }, + }, + grid: { + display: false, + }, + ticks: { + display: true, + maxRotation: 0, + autoSkipPadding: 8, + }, + }, + y: { + position: 'left', + stacked: true, + suggestedMax: 10, + grid: { + display: true, + }, + ticks: { + display: true, + //mirror: true, + }, + }, + }, + interaction: { + intersect: false, + mode: 'index', + }, + plugins: { + legend: { + display: false, + }, + tooltip: { + enabled: false, + mode: 'index', + animation: { + duration: 0, + }, + external: externalTooltipHandler, + }, + gradient, + }, + }, + plugins: [chartVLine(vLineColor), chartLegend(legendEl)], + }); + + fetching = false; +} + +onMounted(async () => { + renderChart(); +}); +</script> + +<style lang="scss" module> +.root { + padding: 20px; +} +</style> diff --git a/packages/frontend/src/pages/user/activity.pv.vue b/packages/frontend/src/pages/user/activity.pv.vue index d74b641dac..4be6978da0 100644 --- a/packages/frontend/src/pages/user/activity.pv.vue +++ b/packages/frontend/src/pages/user/activity.pv.vue @@ -10,7 +10,7 @@ <script lang="ts" setup> import { markRaw, version as vueVersion, onMounted, onBeforeUnmount, nextTick } from 'vue'; -import { Chart } from 'chart.js'; +import { Chart, ChartDataset } from 'chart.js'; import tinycolor from 'tinycolor2'; import * as misskey from 'misskey-js'; import gradient from 'chartjs-plugin-gradient'; @@ -67,65 +67,33 @@ async function renderChart() { const colorUser2 = '#3498db88'; const colorVisitor2 = '#2ecc7188'; + function makeDataset(label: string, data: ChartDataset['data'], extra: Partial<ChartDataset> = {}): ChartDataset { + return Object.assign({ + label: label, + data: data, + parsing: false, + pointRadius: 0, + borderWidth: 0, + borderJoinStyle: 'round', + borderRadius: 4, + barPercentage: 0.7, + categoryPercentage: 0.7, + fill: true, + } satisfies ChartDataset, extra); + } + chartInstance = new Chart(chartEl, { type: 'bar', data: { - datasets: [{ - parsing: false, - label: 'UPV (user)', - data: format(raw.upv.user).slice().reverse(), - pointRadius: 0, - borderWidth: 0, - borderJoinStyle: 'round', - borderRadius: 4, - backgroundColor: colorUser, - barPercentage: 0.7, - categoryPercentage: 0.7, - fill: true, - stack: 'u', - }, { - parsing: false, - label: 'UPV (visitor)', - data: format(raw.upv.visitor).slice().reverse(), - pointRadius: 0, - borderWidth: 0, - borderJoinStyle: 'round', - borderRadius: 4, - backgroundColor: colorVisitor, - barPercentage: 0.7, - categoryPercentage: 0.7, - fill: true, - stack: 'u', - }, { - parsing: false, - label: 'NPV (user)', - data: format(raw.pv.user).slice().reverse(), - pointRadius: 0, - borderWidth: 0, - borderJoinStyle: 'round', - borderRadius: 4, - backgroundColor: colorUser2, - barPercentage: 0.7, - categoryPercentage: 0.7, - fill: true, - stack: 'n', - }, { - parsing: false, - label: 'NPV (visitor)', - data: format(raw.pv.visitor).slice().reverse(), - pointRadius: 0, - borderWidth: 0, - borderJoinStyle: 'round', - borderRadius: 4, - backgroundColor: colorVisitor2, - barPercentage: 0.7, - categoryPercentage: 0.7, - fill: true, - stack: 'n', - }], + datasets: [ + makeDataset('UPV (user)', format(raw.upv.user).slice().reverse(), { backgroundColor: colorUser, stack: 'u' }), + makeDataset('UPV (visitor)', format(raw.upv.visitor).slice().reverse(), { backgroundColor: colorVisitor, stack: 'u' }), + makeDataset('NPV (user)', format(raw.pv.user).slice().reverse(), { backgroundColor: colorUser2, stack: 'n' }), + makeDataset('UPV (visitor)', format(raw.pv.visitor).slice().reverse(), { backgroundColor: colorVisitor2, stack: 'n' }), + ], }, options: { - aspectRatio: 2.5, + aspectRatio: 3, layout: { padding: { left: 0, diff --git a/packages/frontend/src/pages/user/activity.vue b/packages/frontend/src/pages/user/activity.vue index 3def414674..e74b82fb27 100644 --- a/packages/frontend/src/pages/user/activity.vue +++ b/packages/frontend/src/pages/user/activity.vue @@ -2,11 +2,19 @@ <MkSpacer :content-max="700"> <div class="_gaps"> <MkFolder class="item"> - <template #header>Heatmap</template> + <template #header><i class="ti ti-activity"></i> Heatmap</template> <XHeatmap :user="user" :src="'notes'"/> </MkFolder> <MkFolder class="item"> - <template #header>PV</template> + <template #header><i class="ti ti-pencil"></i> Notes</template> + <XNotes :user="user"/> + </MkFolder> + <MkFolder class="item"> + <template #header><i class="ti ti-users"></i> Following</template> + <XFollowing :user="user"/> + </MkFolder> + <MkFolder class="item"> + <template #header><i class="ti ti-eye"></i> PV</template> <XPv :user="user"/> </MkFolder> </div> @@ -18,6 +26,8 @@ import { computed } from 'vue'; import * as misskey from 'misskey-js'; import XHeatmap from './activity.heatmap.vue'; import XPv from './activity.pv.vue'; +import XNotes from './activity.notes.vue'; +import XFollowing from './activity.following.vue'; import MkFolder from '@/components/MkFolder.vue'; const props = defineProps<{