Working on Forums

This commit is contained in:
Troplo 2021-01-28 18:35:37 +11:00
parent b4a0a57b72
commit 62a7fcb2dc
6 changed files with 526 additions and 64 deletions

View file

@ -0,0 +1,255 @@
$font--role-default: 'Lato', sans-serif;
$font--role-emphasis: 'Lato', sans-serif;
$color__text--primary: rgba(0, 0, 0, 0.87);
$color__text--secondary: rgba(0, 0, 0, 0.54);
$color__lightgray--primary: #F5F5F5;
$color__lightgray--darker: #EEEEEE;
$color__lightgray--darkest: #E0E0E0;
$color__gray--primary: #e7e2e2;
$color__gray--darker: #E0E0E0;
$color__gray--darkest: #BDBDBD;
$color__darkgray--primary: #757575;
$color__darkgray--darker: #525252;
$color__darkgray--darkest: #424242;
$color__orange--primary: #F57C00;
$color__orange--darker: #EF6C00;
$color__orange--darkest: #de621c;
$color__green--primary: rgba(76, 175, 80, 0.86);
$color__green--darker: #349238;
$color__green--darkest: #1B5E20;
$color__blue--primary: #0a8bff;
$color__blue--darker: #0079E5;
$color__blue--darkest: #0D47A1;
$color__red--primary: #e74860;
$color__red--darker: #B71C1C;
// Kaverti Extras
$primary: #24a2dc;
$booster: #fa6ef6;
$booster-alt: #e655d4;
//Breakpoints
$breakpoint--large_screen: 1200px;
$breakpoint--tablet: 870px;
$breakpoint--phone: 550px;
//Breakpoints
$breakpoint--large_screen-thread: 1150px;
$breakpoint--tablet-thread: 850px;
$breakpoint--phone-thread: 500px;
@mixin thread_mobile_breakpoint ($selector) {
@media (max-width: 1150px) and (min-width: $breakpoint--tablet-thread) {
#{selector} {
width: calc(80% - 5rem);
}
}
@media (max-width: $breakpoint--phone-thread) {
#{$selector} {
border-radius: 0;
border-left: 0;
border-right: 0;
}
}
@media (min-width: $breakpoint--tablet-thread) and (max-width: 1150px) {
#{$selector} {
width: calc(80% - 5rem);
}
}
@media (min-width: $breakpoint--phone-thread) and (max-width: $breakpoint--tablet-thread) {
#{$selector} {
margin-left: 2rem;
margin-riɡht: 2rem;
width: calc(100% - 4rem);
}
}
@media (max-width: $breakpoint--phone-thread) {
#{$selector} {
width: 100%;
border-left: 0;
border-right: 0;
border-radius: 0;
}
}
}
@keyframes flash {
0% {
background-color: $color__gray--darker;
}
50% {
background-color: $color__lightgray--darkest;
}
75% {
background-color: $color__gray--primary;
}
to {
background-color: $color__gray--darker;
}
}
@mixin flash {
animation-name: flash;
animation-duration: 1s;
animation-iteration-count: infinite;
animation-timing-function: linear;
}
@mixin loading-overlay($background-color: #fff, $border-radius: 0.25rem) {
width: 100%;
height: 100%;
position: absolute;
background-color: $background-color;
z-index: 1;
top: 0;
position: absolute;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
pointer-events: none;
transition: all 0.2s;
@include user-select(none);
cursor: default;
border-radius: $border-radius;
@at-root #{&}--show {
opacity: 1;
pointer-events: all;
}
@at-root #{&}__message {
font-size: 1.5rem;
color: #fff;
font-style: italic;
}
}
@mixin text($family: $font--role-default, $size: 1rem, $weight: 300) {
font-family: $family;
font-size: $size;
font-weight: $weight;
}
@mixin optional-at-root($sel) {
@at-root #{if(not &, $sel, selector-append(&, $sel))} {
@content;
}
}
@mixin placeholder {
@include optional-at-root('::-webkit-input-placeholder') {
@content;
}
@include optional-at-root(':-moz-placeholder') {
@content;
}
@include optional-at-root('::-moz-placeholder') {
@content;
}
@include optional-at-root(':-ms-input-placeholder') {
@content;
}
}
@mixin user-select($select) {
@each $pre in -webkit-, -moz-, -ms-, -o- {
#{$pre + user-select}: #{$select};
}
#{user-select}: #{$select};
}
.shadow_border {
box-shadow: 0 0 0.3rem rgba(175, 175, 175, 0.25);
}
.shadow_border--hover {
box-shadow: 0 0 0.3rem rgba(175, 175, 175, 0.25), 0 0.2rem 0.35rem rgba(175, 175, 175, 0.25);
}
.tab_button {
padding: 0.5rem 0.75rem;
border-radius: 3rem;
cursor: pointer;
transition: all 0.2s;
margin-right: 0.5rem;
display: inline-block;
position: relative;
top: -0.1rem;
@include user-select(none);
&:hover {
background-color: $color__lightgray--darker;
}
&:active {
background-color: #dcdcdc;
}
&::after {
content: '';
position: absolute;
background-color: $color__blue--primary;
width: calc(100% - 1rem);
left: 0.5rem;
bottom: -0.3rem;
height: 0.2rem;
opacity: 0;
transition: opacity 0.2s;
}
@at-root #{&}--selected {
cursor: default;
font-weight: bold;
&:active, &:hover {
background-color: transparent;
}
&::after {
opacity: 1;
}
}
}
//Colours
$gray-hover: #F5F5F5; //on hover for background-color from white
$gray-0: #EEEEEE; //off-white
$gray-1: #E0E0E0; //lightest
$gray-2: #BDBDBD; //lighter
$gray-3: #9E9E9E; //default
$gray-4: #757575; //darker
$gray-5: #424242; //darkest
$blue-1: #64B5F6;
$blue-2: #2196F3;
$blue-3: #1E88E5;
$blue-4: #1976D2;
$blue-5: #115cd0;
$red-0: #EF5350;
$red-1: #F44336;
$red-2: #E53935;
$red-3: #D32F2F;
$red-4: #C62828;
$red-5: #B71C1C;
//Text colours
$text-primary: rgba(black, 0.87);
$text-secondary: rgba(black, 0.54);
//Font
$font-family: 'Lato', sans-serif;

