This commit is contained in:
Troplo 2020-10-08 22:17:15 +11:00
parent 951fe5b493
commit 0b209c1b21
45 changed files with 8984 additions and 645 deletions

View File

@ -8,4 +8,4 @@
* Copyright (c) 2019 Daniel Eden * Copyright (c) 2019 Daniel Eden
*/ */
@-webkit-keyframes fadeIn{0%{opacity:0}to{opacity:1}}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.fadeIn{-webkit-animation-name:fadeIn;animation-name:fadeIn}@-webkit-keyframes fadeOut{0%{opacity:1}to{opacity:0}}@keyframes fadeOut{0%{opacity:1}to{opacity:0}}.fadeOut{-webkit-animation-name:fadeOut;animation-name:fadeOut}@-webkit-keyframes slideInDown{0%{-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes slideInDown{0%{-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.slideInDown{-webkit-animation-name:slideInDown;animation-name:slideInDown}@-webkit-keyframes slideInLeft{0%{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes slideInLeft{0%{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.slideInLeft{-webkit-animation-name:slideInLeft;animation-name:slideInLeft}@-webkit-keyframes slideInRight{0%{-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes slideInRight{0%{-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.slideInRight{-webkit-animation-name:slideInRight;animation-name:slideInRight}@-webkit-keyframes slideInUp{0%{-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes slideInUp{0%{-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.slideInUp{-webkit-animation-name:slideInUp;animation-name:slideInUp}@-webkit-keyframes slideOutDown{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}}@keyframes slideOutDown{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}}.slideOutDown{-webkit-animation-name:slideOutDown;animation-name:slideOutDown}@-webkit-keyframes slideOutLeft{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}@keyframes slideOutLeft{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}.slideOutLeft{-webkit-animation-name:slideOutLeft;animation-name:slideOutLeft}@-webkit-keyframes slideOutRight{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}}@keyframes slideOutRight{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}}.slideOutRight{-webkit-animation-name:slideOutRight;animation-name:slideOutRight}@-webkit-keyframes slideOutUp{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}}@keyframes slideOutUp{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}}.slideOutUp{-webkit-animation-name:slideOutUp;animation-name:slideOutUp}.animated{-webkit-animation-duration:1s;animation-duration:1s;-webkit-animation-fill-mode:both;animation-fill-mode:both}.animated.infinite{-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite}.animated.delay-1s{-webkit-animation-delay:1s;animation-delay:1s}.animated.delay-2s{-webkit-animation-delay:2s;animation-delay:2s}.animated.delay-3s{-webkit-animation-delay:3s;animation-delay:3s}.animated.delay-4s{-webkit-animation-delay:4s;animation-delay:4s}.animated.delay-5s{-webkit-animation-delay:5s;animation-delay:5s}.animated.fast{-webkit-animation-duration:.8s;animation-duration:.8s}.animated.faster{-webkit-animation-duration:.5s;animation-duration:.5s}.animated.slow{-webkit-animation-duration:2s;animation-duration:2s}.animated.slower{-webkit-animation-duration:3s;animation-duration:3s}@media (prefers-reduced-motion:reduce),(print){.animated{-webkit-animation-duration:1ms!important;animation-duration:1ms!important;-webkit-transition-duration:1ms!important;transition-duration:1ms!important;-webkit-animation-iteration-count:1!important;animation-iteration-count:1!important}} @-webkit-keyframes fadeIn{0%{opacity:0}to{opacity:1}}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.fadeIn{-webkit-animation-name:fadeIn;animation-name:fadeIn}@-webkit-keyframes fadeOut{0%{opacity:1}to{opacity:0}}@keyframes fadeOut{0%{opacity:1}to{opacity:0}}.fadeOut{-webkit-animation-name:fadeOut;animation-name:fadeOut}@-webkit-keyframes slideInDown{0%{-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes slideInDown{0%{-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.slideInDown{-webkit-animation-name:slideInDown;animation-name:slideInDown}@-webkit-keyframes slideInLeft{0%{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes slideInLeft{0%{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.slideInLeft{-webkit-animation-name:slideInLeft;animation-name:slideInLeft}@-webkit-keyframes slideInRight{0%{-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes slideInRight{0%{-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.slideInRight{-webkit-animation-name:slideInRight;animation-name:slideInRight}@-webkit-keyframes slideInUp{0%{-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes slideInUp{0%{-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.slideInUp{-webkit-animation-name:slideInUp;animation-name:slideInUp}@-webkit-keyframes slideOutDown{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}}@keyframes slideOutDown{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}}.slideOutDown{-webkit-animation-name:slideOutDown;animation-name:slideOutDown}@-webkit-keyframes slideOutLeft{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}@keyframes slideOutLeft{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}.slideOutLeft{-webkit-animation-name:slideOutLeft;animation-name:slideOutLeft}@-webkit-keyframes slideOutRight{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}}@keyframes slideOutRight{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}}.slideOutRight{-webkit-animation-name:slideOutRight;animation-name:slideOutRight}@-webkit-keyframes slideOutUp{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}}@keyframes slideOutUp{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}}.slideOutUp{-webkit-animation-name:slideOutUp;animation-name:slideOutUp}.animated{-webkit-animation-duration:1s;animation-duration:1s;-webkit-animation-fill-mode:both;animation-fill-mode:both}.animated.infinite{-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite}.animated.delay-1s{-webkit-animation-delay:1s;animation-delay:1s}.animated.delay-2s{-webkit-animation-delay:2s;animation-delay:2s}.animated.delay-3s{-webkit-animation-delay:3s;animation-delay:3s}.animated.delay-4s{-webkit-animation-delay:4s;animation-delay:4s}.animated.delay-5s{-webkit-animation-delay:5s;animation-delay:5s}.animated.fast{-webkit-animation-duration:.8s;animation-duration:.8s}.animated.faster{-webkit-animation-duration:.5s;animation-duration:.5s}.animated.slow{-webkit-animation-duration:2s;animation-duration:2s}.animated.slower{-webkit-animation-duration:3s;animation-duration:3s}@media (prefers-reduced-motion:reduce),(print){.animated{-webkit-animation-duration:1ms;animation-duration:1ms;-webkit-transition-duration:1ms;transition-duration:1ms;-webkit-animation-iteration-count:1;animation-iteration-count:1!important}}

View File

@ -20,7 +20,7 @@
box-shadow: none; box-shadow: none;
&:focus, &:active { &:focus, &:active {
box-shadow: none!important; box-shadow: none;
} }
} }
@ -31,7 +31,7 @@
.switch, .b-checkbox.checkbox { .switch, .b-checkbox.checkbox {
input[type=checkbox] { input[type=checkbox] {
&:focus + .check, &:focus:checked + .check { &:focus + .check, &:focus:checked + .check {
box-shadow: none!important; box-shadow: none;
} }
} }
} }

View File

@ -106,7 +106,7 @@ table.table {
.table-wrapper.has-mobile-cards { .table-wrapper.has-mobile-cards {
tr { tr {
box-shadow: 0 2px 3px rgba(10, 10, 10, 0.1); box-shadow: 0 2px 3px rgba(10, 10, 10, 0.1);
margin-bottom: 3px!important; margin-bottom: 3px;
} }
td { td {
&.is-progress-col { &.is-progress-col {
@ -119,7 +119,7 @@ table.table {
} }
&.checkbox-cell, &.is-image-cell { &.checkbox-cell, &.is-image-cell {
border-bottom: 0!important; border-bottom: 0;
} }
&.checkbox-cell, &.is-actions-cell { &.checkbox-cell, &.is-actions-cell {

View File

@ -19,6 +19,7 @@
"@sentry/browser": "^5.22.3", "@sentry/browser": "^5.22.3",
"@sentry/integrations": "^5.22.3", "@sentry/integrations": "^5.22.3",
"@sentry/tracing": "^5.22.3", "@sentry/tracing": "^5.22.3",
"@vue-a11y/skip-to": "^2.1.1",
"@vuejs-community/vue-filter-date-format": "^1.6.1", "@vuejs-community/vue-filter-date-format": "^1.6.1",
"axios": "^0.19.0", "axios": "^0.19.0",
"babel": "^6.23.0", "babel": "^6.23.0",

View File

@ -1,21 +1,21 @@
<style> <style>
trpl-title { trpl-title {
font-size: 32px !important; font-size: 32px ;
background: -webkit-linear-gradient(#00b3ff, #007aff) !important; background: -webkit-linear-gradient(#00b3ff, #007aff) ;
-webkit-background-clip: text !important; -webkit-background-clip: text ;
-webkit-text-fill-color: transparent !important; -webkit-text-fill-color: transparent ;
} }
trpl-para { trpl-para {
background: #007aff !important; background: #007aff ;
-webkit-background-clip: text !important; -webkit-background-clip: text ;
-webkit-text-fill-color: transparent !important; -webkit-text-fill-color: transparent ;
font-weight: bold !important; font-weight: bold ;
} }
h1 { h1 {
font-size: 32px !important; font-size: 32px ;
} }
h2 { h2 {
font-size: 24px !important; font-size: 24px ;
} }
mini-br { mini-br {
border-width: 0.1em; border-width: 0.1em;
@ -227,9 +227,10 @@
</tab-view> </tab-view>
</modal-window> </modal-window>
<template> <template>
<b-navbar v-bind:fixed-top="true"> <b-navbar v-bind:fixed-top="true">
<template slot="brand"> <template slot="brand">
<b-navbar-item tag="router-link" :to="{ path: '/' }"> <b-navbar-item tag="router-link" :to="{ path: '/' }">
<VueSkipTo to="#main" label="Skip to main content" />
<img <img
:src = $store.state.meta.logo :src = $store.state.meta.logo
> >
@ -298,7 +299,7 @@
<div class="navbar-dropdown is-boxed"> <div class="navbar-dropdown is-boxed">
<router-link class="navbar-item" :to='"/u/" + this.$store.state.username'>My Profile</router-link> <router-link class="navbar-item" :to='"/u/" + this.$store.state.username'>My Profile</router-link>
<router-link class="navbar-item" to='/settings'>Settings</router-link> <router-link class="navbar-item" to='/settings'>Settings</router-link>
<router-link class="navbar-item is-hidden-desktop " to='/notifications'>Notifications</router-link> <router-link class="navbar-item is-hidden-desktop" to='/notifications'>Notifications</router-link>
<router-link to='/admin' class="navbar-item" v-if='$store.state.admin'>Admin Panel</router-link> <router-link to='/admin' class="navbar-item" v-if='$store.state.admin'>Admin Panel</router-link>
<router-link class="navbar-item is-active" to='/premium'>Upgrade</router-link> <router-link class="navbar-item is-active" to='/premium'>Upgrade</router-link>
<a class="navbar-item" @click='logout'> <a class="navbar-item" @click='logout'>
@ -329,7 +330,7 @@
You are using an outdated client, refresh to update. You are using an outdated client, refresh to update.
</div> </div>
</div> </div>
<div v-if='$store.state.currentStableVersion && $store.state.meta.bannerText && !$store.state.username' class="container is-fullhd" style="padding-left: 5px; padding-right: 5px; padding-top: 20px; padding-bottom: 5px;"> <div v-if='$store.state.meta.bannerText && !$store.state.username' class="container is-fullhd" style="padding-left: 5px; padding-right: 5px; padding-top: 20px; padding-bottom: 5px;">
<div class="notification is-info"> <div class="notification is-info">
{{$store.state.meta.bannerText}} {{$store.state.meta.bannerText}}
</div> </div>
@ -341,7 +342,7 @@
</div> </div>
<not-found v-show='$store.state.show404Page'></not-found> <not-found v-show='$store.state.show404Page'></not-found>
<transition name='fade'> <transition name='fade'>
<router-view v-show='!$store.state.show404Page'></router-view> <router-view id="main" v-show='!$store.state.show404Page'></router-view>
</transition> </transition>
<footer class="footer"> <footer class="footer">
<div class="content has-text-centered"> <div class="content has-text-centered">
@ -359,8 +360,8 @@
@import 'https://kit-pro.fontawesome.com/releases/v5.14.0/css/pro.min.css'; @import 'https://kit-pro.fontawesome.com/releases/v5.14.0/css/pro.min.css';
</style> </style>
<style lang="scss"> <style lang="scss">
$primary: #007aff !important; $primary: #007aff ;
$navbar-dropdown-arrow: #007aff !important; $navbar-dropdown-arrow: #007aff ;
</style> </style>
<script> <script>
import ModalWindow from './components/ModalWindow' import ModalWindow from './components/ModalWindow'
@ -373,7 +374,6 @@
import NotFound from './components/routes/NotFound' import NotFound from './components/routes/NotFound'
import AjaxErrorHandler from './assets/js/errorHandler' import AjaxErrorHandler from './assets/js/errorHandler'
export default { export default {
name: 'app', name: 'app',
components: { components: {
@ -440,6 +440,9 @@
} }
return "https://cdn.kaverti.com/logo.png"; return "https://cdn.kaverti.com/logo.png";
}, },
theme () {
return this.$store.state.theme
},
showAccountModal: { showAccountModal: {
get () { return this.$store.state.accountModal }, get () { return this.$store.state.accountModal },
set (val) { set (val) {
@ -469,6 +472,21 @@
} }
}, },
methods: { methods: {
darkTheme() {
let darkThemeLinkEl = document.createElement("link");
darkThemeLinkEl.setAttribute("rel", "stylesheet");
darkThemeLinkEl.setAttribute("href", "https://cdn.kaverti.com/css/dark.css");
darkThemeLinkEl.setAttribute("id", "dark-theme");
let docHead = document.querySelector("head");
docHead.append(darkThemeLinkEl);
},
disableDarkTheme() {
let darkThemeLinkEl = document.removeElement("dark-theme");
let docHead = document.querySelector("head");
docHead.append(darkThemeLinkEl);
},
recaptcha() { recaptcha() {
this.$recaptcha('login').then((token) => { this.$recaptcha('login').then((token) => {
console.log(token) // Will print the token console.log(token) // Will print the token
@ -574,6 +592,7 @@
this.$store.commit('setAdmin', res.data.admin) this.$store.commit('setAdmin', res.data.admin)
this.$store.commit('setKoins', res.data.koins) this.$store.commit('setKoins', res.data.koins)
this.$store.commit('setDevMode', res.data.developerMode) this.$store.commit('setDevMode', res.data.developerMode)
this.$store.commit('setTheme', res.data.theme)
this.axios.get('/api/v1/kaverti/state') this.axios.get('/api/v1/kaverti/state')
.then(res => { .then(res => {
this.$store.commit('setSettings', res.data) this.$store.commit('setSettings', res.data)
@ -603,6 +622,7 @@
this.$store.commit('setAdmin', res.data.admin) this.$store.commit('setAdmin', res.data.admin)
this.$store.commit('setKoins', res.data.koins) this.$store.commit('setKoins', res.data.koins)
this.$store.commit('setDevMode', res.data.developerMode) this.$store.commit('setDevMode', res.data.developerMode)
this.$store.commit('setTheme', res.data.theme)
this.closeConn() this.closeConn()
}).catch(err => { }).catch(err => {
this.showConn() this.showConn()
@ -704,6 +724,7 @@
this.$store.commit('setAdmin', res.data.admin) this.$store.commit('setAdmin', res.data.admin)
this.$store.commit('setKoins', res.data.koins) this.$store.commit('setKoins', res.data.koins)
this.$store.commit('setDevMode', res.data.developerMode) this.$store.commit('setDevMode', res.data.developerMode)
this.$store.commit('setTheme', res.data.theme)
this.closeConn() this.closeConn()
}).catch(err => { }).catch(err => {
this.showConn() this.showConn()
@ -753,6 +774,7 @@
this.$store.commit('setAdmin', res.data.admin) this.$store.commit('setAdmin', res.data.admin)
this.$store.commit('setKoins', res.data.koins) this.$store.commit('setKoins', res.data.koins)
this.$store.commit('setDevMode', res.data.developerMode) this.$store.commit('setDevMode', res.data.developerMode)
this.$store.commit('setTheme', res.data.theme)
this.closeConn() this.closeConn()
}).catch(err => { }).catch(err => {
this.showConn() this.showConn()
@ -786,6 +808,8 @@
this.$store.commit('setAdmin', res.data.admin) this.$store.commit('setAdmin', res.data.admin)
this.$store.commit('setKoins', res.data.koins) this.$store.commit('setKoins', res.data.koins)
this.$store.commit('setDevMode', res.data.developerMode) this.$store.commit('setDevMode', res.data.developerMode)
this.$store.commit('setTheme', res.data.theme)
this.$store.commit('setTheme', res.data.theme)
this.closeConn() this.closeConn()
}).catch(err => { }).catch(err => {
this.showConn() this.showConn()
@ -798,7 +822,6 @@
this.pollConn() this.pollConn()
} }
}) })
this.axios.get('/api/v1/kaverti/state') this.axios.get('/api/v1/kaverti/state')
.then(res => { .then(res => {
this.$store.commit('setSettings', res.data) this.$store.commit('setSettings', res.data)
@ -813,6 +836,10 @@
this.$store.commit('setEmailVerified', res.data.emailVerified) this.$store.commit('setEmailVerified', res.data.emailVerified)
this.$store.commit('setAdmin', res.data.admin) this.$store.commit('setAdmin', res.data.admin)
this.$store.commit('setKoins', res.data.koins) this.$store.commit('setKoins', res.data.koins)
this.$store.commit('setTheme', res.data.theme)
if(res.data.theme === "dark") {
this.darkTheme()
}
this.closeConn() this.closeConn()
}).catch(err => { }).catch(err => {
this.showConn() this.showConn()
@ -863,7 +890,7 @@
if(val) { if(val) {
this.$refs.ajaxErrorsModalButton.focus() this.$refs.ajaxErrorsModalButton.focus()
} }
} },
} }
} }
</script> </script>
@ -871,9 +898,9 @@
@import url('https://fonts.googleapis.com/css?family=Lato:400,400i,500,500i,700'); @import url('https://fonts.googleapis.com/css?family=Lato:400,400i,500,500i,700');
@import './assets/scss/variables.scss'; @import './assets/scss/variables.scss';
@import './assets/sass/primary'; @import './assets/sass/primary';
$primary: #0ba8e6 !important; $primary: #0ba8e6 ;
$colors: ( $colors: (
"primary": #0ba8e6 !important "primary": #0ba8e6
); );
html, body { html, body {
width: 100%; width: 100%;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -68,7 +68,7 @@ b, strong {
letter-spacing: 0.25px; letter-spacing: 0.25px;
background: none; background: none;
background-color: #fff; background-color: #fff;
color: lighten($color__text--primary, 30%) !important; color: lighten($color__text--primary, 30%) ;
transition: background-color 0.2s, border-color 0.2s, filter 0.2s; transition: background-color 0.2s, border-color 0.2s, filter 0.2s;
outline: none; outline: none;
@ -130,17 +130,17 @@ b, strong {
@mixin filled_button($background, $border, $text: #fff) { @mixin filled_button($background, $border, $text: #fff) {
background-color: $background; background-color: $background;
border-color: $border; border-color: $border;
color: $text !important; color: $text ;
&:hover { &:hover {
background-color: darken($background, 5%); background-color: darken($background, 5%);
border-color: rgba($border, 0.6); border-color: rgba($border, 0.6);
color: darken(#fff, 5%) !important; color: darken(#fff, 5%) ;
} }
&:active { &:active {
background-color: darken($background, 10%); background-color: darken($background, 10%);
border-color: rgba($border, 0.6); border-color: rgba($border, 0.6);
color: darken(#fff, 10%) !important; color: darken(#fff, 10%) ;
} }
} }
@at-root #{&}--blue { @at-root #{&}--blue {
@ -312,4 +312,4 @@ blockquote.twitter-tweet {
} }
.slide-enter-to, .slide-leave { .slide-enter-to, .slide-leave {
max-height: 20rem; max-height: 20rem;
} }

View File

@ -13,15 +13,12 @@
<div class='avatar_icon__header_info'> <div class='avatar_icon__header_info'>
<span class='avatar_icon__username' @click.stop='goToUser'> <span class='avatar_icon__username' @click.stop='goToUser'>
{{proxyUser.username}} {{proxyUser.username}}
<b-tag rounded class="is-success" v-if='proxyUser.system'>SYSTEM</b-tag>&nbsp;
<b-tag rounded class="is-info" v-if='proxyUser.bot'>BOT</b-tag>&nbsp;
<b-tag rounded class="is-danger" v-if='proxyUser.admin'>ADMIN</b-tag>
</span> </span>
<span class='avatar_icon__date'>{{proxyUser.createdAt | formatDate('date') }}</span> <span class='avatar_icon__date'>Created: {{proxyUser.createdAt | formatDate('date') }}</span>
</div> </div>
</div> </div>
<div class='avatar_icon__description' v-if='proxyUser.description'> <div class='avatar_icon__description' v-if='proxyUser.description'>
{{proxyUser.description}} {{proxyUser.description | stripTags | truncate(30)}}
</div> </div>
</template> </template>

View File

@ -53,42 +53,42 @@
@import '../assets/scss/variables.scss'; @import '../assets/scss/variables.scss';
.fancy_input { .fancy_input {
display: inline-flex !important; display: inline-flex ;
flex-direction: column !important; flex-direction: column ;
position: relative !important; position: relative ;
margin-top: 0.25rem !important; margin-top: 0.25rem ;
margin-bottom: 0.5rem !important; margin-bottom: 0.5rem ;
@at-root #{&}__input { @at-root #{&}__input {
transition: border-color 0.2s !important; transition: border-color 0.2s ;
width: 100% !important; width: 100% ;
@at-root #{&}--large { @at-root #{&}--large {
padding: 0.5rem !important; padding: 0.5rem ;
} }
@at-root #{&}--error { @at-root #{&}--error {
border-color: $color__red--primary !important; border-color: $color__red--primary ;
} }
} }
@at-root #{&}__placeholder { @at-root #{&}__placeholder {
position: absolute !important; position: absolute ;
top: 0.35rem !important !important; top: 0.35rem ;
background-color: #fff; background-color: #fff;
left: 0.35rem !important; left: 0.35rem ;
color: $color__gray--darkest !important; color: $color__gray--darkest ;
pointer-events: none !important; pointer-events: none ;
transition: top 0.2s, font-size 0.2s !important; transition: top 0.2s, font-size 0.2s ;
@at-root #{&}--large { @at-root #{&}--large {
top: 0.55rem !important; top: 0.55rem ;
left: 0.6rem !important; left: 0.6rem ;
} }
@at-root #{&}--active { @at-root #{&}--active {
top: -0.5rem !important; top: -0.5rem ;
font-size: 0.75rem !important; font-size: 0.75rem ;
transition: top 0.2s, font-size 0.2s !important; transition: top 0.2s, font-size 0.2s ;
} }
} }
} }

View File

@ -90,7 +90,7 @@
padding: 0.1rem 0.25rem; padding: 0.1rem 0.25rem;
top: -1.75rem; top: -1.75rem;
right: 0; right: 0;
&:first-letter{ text-transform: capitalize; } &:first-letter{ text-transform: capitalize; }
opacity: 0; opacity: 0;
@ -122,8 +122,8 @@
@media (max-width: 420px) { @media (max-width: 420px) {
.fancy_textarea { .fancy_textarea {
@at-root #{&}__container { @at-root #{&}__container {
width: 100% !important; width: 100% ;
} }
} }
} }
</style> </style>

View File

@ -322,7 +322,6 @@
@include user-select(none); @include user-select(none);
@include text($font--role-default, 1rem, 600); @include text($font--role-default, 1rem, 600);
color: $color__darkgray--primary; color: $color__darkgray--primary;
border: thin solid $color__gray--primary;
transition: background-color 0.2s; transition: background-color 0.2s;
&:hover { &:hover {

View File

@ -41,7 +41,7 @@
@at-root #{&}--dark { @at-root #{&}--dark {
span { span {
background-color: $color__darkgray--primary !important; background-color: $color__darkgray--primary ;
} }
} }

View File

@ -55,12 +55,10 @@
@import '../assets/scss/variables.scss'; @import '../assets/scss/variables.scss';
.more_threads { .more_threads {
background-color: #fff;
border-radius: 0.25rem; border-radius: 0.25rem;
width: 80%; width: 80%;
padding: 1rem; padding: 1rem;
margin-top: 1.5rem; margin-top: 1.5rem;
border: thin solid $color__gray--darker;
@at-root #{&}__header { @at-root #{&}__header {
font-size: 1.5rem; font-size: 1.5rem;
@ -78,28 +76,17 @@
border-bottom: thin solid $color__gray--darker; border-bottom: thin solid $color__gray--darker;
transition: all 0.2s; transition: all 0.2s;
&:hover {
background-color: $color__lightgray--primary;
}
&:active {
background-color: $color__lightgray--darker;
}
@at-root #{&}--header { @at-root #{&}--header {
cursor: default; cursor: default;
font-size: 1rem; font-size: 1rem;
font-weight: 500; font-weight: 500;
border-bottom: 0.125rem solid $color__gray--darker; }
&:hover { background-color: #fff; }
}
} }
@at-root #{&}__name { @at-root #{&}__name {
word-break: break-all; word-break: break-all;
padding-right: 0.5rem; padding-right: 0.5rem;
} }
@at-root #{&}__date_created { @at-root #{&}__date_created {
color: $color__text--secondary;
font-size: 1rem; font-size: 1rem;
flex-shrink: 0; flex-shrink: 0;
} }

View File

@ -424,7 +424,7 @@
.notification_button__menu { .notification_button__menu {
width: 100%; width: 100%;
left: unset; left: unset;
right: unset !important; right: unset ;
} }
} }

View File

@ -4,7 +4,7 @@
<div class='post_scrubber__line' ref='line' @click='lineClick'></div> <div class='post_scrubber__line' ref='line' @click='lineClick'></div>
<div <div
class='post_scrubber__dragger' class='post_scrubber__dragger'
:class='{ "post_scrubber--no_top_transition": dragging }' :class='{ "post_scrubber--no_top_transition": dragging }'
:style='{ :style='{
"top": draggerYCoord + "px" "top": draggerYCoord + "px"
@ -16,7 +16,7 @@
<div <div
class='post_scrubber__dragger_info' class='post_scrubber__dragger_info'
:class='{ "post_scrubber--no_top_transition": dragging }' :class='{ "post_scrubber--no_top_transition": dragging }'
:style='{ :style='{
"top": draggerYCoord + "px" "top": draggerYCoord + "px"
}' }'
@ -122,7 +122,7 @@
this.clientY = e.clientY this.clientY = e.clientY
} }
}) })
window.addEventListener('mouseup', () => { window.addEventListener('mouseup', () => {
if(this.dragging) { if(this.dragging) {
this.dragging = false this.dragging = false
this.$emit('input', this.currentPost-1) this.$emit('input', this.currentPost-1)
@ -143,12 +143,11 @@
margin-left: 0.25rem; margin-left: 0.25rem;
@at-root #{&}--no_top_transition { @at-root #{&}--no_top_transition {
transition: background-color 0.2s !important; transition: background-color 0.2s ;
} }
@at-root #{&}__line { @at-root #{&}__line {
height: 100%; height: 100%;
background-color: $color__gray--darker;
border-radius: 1rem; border-radius: 1rem;
width: 0.125rem; width: 0.125rem;
} }
@ -193,14 +192,11 @@
width: 10rem; width: 10rem;
margin-top: calc(-1.5rem / 2 - 0.125rem); margin-top: calc(-1.5rem / 2 - 0.125rem);
pointer-events: none; pointer-events: none;
background-color: #fff;
left: 1rem; left: 1rem;
font-size: 0.9rem; font-size: 0.9rem;
border-radius: 0.125rem; border-radius: 0.125rem;
padding: 0.25rem; padding: 0.25rem;
transition: top 0.3s; transition: top 0.3s;
}
@extend .shadow_border;
}
} }
</style> </style>

View File

@ -98,7 +98,7 @@
} }
@at-root #{&}__button--selected { @at-root #{&}__button--selected {
color: $color__blue--darker !important; color: $color__blue--darker ;
} }
@at-root #{&}__option { @at-root #{&}__option {

View File

@ -95,7 +95,7 @@
transition: transform 0.2s; transition: transform 0.2s;
} }
@at-root #{&}--selected { @at-root #{&}--selected {
color: $color__blue--darker !important; color: $color__blue--darker ;
} }
} }

