mirror of
https://github.com/HangarMC/Hangar.git
synced 2025-01-30 14:30:08 +08:00
implement notification page
This commit is contained in:
parent
91415c0499
commit
a4787254aa
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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" },
|
||||
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user