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
- [ ] maybe deployment alongside the existing frontend? (server is working now)
- [ ] figure out why vite isn't serving the manifest
- [ ] cors?
- [x] investigate why eslint/prettier don't auto fix
## Big list of pages!
@ -71,9 +72,9 @@ once QA has passed, the checkboxes can be removed and the page can be ~~striked
- [ ] design
- [ ] qa
- notifications
- [ ] fetch
- [ ] layout
- [ ] functionality
- [x] fetch
- [x] layout
- [x] functionality (cors error)
- [ ] design
- [ ] qa
- staff

View File

@ -1,5 +1,3 @@
<script setup lang="ts"></script>
<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">
<div class="footerContent w-screen">
@ -48,8 +46,6 @@
</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)">
<!--- <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
class="fill-background-light-10 h-240px min-w-full"
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]" />
{{ t("nav.login") }}
</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]" />
{{ t("nav.signup") }}
</a>
</router-link>
</div>
</div>
</div>

View File

@ -24,9 +24,9 @@ export async function useAuthors(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) {
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();
// TODO: versions, categories, platforms and licences should be all loaded from backend eventually (see internal.BackendDataController)
const backendData = useBackendDataStore();
const sorters = [
{ id: "stars", label: i18n.t("project.sorting.mostStars") },
@ -20,6 +19,7 @@ const sorters = [
{ id: "updated", label: i18n.t("project.sorting.recentlyUpdated") },
];
// todo versions need to be extracted from the platforms
const versions = [
{ version: "1.18.1" },
{ version: "1.18" },

View File

@ -1,36 +1,127 @@
<script lang="ts" setup>
import { useContext } from "vite-ssr/vue";
import { useI18n } from "vue-i18n";
import { useRoute } from "vue-router";
import { useRoute, useRouter } from "vue-router";
import { useInvites, useNotifications } from "~/composables/useApiHelper";
import { handleRequestError } from "~/composables/useErrorHandling";
import { HangarNotification, Invites } from "hangar-internal";
import { computed, Ref } from "vue";
import { HangarNotification, Invite, Invites } from "hangar-internal";
import { computed, ref, Ref } from "vue";
import { useApi, useInternalApi } from "~/composables/useApi";
import Vue from "@vitejs/plugin-vue";
const ctx = useContext();
const i18n = useI18n();
const { params } = useRoute();
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 filteredInvites = computed(() => {
return invites ? [...invites.value.project, invites.value.organization] : [];
const filters = ref({
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>
<template>
<h1>Notifications</h1>
<div class="flex">
<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 />
Message: {{ notification.message }}
</span>
Message: {{ i18n.t(notification.message[0], notification.message.slice(1)) }}<br />
<button @click="markNotificationRead(notification)">Mark read</button>
</div>
<div v-if="!filteredNotifications.length">
{{ i18n.t(`notifications.empty.${filters.notification}`) }}
</div>
</div>
<div>
<span v-for="(invite, index) in filteredInvites" :key="index">
{{ invite }}
</span>
<h1>Invites</h1>
<select v-model="filters.invite">
<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>
</template>