View File

@ -86,7 +86,6 @@
@import '../assets/scss/variables.scss'; @import '../assets/scss/variables.scss';
.thread_display { .thread_display {
background-color: #fff;
border: thin solid $color__gray--darker; border: thin solid $color__gray--darker;
border-radius: 0.25rem; border-radius: 0.25rem;
cursor: pointer; cursor: pointer;

View File

@ -262,7 +262,6 @@
border-radius: 0.25rem; border-radius: 0.25rem;
@at-root #{&}--highlighted { @at-root #{&}--highlighted {
background-color: $color__lightgray--darkest;
animation-name: shake; animation-name: shake;
animation-iteration-count: 5; animation-iteration-count: 5;
animation-timing-function: linear; animation-timing-function: linear;

View File

@ -15,7 +15,7 @@
@import '../assets/scss/variables.scss'; @import '../assets/scss/variables.scss';
.slide-fade-enter-active, .slide-fade-leave-active { .slide-fade-enter-active, .slide-fade-leave-active {
transition: all 0.4s cubic-bezier(0.18, 0.89, 0.32, 1.28) !important; transition: all 0.4s cubic-bezier(0.18, 0.89, 0.32, 1.28) ;
} }
.slide-fade-enter, .slide-fade-leave-to { .slide-fade-enter, .slide-fade-leave-to {
transform: translateX(25rem); transform: translateX(25rem);
@ -65,7 +65,7 @@
transition: background-color 0.2s; transition: background-color 0.2s;
width: 1rem; width: 1rem;
height: 1rem; height: 1rem;
@include user-select(none); @include user-select(none);
&:hover { &:hover {
@ -87,4 +87,4 @@
name: 'ThreadPostNotification', name: 'ThreadPostNotification',
props: ['post'] props: ['post']
} }
</script> </script>

View File

@ -6,6 +6,28 @@
<avatar-icon :user='user' size='small'></avatar-icon> <avatar-icon :user='user' size='small'></avatar-icon>
<div class='user_display__username'> <div class='user_display__username'>
{{user.username}} {{user.username}}
<tooltips style="padding-left: 5px;">
<b-tooltip v-if='user && user.system' class="is-success" label="This user is a system user operated by administrators that mainly run API operations.">
<b-tag rounded class="is-success">&nbsp;SYSTEM&nbsp;<i class="fas fa-info-circle"></i></b-tag>
</b-tooltip>
&nbsp;
<b-tooltip v-if='user && user.bot' class="is-info" label="This user is a bot account that can run automated API operations.">
<b-tag rounded class="is-info">&nbsp;BOT&nbsp;<i class="fas fa-info-circle"></i></b-tag>
</b-tooltip>
&nbsp;
<b-tooltip v-if='user && user.admin' class="is-danger" label="User is an official Kaverti administrator.">
<b-tag class="is-danger" rounded>&nbsp;ADMIN&nbsp;<i class="fas fa-info-circle"></i></b-tag>
</b-tooltip>
&nbsp;
<b-tooltip v-if='user && user.hidden' class="is-info" label="User is not discoverable in the user list.">
<b-tag rounded>&nbsp;HIDDEN&nbsp;<i class="fas fa-info-circle"></i></b-tag>
</b-tooltip>
&nbsp;
<b-tooltip v-if='user && user.booster' class="is-light" label="User is boosting the Kaverti Discord server.">
<b-tag class="is-light" rounded>&nbsp;BOOSTER&nbsp;<i class="fas fa-info-circle"></i></b-tag>
</b-tooltip>
&nbsp;
</tooltips>
</div> </div>
</div> </div>
</template> </template>
@ -34,7 +56,7 @@
margin-bottom: 1rem; margin-bottom: 1rem;
padding: 0.25rem 0.5rem; padding: 0.25rem 0.5rem;
transition: box-shadow 0.2s; transition: box-shadow 0.2s;
&:hover { &:hover {
@extend .shadow_border--hover; @extend .shadow_border--hover;
} }
@ -45,4 +67,4 @@
margin-left: 1rem; margin-left: 1rem;
} }
} }
</style> </style>

