implement notification page

This commit is contained in:
MiniDigger 2022-03-17 10:58:27 +01:00 committed by MiniDigger | Martin
parent 91415c0499
commit a4787254aa
6 changed files with 112 additions and 24 deletions

View File

@ -14,6 +14,7 @@ Stuff that needs to be done before I consider this a successful POC
- [ ] snackbar - [ ] snackbar
- [ ] maybe deployment alongside the existing frontend? (server is working now) - [ ] maybe deployment alongside the existing frontend? (server is working now)
- [ ] figure out why vite isn't serving the manifest - [ ] figure out why vite isn't serving the manifest
- [ ] cors?
- [x] investigate why eslint/prettier don't auto fix - [x] investigate why eslint/prettier don't auto fix
## Big list of pages! ## Big list of pages!
@ -71,9 +72,9 @@ once QA has passed, the checkboxes can be removed and the page can be ~~striked
- [ ] design - [ ] design
- [ ] qa - [ ] qa
- notifications - notifications
- [ ] fetch - [x] fetch
- [ ] layout - [x] layout
- [ ] functionality - [x] functionality (cors error)
- [ ] design - [ ] design
- [ ] qa - [ ] qa
- staff - staff

View File

@ -1,5 +1,3 @@
<script setup lang="ts"></script>
<template> <template>
<footer class="relative flex items-end mt-10 bg-gradient-to-r from-[#004ee9] to-[#367aff] px-8 pt-15 pb-2 text-background-light-10 min-h-70"> <footer class="relative flex items-end mt-10 bg-gradient-to-r from-[#004ee9] to-[#367aff] px-8 pt-15 pb-2 text-background-light-10 min-h-70">
<div class="footerContent w-screen"> <div class="footerContent w-screen">
@ -48,8 +46,6 @@
</div> </div>
</div> </div>
<div class="footerShape absolute z-1 left-0 right-0 top-0 overflow-hidden pointer-events-none" style="transform: scaleY(-1) scaleX(-1)"> <div class="footerShape absolute z-1 left-0 right-0 top-0 overflow-hidden pointer-events-none" style="transform: scaleY(-1) scaleX(-1)">
<!--- <svg class="fill-current" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 218" preserveAspectRatio="none"><path d="M0 218h1200v-31.3l-40 4.4c-40 4.8-120 13.1-200 0-80-13.6-160-48.6-240-66.7-80-17.8-160-17.8-240-8.8-80 8.6-160 26.9-240 8.8-80-17.7-160-71.1-200-97.7L0 0v218z"></path></svg>
-->
<svg <svg
class="fill-background-light-10 h-240px min-w-full" class="fill-background-light-10 h-240px min-w-full"
dark="fill-background-dark-80" dark="fill-background-dark-80"

View File

@ -137,10 +137,10 @@ authLog("render with user " + authStore.user?.name);
<icon-mdi-key-outline class="mr-1 text-[1.2em]" /> <icon-mdi-key-outline class="mr-1 text-[1.2em]" />
{{ t("nav.login") }} {{ t("nav.login") }}
</a> </a>
<a class="flex items-center rounded-md px-2 py-2" href="/signup" hover="text-primary-100 bg-primary-50"> <router-link class="flex items-center rounded-md px-2 py-2" to="/signup" hover="text-primary-100 bg-primary-50">
<icon-mdi-clipboard-outline class="mr-1 text-[1.2em]" /> <icon-mdi-clipboard-outline class="mr-1 text-[1.2em]" />
{{ t("nav.signup") }} {{ t("nav.signup") }}
</a> </router-link>
</div> </div>
</div> </div>
</div> </div>

View File

@ -24,9 +24,9 @@ export async function useAuthors(blocking = true) {
} }
export async function useInvites(blocking = true) { export async function useInvites(blocking = true) {
return await useInitialState("useAuthors", () => useInternalApi<Invites>("invites", false), blocking); return await useInitialState("useInvites", () => useInternalApi<Invites>("invites", false), blocking);
} }
export async function useNotifications(blocking = true) { export async function useNotifications(blocking = true) {
return await useInitialState("useAuthors", () => useInternalApi<HangarNotification[]>("notifications", false), blocking); return await useInitialState("useNotifications", () => useInternalApi<HangarNotification[]>("notifications", false), blocking);
} }

View File