View file

@ -72,6 +72,16 @@ const routes = [
name: 'Forums', name: 'Forums',
component: route('Forums') component: route('Forums')
}, },
{
path: '/forums/thread/:id',
name: 'ForumThread',
component: route('ForumThread')
},
{
path: '/forums/create',
name: 'ForumThreadCreate',
component: route('ForumThreadCreate')
},
{ {
path: '/roadmap', path: '/roadmap',
name: 'Roadmap', name: 'Roadmap',

55
src/views/ForumThread.vue Normal file
View file

@ -0,0 +1,55 @@
<template>
<main>
<div class="section">
<h1 class="title">{{thread.name}}</h1>
<div class="column is-9" v-for='(post) in thread.Posts' :key='"threadPost-" + post.id'>
<div class="media box">
<div class="media-left">
<img :src="'https://cdn.kaverti.com/user/avatars/headshot/' + post.User.picture + '.png'">
</div>
<div class="media-content">
<div v-html="post.content"></div>
</div>
</div>
</div>
</div>
</main>
</template>
<script>
import AjaxErrorHandler from "../../assets/js/errorHandler";
export default {
name: "ForumThread",
data() {
return {
thread: [],
offset: 0,
loading: true
}
},
methods: {
getThread(initial) {
this.loading = true
if(initial) {
this.offset = 0
this.thread = []
}
this.axios
.get(process.env.VUE_APP_APIENDPOINT + process.env.VUE_APP_APIVERSION + '/' + 'forums/thread/' + this.$route.params.id)
.then((res) => {
if(initial) {
this.thread = res.data
} else {
this.thread.Posts.push(...res.data.Posts)
}
})
.catch((e) => {
AjaxErrorHandler(this.$store)(e)
})
}
},
mounted () {
this.getThread(true)
}
}
</script>

View file

@ -0,0 +1,15 @@
<template>
<main>
</main>
</template>
<script>
export default {
name: "ForumThreadCreate"
}
</script>
<style scoped>
</style>

View file

@ -1,8 +1,8 @@
<template> <template>
<main class="section" v-if="$store.state.enableBrokenRoutes"> <main class="section">
<div class="columns is-multiline has-text-centered"> <div class="columns is-multiline has-text-centered">
<div class="column is-2" v-if="!loadingCategory && categories.length"> <div class="column is-2" v-if="!loadingCategory && categories.length">
<b-button class="is-info">Create thread</b-button> <b-button @click="$router.push('/forums/create')" class="is-info">Create thread</b-button>
<br><br> <br><br>
<div class="box"> <div class="box">
<router-link <router-link
@ -35,33 +35,51 @@
<div class="column box" v-if="!threads.length && !loadingThreads"> <div class="column box" v-if="!threads.length && !loadingThreads">
<NoItems type="forum threads"></NoItems> <NoItems type="forum threads"></NoItems>
</div> </div>
<div class="column column is-9"> <div class="column column is-9" v-if="threads.length">
<div class="box content"> <div class="box content">
<article class="media" v-for='(thread) in threads' :key='"thread-" + thread.id'> <article class='thread_display box' v-for='(thread) in threads' :key='"thread-" + thread.id' @click="goToThread(thread)">
<figure class="media-left"> <div style='width: calc(100% - 3rem);'>
<p class="image is-64x64"> <div class='thread_display__header'>
<img :src="'https://cdn.kaverti.com/user/avatars/headshot/' + thread.User.picture + '.png'"> <span class='thread_display__name'>
</p> {{thread.name}}
By </span>
{{thread.User.username}} <div class='thread_display__meta_bar'>
</figure> <div>
<div class="media-content"> By
<div class="content"> <span class='thread_display__username' ref='username'>{{thread.User.username}}</span>
<p> in
<strong>{{thread.name}}</strong> <span class='thread_display__category' ref='category'>{{thread.Category.name}}</span>
<br> &middot;
{{thread.Posts[0].plainText}} <span class='thread_display__date'>{{thread.createdAt | formatDate}}</span>
</p> </div>
</div>
</div> </div>
<div class="media-left"> <div class='thread_display__replies_bar'>
Created At: {{thread.createdAt | formatDate()}} <div
class='thread_display__latest_reply'
v-if='thread.Posts.length === 2'
>
<font-awesome-icon :icon='["fa", "reply"]' fixed-width />
<span class='thread_display__latest_reply__text'>Latest reply by &nbsp;</span>
<span class='thread_display__username'>{{replyUsername}}</span>
&middot;
<span class='thread_display__date'>{{thread.Posts[1].createdAt | formatDate}}</span>
</div>
<span title='Replies to thread' v-if="thread.Posts[0]">
Replies: {{thread.postsCount - 1}}
</span>
<span title='Replies to thread' v-else>
Replies: 0
</span>
</div>
<div class='thread_display__content'>
{{thread.Posts[0].content}}
</div> </div>
</div> </div>
</article> </article>
</div> </div>
</div> </div>
</div> </div>
<div class="section" v-if="!$store.state.enableBrokenRoutes">This route has been disabled, enable Broken Routes in Developer Options</div>
</main> </main>
</template> </template>
<script> <script>
@ -83,18 +101,16 @@ export default {
} }
}, },
methods: { methods: {
goToThread(thread) {
this.$router.push('/forums/thread/' + thread.id)
},
getThreads (initial) { getThreads (initial) {
if(this.nextURL === null && !initial) return if(this.nextURL === null && !initial) return
let URL = process.env.VUE_APP_APIENDPOINT + process.env.VUE_APP_APIVERSION + '/' + 'forums/category/' + this.selectedCategory
if(!initial) {
URL = this.nextURL || URL
}
this.loadingThreads = true this.loadingThreads = true
this.axios this.axios
.get(URL) .get(process.env.VUE_APP_APIENDPOINT + process.env.VUE_APP_APIVERSION + '/' + 'forums/category/' + this.selectedCategory)
.then(res => { .then(res => {
this.loadingThreads = false this.loadingThreads = false
@ -145,4 +161,99 @@ export default {
this.getThreads(true) this.getThreads(true)
} }
} }
</script> </script>
<style lang='scss' scoped>
@import '../assets/scss/variables';
.thread_display {
cursor: pointer;
display: flex;
margin-bottom: 1rem;
padding: 0.75rem;
position: relative;
transition: background-color 0.2s, box-shadow 0.2s;
&:hover {
@extend .shadow_border--hover;
}
@at-root #{&}__icon {
margin-right: 0.5rem;
}
@at-root #{&}__username,
#{&}__category,
#{&}__date {
color: $color--text__primary;
}
@at-root #{&}__header {
display: flex;
justify-content: space-between;
}
@at-root #{&}__name {
font-weight: 500;
font-size: 1.25rem;
overflow: hidden;
text-overflow: ellipsis;
line-height: 1.5rem;
height: 1.5rem;
}
@at-root #{&}__meta_bar {
color: $color--gray__darkest;
flex-shrink: 0;
line-height: 1.5rem;
height: 1.5rem;
}
@at-root #{&}__replies_bar {
display: flex;
justify-content: space-between;
}
@at-root #{&}__latest_reply {
color: $color--text__secondary;
.fa {
color: $color--text__primary;
font-size: 0.75rem;
}
}
@at-root #{&}__replies {
width: 4rem;
text-align: right;
}
@at-root #{&}__content {
margin-top: 0.5rem;
word-break: break-all;
}
}
@media (max-width: 420px) {
.thread_display {
@at-root #{&}__header {
flex-direction: column;
}
@at-root #{&}__meta_bar {
font-size: 0.9rem;
margin-bottom: 0.25rem;
}
@at-root #{&}__replies_bar {
position: relative;
left: -3.25rem;
width: calc(100% + 3.25rem);
}
@at-root #{&}__latest_reply {
.fa {
margin-right: 0.25rem;
}
@at-root #{&}__text {
display: none;
}
}
}
}
</style>