View File

@ -55,6 +55,12 @@
<th> <th>
<sort-menu v-model='tableSort' column='createdAt' display='Account created at'></sort-menu> <sort-menu v-model='tableSort' column='createdAt' display='Account created at'></sort-menu>
</th> </th>
<th>
<sort-menu v-model='tableSort' column='postCount' display='Posts count'></sort-menu>
</th>
<th>
<sort-menu v-model='tableSort' column='threadCount' display='Threads count'></sort-menu>
</th>
</tr> </tr>
<tr v-for='user in users' :key='"user-row" + user.username' v-show="user && !user.hidden"> <tr v-for='user in users' :key='"user-row" + user.username' v-show="user && !user.hidden">
<td class='admin_users__user_column'> <td class='admin_users__user_column'>
@ -78,8 +84,8 @@
<b-tag rounded>&nbsp;HIDDEN&nbsp;<i class="fas fa-info-circle"></i></b-tag> <b-tag rounded>&nbsp;HIDDEN&nbsp;<i class="fas fa-info-circle"></i></b-tag>
</b-tooltip> </b-tooltip>
&nbsp; &nbsp;
<b-tooltip v-if='user && user.booster' class="is-success" label="User is boosting the Kaverti Discord server."> <b-tooltip v-if='user && user.booster' class="is-light" label="User is boosting the Kaverti Discord server.">
<b-tag class="is-success" rounded>&nbsp;BOOSTER&nbsp;<i class="fas fa-info-circle"></i></b-tag> <b-tag class="is-light" rounded>&nbsp;BOOSTER&nbsp;<i class="fas fa-info-circle"></i></b-tag>
</b-tooltip> </b-tooltip>
&nbsp; &nbsp;
<b-button v-if="!user.admin" class="is-info is-small" rounded @click='toggleBadgeModal(user)'> <b-button v-if="!user.admin" class="is-info is-small" rounded @click='toggleBadgeModal(user)'>

