You've already forked AstralRinth
forked from didirus/AstralRinth
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:
76
components/ui/AvatarIcon.vue
Normal file
76
components/ui/AvatarIcon.vue
Normal 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>
|
||||
130
components/ui/search/LogoAnimated.vue
Normal file
130
components/ui/search/LogoAnimated.vue
Normal 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>
|
||||
@@ -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;
|
||||
|
||||
@@ -87,6 +87,7 @@ export default {
|
||||
'~/plugins/compiled-markdown-directive.js',
|
||||
'~/plugins/vue-syntax.js',
|
||||
'~/plugins/auth.js',
|
||||
'~/plugins/user.js',
|
||||
],
|
||||
/*
|
||||
** Auto import components
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
3
plugins/user.js
Normal file
@@ -0,0 +1,3 @@
|
||||
export default ({ store }, inject) => {
|
||||
inject('user', store.state.user)
|
||||
}
|
||||
@@ -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
35
store/user.js
Normal 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)
|
||||
}
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user