View file

@ -1,12 +1,12 @@
<template> <template>
<main class="section"> <main class="section">
<div class="columns is-centered" v-if="$store.state.user.username"> <div class="columns is-centered" v-if="$store.state.user.username">
<div class="column is-4 is-vcentered has-text-centered"> <div class="column is-4 has-text-centered">
<h1 class="title has-text-centered">{{$store.state.user.username}}</h1> <h1 class="title has-text-centered">{{$store.state.user.username}}</h1>
<div class="box"> <div class="box">
<img :src="'https://cdn.kaverti.com/user/avatars/full/' + $store.state.user.avatar + '.png'" :alt="$store.state.user.username + '\'s avatar'" width="50%"> <img :src="'https://cdn.kaverti.com/user/avatars/full/' + $store.state.user.avatar + '.png'" :alt="$store.state.user.username + '\'s avatar'" width="50%">
</div> </div>
<h1 class="title">Recent Blog Posts</h1> <h1 class="title">{{$t('home.news')}}</h1>
<div v-if="blogs.length"> <div v-if="blogs.length">
<div class="box" v-for='(blog) in blogs' :key='"blog-" + blog.id'> <div class="box" v-for='(blog) in blogs' :key='"blog-" + blog.id'>
<h2 class="subtitle">{{blog.name}}</h2> <h2 class="subtitle">{{blog.name}}</h2>
@ -17,44 +17,40 @@
<NoItems type="blog posts"></NoItems> <NoItems type="blog posts"></NoItems>
</div> </div>
</div> </div>
<div v-if="wallPosts.length"> <div class="column has-text-centered is-vcentered" v-if="!loading">
<h1 class="title has-text-centered">{{$t('home.globalWall')}}</h1> <h1 class="title">{{$t('home.globalWall')}}</h1>
<div class="column" v-for='(post) in wallPosts' :key='"globalPost-" + post.id'> <div class="">
<div class="card" v-if="wallPosts.length"> <b-input placeholder="What's up?" v-model="wallText" maxlength="256" type="textarea"></b-input>
<div class="card-content"> <b-tag>
<article class="media"> Markdown is no longer available on wall posts.
<figure class="media-left"> </b-tag>
<p class="image is-64x64"> <b-button :loading="loadingWallButton" @click="postWall()" class="is-info" style="float: left">Post</b-button>
<img :src="'https://cdn.kaverti.com/user/avatars/headshot/' + post.fromUser.picture + '.png'"> <br><hr>
<div class="box" v-for='(post) in wallPosts' :key='"wallPosts-" + post.id'>
<article class="media">
<figure class="media-left">
<p class="image is-64x64">
<img :src="'https://cdn.kaverti.com/user/avatars/headshot/' + post.fromUser.picture + '.png'">
</p>
</figure>
<div class="media-content">
<div class="content">
<p>
<strong>{{post.fromUser.username}}</strong> <small>{{ post.createdAt | formatDate() }}</small>
<br>
{{post.plainText}}
</p> </p>
By
{{post.fromUser.username}}
</figure>
<div class="media-content">
<div class="content">
<p>
<strong>{{post.name}}</strong>
<br>
<div
v-html='post.content'
></div>
</div>
<div class="media-left">
Created At: {{post.createdAt | formatDate()}}
</div>
</div> </div>
</article> </div>
</div> <div class="media-right">
<b-tooltip label="Delete">
<button class="delete"></button>
</b-tooltip>
</div>
</article>
</div> </div>
</div> </div>
</div> </div>
<div class="column" v-if="!wallPosts.length">
<h1 class="title has-text-centered">{{$t('home.globalWall')}}</h1>
<div class="box" v-if="!wallPosts.length">
<NoItems type="wall posts">
</NoItems>
</div>
</div>
</div> </div>
<div v-if="!$store.state.user.username"> <div v-if="!$store.state.user.username">
This route requires authentication This route requires authentication
@ -75,11 +71,30 @@ export default {
return { return {
blogs: [], blogs: [],
wallOffset: 0, wallOffset: 0,
wallPosts: [] wallPosts: [],
wallText: '',
mentions: '',
loadingWallButton: false,
loadingWall: true
} }
}, },
methods: { methods: {
postWall() {
this.loadingWallButton = true
this.axios.post(process.env.VUE_APP_APIENDPOINT + process.env.VUE_APP_APIVERSION + `/` + `wall/post`, {
username: "GlobalWall",
content: this.wallText
}).then(() => {
this.loadingWallButton = false
this.getWall(true)
}).catch(e => {
this.loadingWallButton = false
AjaxErrorHandler(this.$store)(e)
})
},
getWall(initial) { getWall(initial) {
this.loadingWall = true
if(initial) { if(initial) {
this.wallOffset = 0 this.wallOffset = 0
} }
@ -87,6 +102,7 @@ export default {
.get(process.env.VUE_APP_APIENDPOINT + process.env.VUE_APP_APIVERSION + `/` + `user/GlobalWall?wall=true&offset=` + this.wallOffset) .get(process.env.VUE_APP_APIENDPOINT + process.env.VUE_APP_APIVERSION + `/` + `user/GlobalWall?wall=true&offset=` + this.wallOffset)
.then(res => { .then(res => {
this.loadingPosts = false this.loadingPosts = false
this.loadingWall = false
if(initial) { if(initial) {
this.wallPosts = res.data.userWalls this.wallPosts = res.data.userWalls
} else { } else {