Skip to content

Instantly share code, notes, and snippets.

@imposibrus
Last active August 23, 2021 08:48
Show Gist options
  • Save imposibrus/e6227d0c5632508442c53c2d86869933 to your computer and use it in GitHub Desktop.
Save imposibrus/e6227d0c5632508442c53c2d86869933 to your computer and use it in GitHub Desktop.
Vue.js example components
<template>
<div>
<section class="page-content__content page-content__wrap-items">
<div class="community page-content__community">
<div class="community__head">
<figure class="community__cover">
<img :src="community.image_big_url" class="community__cover-img" alt="">
</figure>
<div class="community__head-panel">
<div class="community__head-avatar">
<user-avatar
:roles="community.role_names"
:user-image="community.image_small_url"
:class="'user-avatar--sm'"
/>
</div>
<h1 class="community__name">
{{ community.name }}
</h1>
<div class="tags tags--big tags--direction community__head-tags">
<div class="tags__item">
{{ community.direction_name }}
</div>
</div>
</div>
</div>
<div class="community__item">
<div class="community__subtitle">
О сообществе
</div>
<p class="community__text">
{{ community.description }}
</p>
</div>
<div class="community__item">
<router-link :to="`/${community.id }/themes`" class="community__subtitle community__subtitle--link">
Обсуждения <span class="community__head-count">{{ community.themes_count }}</span>
</router-link>
</div>
<section class="discuss">
<discuss-add-post :community="community" />
<div class="discuss__content">
<discuss-entry v-for="discussion in community.discussions" :key="discussion.id"
:discussion="discussion"
/>
</div>
</section>
</div>
</section>
</div>
</template>
<script>
import DiscussEntry from './DiscussEntry.vue';
import UserAvatar from './UserAvatar.vue';
import DiscussAddPost from './DiscussAddPost.vue';
import { getCommunityByRouterParam } from './mixins';
export default {
name: 'Community',
components: { DiscussAddPost, UserAvatar, DiscussEntry },
mixins: [getCommunityByRouterParam],
props: {
id: {
type: String,
default: '0',
},
},
};
</script>
<style lang="scss" scoped>
.fade-enter-active,
.fade-enter {
opacity: 0;
animation-name: fade-in-down;
animation-duration: 0.5s;
animation-fill-mode: both;
will-change: transform, opacity;
}
.fade-leave-to,
.fade-leave-active {
opacity: 0;
animation-name: fadeOutUp;
animation-duration: 0.35s;
animation-fill-mode: both;
will-change: transform, opacity;
}
</style>
<template>
<article class="page-content__panel panel-item panel-item--community">
<div class="panel-item__panel">
<router-link :to="`/${community.id }`" class="panel-item__community-link">
<div class="panel-item__community-head">
<user-avatar
:roles="community.role_names"
:user-image="community.image_small_url"
:class="'user-avatar--lg'"
/>
<h2 class="panel-item__community-name">
{{ community.name }}
</h2>
</div>
<div class="panel-item__community-info">
<div class="panel-item__count-message">
<span class="panel-item__number-message">{{ community.themes_count }} </span>
{{ getPluralDiscuss }} /&nbsp;<span
class="panel-item__number-message"
>{{ community.comments_total_count }} </span> {{
getPluralComment }}
</div>
<div class="panel-item__last-message">
Последнее сообщение:
<time :datetime="getFormatTime">{{ getTime }}</time>
</div>
<div class="tags tags--big tags--direction panel-item__tags">
<div class="tags__item">
{{ community.direction_name }}
</div>
</div>
</div>
</router-link>
</div>
</article>
</template>
<script>
import format from 'date-fns/format';
import { noun } from 'plural-ru';
import Utils from '../Utils';
import UserAvatar from './UserAvatar.vue';
export default {
name: 'CommunityListItem',
components: { UserAvatar },
props: {
community: {
type: Object,
default: () => ({}),
},
},
computed: {
getTime() {
return Utils.getTime(this.community.last_updated);
},
getFormatTime() {
return format(Utils.normalizeTimestamp(this.community.last_updated),
'YYYY-MM-DDTHH:mm',);
},
getPluralComment() {
return noun(
this.community.comments_total_count,
'комментарий',
'комментария',
'комментариев',
);
},
getPluralDiscuss() {
return noun(
this.community.themes_count,
'обсуждение',
'обсуждения',
'обсуждений',
);
},
},
methods: {},
};
</script>
<template>
<div>
<transition name="fade" mode="out-in">
<!--Кнопка добавление нового обсуждения-->
<div v-show="!expanded" class="discuss__start">
<user-avatar
:roles="[roleName]"
:user-image="avatar"
:class="'user-avatar--md'"
/>
<div class="discuss__start-input" @click="placeholderClick">
<div class="discuss__placeholder">
Добавить запись...
</div>
</div>
</div>
</transition>
<transition name="fade" mode="in-out">
<!--Форма добавление нового обсуждения-->
<div v-show="expanded" class="discuss__add">
<form :class="{'discuss__form--loading': loading}" action="#" method="POST" class="discuss__form form" @submit.prevent="formSubmit">
<div class="form__row discuss__form-row">
<div class="form__line">
<div :class="{'form__wrap-input--invalid': errors.has('text')}" class="form__wrap-input discuss__wrap-textarea">
<textarea ref="textarea"
v-model="text"
v-validate="'required|min:5'"
class="form__input form__input--shadow-grey discuss__add-area-text js-textareaAutosize"
name="text"
rows="1"
placeholder="Напишите..."
></textarea>
<span v-show="errors.has('text')" class="form__error-msg">{{ errors.first('text') }}</span>
</div>
</div>
</div>
<discuss-drop-zone @filesChanged="filesChanged" />
<div class="discuss__form-action">
<button class="btn-main btn-main--color-main" type="submit">
Готово
</button>
<button class="btn-main btn-main--color-grey" type="button" @click="collapse">
Отменить
</button>
</div>
</form>
</div>
</transition>
</div>
</template>
<script>
import get from 'lodash/get';
import russian from 'vee-validate/dist/locale/ru';
import swal from 'sweetalert';
import UserAvatar from './UserAvatar.vue';
import DiscussDropZone from './DiscussDropZone.vue';
import { createNamespacedHelpers } from 'vuex';
const communitiesModule = createNamespacedHelpers('communities');
export default {
name: 'DiscussAddPost',
components: { UserAvatar, DiscussDropZone },
props: {
community: {
type: Object,
default: () => ({}),
},
theme: {
type: Object,
default: () => ({}),
},
},
data() {
return {
expanded: false,
loading: false,
files: [],
text: '',
};
},
computed: {
currentUser() {
return window.authUser;
},
roleName() {
return get(this.currentUser, 'role.name');
},
avatar() {
return get(this.currentUser, 'avatar');
},
$textarea() {
return $(this.$refs.textarea);
},
},
created() {
this.$validator.localize('ru', {
messages: russian.messages,
attributes: {
text: 'Текст',
},
});
},
methods: {
...communitiesModule.mapActions(['savePost']),
postEditCancel() {
this.expanded = !this.expanded;
},
collapse() {
this.text = '';
this.files = [];
this.expanded = false;
this.$textarea.trigger('blur');
this.errors.clear();
},
async formSubmit(e) {
if (await this.$validator.validateAll()) {
try {
await this.savePost({
communityId: this.community.id,
body: {
files: this.files,
text: this.text,
theme_id: this.theme.id,
},
});
this.collapse();
this.loading = false;
} catch (e) {
swal({
icon: 'error',
title: 'Ой-ой!',
text: 'Ошибка при сохранении',
});
}
e.preventDefault();
this.$textarea.trigger('blur');
this.loading = true;
}
},
filesChanged(files) {
this.files = files;
},
placeholderClick() {
this.expanded = !this.expanded;
this.$nextTick(() => {
this.$textarea.trigger('focus');
});
},
},
};
</script>
<style lang="scss" scoped>
.fade-enter-active,
.fade-enter {
opacity: 0;
animation-name: fade-in-down;
animation-duration: 0.5s;
animation-fill-mode: both;
will-change: transform, opacity;
}
.fade-leave-to,
.fade-leave-active {
opacity: 0;
animation-name: fadeOutUp;
animation-duration: 0.35s;
animation-fill-mode: both;
will-change: transform, opacity;
}
</style>
<template>
<div :class="cssClasses.entry">
<div class="entry-discuss__header">
<user-avatar
:url="`/page/id${user.id}`"
:roles="[user.role.name]"
:user-image="user.avatar"
:class="'user-avatar--md'"
>
<template slot="user-info">
<div class="user-avatar__info">
<div v-if="type === 'comment'" class="user-avatar__info-head">
<div class="user-avatar__name">
{{ user.first_name | capitalize }} {{ user.last_name |
capitalize }}
</div>
</div>
<div v-else class="user-avatar__name">
{{ user.first_name | capitalize }} {{ user.last_name |
capitalize }}
</div>
<div class="user-avatar__sub-info">
<time :datetime="getFormatTime" class="user-avatar__date">{{ getTime }}</time>
<template v-if="discussion.is_changed">
<div v-if="isUserAdmin"
:class="{'user-avatar__change': true, 'user-avatar__change--original': changedHovered}"
@mouseover="changedMouseOver" @mouseout="changedMouseOut" @click="showOriginal"
>
{{ changedText }}
</div>
<div v-else class="user-avatar__change">
Изменено
</div>
</template>
</div>
</div>
</template>
</user-avatar>
<div v-if="canEdit" class="entry-discuss__header-action">
<button class="entry-discuss__header-btn" type="button" @click="deleteDiscussion">
<svg class="entry-discuss__header-icon">
<use xlink:href="#close-yellow"></use>
</svg>
</button>
</div>
<div v-if="discussion.user_to" class="entry-discuss__answer">
ответила <span>{{ discussion.user_to.first_name | capitalize }}</span>
</div>
</div>
<body-content :text="discussionText" :files="discussion.files" :is-original="shouldShowOriginal"
:css-classes="{picture: cssClasses.bodyPicture}"
/>
<delete-discuss-entry v-show="deleteModalShown"
:type="type"
:loading="loading"
@confirm="deleteConfirm"
@cancel="cancelDelete"
/>
</div>
</template>
<script>
import format from 'date-fns/format';
import get from 'lodash/get';
import swal from 'sweetalert';
import Utils from '../Utils';
import BodyContent from './BodyContent.vue';
import Likes2 from './Likes2.vue';
import UserAvatar from './UserAvatar.vue';
import DeleteDiscussEntry from './DeleteDiscussEntry.vue';
import { getCommunityByRouterParam } from './mixins';
import { createNamespacedHelpers } from 'vuex';
const communitiesModule = createNamespacedHelpers('communities');
export default {
name: 'DiscussBody',
components: { BodyContent, Likes2, UserAvatar, DeleteDiscussEntry },
mixins: [getCommunityByRouterParam],
props: {
discussion: {
type: Object,
default: () => ({}),
},
type: {
type: String,
default: 'post',
},
cssClasses: {
type: Object,
default: () => ({
entry: 'entry-discuss__entry',
bodyPicture: '',
}),
},
themeId: {
type: Number,
default: null,
},
},
data() {
return {
deleteModalShown: false,
loading: false,
changedHovered: false,
shouldShowOriginal: false,
};
},
computed: {
getTime() {
return Utils.getTime(this.discussion.updated_at);
},
getFormatTime() {
return format(Utils.normalizeTimestamp(this.discussion.updated_at),
'YYYY-MM-DDTHH:mm',);
},
user() {
return this.discussion.user || this.discussion.user_from;
},
isMyDiscussion() {
return get(window.authUser, 'id') === get(this.user, 'id');
},
isUserAdmin() {
return get(window.authUser, 'role.name') === 'superadmin';
},
canEdit() {
return this.isMyDiscussion || this.isUserAdmin;
},
requestBody() {
return {
communityId: this.community.id,
[this.$props.type]: this.discussion,
themeId: this.themeId,
};
},
changedText() {
return this.changedHovered ? 'Оригинал текста' : 'Изменено';
},
discussionText() {
return this.shouldShowOriginal
? this.discussion.text_orig
: this.discussion.text;
},
likesCount() {
return this.discussion.likes_count;
},
},
methods: {
...communitiesModule.mapActions([
'deletePost',
'deleteComment',
'likePost',
'likeComment',
]),
deleteDiscussion() {
this.deleteModalShown = true;
},
requestAction(action) {
return this.$props.type === 'post'
? action + 'Post'
: action + 'Comment';
},
requestConfirm(action) {
this.loading = true;
this[this.requestAction(action)](this.requestBody)
.then(() => {
this.loading = false;
this.deleteModalShown = false;
})
.catch(() => {
swal({
icon: 'error',
title: 'Ой-ой!',
text: 'Ошибка при удалении',
});
});
},
likeDiscussion() {
this.requestConfirm('like');
},
deleteConfirm() {
this.requestConfirm('delete');
},
cancelDelete() {
this.deleteModalShown = false;
},
changedMouseOver() {
this.changedHovered = true;
},
changedMouseOut() {
if (!this.shouldShowOriginal) {
this.changedHovered = false;
}
},
showOriginal() {
this.shouldShowOriginal = !this.shouldShowOriginal;
},
reply() {
this.$emit('reply');
},
declination(user) {
return Utils.declination(user);
},
},
};
</script>
<template>
<div class="entry-discuss discuss__item">
<!-- Запись обсуждения -->
<discuss-body :discussion="discussion"
@reply="commentTarget(discussion.user)"
/>
<!--Комментария -->
<div class="entry-discuss__comments">
<div v-show="showMoreVisible" class="btn-wrapper entry-discuss__btn-wrapper">
<button
type="button"
class="btnV2 btnV2-grey btn-more entry-discuss__more-comment"
@click="loadMoreComments"
>
Показать все {{ discussion.comments_count }} {{ getPluralComment }}
</button>
</div>
<discuss-body
v-for="comment in comments"
:key="comment.id"
:discussion="comment"
:theme-id="discussion.theme_id"
:css-classes="{entry: 'entry-discuss__comment', bodyPicture: 'entry-discuss__picture--comment'}"
type="comment"
@reply="commentTarget(comment.user_from)"
/>
</div>
<!--Добавление комментария -->
<discuss-add-comment
ref="addComment"
:discussion="discussion"
:to-user-id="toUserId"
:to-user-name="toUserName"
@clearReply="clearTarget()"
/>
</div>
</template>
<script>
import { noun } from 'plural-ru';
import DiscussAddComment from './DiscussAddComment.vue';
import { COMMENTS_LIMIT } from './constants';
import DiscussBody from './DiscussBody.vue';
import { getCommunityByRouterParam } from './mixins';
export default {
name: 'DiscussEntry',
components: {
DiscussBody,
DiscussAddComment,
},
mixins: [getCommunityByRouterParam],
props: {
discussion: {
type: Object,
default: () => ({}),
},
},
data() {
return {
visibleCommentsCount: 5,
commentsExpanded: false,
toUserId: null,
toUserName: null,
};
},
computed: {
getPluralComment() {
return noun(
this.discussion.comments_count,
'комментарий',
'комментария',
'комментриев',
);
},
hasMoreComments() {
return this.discussion.comments_count > COMMENTS_LIMIT;
},
showMoreVisible() {
return !this.commentsExpanded && this.hasMoreComments;
},
comments() {
return this.discussion.comments.slice(0, this.visibleCommentsCount);
},
restComments() {
return false;
},
},
methods: {
loadMoreComments() {
this.visibleCommentsCount = 999;
this.commentsExpanded = true;
},
commentTarget(user) {
this.toUserId = user.id;
this.toUserName = user.first_name;
this.$refs.addComment.formClick();
},
clearTarget() {
this.toUserId = null;
this.toUserName = null;
},
},
};
</script>
import { createNamespacedHelpers } from 'vuex';
const communitiesModule = createNamespacedHelpers('communities');
export const getCommunityByRouterParam = {
computed: {
...communitiesModule.mapGetters(['getCommunityById']),
community() {
return (
this.getCommunityById(parseInt(this.$route.params.id, 10)) || {}
);
},
},
};
<template>
<transition-group :class="mod" name="list-fade" tag="ul" class="previews dialog__previews">
<li v-for="(preview, index) in allFiles" :key="index" class="working previews__item">
<span v-if="isUpload" :data-index="index" class="previews__close" @click="closeClick($event, preview)">Закрыть</span>
<div class="swiper-preview-file">
<a :href="preview.url" class="swiper-preview-file__link" @click.prevent="previewClick(preview)">
<img :src="preview.src" class="previews__img" alt="">
</a>
</div>
<div v-if="isUpload" id="progress" class="progress">
<div class="progress__bar" style="width: 0;"></div>
</div>
</li>
</transition-group>
</template>
<script>
import { genericFancyBox } from './fancyBoxHelper';
import * as globalModule from 'vuex';
export default {
name: 'Previews',
props: {
allFiles: {
type: Array,
default: () => [],
},
isUpload: {
type: Boolean,
default: false,
},
mod: {
type: String,
default: '',
},
},
computed: {
...globalModule.mapGetters(['modalBg']),
},
methods: {
...globalModule.mapActions(['setModalBg']),
closeClick(e, preview) {
if (!preview.id && preview.jqXHR) {
preview.jqXHR.abort();
}
this.$emit(
'closePreview', e, preview
);
},
previewClick(clickedPreview) {
const currentIndex = this.allFiles.indexOf(clickedPreview),
slides = this.allFiles.map(file => ({ href: file.url }));
genericFancyBox(slides, currentIndex, this.modalBg, color => {
this.setModalBg(color);
});
},
},
};
</script>
<template>
<a v-if="url"
:href="url"
:class="outerSizeImage"
class="user-avatar user-avatar--link"
target="_blank"
rel="noopener noreferrer"
>
<div class="user-avatar__wrap-picture">
<div class="user-avatar__picture">
<img :src="userImage" alt="" class="user-avatar__img">
</div>
<div v-if="showRoles" class="user-avatar__positions">
<div v-for="(role, index) in roles" :key="index" class="user-avatar__position">
<svg class="user-avatar__icon">
<use :xlink:href="`#user-position-${role}`"></use>
</svg>
</div>
</div>
</div>
<slot name="user-info"></slot>
</a>
<div v-else :class="outerSizeImage" class="user-avatar">
<div class="user-avatar__wrap-picture">
<div class="user-avatar__picture">
<img :src="userImage" alt="" class="user-avatar__img">
</div>
<div v-if="showRoles" class="user-avatar__positions">
<div v-for="(role, index) in roles" :key="index" class="user-avatar__position">
<svg class="user-avatar__icon">
<use :xlink:href="`#user-position-${role}`"></use>
</svg>
</div>
</div>
</div>
<slot name="user-info"></slot>
</div>
</template>
<script>
export default {
name: 'UserAvatar',
props: {
url: {
type: String,
default: '',
},
outerSizeImage: {
type: String,
default: '',
},
userImage: {
type: String,
default: '',
},
roles: {
type: Array,
default() {
return [];
},
},
showRoles: {
type: Boolean,
default: true,
},
},
};
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment