Skip to content

Commit

Permalink
Finish admin tweets page
Browse files Browse the repository at this point in the history
  • Loading branch information
smallpaes committed Sep 26, 2019
1 parent 5c8faa5 commit c9df680
Show file tree
Hide file tree
Showing 7 changed files with 255 additions and 2 deletions.
5 changes: 5 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"bootstrap": "^4.3.1",
"core-js": "^2.6.5",
"jquery": "^3.4.1",
"moment": "^2.24.0",
"popper.js": "^1.15.0",
"sweetalert2": "^8.17.6",
"vue": "^2.6.10",
Expand Down
6 changes: 6 additions & 0 deletions src/apis/admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,11 @@ import { apiHelper } from "../utils/helpers"
export default {
getUsers() {
return apiHelper.get('/admin/users')
},
getTweets() {
return apiHelper.get('/admin/tweets')
},
deleteTweet({ tweetId }) {
return apiHelper.delete(`/admin/tweets/${tweetId}`)
}
}
153 changes: 153 additions & 0 deletions src/components/AdminTweetCard.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
<template>
<div class="card">
<div class="card-header bg-white" id="headingOne">
<div class="media">
<img :src="tweet.User.avatar | placeholderImage" class="mr-3 rounded-circle" alt="avatar" />
<div class="media-body">
<h6 class="mt-0 font-weight-bold d-inline-block">
<router-link
:to="{name: 'user-tweets', params: {id: tweet.User.id}}"
>{{tweet.User.name}}</router-link>
</h6>
<small class="text-muted ml-2">&sdot; {{tweet.createdAt | fromNow}}</small>
<button
@click.stop="deleteTweet(tweet.id)"
:disabled="isProcessing"
type="button"
class="close"
aria-label="Close"
>
<span aria-hidden="true">&times;</span>
</button>
<p>{{tweet.description}}</p>
<a href class="action-btn">
<i class="far fa-heart mr-1"></i>
{{tweet.LikesCount}}
</a>
<button
class="btn btn-link action-btn"
type="button"
data-toggle="collapse"
:data-target="'#index' + tweet.id"
aria-expanded="false"
:aria-controls="'index' + tweet.id"
>
<i class="far fa-comment mr-1"></i>
{{tweet.RepliesCount}}
</button>
</div>
</div>
</div>

<div
:id="'index' + tweet.id"
class="collapse"
aria-labelledby="headingOne"
data-parent="#tweets"
>
<div v-for="reply in tweet.Replies" :key="reply.id" class="card-body reply-box">
<div class="media">
<img :src="reply.User.avatar | placeholderImage" class="mr-3 rounded-circle" alt="avatar" />
<div class="media-body">
<h6 class="mt-0 font-weight-bold d-inline-block">
<router-link
:to="{name: 'user-tweets', params: {id: reply.User.id}}"
>{{reply.User.name}}</router-link>
</h6>
<small class="text-muted ml-2">&sdot; {{reply.createdAt | fromNow}}</small>
<p>{{reply.comment}}</p>
</div>
</div>
</div>
</div>
</div>
</template>

<script>
import { fromNowFilter, placeholderImageCreator } from "../utils/mixins";
import adminAPI from "../apis/admin";
import { Toast } from "../utils/helpers";
export default {
props: {
tweet: {
type: Object,
required: true
}
},
data() {
return {
isProcessing: false
};
},
mixins: [fromNowFilter, placeholderImageCreator],
methods: {
async deleteTweet(tweetId) {
try {
// update process status
this.isProcessing = true;
const { data, statusText } = await adminAPI.deleteTweet({ tweetId });
// error handling
if (statusText !== "Accepted" || data.status !== "success") {
throw new Error(statusText);
}
// notify parent component of the action
this.$emit("after-delete", tweetId);
// update process status
this.isProcessing = false;
} catch (error) {
// update process status
this.isProcessing = false;
Toast.fire({
type: "error",
title: "Cannot delete the tweet, please try again later"
});
}
}
}
};
</script>

<style scoped>
img {
width: 50px;
}
.media-body p {
font-size: 0.9rem;
line-height: 20px;
}
.media-body a,
.media-body button {
color: #4c4c4c;
}
.action-btn {
color: #82878b;
}
.media-body a[data-v-7f2df0f2]:hover,
.media-body a:hover,
.media-body button:hover {
color: #1da1f2;
text-decoration: none;
}
.media-body button:focus {
text-decoration: none;
}
.close {
font-weight: 100;
}
.reply-box {
background: rgba(0, 0, 0, 0.02);
border-bottom: 1px solid rgba(0, 0, 0, 0.125);
}
.reply-box:last-child {
border-bottom: none;
}
</style>
6 changes: 6 additions & 0 deletions src/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,12 @@ const router = new Router({
component: () => import('./views/AdminUsers.vue'),
beforeEnter: authorizeIsAdmin
},
{
path: '/admin/tweets',
name: 'admin-tweets',
component: () => import('./views/AdminTweets.vue'),
beforeEnter: authorizeIsAdmin
},
{
path: '*',
name: 'not-found',
Expand Down
17 changes: 15 additions & 2 deletions src/utils/mixins.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,21 @@
export const placeholderImageCreator = {
import moment from "moment"

const placeholderImageCreator = {
filters: {
placeholderImage(src) {
if (src) return src;
return "https://via.placeholder.com/500x500/d3d3d3?text=No+Image";
}
}
}
}

const fromNowFilter = {
filters: {
fromNow(date) {
if (!date) return;
return moment(date).fromNow(true);
}
}
}

export { placeholderImageCreator, fromNowFilter }
69 changes: 69 additions & 0 deletions src/views/AdminTweets.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<template>
<Spinner v-if="isLoading" />
<section v-else class="container">
<div class="row">
<div class="col-12 col-md-10 offset-md-1 col-lg-8 offset-lg-2">
<h4 class="title">User Admin</h4>
<div class="accordion my-3" id="tweets">
<AdminTweetCard
v-for="tweet in tweets"
:key="tweet.id"
:tweet="tweet"
@after-delete="handleAfterDelete"
/>
</div>
</div>
</div>
</section>
</template>s

<script>
import AdminTweetCard from "../components/AdminTweetCard";
import { fromNowFilter, placeholderImageCreator } from "../utils/mixins";
import adminAPI from "../apis/admin";
import Spinner from "../components/Spinner";
import { Toast } from "../utils/helpers";
export default {
data() {
return {
tweets: [],
isLoading: true
};
},
components: {
Spinner,
AdminTweetCard
},
mixins: [fromNowFilter, placeholderImageCreator],
created() {
this.fetchTweets();
},
methods: {
async fetchTweets() {
try {
const { data, statusText } = await adminAPI.getTweets();
// error handling
if (statusText !== "OK" || data.status !== "success") {
throw new Error(statusText);
}
// update data
this.tweets = data.tweets;
// update loading status
this.isLoading = false;
} catch (error) {
// update loading status
this.isLoading = false;
Toast.fire({
type: "error",
title: "Cannot get tweet data, please try again later"
});
}
},
handleAfterDelete(tweetId) {
// update the data
this.tweets = this.tweets.filter(tweet => tweet.id !== tweetId);
}
}
};
</script>

0 comments on commit c9df680

Please sign in to comment.