Add Carousel and MovieCard components
This commit is contained in:
49
nuxt/components/Carousel.vue
Normal file
49
nuxt/components/Carousel.vue
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<template>
|
||||||
|
<carousel
|
||||||
|
:perPageCustom="[
|
||||||
|
[320, 1],
|
||||||
|
[480, 2],
|
||||||
|
[768, 3]
|
||||||
|
]"
|
||||||
|
:loop="true"
|
||||||
|
:navigationEnabled="true"
|
||||||
|
:paginationEnabled="false"
|
||||||
|
:navigationPrevLabel="arrowLeft"
|
||||||
|
:navigationNextLabel="arrowRight"
|
||||||
|
>
|
||||||
|
<slide v-for="movie in dataSource" :key="movie.imdbID">
|
||||||
|
<MovieCard :loading="loading" :movie="movie" />
|
||||||
|
</slide>
|
||||||
|
</carousel>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { Carousel, Slide } from "vue-carousel";
|
||||||
|
import { mapState } from "vuex";
|
||||||
|
import { Icon } from "ant-design-vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: { Carousel, Slide, Icon },
|
||||||
|
props: {
|
||||||
|
dataSource: {
|
||||||
|
type: Array,
|
||||||
|
default: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState({
|
||||||
|
loading: "loading"
|
||||||
|
})
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
arrowLeft:
|
||||||
|
'<svg viewBox="64 64 896 896" data-icon="arrow-left" width="1em" height="1em" fill="currentColor" aria-hidden="true" focusable="false" class=""><path d="M872 474H286.9l350.2-304c5.6-4.9 2.2-14-5.2-14h-88.5c-3.9 0-7.6 1.4-10.5 3.9L155 487.8a31.96 31.96 0 0 0 0 48.3L535.1 866c1.5 1.3 3.3 2 5.2 2h91.5c7.4 0 10.8-9.2 5.2-14L286.9 550H872c4.4 0 8-3.6 8-8v-60c0-4.4-3.6-8-8-8z"></path></svg>',
|
||||||
|
arrowRight:
|
||||||
|
'<svg viewBox="64 64 896 896" data-icon="arrow-right" width="1em" height="1em" fill="currentColor" aria-hidden="true" focusable="false" class=""><path d="M869 487.8L491.2 159.9c-2.9-2.5-6.6-3.9-10.5-3.9h-88.5c-7.4 0-10.8 9.2-5.2 14l350.2 304H152c-4.4 0-8 3.6-8 8v60c0 4.4 3.6 8 8 8h585.1L386.9 854c-5.6 4.9-2.2 14 5.2 14h91.5c1.9 0 3.8-.7 5.2-2L869 536.2a32.07 32.07 0 0 0 0-48.4z"></path></svg>'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style></style>
|
||||||
62
nuxt/components/MovieCard.vue
Normal file
62
nuxt/components/MovieCard.vue
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
<template>
|
||||||
|
<a-card :loading="loading" :bordered="false">
|
||||||
|
<a-spin class="spinner" v-if="loading" slot="cover" />
|
||||||
|
<img
|
||||||
|
v-else
|
||||||
|
slot="cover"
|
||||||
|
:alt="movie.Title"
|
||||||
|
:src="
|
||||||
|
movie.Poster == 'N/A'
|
||||||
|
? 'https://placeimg.com/200/200/any?1'
|
||||||
|
: movie.Poster
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<a-card-meta :title="movie.Title">
|
||||||
|
<template slot="description">
|
||||||
|
{{ movie.Director }} -
|
||||||
|
<span style="font-size: 12px; font-style: oblique;">{{
|
||||||
|
movie.Year
|
||||||
|
}}</span>
|
||||||
|
</template>
|
||||||
|
</a-card-meta>
|
||||||
|
</a-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
movie: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {}
|
||||||
|
},
|
||||||
|
loading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.spinner {
|
||||||
|
text-align: center;
|
||||||
|
background: rgba(207, 216, 220, 0.4);
|
||||||
|
border-radius: 4px 4px 0 0;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 30px 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-card {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-card-cover img {
|
||||||
|
max-height: 256px;
|
||||||
|
object-fit: scale-down;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-card-cover {
|
||||||
|
background-color: rgb(13, 10, 57);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -42,4 +42,19 @@ h2 {
|
|||||||
height: 48px;
|
height: 48px;
|
||||||
margin: 0 12px 0 0;
|
margin: 0 12px 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 414px) {
|
||||||
|
.NuxtLogo {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
39
nuxt/package-lock.json
generated
39
nuxt/package-lock.json
generated
@@ -5204,6 +5204,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"dom-walk": {
|
||||||
|
"version": "0.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz",
|
||||||
|
"integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w=="
|
||||||
|
},
|
||||||
"domain-browser": {
|
"domain-browser": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz",
|
||||||
@@ -6439,6 +6444,15 @@
|
|||||||
"is-glob": "^4.0.1"
|
"is-glob": "^4.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"global": {
|
||||||
|
"version": "4.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz",
|
||||||
|
"integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==",
|
||||||
|
"requires": {
|
||||||
|
"min-document": "^2.19.0",
|
||||||
|
"process": "^0.11.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"globals": {
|
"globals": {
|
||||||
"version": "11.12.0",
|
"version": "11.12.0",
|
||||||
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
|
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
|
||||||
@@ -9386,6 +9400,14 @@
|
|||||||
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-3.1.0.tgz",
|
||||||
"integrity": "sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ=="
|
"integrity": "sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ=="
|
||||||
},
|
},
|
||||||
|
"min-document": {
|
||||||
|
"version": "2.19.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz",
|
||||||
|
"integrity": "sha1-e9KC4/WELtKVu3SM3Z8f+iyCRoU=",
|
||||||
|
"requires": {
|
||||||
|
"dom-walk": "^0.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"minimalistic-assert": {
|
"minimalistic-assert": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
|
||||||
@@ -13986,6 +14008,23 @@
|
|||||||
"resolved": "https://registry.npmjs.org/vue/-/vue-2.6.12.tgz",
|
"resolved": "https://registry.npmjs.org/vue/-/vue-2.6.12.tgz",
|
||||||
"integrity": "sha512-uhmLFETqPPNyuLLbsKz6ioJ4q7AZHzD8ZVFNATNyICSZouqP2Sz0rotWQC8UNBF6VGSCs5abnKJoStA6JbCbfg=="
|
"integrity": "sha512-uhmLFETqPPNyuLLbsKz6ioJ4q7AZHzD8ZVFNATNyICSZouqP2Sz0rotWQC8UNBF6VGSCs5abnKJoStA6JbCbfg=="
|
||||||
},
|
},
|
||||||
|
"vue-carousel": {
|
||||||
|
"version": "0.18.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/vue-carousel/-/vue-carousel-0.18.0.tgz",
|
||||||
|
"integrity": "sha512-a2zxh7QJioDxNMguqcuJ7TPbfgK5bGDaAXIia7NWxPAWsEvNE4ZtHgsGu40L5Aha4uyjmNKXvleB14QAXFoKig==",
|
||||||
|
"requires": {
|
||||||
|
"global": "^4.3.2",
|
||||||
|
"regenerator-runtime": "^0.12.1",
|
||||||
|
"vue": "^2.5.17"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"regenerator-runtime": {
|
||||||
|
"version": "0.12.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz",
|
||||||
|
"integrity": "sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"vue-client-only": {
|
"vue-client-only": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/vue-client-only/-/vue-client-only-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/vue-client-only/-/vue-client-only-2.0.0.tgz",
|
||||||
|
|||||||
@@ -16,7 +16,8 @@
|
|||||||
"ant-design-vue": "^1.6.5",
|
"ant-design-vue": "^1.6.5",
|
||||||
"core-js": "^3.6.5",
|
"core-js": "^3.6.5",
|
||||||
"lodash": "^4.17.20",
|
"lodash": "^4.17.20",
|
||||||
"nuxt": "^2.14.5"
|
"nuxt": "^2.14.5",
|
||||||
|
"vue-carousel": "^0.18.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vue/test-utils": "^1.1.0",
|
"@vue/test-utils": "^1.1.0",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<TheHeader />
|
<TheHeader />
|
||||||
<a-layout-content>
|
<a-layout-content>
|
||||||
<a-row type="flex" align="middle" justify="center" style="height: 100%;">
|
<a-row type="flex" align="middle" justify="center" style="height: 100%;">
|
||||||
<a-col :span="12">
|
<a-col :xs="18" :md="12" :lg="8">
|
||||||
<SearchInput />
|
<SearchInput />
|
||||||
</a-col>
|
</a-col>
|
||||||
</a-row>
|
</a-row>
|
||||||
|
|||||||
@@ -3,54 +3,16 @@
|
|||||||
<TheHeader />
|
<TheHeader />
|
||||||
<a-layout-content>
|
<a-layout-content>
|
||||||
<a-row type="flex" align="middle" justify="center" style="height: 100%;">
|
<a-row type="flex" align="middle" justify="center" style="height: 100%;">
|
||||||
<a-col :span="12">
|
<a-col :xs="20" :md="18">
|
||||||
<nuxt-link to="/">
|
<a-page-header
|
||||||
<a-button type="primary"> <a-icon type="left" />Go back </a-button>
|
:ghost="false"
|
||||||
</nuxt-link>
|
:title="`Your search : "${search}"`"
|
||||||
|
:sub-title="`${nbResults} match`"
|
||||||
<h3 style="color: #FFFFFF;">
|
@back="() => $router.push('/')"
|
||||||
Your search: "{{ search }}" ({{ nbResults }} match)
|
|
||||||
</h3>
|
|
||||||
<a-carousel arrows dot-position="top">
|
|
||||||
<div
|
|
||||||
slot="prevArrow"
|
|
||||||
class="custom-slick-arrow"
|
|
||||||
style="left: 10px;zIndex: 1"
|
|
||||||
>
|
>
|
||||||
<a-icon type="left-circle" />
|
<Carousel v-if="!error" :dataSource="movies" />
|
||||||
</div>
|
<a-alert v-else :message="error" type="error" show-icon />
|
||||||
<div
|
</a-page-header>
|
||||||
slot="nextArrow"
|
|
||||||
class="custom-slick-arrow"
|
|
||||||
style="right: 10px"
|
|
||||||
>
|
|
||||||
<a-icon type="right-circle" />
|
|
||||||
</div>
|
|
||||||
<a-card
|
|
||||||
:bordered="false"
|
|
||||||
:key="movie.imdbID"
|
|
||||||
v-for="movie in movies"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
slot="cover"
|
|
||||||
:alt="movie.Title"
|
|
||||||
:src="
|
|
||||||
movie.Poster === 'N/A'
|
|
||||||
? 'https://place-hold.it/285x380'
|
|
||||||
: movie.Poster
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
<a-card-meta :title="movie.Title">
|
|
||||||
<template slot="description">
|
|
||||||
{{ movie.Director }}
|
|
||||||
-
|
|
||||||
<span style="font-size: 12px; font-style: oblique;">{{
|
|
||||||
movie.Year
|
|
||||||
}}</span>
|
|
||||||
</template>
|
|
||||||
</a-card-meta>
|
|
||||||
</a-card>
|
|
||||||
</a-carousel>
|
|
||||||
</a-col>
|
</a-col>
|
||||||
</a-row>
|
</a-row>
|
||||||
</a-layout-content>
|
</a-layout-content>
|
||||||
@@ -65,19 +27,22 @@ export default {
|
|||||||
middleware: "redirect",
|
middleware: "redirect",
|
||||||
async asyncData({ store }) {
|
async asyncData({ store }) {
|
||||||
await store.dispatch("getMovies");
|
await store.dispatch("getMovies");
|
||||||
|
|
||||||
|
await store.dispatch("setDirectors");
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState({
|
...mapState({
|
||||||
search: "search",
|
search: "search",
|
||||||
movies: "movies",
|
movies: "movies",
|
||||||
nbResults: "nbResults",
|
nbResults: "nbResults",
|
||||||
error: "error"
|
error: "error",
|
||||||
|
loading: "loading"
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style>
|
||||||
#main {
|
#main {
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -88,28 +53,49 @@ export default {
|
|||||||
background-image: url("~assets/background.jpg");
|
background-image: url("~assets/background.jpg");
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-carousel >>> .slick-slide {
|
.VueCarousel-navigation-prev {
|
||||||
|
left: -8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VueCarousel-navigation-next {
|
||||||
|
right: -8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-page-header {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
padding: 24px 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-page-header-content {
|
||||||
|
overflow: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VueCarousel-slide {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 768px) {
|
||||||
|
.VueCarousel-slide {
|
||||||
|
max-width: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 992px) {
|
||||||
|
.VueCarousel-slide {
|
||||||
|
max-width: 33.333333%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 1200px) {
|
||||||
|
.VueCarousel-slide {
|
||||||
|
max-width: 25%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 414px) {
|
||||||
|
.ant-page-header-heading-sub-title {
|
||||||
|
width: 100%;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-carousel >>> .slick-slide .ant-card {
|
|
||||||
width: 285px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-carousel >>> .custom-slick-arrow {
|
|
||||||
width: 25px;
|
|
||||||
height: 25px;
|
|
||||||
font-size: 25px;
|
|
||||||
color: #fff;
|
|
||||||
background-color: rgba(31, 45, 61, 0.11);
|
|
||||||
opacity: 0.3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-carousel >>> .custom-slick-arrow:before {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.ant-carousel >>> .custom-slick-arrow:hover {
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export const state = () => ({
|
|||||||
movies: [],
|
movies: [],
|
||||||
nbResults: 0,
|
nbResults: 0,
|
||||||
loading: false,
|
loading: false,
|
||||||
error: ""
|
error: null
|
||||||
});
|
});
|
||||||
|
|
||||||
export const mutations = {
|
export const mutations = {
|
||||||
@@ -18,50 +18,62 @@ export const mutations = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const actions = {
|
export const actions = {
|
||||||
async getMovies({ commit, state, dispatch }) {
|
async getMovies({ commit, state }) {
|
||||||
commit("setLoading", true);
|
commit("setLoading", true);
|
||||||
const response = await this.$axios.$get("", {
|
|
||||||
|
return await this.$axios
|
||||||
|
.$get("", {
|
||||||
params: {
|
params: {
|
||||||
apikey: "a4bf96a7",
|
apikey: "a4bf96a7",
|
||||||
s: state.search,
|
s: state.search,
|
||||||
type: "movie"
|
type: "movie"
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (response.Response === "False") throw new Error(response.Error);
|
||||||
|
|
||||||
if (response.Response === "True") {
|
|
||||||
commit("setNbResults", response.totalResults);
|
commit("setNbResults", response.totalResults);
|
||||||
dispatch("setDirectors", response.Search).then(function(res) {
|
commit("setMovies", response.Search);
|
||||||
console.log(res);
|
commit("setError", null);
|
||||||
commit("setMovies", res);
|
})
|
||||||
});
|
.catch(e => {
|
||||||
} else if (response.Response === "False") {
|
|
||||||
commit("setMovies", []);
|
commit("setMovies", []);
|
||||||
commit("setNbResults", 0);
|
commit("setNbResults", 0);
|
||||||
commit("setError", response.Error);
|
commit("setError", e.message);
|
||||||
}
|
})
|
||||||
|
.finally(() => {
|
||||||
commit("setLoading", false);
|
commit("setLoading", false);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
async getDirector({}, id) {
|
async getDirector({}, id) {
|
||||||
const response = await this.$axios.$get("", {
|
return await this.$axios
|
||||||
|
.$get("", {
|
||||||
params: {
|
params: {
|
||||||
apikey: "a4bf96a7",
|
apikey: "a4bf96a7",
|
||||||
i: id,
|
i: id,
|
||||||
type: "movie"
|
type: "movie"
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (response.Response === "False") throw new Error(response.Error);
|
||||||
|
|
||||||
if (response.Response === "True") return response.Director;
|
return response.Director;
|
||||||
else if (response.Response === "False") throw new Error(response.Error);
|
})
|
||||||
|
.catch(e => commit("setError", e.message));
|
||||||
},
|
},
|
||||||
setDirectors({ dispatch }, data) {
|
setDirectors({ state, dispatch, commit }) {
|
||||||
|
commit("setLoading", true);
|
||||||
|
|
||||||
let tmp = [];
|
let tmp = [];
|
||||||
data.forEach(async (movie, index) => {
|
state.movies.forEach(async movie => {
|
||||||
const director = await dispatch("getDirector", movie.imdbID);
|
return await dispatch("getDirector", movie.imdbID).then(director => {
|
||||||
movie.Director = director;
|
movie.Director = director;
|
||||||
tmp.push(movie);
|
tmp.push(movie);
|
||||||
});
|
});
|
||||||
return tmp;
|
});
|
||||||
|
|
||||||
|
commit("setMovies", tmp);
|
||||||
|
commit("setLoading", false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user