mirror of
https://github.com/notherealmarco/WASAPhoto.git
synced 2025-03-14 06:06:15 +01:00
Add user profile and search in the frontend, fix PutFollow, DeleteFollow behavior in the backend
This commit is contained in:
parent
c4611b92f8
commit
a2d7eb8d13
9 changed files with 367 additions and 16 deletions
BIN
cmd/webapi/__debug_bin
Executable file
BIN
cmd/webapi/__debug_bin
Executable file
Binary file not shown.
Binary file not shown.
|
@ -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) {
|
||||
|
||||
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
|
||||
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
|
||||
}
|
||||
|
||||
status, err := rt.db.FollowUser(uid, followed)
|
||||
status, err := rt.db.FollowUser(follower, uid)
|
||||
|
||||
if err != nil {
|
||||
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) {
|
||||
|
||||
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
|
||||
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
|
||||
}
|
||||
|
||||
status, err := rt.db.UnfollowUser(uid, followed)
|
||||
status, err := rt.db.UnfollowUser(follower, uid)
|
||||
|
||||
if err != nil {
|
||||
helpers.SendInternalError(err, "Database error: UnfollowUser", w, rt.baseLogger)
|
||||
|
|
94
webui/src/components/UserCard.vue
Normal file
94
webui/src/components/UserCard.vue
Normal 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>
|
|
@ -5,6 +5,7 @@ import { axios, updateToken as axiosUpdate } from './services/axios.js';
|
|||
import ErrorMsg from './components/ErrorMsg.vue'
|
||||
import LoadingSpinner from './components/LoadingSpinner.vue'
|
||||
import PostCard from './components/PostCard.vue'
|
||||
import UserCard from './components/UserCard.vue'
|
||||
import 'bootstrap-icons/font/bootstrap-icons.css'
|
||||
|
||||
import './assets/dashboard.css'
|
||||
|
@ -16,5 +17,6 @@ app.config.globalProperties.$axiosUpdate = axiosUpdate;
|
|||
app.component("ErrorMsg", ErrorMsg);
|
||||
app.component("LoadingSpinner", LoadingSpinner);
|
||||
app.component("PostCard", PostCard);
|
||||
app.component("UserCard", UserCard);
|
||||
app.use(router)
|
||||
app.mount('#app')
|
|
@ -1,15 +1,18 @@
|
|||
import {createRouter, createWebHashHistory} from 'vue-router'
|
||||
import HomeView from '../views/HomeView.vue'
|
||||
import ProfileView from '../views/ProfileView.vue'
|
||||
import LoginView from '../views/LoginView.vue'
|
||||
import SearchView from '../views/SearchView.vue'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(import.meta.env.BASE_URL),
|
||||
routes: [
|
||||
{path: '/', component: HomeView},
|
||||
{path: '/login', component: LoginView},
|
||||
{path: '/search', component: SearchView},
|
||||
{path: '/link1', component: HomeView},
|
||||
{path: '/link2', component: HomeView},
|
||||
{path: '/some/:id/link', component: HomeView},
|
||||
{path: '/profile/:user_id', component: ProfileView},
|
||||
]
|
||||
})
|
||||
|
||||
|
|
|
@ -4,18 +4,28 @@ export default {
|
|||
return {
|
||||
errormsg: null,
|
||||
loading: false,
|
||||
stream_data: null,
|
||||
stream_data: [],
|
||||
data_ended: false,
|
||||
start_idx: 0,
|
||||
limit: 1,
|
||||
my_id: sessionStorage.getItem("token"),
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
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.errormsg = null;
|
||||
try {
|
||||
let response = await this.$axios.get("/stream");
|
||||
this.stream_data = response.data;
|
||||
this.errormsg = this.stream_data; // TODO: temporary
|
||||
let response = await this.$axios.get("/stream?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) {
|
||||
|
@ -24,9 +34,23 @@ export default {
|
|||
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.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>
|
||||
|
@ -41,12 +65,9 @@ export default {
|
|||
<button type="button" class="btn btn-sm btn-outline-secondary" @click="refresh">
|
||||
Refresh
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" @click="exportList">
|
||||
Export
|
||||
</button>
|
||||
</div>
|
||||
<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
|
||||
</button>
|
||||
</div>
|
||||
|
@ -70,6 +91,10 @@ export default {
|
|||
: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>
|
||||
|
|
129
webui/src/views/ProfileView.vue
Normal file
129
webui/src/views/ProfileView.vue
Normal 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>
|
98
webui/src/views/SearchView.vue
Normal file
98
webui/src/views/SearchView.vue
Normal 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>
|
Loading…
Reference in a new issue