View File

@ -1,3 +1,5 @@
<template> <template>
<h1>Unused</h1> <main>
</main>
</template> </template>

View File

@ -43,8 +43,27 @@
<select-options <select-options
:options='filterOptions' :options='filterOptions'
v-model='selectedFilterOption' v-model='selectedFilterOption'
class='thread_sorting__filter' class=''
></select-options> ></select-options>
<br/>
<div class='threads_main__side_bar__title'>
Leaderboard:
</div>
<div style="padding-right: 10px">
<div class="box" v-for='user in users' :key='"user-row" + user.username' v-show="user && !user.hidden">
<header class="content">
{{user.postCount}} <leaderpara v-if="user.postCount === 1">post</leaderpara> <leaderpara v-else>posts</leaderpara>
<p>
<router-link :to='"/user/" + user.username'>{{user.username}}</router-link>
</p>
</header>
</div>
<p name='fade' mode='out-in'>
<center><loading-message key='loading' v-if='loading'></loading-message></center>
<center><div class='overlay_message' v-if='!loading && !users.length'>
Something went wrong while loading the users, check your internet connection, or check the <a href="https://status.troplo.com">Service Status</a>
</div></center></p>
</div>
</div> </div>
@ -110,8 +129,9 @@
SelectOptions SelectOptions
}, },
data () { data () {
return { return {offset: 0,
filterOptions: [ users: [],
filterOptions: [
{name: 'New', value: 'NEW'}, {name: 'New', value: 'NEW'},
{name: 'Most active', value: 'MOST_ACTIVE'}, {name: 'Most active', value: 'MOST_ACTIVE'},
{name: 'No replies', value: 'NO_REPLIES'} {name: 'No replies', value: 'NO_REPLIES'}
@ -167,6 +187,33 @@
} }
}, },
methods: { methods: {
fetchLeaderboardData () {
if(this.offset === null) return;
let url = `/api/v1/users/leaderboard?
sort=postCount
&order=desc
&offset=0
`;
this.loading = true;
this.axios
.get(url)
.then(res => {
this.users.push(...res.data);
this.loading = /*loading =*/ false;
})
.catch(e => {
AjaxErrorHandler(this.$store)(e);
this.loading = /*loading =*/ false;
});
},
resetLeaderboard () {
this.offset = 0;
this.users = [];
this.fetchLeaderboardData();
},
navigateToThread (slug, id) { navigateToThread (slug, id) {
this.$router.push('/thread/' + slug + '/' + id); this.$router.push('/thread/' + slug + '/' + id);
}, },
@ -248,7 +295,7 @@
this.$store.commit('setAccountTabs', 0) this.$store.commit('setAccountTabs', 0)
this.$store.commit('setAccountModalState', true) this.$store.commit('setAccountModalState', true)
} }
this.fetchLeaderboardData()
logger('index') logger('index')
}, },
destroyed () { destroyed () {
@ -269,7 +316,6 @@
.forum_description { .forum_description {
padding: 1rem; padding: 1rem;
margin-bottom: 2rem; margin-bottom: 2rem;
background-color: #fff;
border-radius: 0.25rem; border-radius: 0.25rem;
border: thin solid $color__gray--darker; border: thin solid $color__gray--darker;
} }
@ -285,7 +331,6 @@
@at-root #{&}__display { @at-root #{&}__display {
padding-right: 0.5rem; padding-right: 0.5rem;
border-right: thin solid $color__gray--primary;
margin-right: 1.25rem; margin-right: 1.25rem;
width: 10rem; width: 10rem;
} }
@ -300,16 +345,13 @@
.threads_main__side_bar { .threads_main__side_bar {
width: 12rem; width: 12rem;
height: 0%; height: 0%;
background: #fff;
margin-right: 1rem; margin-right: 1rem;
border-radius: 0.25rem; border-radius: 0.25rem;
border: thin solid $color__gray--darker;
padding: 0.5rem 0 1rem 1rem; padding: 0.5rem 0 1rem 1rem;
position: sticky; position: sticky;
top: 4.5rem; top: 4.5rem;
@at-root #{&}__title { @at-root #{&}__title {
color: $color__darkgray--darker;
cursor: default; cursor: default;
font-weight: 500; font-weight: 500;
font-size: 1.125rem; font-size: 1.125rem;
@ -333,15 +375,9 @@
width: 0.9rem; width: 0.9rem;
border-radius: 0.25rem; border-radius: 0.25rem;
margin-top: 0.25rem; margin-top: 0.25rem;
background-color: $color__gray--darkest;
transition: all 0.2s; transition: all 0.2s;
} }
@at-root #{&}__text {
filter: saturate(0.75), brightness(0.75);
}
@at-root #{&}--selected { @at-root #{&}--selected {
font-weight: 600; font-weight: 600;
} }
@ -357,37 +393,30 @@
font-size: 1.25rem; font-size: 1.25rem;
margin: 0 0 1rem 0; margin: 0 0 1rem 0;
background-color: $color__lightgray--primary;
border-color: $color__gray--darker;
width: 100%; width: 100%;
font-weight: 300; font-weight: 300;
} }
.thread { .thread {
background-color: #fff;
padding: 0.5rem 0; padding: 0.5rem 0;
cursor: default; cursor: default;
text-align: left; text-align: left;
transition: background-color 0.2s; transition: background-color 0.2s;
&:hover { &:hover {
background-color: $color__lightgray--primary;
} }
td, th { td, th {
padding: 0.3rem 0.5rem; padding: 0.3rem 0.5rem;
border-bottom: solid thin $color__lightgray--primary;
} }
@at-root #{&}--header { @at-root #{&}--header {
&:hover { &:hover {
background-color: #fff;
} }
th { th {
font-weight: 400; font-weight: 400;
padding-bottom: 0.25rem; padding-bottom: 0.25rem;
border-bottom: thin solid $color__lightgray--darkest;
} }
} }
@ -399,7 +428,6 @@
display: inline-block; display: inline-block;
} }
@at-root #{&}__date { @at-root #{&}__date {
color: $color__text--secondary;
display: inline-block; display: inline-block;
} }
} }

