Fix notification & follow list sorting, Add notification badge + loading animation (#206)

* Order notifications and followed mods

Fixes modrinth/knossos#195

* Add user notification badge on avatar

Closes modrinth/knossos#145

* Add loading animation

* Chain calls, remove console.log

* Chain calls

* Fix formatting to match prettier

* Remove unused userFollows

* Create user vuex store

* Add notification count indication on dashboard

* Fix background for light mode

* Move delay check to action, add force parameter

* Slightly decrease notification badge opacity on dashboard

* Remove SVG for image masking, use border around bubble

Also adds CSS for when the dropdown is opened/hovered

* Fix merge conflicts

Co-authored-by: Geometrically <18202329+Geometrically@users.noreply.github.com>
This commit is contained in:
venashial
2021-05-27 09:32:34 -07:00
committed by GitHub
parent 4d64df37f5
commit 2c22837d9f
11 changed files with 287 additions and 25 deletions

View File

@@ -0,0 +1,76 @@
<template>
<div class="avatar-icon">
<img :src="this.$auth.user.avatar_url" class="icon" />
<div v-if="notifCount > 0" class="bubble" :class="{ dropdownBg }">
{{ displayNotifCount }}
</div>
</div>
</template>
<script>
export default {
name: 'AvatarIcon',
props: {
notifCount: {
type: Number,
default: 0,
},
dropdownBg: {
type: Boolean,
default: false,
},
},
computed: {
displayNotifCount() {
return this.notifCount < 100 ? this.notifCount : '99+'
},
},
}
</script>
<style lang="scss" scoped>
.avatar-icon {
position: relative;
height: 2rem;
width: 2rem;
margin-left: 0.5rem;
margin-right: 0.25rem;
.icon {
height: 100%;
width: 100%;
border-radius: 50%;
}
.bubble {
position: absolute;
bottom: -0.25rem;
right: -0.3rem;
border-radius: 0.9rem;
height: 0.9rem;
min-width: 0.45rem;
padding: 0 0.22rem;
font-size: 0.65rem;
display: flex;
justify-content: center;
align-items: center;
background-color: #e02914;
color: white;
border: 0.15rem solid var(--color-raised-bg);
&.dropdownBg {
border-color: var(--color-button-bg);
}
}
}
</style>
<style lang="scss">
.dropdown:hover {
.bubble {
border-color: var(--color-button-bg);
}
}
</style>

View File

@@ -0,0 +1,130 @@
<template>
<div>
<svg
class="rotate outer"
width="100%"
height="100%"
viewBox="0 0 590 591"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xml:space="preserve"
xmlns:serif="http://www.serif.com/"
style="
fill-rule: evenodd;
clip-rule: evenodd;
stroke-linejoin: round;
stroke-miterlimit: 2;
"
>
<g transform="matrix(1,0,0,1,652.392,-0.400578)">
<g transform="matrix(4.16667,0,0,4.16667,-735.553,0)">
<g transform="matrix(0.24,0,0,0.24,0,0)">
<path
d="M134.44,316.535C145.027,441.531 249.98,539.829 377.711,539.829C474.219,539.829 557.724,483.712 597.342,402.371L645.949,419.197C599.165,520.543 496.595,590.954 377.711,590.954C221.751,590.954 93.869,469.779 83.161,316.535L134.44,316.535ZM83.946,265.645C99.012,116.762 224.88,0.401 377.711,0.401C540.678,0.401 672.987,132.71 672.987,295.677C672.987,321.817 669.583,347.168 663.194,371.313L614.709,354.529C619.381,335.689 621.862,315.971 621.862,295.677C621.862,160.926 512.461,51.526 377.711,51.526C253.133,51.526 150.223,145.03 135.392,265.645L83.946,265.645Z"
style="fill: rgb(94, 165, 69)"
/>
</g>
</g>
</g></svg
><svg
class="rotate inner"
width="100%"
height="100%"
viewBox="0 0 590 591"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xml:space="preserve"
xmlns:serif="http://www.serif.com/"
style="
fill-rule: evenodd;
clip-rule: evenodd;
stroke-linejoin: round;
stroke-miterlimit: 2;
"
>
<g transform="matrix(1,0,0,1,652.392,-0.400578)">
<g transform="matrix(4.16667,0,0,4.16667,-735.553,0)">
<g transform="matrix(0.24,0,0,0.24,0,0)">
<path
d="M376.933,153.568C298.44,153.644 234.735,217.396 234.735,295.909C234.735,374.47 298.516,438.251 377.077,438.251C381.06,438.251 385.005,438.087 388.914,437.764L403.128,487.517C394.611,488.667 385.912,489.261 377.077,489.261C270.363,489.261 183.725,402.623 183.725,295.909C183.725,189.195 270.363,102.557 377.077,102.557C379.723,102.557 382.357,102.611 384.983,102.717L376.933,153.568ZM435.127,111.438C513.515,136.114 570.428,209.418 570.428,295.909C570.428,375.976 521.655,444.742 452.22,474.093L438.063,424.541C486.142,401.687 519.418,352.653 519.418,295.909C519.418,234.923 480.981,182.843 427.029,162.593L435.127,111.438Z"
style="fill: rgb(94, 165, 69)"
/>
</g>
</g>
</g>
</svg>
<svg
width="100%"
height="100%"
viewBox="0 0 590 591"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xml:space="preserve"
xmlns:serif="http://www.serif.com/"
style="
fill-rule: evenodd;
clip-rule: evenodd;
stroke-linejoin: round;
stroke-miterlimit: 2;
"
>
<g transform="matrix(1,0,0,1,652.392,-0.400578)">
<g transform="matrix(4.16667,0,0,4.16667,-735.553,0)">
<g transform="matrix(0.24,0,0,0.24,0,0)">
<path
d="M300.366,311.86L283.216,266.381L336.966,211.169L404.9,196.531L424.57,220.74L393.254,252.46L365.941,261.052L346.425,281.11L355.987,307.719L375.387,328.306L402.745,321.031L422.216,299.648L464.729,286.185L477.395,314.677L433.529,368.46L360.02,391.735L327.058,355.031L138.217,468.344C129.245,456.811 118.829,440.485 112.15,424.792L300.366,311.86Z"
style="fill: rgb(94, 165, 69)"
/>
</g>
</g>
<g transform="matrix(4.16667,0,0,4.16667,-735.553,0)">
<g transform="matrix(0.24,0,0,0.24,0,0)">
<path
d="M655.189,194.555L505.695,234.873C513.927,256.795 516.638,269.674 518.915,283.863L668.152,243.609C665.764,227.675 661.5,211.444 655.189,194.555Z"
style="fill: rgb(94, 165, 69)"
/>
</g>
</g>
</g>
</svg>
</div>
</template>
<script>
export default {
name: 'LogoAnimated',
}
</script>
<style lang="scss" scoped>
div {
display: flex;
justify-content: center;
align-items: center;
height: 5rem;
margin-top: 1rem;
svg {
width: 5rem;
height: 5rem;
position: absolute;
&.rotate {
animation: rotate 4s infinite linear;
&.inner {
animation: rotate 6s infinite linear reverse;
}
}
@keyframes rotate {
0% {
transform: rotate(0);
}
100% {
transform: rotate(360deg);
}
}
}
}
</style>

View File

@@ -59,7 +59,10 @@
<button class="control" @click="toggleDropdown">
<div class="avatar">
<span>{{ this.$auth.user.username }}</span>
<img :src="this.$auth.user.avatar_url" class="icon" />
<AvatarIcon
:notif-count="this.$user.notifications.count"
:dropdown-bg="isDropdownOpen"
/>
</div>
<DropdownIcon class="dropdown-icon" />
</button>
@@ -77,6 +80,12 @@
<span>Notifications</span>
</NuxtLink>
</li>
<li>
<NuxtLink to="/dashboard/settings">
<SettingsIcon />
<span>Settings</span>
</NuxtLink>
</li>
<!--<li v-tooltip="'Not implemented yet'" class="hidden">
<NuxtLink :to="userTeamsUrl" disabled>
<UsersIcon />
@@ -149,6 +158,7 @@ import ModrinthLogoSmall from '~/assets/images/logo.svg?inline'
import HamburgerIcon from '~/assets/images/utils/hamburger.svg?inline'
import NotificationIcon from '~/assets/images/sidebar/notifications.svg?inline'
import SettingsIcon from '~/assets/images/sidebar/settings.svg?inline'
import DropdownIcon from '~/assets/images/utils/dropdown.svg?inline'
import MoonIcon from '~/assets/images/utils/moon.svg?inline'
@@ -160,6 +170,8 @@ import GitHubIcon from '~/assets/images/utils/github.svg?inline'
import CookieConsent from '~/components/ads/CookieConsent'
import AvatarIcon from '~/components/ui/AvatarIcon'
export default {
components: {
ModrinthLogo,
@@ -174,6 +186,8 @@ export default {
NotificationIcon,
HamburgerIcon,
CookieConsent,
AvatarIcon,
SettingsIcon,
},
directives: {
ClickOutside,
@@ -198,8 +212,12 @@ export default {
$route() {
this.$refs.nav.className = 'right-group'
document.body.style.overflow = 'auto'
this.$store.dispatch('user/fetchNotifications')
},
},
created() {
this.$store.dispatch('user/fetchNotifications', { force: true })
},
beforeCreate() {
if (this.$route.query.code) {
this.$router.push(this.$route.path)
@@ -397,13 +415,6 @@ export default {
align-items: center;
display: flex;
flex-grow: 1;
.icon {
border-radius: 50%;
height: 2rem;
width: 2rem;
margin-left: 0.5rem;
margin-right: 0.25rem;
}
span {
display: block;
overflow: hidden;

View File

@@ -87,6 +87,7 @@ export default {
'~/plugins/compiled-markdown-directive.js',
'~/plugins/vue-syntax.js',
'~/plugins/auth.js',
'~/plugins/user.js',
],
/*
** Auto import components

View File

@@ -10,6 +10,9 @@
<nuxt-link :to="'/dashboard/notifications'" class="tab last">
<NotificationsIcon />
Notifications
<div v-if="this.$user.notifications.count > 0" class="notif-count">
{{ this.$user.notifications.count }}
</div>
</nuxt-link>
<nuxt-link :to="'/dashboard/follows'" class="tab last">
<FollowIcon />
@@ -88,4 +91,17 @@ export default {
.hideSmall {
padding-top: 0;
}
.notif-count {
display: flex;
justify-content: center;
align-items: center;
background-color: rgba(180, 180, 180, 0.4);
border-radius: 2rem;
padding: 0.1rem 0.35rem;
margin: 0 0.2rem 0 auto;
font-size: 0.9rem;
}
</style>

View File

@@ -68,7 +68,8 @@ export default {
await axios.get(
`https://api.modrinth.com/api/v1/mods?ids=${JSON.stringify(res.data)}`
)
).data
).data.sort((a, b) => a.title > b.title)
return {
mods,
}

View File

@@ -65,7 +65,7 @@ export default {
`https://api.modrinth.com/api/v1/user/${data.$auth.user.id}/notifications`,
data.$auth.headers
)
).data
).data.sort((a, b) => new Date(b.created) - new Date(a.created))
return {
notifications,
@@ -94,6 +94,7 @@ export default {
)
this.notifications.splice(index, 1)
this.$store.dispatch('user/fetchNotifications', { force: true })
} catch (err) {
this.$notify({
group: 'main',

View File

@@ -71,6 +71,7 @@
ethical-ads-big
/>
<div v-if="results === null" class="no-results">
<LogoAnimated />
<p>Loading...</p>
</div>
<div v-else>
@@ -318,6 +319,7 @@ import axios from 'axios'
import SearchResult from '~/components/ui/ProjectCard'
import Pagination from '~/components/ui/Pagination'
import SearchFilter from '~/components/ui/search/SearchFilter'
import LogoAnimated from '~/components/ui/search/LogoAnimated'
import Checkbox from '~/components/ui/Checkbox'
import MFooter from '~/components/layout/MFooter'
@@ -373,6 +375,7 @@ export default {
ServerSide,
SearchIcon,
ExitIcon,
LogoAnimated,
},
async fetch() {
if (this.$route.query.q) this.query = this.$route.query.q

3
plugins/user.js Normal file
View File

@@ -0,0 +1,3 @@
export default ({ store }, inject) => {
inject('user', store.state.user)
}

View File

@@ -1,6 +1,5 @@
export const state = () => ({
user: null,
userFollows: [],
token: '',
headers: {},
})
@@ -9,9 +8,6 @@ export const mutations = {
SET_USER(state, user) {
state.user = user
},
SET_USER_FOLLOWS(state, follows) {
state.userFollows = follows
},
SET_TOKEN(state, token) {
state.token = token
},
@@ -42,15 +38,4 @@ export const actions = {
console.error('Request for user info encountered an error: ', e)
}
},
async fetchUserFollows({ commit }, { userId, token }) {
const follows = await this.$axios.get(
`https://api.modrinth.com/api/v1/user/${userId}/follows`,
{
headers: {
Authorization: token,
},
}
)
commit('SET_USER_FOLLOWS', follows)
},
}

35
store/user.js Normal file
View File

@@ -0,0 +1,35 @@
export const state = () => ({
notifications: {
count: 0,
lastUpdated: 0,
},
})
export const mutations = {
SET_NOTIFICATIONS(state, count) {
state.notifications.count = count
state.notifications.lastUpdated = Date.now()
},
}
export const actions = {
async fetchNotifications(
{ commit, state, rootState },
{ force = false } = {}
) {
if (
rootState.auth.user &&
rootState.auth.user.id &&
(force || Date.now() - state.notifications.lastUpdated > 300000)
) {
const notifications = (
await this.$axios.get(
`https://api.modrinth.com/api/v1/user/${rootState.auth.user.id}/notifications`,
rootState.auth.headers
)
).data
commit('SET_NOTIFICATIONS', notifications.length)
}
},
}