@ -10,7 +10,6 @@ import { useContext } from "vite-ssr/vue";
const i18n = useI18n(); const i18n = useI18n();
// TODO: versions, categories, platforms and licences should be all loaded from backend eventually (see internal.BackendDataController)
const backendData = useBackendDataStore(); const backendData = useBackendDataStore();
const sorters = [ const sorters = [
{ id: "stars", label: i18n.t("project.sorting.mostStars") }, { id: "stars", label: i18n.t("project.sorting.mostStars") },
@ -20,6 +19,7 @@ const sorters = [
{ id: "updated", label: i18n.t("project.sorting.recentlyUpdated") }, { id: "updated", label: i18n.t("project.sorting.recentlyUpdated") },
]; ];
// todo versions need to be extracted from the platforms
const versions = [ const versions = [
{ version: "1.18.1" }, { version: "1.18.1" },
{ version: "1.18" }, { version: "1.18" },

View File

@ -1,36 +1,127 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useContext } from "vite-ssr/vue"; import { useContext } from "vite-ssr/vue";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { useRoute } from "vue-router"; import { useRoute, useRouter } from "vue-router";
import { useInvites, useNotifications } from "~/composables/useApiHelper"; import { useInvites, useNotifications } from "~/composables/useApiHelper";
import { handleRequestError } from "~/composables/useErrorHandling"; import { handleRequestError } from "~/composables/useErrorHandling";
import { HangarNotification, Invites } from "hangar-internal"; import { HangarNotification, Invite, Invites } from "hangar-internal";
import { computed, Ref } from "vue"; import { computed, ref, Ref } from "vue";
import { useApi, useInternalApi } from "~/composables/useApi";
import Vue from "@vitejs/plugin-vue";
const ctx = useContext(); const ctx = useContext();
const i18n = useI18n(); const i18n = useI18n();
const { params } = useRoute(); const { params } = useRoute();
const notifications = (await useNotifications().catch((e) => handleRequestError(e, ctx, i18n))) as Ref<HangarNotification[]>; const notifications = (await useNotifications().catch((e) => handleRequestError(e, ctx, i18n))) as Ref<HangarNotification[]>;
const invites = (await useInvites().catch((e) => handleRequestError(e, ctx, i18n))) as Ref<Invites>; const invites = (await useInvites().catch((e) => handleRequestError(e, ctx, i18n))) as Ref<Invites>;
const filteredInvites = computed(() => { const filters = ref({
return invites ? [...invites.value.project, invites.value.organization] : []; notification: "unread" as "unread" | "read" | "all",
invite: "all" as "organizations" | "projects" | "all",
}); });
const notificationFilter = [
{ text: i18n.t("notifications.unread"), value: "unread" },
{ text: i18n.t("notifications.read"), value: "read" },
{ text: i18n.t("notifications.all"), value: "all" },
];
const inviteFilter = [
{ text: i18n.t("notifications.invite.organizations"), value: "organizations" },
{ text: i18n.t("notifications.invite.projects"), value: "projects" },
{ text: i18n.t("notifications.invite.all"), value: "all" },
];
const filteredInvites = computed(() => {
if (!invites || !invites.value) return [];
switch (filters.value.invite) {
case "projects":
return invites.value.project;
case "organizations":
return invites.value.organization;
default:
return [...invites.value.project, ...invites.value.organization];
}
});
const filteredNotifications = computed(() => {
if (!notifications || !notifications.value) return [];
switch (filters.value.notification) {
case "unread":
return notifications.value.filter((n) => !n.read);
case "read":
return notifications.value.filter((n) => n.read);
default:
return notifications.value;
}
});
function markAllAsRead() {
for (const notification of notifications.value.filter((n) => !n.read)) {
markNotificationRead(notification, false);
}
}
async function markNotificationRead(notification: HangarNotification, router = true) {
const result = await useInternalApi(`notifications/${notification.id}`, true, "post").catch((e) => handleRequestError(e, ctx, i18n));
if (!result) return;
delete notifications.value[notifications.value.findIndex((n) => n.id === notification.id)];
if (notification.action && router) {
await useRouter().push(notification.action);
}
}
async function updateInvite(invite: Invite, status: "accept" | "decline" | "unaccept") {
const result = await useInternalApi(`invites/${invite.type}/${invite.roleTableId}/${status}`, true, "post").catch((e) => handleRequestError(e, ctx, i18n));
if (!result) return;
if (status === "accept") {
invite.accepted = true;
} else if (status === "unaccept") {
invite.accepted = false;
} else {
delete invites.value[invite.type][invites.value[invite.type].indexOf(invite)];
}
// this.$util.success(this.$t(`notifications.invite.msgs.${status}`, [invite.name])); // TODO success notification
}
</script> </script>
<template> <template>
<h1>Notifications</h1>
<div class="flex"> <div class="flex">
<div> <div>
<span v-for="notification in notifications" :key="notification.id"> <h1>Notifications</h1>
<select v-model="filters.notification">
<option v-for="filter in notificationFilter" :key="filter.value" :value="filter.value">{{ filter.text }}</option>
</select>
<button v-if="filteredNotifications && filters && filters.notification === 'unread'" @click="markAllAsRead">
{{ i18n.t("notifications.readAll") }}
</button>
<div v-for="notification in filteredNotifications" :key="notification.id">
Type: {{ notification.type }}<br /> Type: {{ notification.type }}<br />
Message: {{ notification.message }} Message: {{ i18n.t(notification.message[0], notification.message.slice(1)) }}<br />
</span> <button @click="markNotificationRead(notification)">Mark read</button>
</div>
<div v-if="!filteredNotifications.length">
{{ i18n.t(`notifications.empty.${filters.notification}`) }}
</div>
</div> </div>
<div> <div>
<span v-for="(invite, index) in filteredInvites" :key="index"> <h1>Invites</h1>
{{ invite }} <select v-model="filters.invite">
</span> <option v-for="filter in inviteFilter" :key="filter.value" :value="filter.value">{{ filter.text }}</option>
</select>
<div v-for="(invite, index) in filteredInvites" :key="index">
{{ i18n.t(!invite.accepted ? "notifications.invited" : "notifications.inviteAccepted", [invite.type]) }}:
<router-link :to="invite.url" exact>{{ invite.name }}</router-link>
<template v-if="invite.accepted">
<button @click="updateInvite(invite, 'unaccept')">{{ i18n.t("notifications.invite.btns.unaccept") }}</button>
</template>
<template v-else>
<button @click="updateInvite(invite, 'accept')">{{ i18n.t("notifications.invite.btns.accept") }}</button>
<button @click="updateInvite(invite, 'decline')">{{ i18n.t("notifications.invite.btns.decline") }}</button>
</template>
</div>
<div v-if="!filteredInvites.length">
{{ i18n.t("notifications.empty.invites") }}
</div>
</div> </div>
</div> </div>
</template> </template>