Add user profile and search in the frontend, fix PutFollow, DeleteFollow behavior in the backend

This commit is contained in:
Marco Realacci 2022-12-09 22:32:02 +01:00
parent c4611b92f8
commit a2d7eb8d13
9 changed files with 367 additions and 16 deletions

BIN
cmd/webapi/__debug_bin Executable file

Binary file not shown.

Binary file not shown.

View file

@ -72,14 +72,14 @@ func (rt *_router) GetFollowersFollowing(w http.ResponseWriter, r *http.Request,
func (rt *_router) PutFollow(w http.ResponseWriter, r *http.Request, ps httprouter.Params, ctx reqcontext.RequestContext) { func (rt *_router) PutFollow(w http.ResponseWriter, r *http.Request, ps httprouter.Params, ctx reqcontext.RequestContext) {
uid := ps.ByName("user_id") uid := ps.ByName("user_id")
followed := ps.ByName("follower_uid") follower := ps.ByName("follower_uid")
// send error if the user has no permission to perform this action // send error if the user has no permission to perform this action
if !authorization.SendAuthorizationError(ctx.Auth.UserAuthorized, uid, rt.db, w, rt.baseLogger, http.StatusNotFound) { if !authorization.SendAuthorizationError(ctx.Auth.UserAuthorized, follower, rt.db, w, rt.baseLogger, http.StatusNotFound) {
return return
} }
status, err := rt.db.FollowUser(uid, followed) status, err := rt.db.FollowUser(follower, uid)
if err != nil { if err != nil {
helpers.SendInternalError(err, "Database error: FollowUser", w, rt.baseLogger) helpers.SendInternalError(err, "Database error: FollowUser", w, rt.baseLogger)
@ -102,14 +102,14 @@ func (rt *_router) PutFollow(w http.ResponseWriter, r *http.Request, ps httprout
func (rt *_router) DeleteFollow(w http.ResponseWriter, r *http.Request, ps httprouter.Params, ctx reqcontext.RequestContext) { func (rt *_router) DeleteFollow(w http.ResponseWriter, r *http.Request, ps httprouter.Params, ctx reqcontext.RequestContext) {
uid := ps.ByName("user_id") uid := ps.ByName("user_id")
followed := ps.ByName("follower_uid") follower := ps.ByName("follower_uid")
// send error if the user has no permission to perform this action // send error if the user has no permission to perform this action
if !authorization.SendAuthorizationError(ctx.Auth.UserAuthorized, uid, rt.db, w, rt.baseLogger, http.StatusNotFound) { if !authorization.SendAuthorizationError(ctx.Auth.UserAuthorized, follower, rt.db, w, rt.baseLogger, http.StatusNotFound) {
return return
} }
status, err := rt.db.UnfollowUser(uid, followed) status, err := rt.db.UnfollowUser(follower, uid)
if err != nil { if err != nil {
helpers.SendInternalError(err, "Database error: UnfollowUser", w, rt.baseLogger) helpers.SendInternalError(err, "Database error: UnfollowUser", w, rt.baseLogger)

View file

@ -0,0 +1,94 @@
<script>
export default {
props: ["user_id", "name", "followed", "banned", "my_id", "show_new_post"],
data: function() {
return {
errorMsg: "aaa",
user_followed: this.post_followed,
user_banned: this.banned,
myself: this.my_id == this.user_id,
showModal: false,
}
},
methods: {
visit() {
this.$router.push({ path: "/profile/" + this.user_id });
},
follow() {
this.$axios.put("/users/" + this.user_id + "/followers/" + this.my_id)
.then(response => {
this.user_followed = true
this.$emit('updateInfo')
})
.catch(error => alert(error.toString()));
},
unfollow() {
this.$axios.delete("/users/" + this.user_id + "/followers/" + this.my_id)
.then(response => {
this.user_followed = false
this.$emit('updateInfo')
})
.catch(error => alert(error.toString()));
},
ban() {
this.$axios.put("/users/" + this.my_id + "/bans/" + this.user_id)
.then(response => {
this.user_banned = true
this.$emit('updateInfo')
})
.catch(error => alert(error.toString()));
},
unban() {
this.$axios.delete("/users/" + this.my_id + "/bans/" + this.user_id)
.then(response => {
this.user_banned = true
this.$emit('updateInfo')
})
.catch(error => alert(error.toString()));
},
openModal() {
var modal = document.getElementById("exampleModal1");
modal.modal();
},
},
created() {
},
}
</script>
<template>
<div class="card mb-3">
<div class="container">
<div class="row">
<div class="col-10">
<div class="card-body h-100 d-flex align-items-center">
<a @click="visit"><h5 class="card-title mb-0">{{ name }}</h5></a>
</div>
</div>
<div class="col-2">
<div class="card-body d-flex justify-content-end">
<div v-if="!myself" class="d-flex">
<button v-if="!user_banned" @click="ban" type="button" class="btn btn-outline-danger me-2">Ban</button>
<button v-if="user_banned" @click="unban" type="button" class="btn btn-danger me-2">Banned</button>
<button v-if="!user_followed" @click="follow" type="button" class="btn btn-outline-primary">Follow</button>
<button v-if="user_followed" @click="unfollow" type="button" class="btn btn-primary">Following</button>
</div>
<div v-if="(myself && !show_new_post)">
<button disabled type="button" class="btn btn-secondary">Yourself</button>
</div>
<div v-if="(myself && show_new_post)" class="d-flex">
<button type="button" class="btn btn-primary" @click="showModal = true">Post</button>
</div>
</div>
</div>
</div>
</div>
</div>
</template>

View file

@ -5,6 +5,7 @@ import { axios, updateToken as axiosUpdate } from './services/axios.js';
import ErrorMsg from './components/ErrorMsg.vue' import ErrorMsg from './components/ErrorMsg.vue'
import LoadingSpinner from './components/LoadingSpinner.vue' import LoadingSpinner from './components/LoadingSpinner.vue'
import PostCard from './components/PostCard.vue' import PostCard from './components/PostCard.vue'
import UserCard from './components/UserCard.vue'
import 'bootstrap-icons/font/bootstrap-icons.css' import 'bootstrap-icons/font/bootstrap-icons.css'
import './assets/dashboard.css' import './assets/dashboard.css'
@ -16,5 +17,6 @@ app.config.globalProperties.$axiosUpdate = axiosUpdate;
app.component("ErrorMsg", ErrorMsg); app.component("ErrorMsg", ErrorMsg);
app.component("LoadingSpinner", LoadingSpinner); app.component("LoadingSpinner", LoadingSpinner);
app.component("PostCard", PostCard); app.component("PostCard", PostCard);
app.component("UserCard", UserCard);
app.use(router) app.use(router)
app.mount('#app') app.mount('#app')

View file

@ -1,15 +1,18 @@
import {createRouter, createWebHashHistory} from 'vue-router' import {createRouter, createWebHashHistory} from 'vue-router'
import HomeView from '../views/HomeView.vue' import HomeView from '../views/HomeView.vue'
import ProfileView from '../views/ProfileView.vue'
import LoginView from '../views/LoginView.vue' import LoginView from '../views/LoginView.vue'
import SearchView from '../views/SearchView.vue'
const router = createRouter({ const router = createRouter({
history: createWebHashHistory(import.meta.env.BASE_URL), history: createWebHashHistory(import.meta.env.BASE_URL),
routes: [ routes: [
{path: '/', component: HomeView}, {path: '/', component: HomeView},
{path: '/login', component: LoginView}, {path: '/login', component: LoginView},
{path: '/search', component: SearchView},
{path: '/link1', component: HomeView}, {path: '/link1', component: HomeView},
{path: '/link2', component: HomeView}, {path: '/link2', component: HomeView},
{path: '/some/:id/link', component: HomeView}, {path: '/profile/:user_id', component: ProfileView},
] ]
}) })

View file

@ -4,18 +4,28 @@ export default {
return { return {
errormsg: null, errormsg: null,
loading: false, loading: false,
stream_data: null, stream_data: [],
data_ended: false,
start_idx: 0,
limit: 1,
my_id: sessionStorage.getItem("token"), my_id: sessionStorage.getItem("token"),
} }
}, },
methods: { methods: {
async refresh() { async refresh() {
this.limit = Math.round(window.innerHeight / 450);
this.start_idx = 0;
this.data_ended = false;
this.stream_data = [];
this.loadContent();
},
async loadContent() {
this.loading = true; this.loading = true;
this.errormsg = null; this.errormsg = null;
try { try {
let response = await this.$axios.get("/stream"); let response = await this.$axios.get("/stream?start_index=" + this.start_idx + "&limit=" + this.limit);
this.stream_data = response.data; if (response.data.length == 0) this.data_ended = true;
this.errormsg = this.stream_data; // TODO: temporary else this.stream_data = this.stream_data.concat(response.data);
this.loading = false; this.loading = false;
} catch (e) { } catch (e) {
if (e.response.status == 401) { if (e.response.status == 401) {
@ -24,9 +34,23 @@ export default {
this.errormsg = e.toString(); this.errormsg = e.toString();
} }
}, },
scroll () {
window.onscroll = () => {
let bottomOfWindow = Math.max(window.pageYOffset, document.documentElement.scrollTop, document.body.scrollTop) + window.innerHeight === document.documentElement.offsetHeight
if (bottomOfWindow && !this.data_ended) {
this.start_idx += this.limit;
this.loadContent();
}
}
},
}, },
mounted() { mounted() {
this.refresh() // this way we are sure that we fill the first page
// 450 is a bit more of the max height of a post
// todo: may not work in 4k screens :/
this.limit = Math.round(window.innerHeight / 450);
this.scroll();
this.loadContent();
} }
} }
</script> </script>
@ -41,12 +65,9 @@ export default {
<button type="button" class="btn btn-sm btn-outline-secondary" @click="refresh"> <button type="button" class="btn btn-sm btn-outline-secondary" @click="refresh">
Refresh Refresh
</button> </button>
<button type="button" class="btn btn-sm btn-outline-secondary" @click="exportList">
Export
</button>
</div> </div>
<div class="btn-group me-2"> <div class="btn-group me-2">
<button type="button" class="btn btn-sm btn-outline-primary" @click="newItem"> <button type="button" class="btn btn-sm btn-outline-primary" @click="newPost">
New New
</button> </button>
</div> </div>
@ -70,6 +91,10 @@ export default {
:my_id="my_id" /> :my_id="my_id" />
</div> </div>
<div v-if="data_ended" class="alert alert-secondary text-center" role="alert">
Hai visualizzato tutti i post. Hooray! 👻
</div>
<LoadingSpinner :loading="loading" /><br /> <LoadingSpinner :loading="loading" /><br />
</div> </div>
</div> </div>

View file

@ -0,0 +1,129 @@
<script>
export default {
data: function() {
return {
errormsg: null,
loading: false,
stream_data: [],
data_ended: false,
start_idx: 0,
limit: 1,
my_id: sessionStorage.getItem("token"),
user_data: [],
}
},
methods: {
async refresh() {
this.getMainData();
this.limit = Math.round(window.innerHeight / 450);
this.start_idx = 0;
this.data_ended = false;
this.stream_data = [];
this.loadContent();
},
async getMainData() {
try {
let response = await this.$axios.get("/users/" + this.$route.params.user_id);
this.user_data = response.data;
} catch(e) {
this.errormsg = e.toString();
}
},
async loadContent() {
this.loading = true;
this.errormsg = null;
try {
let response = await this.$axios.get("/users/" + this.$route.params.user_id + "/photos" + "?start_index=" + this.start_idx + "&limit=" + this.limit);
if (response.data.length == 0) this.data_ended = true;
else this.stream_data = this.stream_data.concat(response.data);
this.loading = false;
} catch (e) {
if (e.response.status == 401) { // todo: move from here
this.$router.push({ path: "/login" });
}
this.errormsg = e.toString();
}
},
scroll () {
window.onscroll = () => {
let bottomOfWindow = Math.max(window.pageYOffset, document.documentElement.scrollTop, document.body.scrollTop) + window.innerHeight === document.documentElement.offsetHeight
if (bottomOfWindow && !this.data_ended) {
this.start_idx += this.limit;
this.loadContent();
}
}
},
},
mounted() {
// this way we are sure that we fill the first page
// 450 is a bit more of the max height of a post
// todo: may not work in 4k screens :/
this.getMainData();
this.limit = Math.round(window.innerHeight / 450);
this.scroll();
this.loadContent();
}
}
</script>
<template>
<div class="mt-5">
<div class="container">
<div class="row justify-content-md-center">
<div class="col-xl-6 col-lg-9">
<ErrorMsg v-if="errormsg" :msg="errormsg"></ErrorMsg>
<UserCard :user_id = "$route.params.user_id"
:name = "user_data['name']"
:followed = "user_data['followed']"
:banned = "user_data['banned']"
:my_id = "my_id"
:show_new_post = "true"
@updateInfo = "getMainData" />
<div class="row text-center mt-2 mb-3">
<div class="col-4" style="border-right: 1px">
<h3>{{ user_data["photos"] }}</h3>
<h6>Photos</h6>
</div>
<div class="col-4">
<h3>{{ user_data["followers"] }}</h3>
<h6>Followers</h6>
</div>
<div class="col-4">
<h3>{{ user_data["following"] }}</h3>
<h6>Following</h6>
</div>
</div>
<div id="main-content" v-for="item of stream_data">
<PostCard :user_id = "$route.params.user_id"
:photo_id = "item.photo_id"
:name = "user_data['name']"
:date = "item.date"
:comments = "item.comments"
:likes = "item.likes"
:liked = "item.liked"
:my_id = "my_id" />
</div>
<div v-if="data_ended" class="alert alert-secondary text-center" role="alert">
Hai visualizzato tutti i post. Hooray! 👻
</div>
<LoadingSpinner :loading="loading" /><br />
</div>
</div>
</div>
</div>
</template>
<style>
</style>

View file

@ -0,0 +1,98 @@
<script>
export default {
data: function() {
return {
errormsg: null,
loading: false,
stream_data: [],
data_ended: false,
start_idx: 0,
limit: 1,
field_username: "",
my_id: sessionStorage.getItem("token"),
}
},
methods: {
async refresh() {
this.limit = Math.round(window.innerHeight / 72);
this.start_idx = 0;
this.data_ended = false;
this.stream_data = [];
this.loadContent();
},
async loadContent() {
this.loading = true;
this.errormsg = null;
if (this.field_username == "") {
this.errormsg = "Please enter a username";
this.loading = false;
return;
}
try {
let response = await this.$axios.get("/users?query=" + this.field_username + "&start_index=" + this.start_idx + "&limit=" + this.limit);
if (response.data.length == 0) this.data_ended = true;
else this.stream_data = this.stream_data.concat(response.data);
this.errormsg = this.stream_data; // todo: temoprary
this.loading = false;
} catch (e) {
this.errormsg = e.toString();
if (e.response.status == 401) {
this.$router.push({ path: "/login" });
}
}
},
scroll () {
window.onscroll = () => {
let bottomOfWindow = Math.max(window.pageYOffset, document.documentElement.scrollTop, document.body.scrollTop) + window.innerHeight === document.documentElement.offsetHeight
if (bottomOfWindow && !this.data_ended) {
this.start_idx += this.limit;
this.loadContent();
}
}
},
},
mounted() {
// this way we are sure that we fill the first page
// 72 is a bit more of the max height of a card
// todo: may not work in 4k screens :/
this.limit = Math.round(window.innerHeight / 72);
this.scroll();
this.loadContent();
}
}
</script>
<template>
<div>
<div
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2">Search</h1>
</div>
<div class="container">
<div class="row justify-content-md-center">
<div class="col-xl-6 col-lg-9">
<ErrorMsg v-if="errormsg" :msg="errormsg"></ErrorMsg>
<div class="form-floating mb-4">
<input v-model="field_username" @input="refresh" id="formUsername" class="form-control" placeholder="name@example.com"/>
<label class="form-label" for="formUsername">Search by username</label>
</div>
<div id="main-content" v-for="item of stream_data">
<UserCard :user_id="item.user_id" :name="item.name" :my_id="my_id" />
</div>
<LoadingSpinner :loading="loading" /><br />
</div>
</div>
</div>
</div>
</template>
<style>
</style>