View File

@ -407,7 +407,7 @@
.notification_button__menu { .notification_button__menu {
width: 100%; width: 100%;
left: unset; left: unset;
right: unset !important; right: unset ;
} }
} }

View File

@ -6,15 +6,15 @@
<b-menu-item <b-menu-item
:label=item.name :label=item.name
:icon=item.icon :icon=item.icon
:key='"menu-item-" + index' :key='index'
v-for='(item, index) in menuItems' v-for='(item, index) in menuItems'
:class="{'settings_menu__item--selected': index === selected}" :class="{'': index === selected}"
@click='$router.push("/settings/" + item.route)' @click='$router.push("/settings/" + item.route)'
></b-menu-item> ></b-menu-item>
</b-menu-list> </b-menu-list>
</b-menu> </b-menu>
</div> </div>
<div class='settings_page'> <div class='column box'>
<router-view></router-view> <router-view></router-view>
</div> </div>
</div> </div>
@ -93,8 +93,6 @@
.settings_menu { .settings_menu {
width: 15rem; width: 15rem;
border: thin solid $color__gray--darker;
background-color: #fff;
padding: 1rem; padding: 1rem;
border-radius: 0.25rem; border-radius: 0.25rem;

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="box"> <div>
<center> <center>
<img src="https://cdn.kaverti.com/icon.png" width="10%"> <img src="https://cdn.kaverti.com/icon.png" width="10%">
<h1>Kaverti v{{this.$store.state.clientVersion}}</h1> <h1>Kaverti v{{this.$store.state.clientVersion}}</h1>

View File

@ -1,9 +1,9 @@
<style> <style>
h1 { h1 {
font-size: 32px !important; font-size: 32px ;
} }
h2 { h2 {
font-size: 24px !important; font-size: 24px ;
} }
</style> </style>
<template> <template>

View File

@ -1,9 +1,135 @@
<template> <template>
<main> <main>
<span> <div v-if="$store.state.developerMode">
<center> <center>
<h1>Only available on <a href="https://canary.kaverti.com">Kaverti Canary</a>.</h1> <h1>Experiments</h1>
<div class="column">
<h2>Dark Theme</h2>
<b-switch
passive-type='is-warning'
type='is-dark'
false-value="light"
true-value="dark"
v-model="experiments.theme"
v-if="$store.state.developerMode">
Theme
</b-switch>
<br/>
<br/>
<b-button
class='button is-info'
:loading='experiments.loading'
@click='savePreferences'
>
Save experiment preferences
</b-button>
</div>
</center> </center>
</span> </div>
<div v-else>
<center>
<h2>Please enable Integration Developer Mode to gain access to experiments.</h2>
<p>It's easy! Just go to Settings > General, flip the switch and save! </p>
</center>
</div>
</main> </main>
</template> </template>
<script>
import FancyTextarea from '../FancyTextarea'
import AjaxErrorHandler from '../../assets/js/errorHandler'
import logger from '../../assets/js/logger'
export default {
name: 'settingsGeneral',
components: {
// eslint-disable-next-line vue/no-unused-components
FancyTextarea
},
data () {
return {
experiments: {
theme: '',
loading: false,
error: ''
},
}
},
computed: {},
methods: {
capitalizeFirstLetter(value) {
return value.toUpperCase()
},
saveDescription() {
this.description.error = ''
this.description.loading = true
this.axios
.put('/api/v1/users/preferences', {
description: this.description.value
})
.then(() => {
this.description.loading = false
})
.catch(e => {
this.description.loading = false
AjaxErrorHandler(this.$store)(e)
})
},
savePreferences() {
this.experiments.error = ''
this.experiments.loading = true
this.axios
.put('/api/v1/users/experiments', {
theme: this.experiments.theme
})
.then(() => {
this.experiments.loading = false
this.axios.get('/api/v1/userinfo')
.then(res => {
this.$store.commit('setUsername', res.data.username)
this.$store.commit('setEmail', res.data.email)
this.$store.commit('setEmailVerified', res.data.emailVerified)
this.$store.commit('setAdmin', res.data.admin)
this.$store.commit('setDevMode', res.data.developerMode)
this.$store.commit('setTheme', res.data.theme)
if(res.data.theme === "dark") {
alert("Please refresh to enable this experiment. (theme)")
} else {
alert("Please refresh to disable this experiment. (theme)")
}
})
})
.catch(e => {
this.experiments.loading = false
AjaxErrorHandler(this.$store)(e)
})
}
},
created () {
this.axios.get('/api/v1/userinfo')
.then(res => {
this.$store.commit('setUsername', res.data.username)
this.$store.commit('setEmail', res.data.email)
this.$store.commit('setEmailVerified', res.data.emailVerified)
this.$store.commit('setAdmin', res.data.admin)
this.$store.commit('setDevMode', res.data.developerMode)
})
this.$store.dispatch('setTitle', 'Experiments')
this.$nextTick(() => {
this.axios
.get('/api/v1/userinfo')
.then(res => {
this.experiments.theme = res.data.theme || ''
})
.catch(e => {
AjaxErrorHandler(this.$store)(e)
})
})
logger('settingsGeneral')
}
}
</script>

View File

@ -25,6 +25,17 @@
Save description Save description
</b-button> </b-button>
</div> </div>
<div>
<h2>Preferences</h2>
<b-switch class="is-info" v-model="preferences.developerMode">Developer Mode and get beta features first</b-switch><br>
<b-button
class='button is-info'
:loading='preferences.loading'
@click='savePreferences'
>
Save preferences
</b-button>
</div>
</div> </div>
</template> </template>
@ -47,16 +58,25 @@
loading: false, loading: false,
error: '' error: ''
}, },
preferences: {
theme: '',
developerMode: '',
loading: false,
error: ''
},
} }
}, },
computed: {}, computed: {},
methods: { methods: {
capitalizeFirstLetter(value) {
return value.toUpperCase()
},
saveDescription() { saveDescription() {
this.description.error = '' this.description.error = ''
this.description.loading = true this.description.loading = true
this.axios this.axios
.put('/api/v1/user/' + this.$store.state.username, { .put('/api/v1/users/preferences', {
description: this.description.value description: this.description.value
}) })
.then(() => { .then(() => {
@ -66,6 +86,30 @@
this.description.loading = false this.description.loading = false
AjaxErrorHandler(this.$store)(e) AjaxErrorHandler(this.$store)(e)
}) })
},
savePreferences() {
this.preferences.error = ''
this.preferences.loading = true
this.axios
.put('/api/v1/users/preferences', {
developerMode: this.preferences.developerMode
})
.then(() => {
this.preferences.loading = false
this.axios.get('/api/v1/userinfo')
.then(res => {
this.$store.commit('setUsername', res.data.username)
this.$store.commit('setEmail', res.data.email)
this.$store.commit('setEmailVerified', res.data.emailVerified)
this.$store.commit('setAdmin', res.data.admin)
this.$store.commit('setDevMode', res.data.developerMode)
})
})
.catch(e => {
this.preferences.loading = false
AjaxErrorHandler(this.$store)(e)
})
} }
}, },
created () { created () {
@ -84,6 +128,8 @@
.get('/api/v1/userinfo') .get('/api/v1/userinfo')
.then(res => { .then(res => {
this.description.value = res.data.description || '' this.description.value = res.data.description || ''
this.preferences.developerMode = res.data.developerMode
this.preferences.theme = res.data.theme || ''
}) })
.catch(e => { .catch(e => {
AjaxErrorHandler(this.$store)(e) AjaxErrorHandler(this.$store)(e)
@ -94,66 +140,3 @@
} }
} }
</script> </script>
<style lang='scss' scoped>
@import '../../assets/scss/variables.scss';
.profile_picture_preview {
height: 5rem;
width: 5rem;
}
.profile_picture_modal {
padding-top: 1rem;
transition: all 0.2s;
@at-root #{&}--picture .dragging {
background-color: $color__lightgray--primary;
}
@at-root #{&}__overlay {
@include loading-overlay(rgba(0, 0, 0, 0.5), 0.125rem);
}
@at-root #{&}__upload_button input[type="file"] {
display: none;
}
@at-root #{&}__drag_area {
padding: 1rem;
text-align: center;
@at-root #{&}__image {
width: 5rem;
height: 5rem;
display: inline-block;
margin-top: -1rem;
}
@at-root #{&}__icon {
font-size: 6rem;
color: $color__gray--darker;
transition: all 0.2s;
@at-root #{&}--picture.dragging {
transform: translateY(-0.5rem) scale(1.1);
color: $color__gray--darkest;
}
}
}
}
@media (max-width: $breakpoint--tablet) {
.h1 {
display: none;
}
}
.visuallyhidden {
position: absolute;
overflow: hidden;
clip: rect(0 0 0 0);
height: 1px; width: 1px;
margin: -1px; padding: 0;
border: 0;
}
</style>

