This commit is contained in:
Troplo 2022-08-31 20:26:08 +10:00
parent 686f602b56
commit 7125909046
10 changed files with 404 additions and 20 deletions

View file

@ -24,6 +24,7 @@ app.use("/api/v1/usercontent", require("./routes/usercontent.js"))
app.use("/api/v1/mediaproxy", require("./routes/mediaproxy.js")) app.use("/api/v1/mediaproxy", require("./routes/mediaproxy.js"))
app.use("/api/v1/associations", require("./routes/associations.js")) app.use("/api/v1/associations", require("./routes/associations.js"))
app.use("/api/v1/polls", require("./routes/polls.js")) app.use("/api/v1/polls", require("./routes/polls.js"))
app.use("/api/v1/feedback", require("./routes/feedback.js"))
app.get("/api/v1/state", async (req, res) => { app.get("/api/v1/state", async (req, res) => {
res.json({ res.json({
release: req.app.locals.config.release, release: req.app.locals.config.release,

View file

@ -0,0 +1,42 @@
"use strict"
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.createTable("Feedbacks", {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.BIGINT
},
feedbackText: {
type: Sequelize.TEXT
},
starRating: {
type: Sequelize.INTEGER
},
route: {
type: Sequelize.STRING
},
debug: {
type: Sequelize.JSON
},
userId: {
type: Sequelize.BIGINT
},
tenant: {
type: Sequelize.STRING
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
})
},
down: async (queryInterface, Sequelize) => {
await queryInterface.dropTable("Feedbacks")
}
}

View file

@ -20,7 +20,7 @@ module.exports = (sequelize, DataTypes) => {
feedbackText: DataTypes.TEXT, feedbackText: DataTypes.TEXT,
starRating: DataTypes.BIGINT, starRating: DataTypes.BIGINT,
route: DataTypes.STRING, route: DataTypes.STRING,
userId: DataTypes.STRING, userId: DataTypes.BIGINT,
tenant: DataTypes.STRING, tenant: DataTypes.STRING,
debug: DataTypes.JSON debug: DataTypes.JSON
}, },

View file

@ -2,7 +2,7 @@ const express = require("express")
const router = express.Router() const router = express.Router()
const Errors = require("../lib/errors.js") const Errors = require("../lib/errors.js")
const auth = require("../lib/authorize.js") const auth = require("../lib/authorize.js")
const { User, Theme, Message } = require("../models") const { User, Theme, Message, Feedback } = require("../models")
const { Op } = require("sequelize") const { Op } = require("sequelize")
const dayjs = require("dayjs") const dayjs = require("dayjs")
const fs = require("fs") const fs = require("fs")
@ -33,6 +33,23 @@ router.all("*", auth, async (req, res, next) => {
} }
}) })
router.get("/feedback", auth, async (req, res, next) => {
try {
const feedback = await Feedback.findAndCountAll({
order: [["createdAt", "DESC"]],
include: [
{
model: User,
as: "user"
}
]
})
res.json(feedback)
} catch (err) {
return next(err)
}
})
router.get("/", auth, async (req, res, next) => { router.get("/", auth, async (req, res, next) => {
try { try {
res.json({ res.json({

View file

@ -0,0 +1,25 @@
const express = require("express")
const router = express.Router()
const Errors = require("../lib/errors.js")
const auth = require("../lib/authorize.js")
const { Feedback } = require("../models")
router.post("/", auth, async (req, res, next) => {
try {
await Feedback.create({
feedbackText: req.body.text,
starRating: req.body.starRating,
debug: {
client: req.body.debug
},
route: req.body.route,
userId: req.user.id,
tenant: "colubrina"
})
res.sendStatus(204)
} catch (e) {
next(e)
}
})
module.exports = router

View file

@ -1,6 +1,6 @@
{ {
"name": "colubrina", "name": "colubrina",
"version": "1.0.25", "version": "1.0.26",
"description": "Simple instant communication.", "description": "Simple instant communication.",
"private": true, "private": true,
"author": "Troplo <troplo@troplo.com>", "author": "Troplo <troplo@troplo.com>",

View file

@ -357,6 +357,139 @@
</v-card-actions> </v-card-actions>
</v-card> </v-card>
</v-dialog> </v-dialog>
<v-dialog v-model="shortcuts" width="700">
<v-card color="card" elevation="7">
<v-toolbar color="toolbar">
<v-toolbar-title> Shortcuts </v-toolbar-title>
</v-toolbar>
<v-card-text>
<v-container>
<v-layout row wrap>
<v-card class="mx-2 mt-2">
<v-card-title> QuickSwitcher </v-card-title>
<v-card-text class="text-center">
<v-btn text outlined> CTRL </v-btn>
<v-btn text outlined class="ml-2"> K </v-btn>
</v-card-text>
</v-card>
<v-card class="mx-2 mt-2">
<v-card-title> RouteSwitcher </v-card-title>
<v-card-text class="text-center">
<v-btn text outlined> CTRL </v-btn>
<v-btn text outlined class="ml-2"> B </v-btn>
</v-card-text>
</v-card>
<v-card class="mx-2 mt-2">
<v-card-title> Shortcuts </v-card-title>
<v-card-text class="text-center">
<v-btn text outlined class="ml-2"> CTRL </v-btn>
<v-btn text outlined class="ml-2"> / </v-btn>
</v-card-text>
</v-card>
<v-card class="mx-2 mt-2">
<v-card-title> Toggle CSS </v-card-title>
<v-card-text class="text-center">
<v-btn text outlined> F9 </v-btn>
<span class="ml-2">or</span>
<v-btn text outlined class="ml-2"> CTRL </v-btn>
<v-btn text outlined class="ml-2"> ALT </v-btn>
<v-btn text outlined class="ml-2"> D </v-btn>
</v-card-text>
</v-card>
</v-layout>
</v-container>
</v-card-text>
</v-card>
</v-dialog>
<v-dialog v-model="feedback.modal" width="700">
<v-card color="card" elevation="7">
<v-toolbar color="toolbar">
<v-toolbar-title> Provide Feedback </v-toolbar-title>
</v-toolbar>
<v-card-text>
<v-container>
<v-row>
<v-col cols="12" md="4" sm="6">
Rating:
<v-rating
v-model="feedback.rating"
background-color="grey darken-1"
color="yellow darken-3"
empty-icon="$ratingFull"
hover
></v-rating>
</v-col>
<v-col cols="12">
<v-textarea
class="rounded-xl"
v-model="feedback.text"
label="Enter your Feedback"
required
autofocus
placeholder="Enter your Feedback"
></v-textarea>
</v-col>
<small
>Your feedback will be used to make
{{ $store.state.site.name }} even better.</small
>
</v-row>
</v-container>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn
class="rounded-xl"
color="blue darken-1"
text
@click="feedback.modal = false"
>
Close
</v-btn>
<v-btn
class="rounded-xl"
color="blue darken-1"
text
@click="submitFeedback()"
>
Submit
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-dialog v-model="route.modal" width="700">
<v-card color="card" elevation="7">
<v-toolbar color="toolbar">
<v-toolbar-title> Go to Route </v-toolbar-title>
</v-toolbar>
<v-card-text>
<v-container>
<v-text-field
class="rounded-xl"
v-model="route.value"
autofocus
@keyup.enter="goToRoute()"
label="Route"
required
></v-text-field>
</v-container>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn class="rounded-xl" text @click="route.modal = false">
Close
</v-btn>
<v-btn
class="rounded-xl"
color="blue darken-1"
text
@click="goToRoute()"
>
Go
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-app-bar app color="dark" elevation="5" style="z-index: 15"> <v-app-bar app color="dark" elevation="5" style="z-index: 15">
<v-app-bar-nav-icon <v-app-bar-nav-icon
@click.stop="$store.state.drawer = !$store.state.drawer" @click.stop="$store.state.drawer = !$store.state.drawer"
@ -397,6 +530,20 @@
> >
Debug Debug
</button> </button>
<button
style="display: none"
v-shortkey="['ctrl', 'b']"
@shortkey="route.modal = true"
>
Debug
</button>
<button
style="display: none"
v-shortkey="['ctrl', '/']"
@shortkey="shortcuts = true"
>
Debug
</button>
<template v-if="$route.name === 'Communications'"> <template v-if="$route.name === 'Communications'">
<v-toolbar-title v-if="$store.state.selectedChat?.chat?.type"> <v-toolbar-title v-if="$store.state.selectedChat?.chat?.type">
{{ {{
@ -406,28 +553,66 @@
}} }}
</v-toolbar-title> </v-toolbar-title>
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-btn <v-tooltip bottom>
icon <template v-slot:activator="{ on }">
v-model="$store.state.context.pins.value" <v-btn v-on="on" icon @click="feedback.modal = true">
@click="show($event, 'pins', null, null, true)" <v-icon>mdi-bug</v-icon>
id="pin-button" </v-btn>
> </template>
<v-icon>mdi-pin-outline</v-icon> <span>Provide Feedback</span>
</v-btn> </v-tooltip>
<v-btn <v-tooltip bottom>
icon <template v-slot:activator="{ on }">
@click="$store.state.searchPanel = !$store.state.searchPanel" <v-btn
> icon
<v-icon>mdi-magnify</v-icon> v-model="$store.state.context.pins.value"
</v-btn> @click="show($event, 'pins', null, null, true)"
<v-btn icon @click="$store.state.userPanel = !$store.state.userPanel"> id="pin-button"
<v-icon>mdi-account-group-outline</v-icon> v-on="on"
</v-btn> >
<v-icon>mdi-pin-outline</v-icon>
</v-btn>
</template>
<span>Chat Pins</span>
</v-tooltip>
<v-tooltip bottom>
<template v-slot:activator="{ on }">
<v-btn
icon
@click="$store.state.searchPanel = !$store.state.searchPanel"
v-on="on"
>
<v-icon>mdi-magnify</v-icon>
</v-btn>
</template>
<span>Search Messages</span>
</v-tooltip>
<v-tooltip bottom>
<template v-slot:activator="{ on }">
<v-btn
v-on="on"
icon
@click="$store.state.userPanel = !$store.state.userPanel"
>
<v-icon>mdi-account-group-outline</v-icon>
</v-btn>
</template>
<span>Toggle member list</span>
</v-tooltip>
</template> </template>
<template v-else> <template v-else>
<v-toolbar-title> <v-toolbar-title>
{{ $route.name }} {{ $route.name }}
</v-toolbar-title> </v-toolbar-title>
<v-spacer></v-spacer>
<v-tooltip bottom>
<template v-slot:activator="{ on }">
<v-btn v-on="on" icon @click="feedback.modal = true">
<v-icon>mdi-bug</v-icon>
</v-btn>
</template>
<span>Provide Feedback</span>
</v-tooltip>
</template> </template>
</v-app-bar> </v-app-bar>
<v-navigation-drawer <v-navigation-drawer
@ -682,6 +867,17 @@ export default {
components: { NicknameDialog }, components: { NicknameDialog },
data() { data() {
return { return {
shortcuts: false,
route: {
modal: false,
value: ""
},
feedback: {
modal: false,
route: "",
rating: 0,
text: ""
},
search: "", search: "",
nickname: { nickname: {
dialog: false, dialog: false,
@ -757,6 +953,30 @@ export default {
} }
}, },
methods: { methods: {
goToRoute() {
this.$router.push(this.route.value)
this.route.modal = false
this.route.value = ""
},
submitFeedback() {
this.axios
.post("/api/v1/feedback", {
text: this.feedback.text,
starRating: this.feedback.rating,
route: this.feedback.route
})
.then(() => {
this.feedback.text = ""
this.feedback.rating = 0
this.feedback.modal = false
this.$toast.success("Thank you for making a better Colubrina.")
})
.catch(() => {
this.$toast.error(
"Something went wrong while submitting feedback, you should submit feedback about this."
)
})
},
setNotifications(value) { setNotifications(value) {
this.axios this.axios
.put("/api/v1/communications/settings/" + this.context.user.raw.id, { .put("/api/v1/communications/settings/" + this.context.user.raw.id, {
@ -1049,6 +1269,7 @@ export default {
} }
}, },
mounted() { mounted() {
this.feedback.route = this.$route.path
Vue.axios.defaults.headers.common["Authorization"] = Vue.axios.defaults.headers.common["Authorization"] =
localStorage.getItem("token") localStorage.getItem("token")
this.searchUsers() this.searchUsers()
@ -1108,6 +1329,11 @@ export default {
}) })
} }
}) })
},
watch: {
$route(to) {
this.feedback.route = to.path
}
} }
} }
</script> </script>

View file

@ -139,6 +139,14 @@ const routes = [
import( import(
/* webpackChunkName: "adminLogs" */ "../views/Admin/AdminLogs.vue" /* webpackChunkName: "adminLogs" */ "../views/Admin/AdminLogs.vue"
) )
},
{
path: "feedback",
name: "Feedback",
component: () =>
import(
/* webpackChunkName: "adminFeedback" */ "../views/Admin/AdminFeedback.vue"
)
} }
], ],
component: () => component: () =>

View file

@ -17,6 +17,10 @@
<v-icon>mdi-account-multiple</v-icon>&nbsp; <v-icon>mdi-account-multiple</v-icon>&nbsp;
<span>Users</span> <span>Users</span>
</v-tab> </v-tab>
<v-tab to="/admin/feedback">
<v-icon>mdi-bug</v-icon>&nbsp;
<span>Feedback</span>
</v-tab>
<v-tab to="/admin/themes"> <v-tab to="/admin/themes">
<v-icon>mdi-brush</v-icon>&nbsp; <v-icon>mdi-brush</v-icon>&nbsp;
<span>Themes</span> <span>Themes</span>

View file

@ -0,0 +1,61 @@
<template>
<div id="admin-feedback">
<v-toolbar color="toolbar">
<v-toolbar-title>Feedback ({{ feedback.count }})</v-toolbar-title>
<v-spacer></v-spacer>
<v-btn @click="getFeedback" icon>
<v-icon>mdi-refresh</v-icon>
</v-btn>
</v-toolbar>
<v-container :fluid="$vuetify.breakpoint.lgAndDown">
<v-card
v-for="item in feedback.rows"
:key="item.id"
class="rounded-xl mb-2"
color="card"
>
<v-toolbar color="toolbar">
<v-toolbar-title>
Feedback -
{{ item.user.username }} ({{ item.user.id }})
</v-toolbar-title>
</v-toolbar>
<v-container :fluid="$vuetify.breakpoint.lgAndDown">
{{ item.feedbackText.substring(0, 1000) }}
<br />
<small v-if="item.user">
Route: {{ item.route }}<br />
Rating: {{ item.starRating }}<br />
Created At:
{{ $date(item.createdAt).format("YYYY-MM-DD hh:mm A") }}</small
>
</v-container>
</v-card>
</v-container>
</div>
</template>
<script>
export default {
name: "AdminFeedback",
data() {
return {
feedback: []
}
},
methods: {
getFeedback() {
this.axios
.get(process.env.VUE_APP_BASE_URL + "/api/v1/admin/feedback")
.then((res) => {
this.feedback = res.data
})
}
},
mounted() {
this.getFeedback()
}
}
</script>
<style scoped></style>