View File

@ -422,20 +422,16 @@
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
} }
background-color: #fff;
padding: 1.5rem; padding: 1.5rem;
margin-bottom: 1rem; margin-bottom: 1rem;
width: 80%; width: 80%;
border-radius: 0.25rem; border-radius: 0.25rem;
border: thin solid $color__gray--darker;
} }
.posts { .posts {
width: 80%; width: 80%;
background-color: #fff;
padding: 0.5rem 1rem; padding: 0.5rem 1rem;
border-radius: 0.25rem; border-radius: 0.25rem;
border: thin solid $color__gray--darker;
} }
@ -497,7 +493,7 @@
@media (max-width: $breakpoint--tablet-thread) { @media (max-width: $breakpoint--tablet-thread) {
.route_container { .route_container {
padding-bottom: 2rem !important; padding-bottom: 2rem ;
} }
.thread_side_bar { .thread_side_bar {

View File

@ -284,8 +284,6 @@
} }
.thread_meta_info { .thread_meta_info {
background-color: #fff;
border: thin solid $color__gray--darker;
border-radius: 0.25rem; border-radius: 0.25rem;
padding: 1rem; padding: 1rem;
margin: 1rem 0; margin: 1rem 0;
@ -350,9 +348,7 @@
.editor { .editor {
display: flex; display: flex;
background-color: #fff;
border-radius: 0.25rem; border-radius: 0.25rem;
border: thin solid $color__gray--darker;
transition: all 0.2s; transition: all 0.2s;
@ -430,7 +426,7 @@
width: 100%; width: 100%;
> div, input { > div, input {
width: 100% !important; width: 100% ;
} }
} }
} }

View File

@ -56,10 +56,10 @@
Manage user Manage user
</button> </button>
</menu-button><br> </menu-button><br>
<b-button v-if="user && relationship && user.username !== $store.state.username && $store.state.username" class='is-danger button' icon-left="minus"> <b-button v-if="$store.state.developerMode && relationship && user.username !== $store.state.username && $store.state.username" class='is-danger button' icon-left="minus">
Remove Friend Remove Friend
</b-button> </b-button>
<b-button v-model="relationship.type" :value="1" @click="doRelationship" v-if="user && !relationship && user.username !== $store.state.username && $store.state.username" class='is-info button' icon-left="plus"> <b-button :value="1" @click="doRelationship" v-if="$store.state.developerMode && user && !relationship && user.username !== $store.state.username && $store.state.username" class='is-info button' icon-left="plus">
Send friend request Send friend request
</b-button> </b-button>
</div> </div>

View File

@ -407,7 +407,7 @@ export default {
width: 100%; width: 100%;
> div, input { > div, input {
width: 100% !important; width: 100% ;
} }
} }
} }

View File

@ -43,11 +43,13 @@
</header> </header>
<div class="card-content"> <div class="card-content">
<div class="content limit" v-if="user.description"> <div class="content limit" v-if="user.description">
{{user.description | truncate(200)}}<br/> {{user.description | truncate(200)}}
<time v-bind:datetime="user.createdAt">Created at {{user.createdAt | formatDate}}</time>
</div> </div>
<div class="content" v-if="!user.description"> <div class="content" v-if="!user.description">
No description set.<br><br> No description set.
</div>
<div class="content">
Post count: {{user.postCount}}<br>
<time v-bind:datetime="user.createdAt">Created at {{user.createdAt | formatDate}}</time> <time v-bind:datetime="user.createdAt">Created at {{user.createdAt | formatDate}}</time>
</div> </div>
</div> </div>

View File

@ -31,7 +31,7 @@ import { faBell, faComment } from '@fortawesome/free-regular-svg-icons'
import VueMatomo from 'vue-matomo' import VueMatomo from 'vue-matomo'
import { VueSpinners } from '@saeris/vue-spinners' import { VueSpinners } from '@saeris/vue-spinners'
import moment from 'moment' import moment from 'moment'
import VueSkipTo from '@vue-a11y/skip-to'
import { import {
faBars, faPlus, faGrin, faLink, faCode, faBars, faPlus, faGrin, faLink, faCode,
faTimes, faUnlockAlt, faReply, faHome, faTh, faTimes, faUnlockAlt, faReply, faHome, faTh,
@ -57,6 +57,7 @@ Vue.component('font-awesome-icon', FontAwesomeIcon);
const Index = () => import('./components/routes/Index') const Index = () => import('./components/routes/Index')
const CategorySelect = () => import('./components/routes/CategorySelect') const CategorySelect = () => import('./components/routes/CategorySelect')
const HomeUnauthenticated = () => import('./components/routes/HomeUnauthenticated') const HomeUnauthenticated = () => import('./components/routes/HomeUnauthenticated')
const HomeAuthenticated = () => import('./components/routes/HomeAuthenticated')
const P = () => import('./components/routes/P') const P = () => import('./components/routes/P')
const Thread = () => import('./components/routes/Thread') const Thread = () => import('./components/routes/Thread')
@ -124,6 +125,7 @@ Vue.use(linkExpander)
Vue.use(Buefy, { defaultIconPack: 'fas' }) Vue.use(Buefy, { defaultIconPack: 'fas' })
Vue.use(VPaginator) Vue.use(VPaginator)
Vue.use(VueSpinners) Vue.use(VueSpinners)
Vue.use(VueSkipTo)
Sentry.init({ Sentry.init({
dsn: "https://287b35aa71b54ffea9e66b5ce03d2e29@o444992.ingest.sentry.io/5420646", dsn: "https://287b35aa71b54ffea9e66b5ce03d2e29@o444992.ingest.sentry.io/5420646",
@ -145,6 +147,7 @@ Vue.use({
const router = new VueRouter({ const router = new VueRouter({
routes: [ routes: [
{ path: '/', component: HomeUnauthenticated }, { path: '/', component: HomeUnauthenticated },
{ path: '/dashboard', component: HomeAuthenticated },
{ path: '/category/select', component: CategorySelect }, { path: '/category/select', component: CategorySelect },
{ path: '/category/:category', component: Index }, { path: '/category/:category', component: Index },
{ path: '/p/:id', component: P }, { path: '/p/:id', component: P },

View File

@ -25,9 +25,10 @@ export default new Vuex.Store({
koins: '', koins: '',
email: '', email: '',
emailVerified: true, emailVerified: true,
clientVersion: '0.171-stable', clientVersion: '0.172-stable',
latestClientVersion: '', latestClientVersion: '',
developerMode: false, developerMode: false,
theme: '',
token: null, token: null,
passkey: "register", passkey: "register",
@ -94,6 +95,9 @@ export default new Vuex.Store({
setPassKey(state, passkey) { setPassKey(state, passkey) {
state.passkey = passkey state.passkey = passkey
}, },
setTheme(state, theme) {
state.theme = theme
},
set404Page(state, value) { set404Page(state, value) {
state.show404Page = value state.show404Page = value
}, },

View File

@ -1520,6 +1520,11 @@
resolved "https://npm.open-registry.dev/@types/q/-/q-1.5.2.tgz#690a1475b84f2a884fd07cd797c00f5f31356ea8" resolved "https://npm.open-registry.dev/@types/q/-/q-1.5.2.tgz#690a1475b84f2a884fd07cd797c00f5f31356ea8"
integrity sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw== integrity sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw==
"@vue-a11y/skip-to@^2.1.1":
version "2.1.1"
resolved "https://registry.yarnpkg.com/@vue-a11y/skip-to/-/skip-to-2.1.1.tgz#122fe02a589947f5c4afef66957ffd844bcbda39"
integrity sha512-/N7NVd9hy/oxmvaae8AUZo2IWcgZWjtNgkt/A+rwNqDeCFOREbCLEyzPWqcWvHO1ubzpGVoShGWuICGDqJr9xA==
"@vue/babel-helper-vue-jsx-merge-props@^1.0.0": "@vue/babel-helper-vue-jsx-merge-props@^1.0.0":
version "1.0.0" version "1.0.0"
resolved "https://npm.open-registry.dev/@vue/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-1.0.0.tgz#048fe579958da408fb7a8b2a3ec050b50a661040" resolved "https://npm.open-registry.dev/@vue/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-1.0.0.tgz#048fe579958da408fb7a8b2a3ec050b50a661040"

View File

@ -19,6 +19,10 @@ let Errors = {
'Due to the nature of this action, a login is required.', 'Due to the nature of this action, a login is required.',
401 401
], ],
deniedExperiments: [
'Please enable integration developer mode to access experiments.',
401
],
reCAPTCHA: [ reCAPTCHA: [
'Oh uh! You failed the reCAPTCHA score, please try again or contact support! This could be caused because you are operating the API through an unofficial client, CLI application, or reCAPTCHA is blocked by your network administrator or ISP.', 'Oh uh! You failed the reCAPTCHA score, please try again or contact support! This could be caused because you are operating the API through an unofficial client, CLI application, or reCAPTCHA is blocked by your network administrator or ISP.',
400 400

View File

@ -78,7 +78,12 @@ module.exports = (sequelize, DataTypes) => {
}, },
theme: { theme: {
type: DataTypes.STRING, type: DataTypes.STRING,
defaultValue: 'light' defaultValue: 'light',
values: ['light', 'dark'],
isIn: {
args: [['light', 'dark']],
msg: "Theme can only be one of the pre-defined options"
},
}, },
lastRewardDate: { lastRewardDate: {
type: DataTypes.DATE type: DataTypes.DATE
@ -108,7 +113,12 @@ module.exports = (sequelize, DataTypes) => {
}, },
developerMode: { developerMode: {
type: DataTypes.BOOLEAN, type: DataTypes.BOOLEAN,
defaultValue: false defaultValue: false,
validate: {
isBoolean: {
msg: 'Developer mode can only be true or false.'
}
}
}, },
experimentMode: { experimentMode: {
type: DataTypes.BOOLEAN, type: DataTypes.BOOLEAN,

View File

@ -23,16 +23,16 @@
<style type="text/css"> <style type="text/css">
@media screen and (max-width: 525px) { @media screen and (max-width: 525px) {
table[class="wrapper"] { table[class="wrapper"] {
width: 100% !important; width: 100% ;
} }
td[class="logo"] { td[class="logo"] {
text-align: left; text-align: left;
padding: 20px 0 20px 0 !important; padding: 20px 0 20px 0 ;
} }
td[class="logo"] img { td[class="logo"] img {
margin: 0 auto!important; margin: 0 auto;
} }
td[class="mobile-hide"] { td[class="mobile-hide"] {
@ -40,62 +40,62 @@
} }
img[class="mobile-hide"] { img[class="mobile-hide"] {
display: none !important; display: none ;
} }
img[class="img-max"] { img[class="img-max"] {
max-width: 100% !important; max-width: 100% ;
height: auto !important; height: auto ;
} }
table[class="responsive-table"] { table[class="responsive-table"] {
width: 100%!important; width: 100%;
} }
td[class="padding"] { td[class="padding"] {
padding: 10px 5% 15px 5% !important; padding: 10px 5% 15px 5% ;
} }
td[class="padding-copy"] { td[class="padding-copy"] {
padding: 10px 5% 10px 5% !important; padding: 10px 5% 10px 5% ;
text-align: center; text-align: center;
} }
td[class="padding-meta"] { td[class="padding-meta"] {
padding: 30px 5% 0px 5% !important; padding: 30px 5% 0px 5% ;
text-align: center; text-align: center;
} }
td[class="no-pad"] { td[class="no-pad"] {
padding: 0 0 20px 0 !important; padding: 0 0 20px 0 ;
} }
td[class="no-padding"] { td[class="no-padding"] {
padding: 0 !important; padding: 0 ;
} }
td[class="section-padding"] { td[class="section-padding"] {
padding: 50px 15px 50px 15px !important; padding: 50px 15px 50px 15px ;
} }
td[class="section-padding-bottom-image"] { td[class="section-padding-bottom-image"] {
padding: 50px 15px 0 15px !important; padding: 50px 15px 0 15px ;
} }
td[class="mobile-wrapper"] { td[class="mobile-wrapper"] {
padding: 10px 5% 15px 5% !important; padding: 10px 5% 15px 5% ;
} }
table[class="mobile-button-container"] { table[class="mobile-button-container"] {
margin: 0 auto; margin: 0 auto;
width: 100% !important; width: 100% ;
} }
a[class="mobile-button"] { a[class="mobile-button"] {
width: 80% !important; width: 80% ;
padding: 15px !important; padding: 15px ;
border: 0 !important; border: 0 ;
font-size: 16px !important; font-size: 16px ;
} }
} }
</style> </style>

View File

@ -37,7 +37,7 @@ router.put('/user/scrub', async (req, res, next) => {
if(user.admin) { if(user.admin) {
throw Errors.modifyAdminUser throw Errors.modifyAdminUser
} }
let userUpdate = await User.update({ description: "Scrambled by Admin: " + Math.random().toString(36).substring(2)}, { where: { let userUpdate = await User.update({ description: "Description was removed by an administrator"}, { where: {
username: req.autosan.body.user username: req.autosan.body.user
}}) }})
res.status(200) res.status(200)
@ -74,6 +74,7 @@ router.put('/user/modify', async (req, res, next) => {
let user = await User.findOne({ where: { let user = await User.findOne({ where: {
username: req.body.username username: req.body.username
}}) }})
if(!user) throw Errors.accountDoesNotExist
if(user.admin) { if(user.admin) {
throw Errors.modifyAdminUser throw Errors.modifyAdminUser
} }

View File

@ -271,118 +271,55 @@ router.get('/:username/picture', async (req, res, next) => {
} }
} catch (e) { next(e) } } catch (e) { next(e) }
}) })
router.get('/', async function(req, res) {
router.get('/', async (req, res, next) => {
try { try {
let sortFields = { let sortFields = {
createdAt: 'X.id', createdAt: 'X.id',
username: 'X.username', username: 'X.username',
threadCount: 'threadCount', threadCount: 'threadCount',
postCount: 'postCount', postCount: 'postCount'
description: 'description'
}; };
let offset = Number.isInteger(+req.query.offset) ? +req.query.offset : 0; let offset = Number.isInteger(+req.query.offset) ? +req.query.offset : 0;
let havingClause = ''; let havingClause = '';
if(req.query.role === 'admin') {
if (req.query.role === 'admin') {
havingClause = 'HAVING Users.admin = true'; havingClause = 'HAVING Users.admin = true';
} else if (req.query.role === 'user') { } else if(req.query.role === 'user') {
havingClause = 'HAVING Users.admin = false'; havingClause = 'HAVING Users.admin = false';
} else { } else {
havingClause = ''; havingClause = '';
} }
if(req.query.search) {
if (req.query.search) {
//I.e. if there is not already a HAVING clause //I.e. if there is not already a HAVING clause
if (!havingClause.length) { if(!havingClause.length) {
havingClause = 'HAVING '; havingClause = 'HAVING ';
} else { } else {
havingClause += ' AND '; havingClause += ' AND ';
} }
havingClause += 'Users.username LIKE $search'; havingClause += 'Users.username LIKE $search';
} }
let sql = `
let sql = ` SELECT X.username, X.admin, X.level, X.levelProgress, X.bot, X.booster, X.description, X.bodyColor, X.headColor, X.leftLegColor, X.rightLegColor, X.leftArmColor, X.rightArmColor, X.hidden, X.system, X.createdAt, X.contributor, X.postCount, COUNT(Threads.id) as threadCount
SELECT X.username, X.admin, X.level, X.levelProgress, X.bot, X.booster, X.description, X.koins, X.bodyColor, X.headColor, X.leftLegColor, X.rightLegColor, X.leftArmColor, X.rightArmColor, X.hidden, X.system, X.createdAt, X.contributor, X.postCount, COUNT(Threads.id) as threadCount FROM (
FROM ( SELECT Users.*, COUNT(Posts.id) as postCount
SELECT Users.*, COUNT(Posts.id) as postCount FROM Users
FROM Users LEFT OUTER JOIN Posts
LEFT OUTER JOIN Posts ON Users.id = Posts.UserId
ON Users.id = Posts.UserId GROUP BY Users.id
GROUP BY Users.id ${havingClause}
${havingClause} ) as X
) as X LEFT OUTER JOIN threads
LEFT OUTER JOIN Threads ON X.id = Threads.UserId
ON X.id = Threads.UserId GROUP BY X.id
GROUP BY X.id ORDER BY ${sortFields[req.query.sort] || 'X.id'} ${req.query.order === 'asc' ? 'ASC' : 'DESC'}
ORDER BY ${sortFields[req.query.sort] || 'X.id'} LIMIT 2000
LIMIT 9999 OFFSET ${offset}
OFFSET ${offset}
`; `;
let users = await sequelize.query(sql, { let users = await sequelize.query(sql, {
model: User, model: User,
attributes: {exclude: ['hash', 'email', 'emailVerified', 'koins', 'currency2', 'emailToken', 'passwordResetExpiry', 'passwordResetToken', 'experimentMode', 'developerMode']}, bind: { search: req.query.search + '%' }
bind: {search: req.query.search + '%'}
}); });
res.json(users) res.json(users)
} catch (e) {
}
})
router.all('*', (req, res, next) => {
if(req.session.username) {
next()
} else {
res.status(401)
res.json({
errors: [Errors.requestNotAuthorized]
})
}
})
router.put('/:username', async (req, res, next) => {
try {
if(req.session.username !== req.params.username) {
throw Errors.requestNotAuthorized
}
await Ban.ReadOnlyMode(req.session.username)
if(req.autosan.body.description !== undefined) {
let user = await User.update({ description: req.autosan.body.description }, { where: {
username: req.session.username
}})
res.json({ success: true })
} else if(
req.body.currentPassword !== undefined &&
req.body.newPassword !== undefined
) {
let user = await User.findOne({
where: {
username: req.session.username
}
})
await user.updatePassword(req.body.currentPassword, req.body.newPassword)
res.json({success: true})
} else if(
req.body.emailCurrentPassword !== undefined &&
req.body.newEmail !== undefined
) {
let user = await User.findOne({where: {
username: req.session.username
}})
await user.updateEmail(req.body.emailCurrentPassword, req.body.newEmail)
res.json({ success: true })
} else {
res.json({ success: false })
}
} catch (e) { next(e) } } catch (e) { next(e) }
}) })
module.exports = router; module.exports = router;

View File

@ -97,6 +97,58 @@ router.post('/', async (req, res, next) => {
res.json(user.toJSON()) res.json(user.toJSON())
} catch (e) { next(e) } } catch (e) { next(e) }
}) })
router.get('/leaderboard', async (req, res, next) => {
try {
let sortFields = {
createdAt: 'X.id',
username: 'X.username',
threadCount: 'threadCount',
postCount: 'postCount'
};
let offset = Number.isInteger(+req.query.offset) ? +req.query.offset : 0;
let havingClause = '';
if(req.query.role === 'admin') {
havingClause = 'HAVING Users.admin = true';
} else if(req.query.role === 'user') {
havingClause = 'HAVING Users.admin = false';
} else {
havingClause = '';
}
if(req.query.search) {
//I.e. if there is not already a HAVING clause
if(!havingClause.length) {
havingClause = 'HAVING ';
} else {
havingClause += ' AND ';
}
havingClause += 'Users.username LIKE $search';
}
let sql = `
SELECT X.username, X.postCount, COUNT(Threads.id) as threadCount
FROM (
SELECT Users.*, COUNT(Posts.id) as postCount
FROM Users
LEFT OUTER JOIN Posts
ON Users.id = Posts.UserId
GROUP BY Users.id
${havingClause}
) as X
LEFT OUTER JOIN threads
ON X.id = Threads.UserId
GROUP BY X.id
ORDER BY ${sortFields[req.query.sort] || 'X.id'} ${req.query.order === 'asc' ? 'ASC' : 'DESC'}
LIMIT 5
OFFSET ${offset}
`;
let users = await sequelize.query(sql, {
model: User,
bind: { search: req.query.search + '%' }
});
res.json(users)
} catch (e) { next(e) }
})
router.post('/job-application', async (req, res, next) => { router.post('/job-application', async (req, res, next) => {
try { try {
let userParams = { let userParams = {
@ -405,4 +457,84 @@ router.get('/email-verify/:token', async (req, res) => {
} }
}); });
router.put('/preferences', async (req, res, next) => {
try {
if(!req.session.username) {
throw Errors.requestNotAuthorized
}
await Ban.ReadOnlyMode(req.session.username)
if(req.autosan.body.description !== undefined) {
let user = await User.update({ description: req.autosan.body.description }, { where: {
username: req.session.username
}})
res.json({ success: true })
} else if(
req.body.currentPassword !== undefined &&
req.body.newPassword !== undefined
) {
let user = await User.findOne({
where: {
username: req.session.username
}
})
await user.updatePassword(req.body.currentPassword, req.body.newPassword)
res.json({success: true})
} else if(
req.body.emailCurrentPassword !== undefined &&
req.body.newEmail !== undefined
) {
let user = await User.findOne({where: {
username: req.session.username
}})
await user.updateEmail(req.body.emailCurrentPassword, req.body.newEmail)
res.json({ success: true })
} else if(
req.body.developerMode !== undefined) {
let user = await User.update({developerMode: req.autosan.body.developerMode}, {
where: {
username: req.session.username
}
})
res.json({success: true})
} else {
res.json({ success: false })
}
} catch (e) { next(e) }
})
router.put('/experiments', async (req, res, next) => {
try {
if(!req.session.username) {
throw Errors.requestNotAuthorized
}
let queryObj = {
attributes: {include: ['developerMode']},
where: { username: req.session.username }
}
let user = await User.findOne(queryObj)
if(!user.developerMode) {
throw Errors.deniedExperiments
}
if(req.body.theme !== undefined) {
{
let user = await User.update({theme: req.autosan.body.theme}, {
where: {
username: req.session.username
}
})
res.json({success: true})
}
} else {
res.json({ success: false })
}
} catch (e) { next(e) }
})
module.exports = router; module.exports = router;