Последняя версия с сервера прошлого разработчика

This commit is contained in:
2025-07-10 04:35:51 +00:00
commit c731570032
1174 changed files with 134314 additions and 0 deletions

View File

@@ -0,0 +1,315 @@
<template>
<div class="col-span-7 md:col-span-5 lg:col-span-3 player-widget contain-content">
<div
ref="topbar"
class="
player-line
rounded-md
transition-colors
bg-transparent
hover:bg-orange-min
flex
items-center
space-x-3
p-2
cursor-pointer
"
@click="openFull"
>
<button class="focus:outline-none text-gray-600 hover:text-gray-800" @click.stop="skipTrack('prev')">
<svg
class="w-4 h-4"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<polygon points="19 20 9 12 19 4 19 20"></polygon>
<line x1="5" y1="19"
x2="5" y2="5"
></line>
</svg>
</button>
<button
class="
flex
h-8
items-center
justify-center
w-8
transition-colors
text-gray-600
hover:text-gray-800
"
@click.stop="toggleAudio"
>
<PlayIcon v-show="!playing" class="w-8 h-8" />
<StopIcon v-show="playing" class="w-8 h-8" />
</button>
<button class="focus:outline-none text-gray-600 hover:text-gray-800" @click.stop="skipTrack('next')">
<svg
class="w-4 h-4"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<polygon points="5 4 15 12 5 20 5 4"></polygon>
<line x1="19" y1="5"
x2="19" y2="19"
></line>
</svg>
</button>
<div v-show="count_playlist" class="text-base truncate select-none">
{{ currentSong.name }}
</div>
</div>
<teleport to="body">
<div v-if="open_drop">
<div
style="
position: fixed;
top: 0;
right: 0;
left: 0;
bottom: 0;
z-index: 9998;
background: black;
opacity: 0.2;
"
@click="open_drop = false"
/>
<div
ref="dropdown"
class="z-[9999] player-widget-list w-full lg:w-[48rem] "
>
<div class="shadow-classic rounded-md bg-indigo-200">
<div
v-show="count_playlist"
class="overflow-hidden bg-orange relative h-2 mt-2 mb-2"
@click.prevent="updateSeek"
>
<div
:style="{ transform: `translate3d(${playerProgress}, 0, 0)` }"
class="will-change-transform absolute transition-transform h-full bg-white flex items-center justify-end w-full"
>
</div>
</div>
<div
class="
p-2
lg:p-4
rounded-md
items-center
grid grid-cols-1
md:grid-cols-10
cursor-pointer
"
>
<div class="col-span-7 flex items-center space-x-3">
<button class="focus:outline-none text-gray" @click.stop="skipTrack('prev')">
<svg
class="w-4 h-4"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<polygon points="19 20 9 12 19 4 19 20"></polygon>
<line x1="5" y1="19"
x2="5" y2="5"
></line>
</svg>
</button>
<button
class="
flex
h-8
items-center
justify-center
w-8
transition-colors
text-gray
"
@click.stop="toggleAudio"
>
<PlayIcon v-show="!playing" class="w-8 h-8" />
<StopIcon v-show="playing" class="w-8 h-8" />
</button>
<button class="focus:outline-none text-gray" @click.stop="skipTrack('next')">
<svg
class="w-4 h-4"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<polygon points="5 4 15 12 5 20 5 4"></polygon>
<line x1="19" y1="5"
x2="19" y2="19"
></line>
</svg>
</button>
<div class="text-gray-light text-base truncate select-none">
{{ currentSong.name }}
</div>
</div>
<div v-show="count_playlist" class="col-span-1 text-center text-gray-light text-xs select-none">
{{ seek }}
</div>
<div v-show="count_playlist" class="col-span-2 mt-2 md:mt-0 md:ml-auto flex items-center">
<input v-model="volume" class="w-full custom-input-range"
type="range" max="1"
step="0.1" @click.stop=""
@input="updateVolume(volume)"
/>
<span class="text-gray-light ml-1.5 text-xs">{{ volume * 100 + "%" }}</span>
</div>
</div>
<div class="choices-tabs">
<div class="border-b border-indigo-300">
<nav class="-mb-px flex" aria-label="Tabs">
<a v-for="tab in tabs" :key="tab.key"
href="#" :class="[
current_tab === tab.key ? 'border-orange text-white' : 'border-transparent text-gray-light hover:text-white',
'w-1/2 lg:w-1/4 py-3 lg:py-4 px-1 text-center text-sm border-b-2',
]"
@click.prevent="changeTab(tab.key)"
>{{ tab.name }}</a>
</nav>
</div>
</div>
<div>
<keep-alive>
<component :is="currentTabComponent"></component>
</keep-alive>
</div>
</div>
</div>
</div>
</teleport>
</div>
</template>
<script>
import Popper from 'popper.js'
import { PlayIcon, StopIcon } from '@heroicons/vue/solid'
import { mapActions, mapGetters, mapState } from 'vuex'
import AudioTabNow from '@/Shared/Tabs/AudioTabNow.vue'
import AudioTabFavorite from '@/Shared/Tabs/AudioTabFavorite.vue'
import AudioTabLoaded from '@/Shared/Tabs/AudioTabLoaded.vue'
export default {
name: 'Player',
components: {
AudioTabNow,
AudioTabLoaded,
AudioTabFavorite,
PlayIcon,
StopIcon
},
data() {
return {
tabs: [
{key: 'now', name: 'Текущая'},
{key: 'loaded', name: 'Загруженная'},
{key: 'favorite', name: 'Понравившийся'},
],
open_drop: false,
popper: null,
volume: 0.5,
}
},
computed: {
currentTabComponent() {
return 'audio-tab-' + this.current_tab
},
...mapGetters(['playing', 'count_playlist', 'volume_step']),
...mapState({
current_tab: (state) => state.player.tab,
seek: (state) => state.player.seek,
duration: (state) => state.player.duration,
playerProgress: (state) => state.player.playerProgress,
currentSong: (state) => state.player.currentSong,
}),
},
watch: {
open_drop(open_drop) {
if (open_drop) {
this.$nextTick(() => {
if (window.matchMedia('(min-width: 1024px)').matches) {
this.popper = new Popper(this.$refs.topbar, this.$refs.dropdown, {
placement: 'bottom-start',
modifiers: {
preventOverflow: { boundariesElement: 'scrollParent' },
},
})
}else {
this.popper = new Popper(this.$refs.topbar, this.$refs.dropdown, {
placement: 'bottom-end',
modifiers: {
preventOverflow: {
boundariesElement: 'scrollParent',
padding: 0,
},
offset: {
enabled: true,
offset: '0, 10'
}
},
})
}
})
} else if (this.popper) {
setTimeout(() => this.popper.destroy(), 100)
}
},
},
created() {
this.loadExsitPlaylist()
this.changeVolume(this.volume_step)
this.volume = this.volume_step
},
methods: {
...mapActions([
'toggleAudio',
'updateSeek',
'skipTrack',
'changeVolume',
'tabName',
'loadExsitPlaylist',
]),
openFull() {
this.open_drop = !this.open_drop
},
updateVolume(volume) {
this.changeVolume(volume)
},
changeTab(name){
this.tabName(name)
},
},
}
</script>

View File

@@ -0,0 +1,68 @@
<template>
<div>
<div v-if="type === 'images'" class="bg-indigo-100 overflow-hidden">
<img :class="{ 'blur-sm': classActive }" class="object-contain h-48 w-full" :src="media.url" alt="">
</div>
<div v-if="type === 'videos'" class="overflow-hidden">
<video :class="{ 'blur-sm': classActive }" class="object-contain h-48 w-full" :src="media.url" controls></video>
</div>
<div v-if="type === 'musics'" class="overflow-hidden">
<div :class="{ 'blur-sm': classActive }" class="border border-indigo-100 flex flex-col items-center p-4">
<MusicNoteIcon class="flex-shrink-0 w-12 h-12 text-indigo-300" />
<div class="text-base text-gray font-medium">{{media.name}} / {{media.time}}</div>
</div>
</div>
<button
v-if="edit"
:class="[classActive ? 'bg-green' : 'bg-pink']"
class="block text-sm text-white w-full p-1"
type="button" @click="stateToggle(media.id)">
<span v-show="!classActive">Удалить</span>
<span v-show="classActive">Восстановить</span>
</button>
</div>
</template>
<script>
import { MusicNoteIcon } from "@heroicons/vue/solid";
export default {
components: {
MusicNoteIcon,
},
props: {
media: Object,
edit: {
type: Boolean,
default: true,
},
type: {
type: String,
default: "images",
},
},
emits: ["addRemoveId"],
data() {
return {
activeItemId: 0,
};
},
methods: {
stateToggle(id) {
if (this.activeItemId === 0) {
this.activeItemId = id;
} else {
this.activeItemId = 0;
}
this.$emit("addRemoveId", id);
},
},
computed: {
classActive() {
return this.media.id === this.activeItemId;
},
},
};
</script>

View File

@@ -0,0 +1,24 @@
<template>
<div
class="card-block contain cursor-pointer flex group overflow-hidden relative"
>
<div class="absolute inset-0 flex items-center justify-center z-10 text-white text-lg w-full h-36 md:h-72 object-cover">
18 + Контент
</div>
<div class="blur-2xl flex flex-grow bg-indigo-300 w-full h-36 md:h-72 object-cover">
</div>
</div>
</template>
<script>
export default {
props: {
entity: Object,
user: Object,
feed_id: Number,
},
}
</script>

View File

@@ -0,0 +1,97 @@
<template>
<modal-feed
:modal-feed="modalFeed"
:open="show"
@close-modal="closeModal"
@destroyFeed="destroyFeed"
/>
<InfinityScroll :node-element="lastNodeLement" :next-cursor="nextCursor"
@fromPagination="putFromPagination"
>
<div v-for="feed in feedLists" :key="feed.id"
:ref="el => { if (el && feed.id === lastElementID) lastNodeLement = el }"
>
<feed-node :feed="feed" @open-modal="openModal" />
</div>
</InfinityScroll>
</template>
<script>
import { ref, computed, watch } from 'vue'
import { Inertia } from '@inertiajs/inertia'
import findIndex from 'lodash/findIndex'
import FeedNode from '@/Shared/Feed/FeedNode.vue'
import ModalFeed from '@/Shared/Overlay/ModalFeed.vue'
import InfinityScroll from '@/Shared/Misc/InfinityScroll.vue'
export default {
components: {
FeedNode,
ModalFeed,
InfinityScroll,
},
props: {
feeds: Array,
nextCursor: { type: String, default: '' },
},
setup(props) {
let feedLists = ref(props.feeds)
const show = ref(false)
const modalFeed = ref({})
const containerRef = ref(null)
const lastElementID = computed(
() => feedLists.value[feedLists.value.length - 1]?.id
)
watch(props, (props) => {
feedLists.value = props.feeds
})
const putFromPagination = (lists) => {
for (let list of lists) {
feedLists.value.push(list)
}
}
const destroyFeed = () => {
Inertia.delete(route('feed.destroy', modalFeed.value.id), {
preserveScroll: true,
preserveState: true,
})
const index = findIndex(
feedLists.value,
(item) => item.id === modalFeed.value.id
)
feedLists.value.splice(index, 1)
}
const openModal = (feed) => {
show.value = true
modalFeed.value = feed
}
const closeModal = () => {
show.value = false
}
return {
lastNodeLement: containerRef,
openModal,
closeModal,
destroyFeed,
lastElementID,
show,
modalFeed,
feedLists,
putFromPagination,
}
},
}
</script>

View File

@@ -0,0 +1,95 @@
<template>
<component
:is="currentTypeNode"
:class="{'need-approved': feed.entity?.status == 0}"
:feed_id="feed.id"
:user="feed.user"
:entity="feed.entity"
@click.prevent="openModal(feed)"
@like-feed="likeFeed"
></component>
<p v-if="feed.entity?.price" class="mt-2 text-gray-light text-sm">
Цена: {{ feed.entity.price }}
</p>
<p v-if="feed.entity?.purchase_date" class="mt-2 text-gray-light text-sm">
Дата покупки: {{ feed.entity.purchase_date }}
</p>
</template>
<script>
import { Inertia } from '@inertiajs/inertia'
import { usePage } from '@inertiajs/inertia-vue3'
import axios from 'axios'
import FeedImages from '@/Shared/Feed/Images.vue'
import FeedVideos from '@/Shared/Feed/Videos.vue'
import FeedMusics from '@/Shared/Feed/Musics.vue'
import FeedAdults from '@/Shared/Feed/Adults.vue'
import FeedProhibited from '@/Shared/Feed/Prohibited.vue'
export default {
components: {
FeedImages,
FeedVideos,
FeedMusics,
FeedAdults,
FeedProhibited,
},
props: {
feed: Object,
},
emits: ['openModal'],
computed: {
authUser() {
return usePage().props.value.auth.user
},
currentTypeNode() {
return 'feed-' + this.feed.type.toLowerCase()
},
},
methods: {
openModal(feed) {
if(feed.entity){
this.$emit('openModal', feed)
}
},
likeFeed() {
axios
.post(route('feed.like', this.feed.id))
.then(() => {
if (this.feed.entity.liked) {
this.feed.entity.liked = false
this.feed.entity.likes--
} else {
this.feed.entity.liked = true
this.feed.entity.likes++
}
})
// Inertia.post(
// route('feed.like', this.feed.id),
// {},
// {
// preserveScroll: true,
// preserveState: true,
// }
// )
// if (this.feed.entity.liked) {
// this.feed.entity.liked = false
// this.feed.entity.likes--
// } else {
// this.feed.entity.liked = true
// this.feed.entity.likes++
// }
},
},
}
</script>
<style>
.need-approved{
outline: 5px solid #FF9800;
}
</style>

View File

@@ -0,0 +1,32 @@
<template>
<img :class="{'object-top sm:object-center': existSource}" :src="setImage()"
alt=""
/>
</template>
<script>
export default {
components: {},
props: {
source: String,
type: String,
},
computed: {
existSource () {
return !this.source
}
},
methods: {
setImage() {
if (this.source) {
return this.source
}
// if(this.type == 'music'){
// return '/image/modalimg1.jpg'
// }
// return '/image/card4.jpg'
return '/image/default-placeholder.jpg'
},
}
}
</script>

View File

@@ -0,0 +1,26 @@
<template>
<div
class="
transition-opacity
ease-out
flex
items-center
justify-center
bg-indigo-300 bg-opacity-75
z-10
absolute
inset-x-0
bottom-0
p-2
md:p-3
"
>
<footer class="misc-info flex space-x-4">
<span class="text-white text-xs lg:text-base font-medium">КОНТЕНТ НА МОДЕРАЦИИ</span>
</footer>
</div>
</template>
<script>
</script>

View File

@@ -0,0 +1,68 @@
<template>
<div
class="
transition-opacity
ease-out
flex
items-center
justify-center
opacity-0
group-hover:opacity-100
bg-indigo-300 bg-opacity-75
z-10
absolute
inset-x-0
bottom-0
p-2
md:p-3
"
>
<div class="misc-info flex space-x-4 items-center">
<template v-if="ads == false">
<like-count :likes="likes" :liked="is_like"
@likeFeed="callLike"
/>
<comment-count :comments="comments" />
</template>
<span v-else class="text-gray-light">Реклама</span>
<view-count :count="count" />
</div>
</div>
</template>
<script>
import { Inertia } from '@inertiajs/inertia'
import LikeCount from '@/Shared/Misc/LikeCount.vue'
import ViewCount from '@/Shared/Misc/ViewCount.vue'
import CommentCount from '@/Shared/Misc/CommentCount.vue'
export default {
components: {
LikeCount,
CommentCount,
ViewCount,
},
props: {
likes: Number,
comments: Number,
is_like: Boolean,
ads: {
type: Boolean,
default: false
},
count: {
type: Number,
default: 0
},
},
emits: ['likeFeed'],
methods: {
callLike() {
if (this.$page.props.auth.user) {
this.$emit('likeFeed')
}else{
Inertia.get(route('login'))
}
},
},
}
</script>

View File

@@ -0,0 +1,24 @@
<template>
<div class="absolute z-10 top-3 right-3">
<div class="flex items-center text-white">
<span v-if="count > 1" class="mr-1 text-sm">
{{ count }}
</span>
<svg class="drop-shadow-custom w-5 h-5">
<use v-if="type == 'images'" xlink:href="#imagefeed"></use>
<use v-if="type == 'videos'" xlink:href="#filmmark"></use>
<use v-if="type == 'musics'" xlink:href="#musicmark"></use>
</svg>
</div>
</div>
</template>
<script>
export default {
components: { },
props: {
count: Number,
type: String,
},
}
</script>

View File

@@ -0,0 +1,53 @@
<template>
<div
class="card-block contain group cursor-pointer relative overflow-hidden"
>
<feed-header-misc
:count="entity.collection_medias.length"
type="images"
/>
<feed-footer-misc
v-if="entity.status == 1"
:is_like="entity.liked"
:likes="entity.likes"
:comments="entity.comments"
:count="entity.views_count"
:ads="entity.is_ads"
@like-feed="likeFeed"
/>
<feed-footer-banned v-else />
<div class="relative overflow-hidden">
<img
class="w-full h-36 md:h-72 object-cover"
:src="entity.preview"
alt=""
/>
</div>
</div>
</template>
<script>
import FeedFooterMisc from '@/Shared/Feed/FooterMisc.vue'
import FeedHeaderMisc from '@/Shared/Feed/HeaderMisc.vue'
import FeedFooterBanned from '@/Shared/Feed/FooterBanned.vue'
export default {
components: { FeedFooterMisc, FeedHeaderMisc, FeedFooterBanned },
props: {
entity: Object,
user: Object,
feed_id: Number,
},
emits: ['likeFeed'],
methods: {
likeFeed() {
this.$emit('likeFeed')
},
},
}
</script>

View File

@@ -0,0 +1,126 @@
<template>
<div
class="card-block contain group cursor-pointer relative overflow-hidden"
>
<feed-header-misc
:count="entity.collection_medias.length"
type="musics"
/>
<div :class="[playlist_id === feed_id ? '' : 'transition-opacity ease-out opacity-0 group-hover:opacity-100' , ' absolute inset-0 z-10 flex items-center justify-center']">
<div class="w-full grid grid-cols-7 items-center p-1 md:p-3 bg-indigo-300 bg-opacity-75">
<div class="col-span-1 flex mr-3 text-white" @click.stop>
<div class="inline-block transition-colors hover:text-orange" @click.prevent="startPlay(first_entity)">
<button :class="[ currentSong?.id === first_entity.id ? '' : 'hidden', 'default' ]">
<svg :class="[ playing ? 'hidden' : 'block', 'w-4 h-4 md:w-6 md:h-6' ]">
<use xlink:href="#play"></use>
</svg>
<svg :class="[ playing ? 'block' : 'hidden', 'w-4 h-4 md:w-6 md:h-6' ]">
<use xlink:href="#pause"></use>
</svg>
</button>
<button :class="[ currentSong?.id !== first_entity.id ? '' : 'hidden', 'default' ]">
<svg class="w-4 h-4 md:w-6 md:h-6">
<use xlink:href="#play"></use>
</svg>
</button>
</div>
</div>
<div v-show="playlist_id === feed_id" class="col-span-6 flex flex-col text-white">
<span class="text-xs md:text-base text-gray font-semibold truncate">{{ currentSong.name }}</span>
<span class="text-xs md:text-sm text-gray-light truncate">{{ seek }}</span>
</div>
<div v-show="playlist_id !== feed_id" class="col-span-6 flex flex-col text-white">
<span class="text-xs md:text-base text-gray font-semibold truncate">{{ first_entity.name }}</span>
<span class="text-xs md:text-sm text-gray-light truncate">{{ first_entity.time }}</span>
</div>
</div>
</div>
<feed-footer-misc
v-if="entity.status == 1"
:is_like="entity.liked"
:likes="entity.likes"
:comments="entity.comments"
:count="entity.views_count"
:ads="entity.is_ads"
@like-feed="likeFeed"
/>
<feed-footer-banned v-else />
<div class="relative overflow-hidden">
<feed-preview class="w-full h-36 md:h-72 object-cover" type="music"
:source="entity.preview"
/>
</div>
</div>
</template>
<script>
import { mapActions, mapState, mapGetters } from 'vuex'
import FeedFooterMisc from '@/Shared/Feed/FooterMisc.vue'
import FeedHeaderMisc from '@/Shared/Feed/HeaderMisc.vue'
import FeedPreview from '@/Shared/Feed/FeedPreview.vue'
import FeedFooterBanned from '@/Shared/Feed/FooterBanned.vue'
import axios from 'axios'
export default {
components: { FeedFooterMisc, FeedHeaderMisc, FeedPreview, FeedFooterBanned },
props: {
entity: Object,
user: Object,
feed_id: Number,
},
emits: ['likeFeed'],
computed: {
first_entity() {
const media = this.entity.collection_medias
if (
this.playlist_id === this.feed_id &&
typeof media[this.indexPlaylist] !== 'undefined'
) {
return media[this.indexPlaylist]
}
return media[0]
},
...mapGetters(['playing']),
...mapState({
seek: (state) => state.player.seek,
indexPlaylist: (state) => state.player.index,
playlist_id: (state) => state.player.playlist_id,
currentSong: (state) => state.player.currentSong,
}),
},
methods: {
...mapActions(['toggleAudio', 'newCurrentPlaylist']),
startPlay(music) {
this.addViewShow()
if (this.currentSong?.id === music.id) {
this.toggleAudio()
return
}
this.newCurrentPlaylist([
this.first_entity,
this.entity.collection_medias,
this.feed_id,
])
},
addViewShow() {
axios
.post(route('add.view.feed', this.feed_id))
.then(({ data }) => {
data && this.entity.views_count++
})
},
likeFeed() {
this.$emit('likeFeed')
},
},
}
</script>

View File

@@ -0,0 +1,31 @@
<template>
<div
class="card-block contain cursor-pointer flex group overflow-hidden relative"
>
<div class="absolute inset-0 flex items-center justify-center z-10 text-white text-sm sm:text-lg w-full h-36 md:h-72 object-cover">
<div class="text-center p-2">
<p>Контент доступен по подписке!</p>
<p>
Купить: <inertia-link :href="route('profile.user', user.username)" class="font-semibold underline inline-block mr-2">
{{ user.username }}
</inertia-link>
</p>
</div>
</div>
<div class="blur-2xl flex flex-grow bg-indigo-300 w-full h-36 md:h-72 object-cover">
</div>
</div>
</template>
<script>
export default {
props: {
entity: Object,
user: Object,
feed_id: Number,
},
}
</script>

View File

@@ -0,0 +1,47 @@
<template>
<div
class="card-block contain group cursor-pointer relative overflow-hidden"
>
<feed-header-misc
:count="entity.collection_medias.length"
type="videos"
/>
<feed-footer-misc
v-if="entity.status == 1"
:is_like="entity.liked"
:likes="entity.likes"
:comments="entity.comments"
:count="entity.views_count"
:ads="entity.is_ads"
@like-feed="likeFeed"
/>
<feed-footer-banned v-else />
<div class="relative overflow-hidden">
<feed-preview class="w-full h-36 md:h-72 object-cover" :source="entity.preview" />
</div>
</div>
</template>
<script>
import FeedFooterMisc from '@/Shared/Feed/FooterMisc.vue'
import FeedHeaderMisc from '@/Shared/Feed/HeaderMisc.vue'
import FeedPreview from '@/Shared/Feed/FeedPreview.vue'
import FeedFooterBanned from '@/Shared/Feed/FooterBanned.vue'
export default {
components: { FeedFooterMisc, FeedHeaderMisc, FeedPreview, FeedFooterBanned },
props: {
entity: Object,
user: Object,
feed_id: Number,
},
emits: ['likeFeed'],
methods: {
likeFeed() {
this.$emit('likeFeed')
},
},
}
</script>

View File

@@ -0,0 +1,23 @@
<template>
<div class="shadow-classic rounded-md bg-indigo-200 p-3 md:px-5 md:py-7 relative">
<div class="absolute inset-0 flex items-center justify-center z-10 text-white text-lg w-full object-cover font-medium">
18 + Контент
</div>
<div class="blur-2xl flex flex-grow bg-indigo-300 w-full h-36 object-cover">
</div>
</div>
</template>
<script>
export default {
props: {
entity: Object,
user: Object,
feed_id: Number,
},
}
</script>

View File

@@ -0,0 +1,47 @@
<template>
<div class="mt-3 md:mt-6 feed-footer">
<div class="flex justify-between">
<div v-if="ads"></div>
<div v-else class="misc-info flex space-x-4">
<like-count :likes="likes" :liked="liked"
@likeFeed="likeFeed"
/>
<comment-count :comments="comments" />
<share-count />
</div>
<view-count :count="count" />
</div>
</div>
</template>
<script>
import LikeCount from '@/Shared/Misc/LikeCount.vue'
import ViewCount from '@/Shared/Misc/ViewCount.vue'
import CommentCount from '@/Shared/Misc/CommentCount.vue'
import ShareCount from '@/Shared/Misc/ShareCount.vue'
export default {
components: {
LikeCount,
CommentCount,
ShareCount,
ViewCount,
},
props: {
comments: Number,
likes: Number,
liked: Boolean,
ads: Boolean,
count: {
type: Number,
default: 0
},
},
emits:['likeFeed'],
methods:{
likeFeed(){
this.$emit('likeFeed')
}
}
}
</script>

View File

@@ -0,0 +1,80 @@
<template>
<div class="feed-header flex items-center justify-between" @click.stop="">
<div v-if="entity.is_ads" class="flex items-center gap-3">
<user-avatar :user="user" size="small"
class="w-10 h-10 md:w-14 md:h-14 text-lg"
/>
<div class="flex flex-col">
<span class="text-sm md:text-base block font-medium text-white">{{ user.name }}</span>
</div>
</div>
<div v-else class="flex items-center">
<inertia-link :href="route('profile.user', user.username)" class="flex-shrink-0 block mr-2 md:mr-4">
<user-avatar :user="user" size="small"
class="w-10 h-10 md:w-14 md:h-14 text-lg"
/>
</inertia-link>
<inertia-link :href="route('profile.user', user.username)" class="flex flex-col">
<span class="hover:underline text-sm md:text-base block font-medium text-white">{{ user.name }}</span>
<span class="hover:underline text-xs text-gray-light">{{ created_at }}</span>
</inertia-link>
</div>
<div v-if="entity.is_ads == false" class="flex-shrink-0 text-white">
<dropdown-menu-point>
<MenuItems class="origin-top-right absolute right-0 mt-2 w-64 bg-indigo-300 shadow-lg max-h-60 rounded-md text-base ring-1 ring-indigo-200 overflow-auto focus:outline-none">
<MenuItem v-if="user.id === $page.props.auth.user.id">
<inertia-link :href="route(`${entity.type}.edit`, entity.slug)" class="w-full group flex items-center px-4 py-2 text-base hover:bg-indigo-100 text-gray-light">
<PencilAltIcon class="mr-3 h-5 w-5 text-gray-400 group-hover:text-orange" aria-hidden="true" />
Редактировать
</inertia-link>
</MenuItem>
<MenuItem v-if="user.id === $page.props.auth.user.id">
<button class="w-full group flex items-center px-4 py-2 text-base hover:bg-indigo-100 text-gray-light" @click="onRemoveFeed()">
<MinusCircleIcon class="mr-3 h-5 w-5 text-gray-400 group-hover:text-orange" aria-hidden="true" />
Удалить
</button>
</MenuItem>
<MenuItem v-if="user.id !== $page.props.auth.user.id">
<inertia-link :href="route('complaint.reason', feed_id)" class="group flex items-center px-4 py-2 text-base hover:bg-indigo-100 text-gray-light">
<ExclamationIcon class="mr-3 h-5 w-5 text-gray-400 group-hover:text-orange" aria-hidden="true" />
Пожаловаться
</inertia-link>
</MenuItem>
</MenuItems>
</dropdown-menu-point>
</div>
</div>
</template>
<script>
import { MenuItem, MenuItems } from '@headlessui/vue'
import { MinusCircleIcon, ExclamationIcon, PencilAltIcon } from '@heroicons/vue/solid'
import UserAvatar from '@/Shared/Misc/UserAvatar.vue'
import DropdownMenuPoint from '@/Shared/Form/DropdownMenuPoint.vue'
export default {
components: {
UserAvatar,
DropdownMenuPoint,
MinusCircleIcon,
ExclamationIcon,
PencilAltIcon,
MenuItem,
MenuItems,
},
props: {
user: Object,
created_at: String,
feed_id: Number,
entity: Object,
},
emits: ['onRemoveFeed'],
methods: {
onRemoveFeed() {
this.$emit('onRemoveFeed')
},
},
}
</script>

View File

@@ -0,0 +1,115 @@
<template>
<modal-feed
:modal-feed="modalFeed"
:open="show"
@close-modal="closeModal"
@destroyFeed="destroyFeed"
/>
<modal-warning
:feed_id="modalFeedId"
:open="warningShow"
@action="deleteActionModal"
/>
<InfinityScroll :node-element="lastNodeLement" :next-cursor="nextCursor"
@fromPagination="putFromPagination"
>
<div v-for="feed in feedLists" :key="feed.id"
:ref="el => { if (el && feed.id === lastElementID) lastNodeLement = el }"
>
<feed-list-node :feed="feed" @onRemoveFeed="onRemoveFeed"
@open-modal="openModal"
/>
</div>
</InfinityScroll>
</template>
<script>
import { ref } from 'vue'
import { Inertia } from '@inertiajs/inertia'
import filter from 'lodash/filter'
import FeedListNode from '@/Shared/FeedList/FeedListNode.vue'
import ModalFeed from '@/Shared/Overlay/ModalFeed.vue'
import ModalWarning from '@/Shared/Overlay/ModalWarning.vue'
import InfinityScroll from '@/Shared/Misc/InfinityScroll.vue'
export default {
components: {
FeedListNode,
ModalFeed,
ModalWarning,
InfinityScroll,
},
props: {
feeds: Array,
nextCursor: { type: String, default: '' },
},
setup() {
const containerRef = ref(null)
return {
lastNodeLement: containerRef,
}
},
data() {
return {
show: false,
modalFeedId: 0,
warningShow: false,
feedLists: [],
modalFeed: {},
}
},
computed: {
lastElementID() {
return this.feedLists[this.feedLists.length - 1]?.id
},
},
mounted() {
this.feedLists = this.feeds
},
methods: {
putFromPagination(lists) {
for (let list of lists) {
this.feedLists.push(list)
}
},
deleteActionModal(removeFeed) {
if (removeFeed) {
this.destroyFeed(this.modalFeedId)
}
this.warningShow = false
},
onRemoveFeed(id) {
this.warningShow = true
this.modalFeedId = id
},
destroyFeed(id) {
Inertia.delete(route('feed.destroy', id), {
preserveScroll: true,
preserveState: true,
})
this.feedLists = filter(this.feedLists, function (x) {
return x.id !== id
})
},
openModal(feed) {
this.show = true
this.modalFeed = feed
},
closeModal() {
this.show = false
},
},
}
</script>

View File

@@ -0,0 +1,86 @@
<template>
<component
:is="currentTypeNode"
:feed_id="feed.id"
:user="feed.user"
:entity="feed.entity"
@click.prevent="openModal(feed)"
@onRemoveFeed="onRemoveFeed"
@like-feed="likeFeed"
></component>
</template>
<script>
import { Inertia } from '@inertiajs/inertia'
import { usePage } from '@inertiajs/inertia-vue3'
import axios from 'axios'
import FeedImages from '@/Shared/FeedList/Images.vue'
import FeedVideos from '@/Shared/FeedList/Videos.vue'
import FeedMusics from '@/Shared/FeedList/Musics.vue'
import FeedAdults from '@/Shared/FeedList/Adults.vue'
import FeedProhibited from '@/Shared/FeedList/Prohibited.vue'
export default {
components: {
FeedImages,
FeedVideos,
FeedMusics,
FeedAdults,
FeedProhibited,
},
props: {
feed: Object,
},
emits: ['openModal', 'onRemoveFeed'],
computed: {
authUser() {
return usePage().props.value.auth.user
},
currentTypeNode() {
return 'feed-' + this.feed.type.toLowerCase()
},
},
methods: {
onRemoveFeed(id) {
this.$emit('onRemoveFeed', id)
},
openModal(feed) {
if(feed.entity){
this.$emit('openModal', feed)
}
},
likeFeed() {
axios
.post(route('feed.like', this.feed.id))
.then(() => {
if (this.feed.entity.liked) {
this.feed.entity.liked = false
this.feed.entity.likes--
} else {
this.feed.entity.liked = true
this.feed.entity.likes++
}
})
// Inertia.post(
// route('feed.like', this.feed.id),
// {},
// {
// preserveScroll: true,
// preserveState: true,
// }
// )
// if (this.feed.entity.liked) {
// this.feed.entity.liked = false
// this.feed.entity.likes--
// } else {
// this.feed.entity.liked = true
// this.feed.entity.likes++
// }
},
},
}
</script>

View File

@@ -0,0 +1,109 @@
<template>
<div class="">
<div class="flex items-center border-b border-indigo-100">
<div class="flex-shrink-0 mr-5 w-28 md:w-56">
<feed-preview class="h-28 w-28 md:w-56 md:h-56 object-cover" type="music"
:source="preview"
/>
</div>
<div class="flex flex-col">
<span class="text-base md:text-xl font-semibold text-gray">{{ title }}</span>
</div>
</div>
<div data-simplebar class="max-h-72 overflow-auto">
<div class="divide-y divide-indigo-100" @click.stop="">
<div
v-for="media in medias"
:key="media.id" :class="[currentSong?.id === media.id ?
'bg-indigo-300 bg-opacity-25' :
'hover:bg-indigo-300 hover:bg-opacity-25',
'p-4 flex items-center space-x-4']"
@click.prevent="startPlay(media)"
>
<div class="flex">
<toggle-play-button :media_id="media.id" />
</div>
<div class="flex-1 text-sm text-gray-light">
{{ media.name }}
</div>
<div class="text-sm text-gray-light">
<div v-show="currentSong?.id === media.id">
{{ seek }}
</div>
<div v-show="currentSong?.id !== media.id">
{{ media.time }}
</div>
</div>
<music-add-count v-if="$page.props.auth.user" :media_id="media.id"
:liked="media.liked" @addAudio="likeAudio"
/>
</div>
</div>
</div>
</div>
</template>
<script>
import { mapActions, mapState, mapGetters } from 'vuex'
import find from 'lodash/find'
import { Inertia } from '@inertiajs/inertia'
import FeedPreview from '@/Shared/Feed/FeedPreview.vue'
import TogglePlayButton from '@/Shared/Misc/TogglePlayButton.vue'
import MusicAddCount from '@/Shared/Misc/MusicAddCount.vue'
export default {
components: {
FeedPreview,
TogglePlayButton,
MusicAddCount,
},
props: {
medias: Array,
feed_id: Number,
title: String,
preview: String,
},
emits: ['trackClick'],
computed: {
...mapGetters(['playing']),
...mapState({
playlist_id: (state) => state.player.playlist_id,
seek: (state) => state.player.seek,
currentSong: (state) => state.player.currentSong,
}),
},
methods: {
likeAudio(media_id) {
Inertia.post(
route('feed.audio.like', media_id),
{},
{
preserveScroll: true,
preserveState: true,
}
)
let media = find(this.medias, (item) => item.id === media_id)
if(media){
media.liked = !media.liked
}
},
...mapActions(['toggleAudio', 'newCurrentPlaylist', 'skipToIndexByMusic']),
startPlay(music) {
this.$emit('trackClick')
if (this.currentSong?.id === music.id) {
this.toggleAudio()
return
}
// if (this.playlist_id === this.feed_id) {
// this.skipToIndexByMusic(music);
// return;
// }
this.newCurrentPlaylist([music, this.medias, this.feed_id])
},
},
}
</script>

View File

@@ -0,0 +1,104 @@
<template>
<div v-if="disabled === 0 && is_paid === 1 && $page.props.auth.user.id !== user_id" class="mt-3 rounded-md bg-indigo-300 p-4"
@click.stop=""
>
<div v-if="bought == 0" class="flex items-center justify-center">
<div class="mr-5 mb-2 md:mb-0 font-bold text-center md:text-left w-full md:w-auto">
Цена: {{ price }}
</div>
<div>
<button type="button" class="my-1 transition shadow-none hover:shadow-classic2 inline-flex items-center px-8 py-1 md:py-3 justify-center text-sm md:text-base rounded-md text-white bg-pink focus:outline-none"
@click="purchaseFeed"
>
Купить
</button>
</div>
</div>
<div v-else class="flex flex-wrap items-center justify-center">
<div class="paid-feed-msg mr-5 mb-2 md:mb-0 font-bold text-center md:text-left w-full md:w-auto">
Данный товар вами куплен
</div>
<button v-if="disable_btn == 0 && paid_open == 0" type="button"
class="my-1 mx-1 transition shadow-none hover:shadow-classic2 inline-flex items-center px-8 py-1 md:py-3 justify-center text-sm md:text-base rounded-md text-white bg-orange focus:outline-none" @click="replaceFeed"
>
Показать
</button>
<inertia-link :href="route('setting.show.purchases', feed_id)" class="my-1 mx-1 transition shadow-none hover:shadow-classic2 inline-flex items-center px-8 py-1 md:py-3 justify-center text-sm md:text-base rounded-md text-white bg-pink focus:outline-none">
Скачать
</inertia-link>
</div>
</div>
<div v-else-if="is_paid === 1 && $page.props.auth.user.id === user_id" class="p-4 text-center"
@click.stop=""
>
<button v-if="paid_open === 0" type="button"
class="my-1 mx-1 transition shadow-none hover:shadow-classic2 inline-flex items-center px-8 py-1 md:py-2 justify-center text-sm rounded-md text-white bg-orange focus:outline-none" @click="replaceFeed"
>
Показать эксклюзивный контент
</button>
</div>
</template>
<script>
import axios from 'axios'
export default {
components: {},
props: {
user_id: Number,
is_paid: Number,
feed_id: Number,
price: String,
paid_open: {
type: Number,
default: 0,
},
},
emits: ['onReplaceFeed'],
data() {
return {
bought: 0,
disable_btn: 0,
disabled: 1,
}
},
mounted() {
if (this.is_paid === 1) {
axios
.post(route('feeds.purchase_check', this.feed_id))
.then(({ data }) => {
if (data === 1) {
this.bought = 1
}
this.disabled = 0
})
}
},
methods: {
purchaseFeed() {
axios.post(route('feed.purchase', this.feed_id)).then(({ data }) => {
if (data.error == 1) {
alert(data.msg)
} else {
this.$emit('onReplaceFeed', data)
this.disable_btn = 1
this.bought = 1
}
})
},
replaceFeed() {
axios.post(route('feeds.replace', this.feed_id)).then(({ data }) => {
if (data.collection) {
this.$emit('onReplaceFeed', data)
this.disable_btn = 1
}
})
},
},
}
</script>

View File

@@ -0,0 +1,101 @@
<template>
<div class="shadow-classic rounded-md bg-indigo-200 p-3 md:px-5 md:py-7">
<feed-header
:entity="entity"
:feed_id="feed_id"
:created_at="entity.created_at_humans"
:user="user"
@onRemoveFeed="onRemoveFeed"
/>
<div class="mt-3 md:mt-6 feed-body">
<div class="mb-3 md:mb-6 text-gray text-sm md:text-base">
<div @click.stop="">
<div v-if="entity.is_ads" class="prose-content"
v-html="entity.body"
>
</div>
<template v-else>
<feed-tags
v-if="entity.tags.length"
class="mb-1"
:tags="entity.tags"
/>
{{ entity.body }}
</template>
</div>
<feed-paid-block
:is_paid="entity.is_paid"
:user_id="user.id"
:price="entity.price"
:feed_id="feed_id"
:paid_open="entity.paid_open"
@onReplaceFeed="onReplaceFeed"
/>
</div>
<div
:class="[{
'grid-cols-2': countMediaContent > 1
}]"
class="grid gap-1 md:gap-3 md:grid-cols-[repeat(auto-fit,minmax(280px,1fr))]"
>
<div v-for="media in entity.collection_medias" :key="media.id">
<img class="w-full h-full object-cover" :src="media.url"
alt=""
/>
</div>
</div>
</div>
<feed-footer
:likes="entity.likes"
:liked="entity.liked"
:comments="entity.comments"
:count="entity.views_count"
:ads="entity.is_ads"
@likeFeed="likeFeed"
/>
</div>
</template>
<script>
import FeedHeader from '@/Shared/FeedList/FeedHeader.vue'
import FeedFooter from '@/Shared/FeedList/FeedFooter.vue'
import FeedPaidBlock from '@/Shared/FeedList/FeedPaidBlock.vue'
import FeedTags from '@/Shared/Misc/FeedTags.vue'
export default {
components: {
FeedHeader,
FeedFooter,
FeedPaidBlock,
FeedTags,
},
props: {
entity: Object,
user: Object,
feed_id: Number,
},
emits: ['likeFeed', 'onRemoveFeed'],
computed: {
countMediaContent() {
return this.entity.collection_medias.length
}
},
methods: {
onReplaceFeed(data) {
this.entity.collection_medias = data.collection
this.entity.preview = data.preview
this.entity.paid_open = 1
},
onRemoveFeed() {
this.$emit('onRemoveFeed', this.feed_id)
},
likeFeed() {
this.$emit('likeFeed')
},
},
}
</script>

View File

@@ -0,0 +1,90 @@
<template>
<div class="shadow-classic rounded-md bg-indigo-200 p-3 md:px-5 md:py-7">
<feed-header :entity="entity" :feed_id="feed_id"
:created_at="entity.created_at_humans" :user="user"
@onRemoveFeed="onRemoveFeed"
/>
<div class="mt-3 md:mt-6 feed-body">
<div class="mb-3 md:mb-6 text-gray text-sm md:text-base" @click.stop="">
<div>
<feed-tags v-if="entity.tags.length" class="mb-1"
:tags="entity.tags"
/>
{{ entity.body }}
</div>
<feed-paid-block
:is_paid="entity.is_paid"
:user_id="user.id"
:price="entity.price"
:feed_id="feed_id"
:paid_open="entity.paid_open"
@onReplaceFeed="onReplaceFeed"
/>
</div>
<feed-music-body
:feed_id="feed_id"
:title="entity.title"
:preview="entity.preview"
:medias="entity.collection_medias"
@trackClick="addViewShow"
/>
</div>
<feed-footer
:likes="entity.likes"
:liked="entity.liked"
:comments="entity.comments"
:count="entity.views_count"
:ads="entity.is_ads"
@likeFeed="likeFeed"
/>
</div>
</template>
<script>
import FeedHeader from '@/Shared/FeedList/FeedHeader.vue'
import FeedFooter from '@/Shared/FeedList/FeedFooter.vue'
import FeedMusicBody from '@/Shared/FeedList/FeedMusicBody.vue'
import FeedPaidBlock from '@/Shared/FeedList/FeedPaidBlock.vue'
import FeedTags from '@/Shared/Misc/FeedTags.vue'
import axios from 'axios'
export default {
components: {
FeedHeader,
FeedFooter,
FeedMusicBody,
FeedPaidBlock,
FeedTags,
},
props: {
entity: Object,
user: Object,
feed_id: Number,
},
emits: ['likeFeed', 'onRemoveFeed'],
methods: {
addViewShow() {
axios
.post(route('add.view.feed', this.feed_id))
.then(({ data }) => {
data && this.entity.views_count++
})
},
onReplaceFeed(data) {
this.entity.collection_medias = data.collection
this.entity.preview = data.preview
this.entity.paid_open = 1
},
onRemoveFeed() {
this.$emit('onRemoveFeed', this.feed_id)
},
likeFeed() {
this.$emit('likeFeed')
},
},
}
</script>

View File

@@ -0,0 +1,29 @@
<template>
<div class="shadow-classic rounded-md bg-indigo-200 p-3 md:px-5 md:py-7 relative">
<div class="absolute inset-0 flex items-center justify-center z-10 text-white text-sm sm:text-lg w-full object-cover font-medium">
<div class="text-center p-2">
<p>Контент доступен по подписке!</p>
<p>
Купить: <inertia-link :href="route('profile.user', user.username)" class="font-semibold underline inline-block mr-2">
{{ user.username }}
</inertia-link>
</p>
</div>
</div>
<div class="blur-2xl flex flex-grow bg-indigo-300 w-full h-36 object-cover"></div>
</div>
</template>
<script>
export default {
props: {
entity: Object,
user: Object,
feed_id: Number,
},
}
</script>

View File

@@ -0,0 +1,118 @@
<template>
<div class="shadow-classic rounded-md bg-indigo-200 p-3 md:px-5 md:py-7">
<feed-header :entity="entity" :feed_id="feed_id"
:created_at="entity.created_at_humans" :user="user"
@onRemoveFeed="onRemoveFeed"
/>
<div class="mt-3 md:mt-6 feed-body" @click.stop="">
<div class="mb-3 md:mb-6 text-gray text-sm md:text-base">
<div @click.stop="">
<div class="flex items-start border-b border-indigo-100">
<div class="flex-shrink-0 mr-5 w-28 md:w-56">
<feed-preview class="h-28 w-28 md:w-56 md:h-56 object-cover" type="music"
:source="entity.preview"
/>
</div>
<div class="flex flex-col">
<div class="text-base md:text-xl font-semibold text-gray pb-1">
{{ entity.title }}
</div>
<div v-if="entity.is_ads" class="prose-content"
v-html="entity.body"
>
</div>
<template v-else>
<feed-tags v-if="entity.tags.length" class="mb-1"
:tags="entity.tags"
/>
<div class="pb-2">
{{ entity.body }}
</div>
</template>
</div>
</div>
</div>
<feed-paid-block
:is_paid="entity.is_paid"
:user_id="user.id"
:price="entity.price"
:feed_id="feed_id"
:paid_open="entity.paid_open"
@onReplaceFeed="onReplaceFeed"
/>
</div>
<div :class="[{
'grid-cols-2': countMediaContent > 1
}]" class="grid gap-1 md:gap-3 md:grid-cols-[repeat(auto-fit,minmax(280px,1fr))]"
@click="addViewShow"
>
<div v-for="media in entity.collection_medias" :key="media.id"
class="aspect-w-16 aspect-h-9"
>
<video :src="media.url" :poster="entity.preview"
controls
></video>
</div>
</div>
</div>
<feed-footer
:likes="entity.likes"
:liked="entity.liked"
:comments="entity.comments"
:count="entity.views_count"
:ads="entity.is_ads"
@likeFeed="likeFeed"
/>
</div>
</template>
<script>
import FeedHeader from '@/Shared/FeedList/FeedHeader.vue'
import FeedFooter from '@/Shared/FeedList/FeedFooter.vue'
import FeedPaidBlock from '@/Shared/FeedList/FeedPaidBlock.vue'
import FeedPreview from '@/Shared/Feed/FeedPreview.vue'
import FeedTags from '@/Shared/Misc/FeedTags.vue'
import axios from 'axios'
export default {
components: {
FeedHeader,
FeedFooter,
FeedPaidBlock,
FeedTags,
FeedPreview,
},
props: {
entity: Object,
user: Object,
feed_id: Number,
},
emits: ['likeFeed', 'onRemoveFeed'],
computed: {
countMediaContent() {
return this.entity.collection_medias.length
}
},
methods: {
addViewShow(){
axios
.post(route('add.view.feed', this.feed_id))
.then(({ data }) => {
data && this.entity.views_count++
})
},
onReplaceFeed(data) {
this.entity.collection_medias = data.collection
this.entity.preview = data.preview
this.entity.paid_open = 1
},
onRemoveFeed() {
this.$emit('onRemoveFeed', this.feed_id)
},
likeFeed() {
this.$emit('likeFeed')
},
},
}
</script>

View File

@@ -0,0 +1,71 @@
<template>
<button type="button" @click="show = true">
<slot />
<teleport to="body">
<div v-if="show">
<div class="bg-indigo-200 bg-opacity-25" style="position: fixed; top: 0; right: 0; left: 0; bottom: 0; z-index: 99998;" @click="show = false" />
<div ref="dropdown" style="position: absolute; z-index: 99999;" @click.stop="show = autoClose ? false : true">
<slot name="dropdown" />
</div>
</div>
</teleport>
</button>
</template>
<script>
import Popper from 'popper.js'
export default {
props: {
placement: {
type: String,
default: 'bottom-end',
},
offset: {
type: String,
default: '0, 10',
},
boundary: {
type: String,
default: 'scrollParent',
},
autoClose: {
type: Boolean,
default: true,
},
},
data() {
return {
show: false,
}
},
watch: {
show(show) {
if (show) {
this.$nextTick(() => {
this.popper = new Popper(this.$el, this.$refs.dropdown, {
placement: this.placement,
modifiers: {
preventOverflow: { boundariesElement: this.boundary, padding: 0},
offset: {
enabled: true,
offset: this.offset
}
},
})
})
} else if (this.popper) {
setTimeout(() => this.popper.destroy(), 100)
}
},
},
mounted() {
document.addEventListener('keydown', (e) => {
if (e.keyCode === 27) {
this.show = false
}
})
},
}
</script>

View File

@@ -0,0 +1,26 @@
<!-- This example requires Tailwind CSS v2.0+ -->
<template>
<Menu as="div" class="relative inline-block text-left z-50">
<div>
<MenuButton class="transition inline-flex items-center justify-center shadow-classic2 rounded-full bg-orange text-white focus:outline-none w-12 h-12">
<svg class="h-4 w-4 md:h-5 md:w-5 flex-shrink-0" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" ><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>
</MenuButton>
</div>
<transition enter-active-class="transition ease-out duration-100" enter-from-class="transform opacity-0 scale-95" enter-to-class="transform opacity-100 scale-100" leave-active-class="transition ease-in duration-75" leave-from-class="transform opacity-100 scale-100" leave-to-class="transform opacity-0 scale-95">
<slot />
</transition>
</Menu>
</template>
<script>
import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/vue'
export default {
components: {
Menu,
MenuButton,
MenuItem,
MenuItems,
},
}
</script>

View File

@@ -0,0 +1,27 @@
<!-- This example requires Tailwind CSS v2.0+ -->
<template>
<Menu as="div" v-slot="{ open }" class="relative inline-block text-left z-50">
<div>
<MenuButton class="transition inline-flex items-center justify-center focus:outline-none ">
<svg :class="[open ? 'rotate-90' : 'rotate-180' ,'transform-gpu transition-transform transform w-6 h-6']">
<use xlink:href="#more-vertical"></use>
</svg>
</MenuButton>
</div>
<transition enter-active-class="transition ease-out duration-100" enter-from-class="transform opacity-0 scale-95" enter-to-class="transform opacity-100 scale-100" leave-active-class="transition ease-in duration-75" leave-from-class="transform opacity-100 scale-100" leave-to-class="transform opacity-0 scale-95">
<slot />
</transition>
</Menu>
</template>
<script>
import { Menu, MenuButton } from '@headlessui/vue'
export default {
components: {
Menu,
MenuButton,
},
}
</script>

View File

@@ -0,0 +1,69 @@
<template>
<div>
<label v-if="label" class="cursor-pointer text-gray-light text-lg mb-2"
@click="browse"
>{{ label }}:</label>
<div :class="{ error: error }">
<input ref="file" type="file"
:accept="accept" class="hidden"
@change="change"
>
<div v-if="!modelValue" class="py-2">
<button type="button" class="px-6 py-2 bg-indigo-300 focus:ring-4 focus:ring-offset-1 focus:ring-orange focus:ring-opacity-20 focus:ring-offset-orange focus:outline-none focus:border-transparent rounded-sm text-sm text-white"
@click="browse"
>
Выбрать файл
</button>
</div>
<div v-else class="flex flex-col justify-center max-w-2xl mt-3 p-2 border rounded-md">
<div class="flex flex-col md:flex-row md:items-start md:justify-between p-1">
<div class="md:w-5/6 flex-1 flex flex-col pr-1 text-gray">
<span class="truncate">{{ modelValue.name }}</span>
<span class="text-xs text-gray-light">({{ filesize(modelValue.size) }})</span>
</div>
<button type="button" class="md:w-1/6 px-1 py-1 bg-indigo-300 hover:bg-indigo-100 rounded-sm text-xs font-medium text-white"
@click="remove"
>
Удалить
</button>
</div>
</div>
</div>
<div v-if="error" class="text-red text-sm">
{{ error }}
</div>
</div>
</template>
<script>
export default {
props: {
modelValue: File,
label: String,
accept: String,
error: String,
},
watch: {
modelValue(value) {
if (!value) {
this.$refs.file.value = ''
}
},
},
methods: {
filesize(size) {
var i = Math.floor(Math.log(size) / Math.log(1024))
return (size / Math.pow(1024, i) ).toFixed(2) * 1 + ' ' + ['B', 'kB', 'MB', 'GB', 'TB'][i]
},
browse() {
this.$refs.file.click()
},
change(e) {
this.$emit('update:modelValue', e.target.files[0])
},
remove() {
this.$emit('update:modelValue', null)
},
},
}
</script>

View File

@@ -0,0 +1,104 @@
<template>
<div>
<label v-if="label" class="cursor-pointer text-gray-light text-lg mb-2"
@click="browse"
>{{ label }}:</label>
<div :class="{ error: error }">
<input
ref="file"
type="file"
:accept="accept"
multiple
class="hidden"
@change="change"
/>
<div v-if="!modelValue" class="py-2">
<button
type="button"
class="px-6 py-2 bg-indigo-300 focus:ring-4 focus:ring-offset-1 focus:ring-orange focus:ring-opacity-20 focus:ring-offset-orange focus:outline-none focus:border-transparent rounded-sm text-sm text-white"
@click="browse"
>
Выбрать файлы
</button>
</div>
<div v-else class="flex flex-col justify-center max-w-2xl mt-3 p-2 border rounded-md">
<div
v-for="(file, index) in modelValue"
:key="index"
class="flex flex-col md:flex-row md:items-start md:justify-between p-1"
>
<div class="md:w-5/6 text-sm md:text-base flex-1 pr-2 text-gray flex flex-col">
<span class="truncate">{{ file.name }}</span>
<span class="text-xs text-gray-light">({{ filesize(file.size) }})</span>
</div>
<button
type="button"
class="md:w-1/6 px-1 py-1 bg-indigo-300 hover:bg-indigo-100 rounded-sm text-xs font-medium text-white"
@click="remove(index)"
>
Удалить
</button>
</div>
</div>
</div>
<div v-if="error" class="text-red text-sm">
{{ error }}
</div>
</div>
</template>
<script>
export default {
props: {
modelValue: FileList,
label: String,
accept: String,
error: String,
},
emits:['update:modelValue'],
watch: {
modelValue(value) {
if (!value) {
this.$refs.file.value = ''
}
},
},
methods: {
filesize(size) {
var i = Math.floor(Math.log(size) / Math.log(1024))
return (
(size / Math.pow(1024, i)).toFixed(2) * 1 +
' ' +
['B', 'kB', 'MB', 'GB', 'TB'][i]
)
},
browse() {
this.$refs.file.click()
},
change(e) {
this.$emit('update:modelValue', e.target.files)
},
remove(file_index) {
const dt = new DataTransfer()
let files = Array.from(this.$refs.file.files)
files.map(function (file, index) {
if (index !== file_index) {
dt.items.add(file)
}
})
if (dt.files.length) {
this.$refs.file.files = dt.files
this.$emit('update:modelValue', dt.files)
} else {
this.$refs.file.files = null
this.$emit('update:modelValue', null)
}
},
},
}
</script>

View File

@@ -0,0 +1,120 @@
<template>
<div>
<label v-if="label" @click="browse" class="cursor-pointer text-gray-light text-lg mb-2">{{ label }}:</label>
<div :class="{ error: error }">
<input
ref="file"
type="file"
:accept="accept"
multiple
class="hidden"
@change="change"
/>
<div v-if="!modelValue" class="py-2">
<button
type="button"
class="px-6 py-2 bg-indigo-300 focus:ring-4 focus:ring-offset-1 focus:ring-orange focus:ring-opacity-20 focus:ring-offset-orange focus:outline-none focus:border-transparent rounded-sm text-sm text-white"
@click="browse"
>
Выбрать файлы
</button>
</div>
<div v-else class="flex flex-col justify-center max-w-lg mt-3 p-2 border rounded-md">
<div
v-for="(file, index) in modelValue"
:key="index"
class="flex items-center justify-between p-1"
>
<div class="flex-1 pr-1 text-gray">
{{ file.name }}
<span class="text-xs text-gray-light"
>({{ filesize(file.size) }})</span
>
</div>
<button
type="button"
class="px-4 py-1 bg-indigo-300 hover:bg-indigo-100 rounded-sm text-xs font-medium text-white"
@click="remove(index)"
>
Удалить
</button>
</div>
</div>
</div>
<div v-if="error" class="text-red text-sm">{{ error }}</div>
</div>
</template>
<script>
import helper from "@/includes/helper";
export default {
props: {
modelValue: FileList,
label: String,
accept: String,
error: String,
},
emits: ["fileTime", "loadFileStart", "update:modelValue"],
watch: {
modelValue(value) {
if (!value) {
this.$refs.file.value = "";
}
},
},
methods: {
filesize(size) {
var i = Math.floor(Math.log(size) / Math.log(1024));
return (
(size / Math.pow(1024, i)).toFixed(2) * 1 +
" " +
["B", "kB", "MB", "GB", "TB"][i]
);
},
browse() {
this.$refs.file.click();
},
change(e) {
const files = Array.from(e.target.files);
const that = this;
files.map(function (file) {
that.$emit("loadFileStart");
var audioCtx = new (AudioContext || webkitAudioContext)();
var readerAudio = new FileReader();
readerAudio.readAsArrayBuffer(file);
readerAudio.onload = function (ev) {
audioCtx.decodeAudioData(ev.target.result).then(function (buffer) {
that.$emit(
"fileTime",
file.name + "," + that.formatTimeSong(buffer.duration)
);
});
};
});
this.$emit("update:modelValue", e.target.files);
},
formatTimeSong(value) {
return helper.formatTime(value);
},
remove(file_index) {
const dt = new DataTransfer();
let files = Array.from(this.$refs.file.files);
files.map(function (file, index) {
if (index !== file_index) {
dt.items.add(file);
}
});
if (dt.files.length) {
this.$refs.file.files = dt.files;
this.$emit("update:modelValue", dt.files);
} else {
this.$refs.file.files = null;
this.$emit("update:modelValue", null);
}
},
},
};
</script>

View File

@@ -0,0 +1,14 @@
<template>
<button :disabled="loading">
<div v-if="loading" class="btn-spinner mr-2" />
<slot />
</button>
</template>
<script>
export default {
props: {
loading: Boolean,
},
}
</script>

View File

@@ -0,0 +1,57 @@
<template>
<div class="flex items-center">
<div class="flex w-full rounded">
<dropdown :auto-close="false" class="px-4 md:px-6 border border-indigo-300 rounded-l bg-indigo-200 hover:bg-indigo-100 focus:z-10 focus:ring-4 focus:ring-offset-1 focus:ring-orange focus:ring-opacity-20 focus:ring-offset-orange focus:border-transparent" placement="bottom-start">
<div class="flex text-gray items-baseline">
<span class="text-base hidden md:inline">Фильтр</span>
<svg class="w-2 h-2 fill-current md:ml-2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 961.243 599.998">
<path d="M239.998 239.999L0 0h961.243L721.246 240c-131.999 132-240.28 240-240.624 239.999-.345-.001-108.625-108.001-240.624-240z" />
</svg>
</div>
<template v-slot:dropdown>
<div class="mt-2 px-4 py-6 w-screen shadow-xl bg-indigo-300 rounded-md" :style="{ maxWidth: `${maxWidth}px` }">
<slot />
</div>
</template>
</dropdown>
<div class="flex-1 relative">
<div class="absolute inset-y-0 left-3 flex items-center z-[1]">
<svg class="flex-shrink-0 h-5 w-5 text-gray-light" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24" fill="currentColor">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M11 4a7 7 0 100 14 7 7 0 000-14zm-9 7a9 9 0 1118 0 9 9 0 01-18 0z" />
<path fill-rule="evenodd" clip-rule="evenodd"
d="M15.943 15.943a1 1 0 011.414 0l4.35 4.35a1 1 0 01-1.414 1.414l-4.35-4.35a1 1 0 010-1.414z" />
</svg>
</div>
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
autocomplete="off"
name="search"
class="relative w-full focus:ring-4 focus:ring-offset-1 focus:ring-orange focus:ring-opacity-20 focus:ring-offset-orange focus:border-transparent text-gray border border-indigo-300 bg-indigo-200 rounded-r placeholder-gray-light !pl-10 h-14" placeholder="Поиск"
type="search">
</div>
</div>
<button class="ml-3 text-sm text-gray focus:text-orange" type="button" @click="$emit('reset')">Сбросить</button>
</div>
</template>
<script>
import Dropdown from "@/Shared/Form/Dropdown.vue";
export default {
components: {
Dropdown,
},
emits: ["update:modelValue", "reset"],
props: {
modelValue: String,
maxWidth: {
type: Number,
default: 300,
},
},
};
</script>

View File

@@ -0,0 +1,190 @@
<template>
<div class="relative" :class="{ 'with-count': showCount }">
<input
v-model="newTag"
type="text"
:list="id"
placeholder="Теги"
class="w-full focus:ring-4 focus:ring-offset-1 focus:ring-orange focus:ring-opacity-20 focus:ring-offset-orange focus:border-transparent text-gray border border-indigo-300 bg-indigo-200 rounded-md"
autocomplete="off"
:style="{ 'padding-left': `${paddingLeft}px` }"
@keydown.prevent.enter="addTag(newTag)"
@blur.prevent="addTag(newTag)"
/>
<datalist v-if="options" :id="id">
<option v-for="option in availableOptions" :key="option"
:value="option"
>
{{ option }}
</option>
</datalist>
<ul ref="tagsUl" class="tags">
<li
v-for="(tag, index) in tags"
:key="tag"
class=""
:class="{ 'duplicate-shake': tag === duplicate }"
>
<span class="inline-flex rounded-full items-center py-0.5 pl-2.5 pr-1 text-sm font-medium bg-orange text-indigo-300">
{{ tag }}
<button type="button" class="flex-shrink-0 ml-0.5 h-4 w-4 rounded-full inline-flex items-center justify-center text-indigo-300 hover:bg-indigo-200 hover:text-white focus:outline-none focus:bg-indigo-200 focus:text-white"
@click="removeTag(index)"
>
<span class="sr-only">Удалить</span>
<svg class="h-2 w-2" stroke="currentColor"
fill="none" viewBox="0 0 8 8"
>
<path stroke-linecap="round" stroke-width="1.5"
d="M1 1l6 6m0-6L1 7"
/>
</svg>
</button>
</span>
</li>
</ul>
<div v-if="showCount" class="count">
<span>{{ tags.length }}</span> tags
</div>
</div>
</template>
<script>
import { toRef, ref, watch, nextTick, onMounted, computed } from 'vue'
export default {
props: {
name: { type: String, default: '' },
modelValue: { type: Array, default: () => [] },
options: { type: [Array, Boolean], default: false },
allowCustom: { type: Boolean, default: true },
showCount: { type: Boolean, default: false },
},
setup(props, { emit }) {
// Tags
const tags = toRef(props, 'modelValue')
const newTag = ref('')
const id = Math.random().toString(36).substring(7)
const addTag = (tag) => {
if (!tag) return // prevent empty tag
// only allow predefined tags when allowCustom is false
if (!props.allowCustom && !props.options.includes(tag)) return
// return early if duplicate
if (tags.value.includes(tag)) {
handleDuplicate(tag)
return
}
tags.value.push(tag)
newTag.value = '' // reset newTag
}
const removeTag = (index) => {
tags.value.splice(index, 1)
}
// handling duplicates
const duplicate = ref(null)
const handleDuplicate = (tag) => {
duplicate.value = tag
setTimeout(() => (duplicate.value = null), 1000)
newTag.value = ''
}
// positioning and handling tag change
const paddingLeft = ref(10)
const tagsUl = ref(null)
const onTagsChange = () => {
// position cursor
const extraCushion = 15
paddingLeft.value = tagsUl.value.clientWidth + extraCushion
// scroll to end of tags
tagsUl.value.scrollTo(tagsUl.value.scrollWidth, 0)
// emit value on tags change
emit('update:modelValue', tags.value)
}
watch(tags, () => nextTick(onTagsChange), { deep: true })
onMounted(onTagsChange)
// options
const availableOptions = computed(() => {
if (!props.options) return false
return props.options.filter((option) => !tags.value.includes(option))
})
return {
tags,
newTag,
addTag,
removeTag,
paddingLeft,
tagsUl,
availableOptions,
id,
duplicate,
}
},
}
</script>
<style scoped>
ul {
list-style: none;
display: flex;
align-items: center;
gap: 7px;
margin: 0;
padding: 0;
position: absolute;
top: 0;
bottom: 0;
left: 10px;
max-width: 75%;
overflow-x: auto;
}
@keyframes shake {
10%,
90% {
transform: scale(0.9) translate3d(-1px, 0, 0);
}
20%,
80% {
transform: scale(0.9) translate3d(2px, 0, 0);
}
30%,
50%,
70% {
transform: scale(0.9) translate3d(-4px, 0, 0);
}
40%,
60% {
transform: scale(0.9) translate3d(4px, 0, 0);
}
}
.duplicate-shake {
animation: shake 1s;
}
.count {
position: absolute;
top: 50%;
transform: translateY(-50%);
right: 10px;
display: block;
font-size: 0.8rem;
white-space: nowrap;
}
.count span {
background: #eee;
padding: 2px;
border-radius: 2px;
}
.with-count input {
padding-right: 60px;
}
.with-count ul {
max-width: 60%;
}
</style>

View File

@@ -0,0 +1,45 @@
<template>
<label v-if="label" class="text-gray-light text-lg mb-2"
:for="id"
>{{ label }}:</label>
<input :id="id" ref="input"
v-bind="$attrs" :class="{ error: error }"
:type="type" :value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
>
<div v-if="error" class="text-red text-sm">
{{ error }}
</div>
</template>
<script>
export default {
props: {
id: {
type: String,
default() {
return `select-input-${Math.random() * 1000}`
},
},
type: {
type: String,
default: 'text',
},
modelValue: [String, Number],
label: String,
error: String,
},
emits: ['update:modelValue'],
methods: {
focus() {
this.$refs.input.focus()
},
select() {
this.$refs.input.select()
},
setSelectionRange(start, end) {
this.$refs.input.setSelectionRange(start, end)
},
},
}
</script>

View File

@@ -0,0 +1,30 @@
<template>
<label class="text-gray-light text-lg mb-2" v-if="label" :for="id">{{ label }}:</label>
<textarea :id="id" ref="input" v-bind="$attrs" :class="{ error: error }" :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" />
<div v-if="error" class="text-red text-sm">{{ error }}</div>
</template>
<script>
export default {
inheritAttrs: false,
props: {
id: {
type: String,
default() {
return `select-input-${Math.random() * 1000}`;
},
},
modelValue: String,
label: String,
error: String,
},
methods: {
focus() {
this.$refs.input.focus()
},
select() {
this.$refs.input.select()
},
},
}
</script>

View File

@@ -0,0 +1,50 @@
<template>
<SwitchGroup as="div" :class="{'pointer-events-none': disabled}"
class="flex items-center"
@click="clicked"
>
<Switch v-model="enabled" :class="[enabled ? 'bg-orange' : 'bg-indigo-200', 'relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500']">
<!-- <span class="sr-only">Use setting</span> -->
<span aria-hidden="true" :class="[enabled ? 'translate-x-5' : 'translate-x-0', 'pointer-events-none inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200']" />
</Switch>
<SwitchLabel as="span" class="ml-3">
<span v-show="!enabled" class="text-sm text-white">{{ textin }}</span>
<span v-show="enabled" class="text-sm text-white">{{ textout }}</span>
</SwitchLabel>
</SwitchGroup>
</template>
<script>
import { Switch, SwitchGroup, SwitchLabel } from '@headlessui/vue'
export default {
components: {
Switch,
SwitchGroup,
SwitchLabel,
},
props: {
textin: String,
textout: String,
user_id: Number,
enabled: {
type: Boolean,
default: false
},
disabled: {
type: Boolean,
default: false
},
},
emits: ['clicked', 'prohibited'],
methods: {
clicked(){
if(this.disabled === false){
this.$emit('clicked', this.user_id)
}else{
this.$emit('prohibited')
}
}
}
}
</script>

149
resources/js/Shared/Layout.vue Executable file
View File

@@ -0,0 +1,149 @@
<template>
<header-bar v-if="$page.props.auth.user" />
<div class="min-h-screen-header flex overflow-hidden">
<sidebar v-if="$page.props.auth.user" />
<div class="flex-1 flex flex-col overflow-hidden contain">
<div class="bg-indigo-300 flex-1 flex items-stretch overflow-hidden">
<!-- pb-10 -->
<main class="flex-1 overflow-y-auto relative">
<button v-if="$page.props.sidebar_layout" class="default text-center right-2 top-3 absolute z-40 flex lg:hidden items-center text-white text-xs"
@click="openSidebar"
>
<svg xmlns="http://www.w3.org/2000/svg" width="24"
height="24" viewBox="0 0 24 24"
fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" class="feather feather-menu"
><line x1="3" y1="12"
x2="21" y2="12"
></line><line x1="3" y1="6"
x2="21" y2="6"
></line><line x1="3" y1="18"
x2="21" y2="18"
></line></svg>
<span class="ml-1">Лидеры</span>
</button>
<section class="min-w-0 flex-1 h-full flex flex-col overflow-hidden lg:order-last">
<flash-messages />
<slot />
<footer :class="{'px-2 md:px-6 2xl:px-28': $page.props.sidebar_layout}" class="bg-indigo-300 mt-auto">
<div class="container mx-auto p-2 text-sm text-gray-light">
<ul class="flex justify-center flex-wrap gap-3 text-xs px-3">
<li>
<a target="_blank" class="hover:underline"
href="/docs/company.pdf"
rel="noopener noreferrer nofollow"
>О компании</a>
</li>
<li>
<a target="_blank" class="hover:underline"
href="/docs/offer_authors.pdf"
rel="noopener noreferrer nofollow"
>Оферта для авторов</a>
</li>
<li>
<a target="_blank" class="hover:underline"
href="/docs/offer_paid_subscription.pdf"
rel="noopener noreferrer nofollow"
>Оферта на платную подписку</a>
</li>
<li>
<a target="_blank" class="hover:underline"
href="/docs/security_policy.pdf"
rel="noopener noreferrer nofollow"
>Политика безопасности</a>
</li>
<li>
<a target="_blank" class="hover:underline"
href="/docs/privacy_policy.pdf"
rel="noopener noreferrer nofollow"
>Политика конфиденциальности</a>
</li>
<li>
<a target="_blank" class="hover:underline"
href="/docs/terms_use.pdf"
rel="noopener noreferrer nofollow"
>Пользовательское соглашение</a>
</li>
<li>
<a target="_blank" class="hover:underline"
href="/docs/personal_data.pdf"
rel="noopener noreferrer nofollow"
>Согласие на обработку персональных данных</a>
</li>
</ul>
</div>
</footer>
</section>
</main>
<section v-if="$page.props.sidebar_layout" ref="userSidebar"
class="lg:mt-16 lg:w-[19rem] xl:w-[26rem] 2xl:w-[32rem] overflow-y-auto lg:block lg:pr-5 2xl:pr-28 transition-transform z-50 absolute top-0 right-0 transform-gpu translate-x-full lg:relative lg:transform-none"
>
<sidebar-secondary />
</section>
</div>
</div>
</div>
</template>
<script>
import FlashMessages from '@/Shared/Misc/FlashMessages.vue'
import HeaderBar from '@/Shared/LayoutParts/HeaderBar.vue'
import Sidebar from '@/Shared/LayoutParts/Sidebar.vue'
import SidebarSecondary from '@/Shared/LayoutParts/SidebarSecondary.vue'
import { onClickOutside } from '@vueuse/core'
import { ref } from 'vue'
export default {
components: {
FlashMessages,
HeaderBar,
Sidebar,
SidebarSecondary,
},
setup(){
const userSidebar = ref(null)
let openSidebar = () => {
if (window.matchMedia('(max-width: 1024px)').matches) {
userSidebar.value.classList.remove('translate-x-full')
userSidebar.value.classList.add('translate-x-0')
userSidebar.value.classList.add('shadow-classic')
}
}
let closeSidebar = () => {
if (window.matchMedia('(max-width: 1024px)').matches) {
userSidebar.value.classList.add('translate-x-full')
userSidebar.value.classList.remove('translate-x-0')
userSidebar.value.classList.remove('shadow-classic')
}
}
if (window.matchMedia('(max-width: 1024px)').matches) {
onClickOutside(userSidebar, () => {
closeSidebar()
})
}
return {
openSidebar,
closeSidebar,
userSidebar
}
},
}
</script>
<style>
.prose-content a{
text-decoration: underline;
}
</style>

View File

@@ -0,0 +1,19 @@
<template>
<div class="header-simple relative z-10 flex items-center justify-center p-3 bg-orange shadow-md">
<inertia-link class="block" href="/">
<img class="block h-12" :src="setLogo()"
alt=""
>
</inertia-link>
</div>
</template>
<script>
export default {
methods: {
setLogo() {
return '/image/logotype.svg'
},
},
}
</script>

View File

@@ -0,0 +1,250 @@
<template>
<header class="main-header-wrp w-full sticky top-0 z-[100]">
<div class="relative z-10 bg-orange shadow-xl">
<!-- contain-content -->
<div
class="
grid grid-cols-12
items-center
pt-2.5
pb-2.5
px-4
sm:px-6
md:space-x-5
lg:space-x-10
"
>
<inertia-link :href="route('dashboard')" class="col-span-1 block flex-shrink-0">
<div class="block md:hidden font-bold text-indigo-300 text-2xl">
T
</div>
<img class="hidden md:block" :src="setLogo()"
alt=""
/>
</inertia-link>
<audio-player />
<search-header ref="searchHeader" />
<div
class="
col-span-4
md:col-span-2
lg:col-span-4
flex
items-center
justify-end
space-x-2
sm:ml-6
sm:space-x-6
"
>
<inertia-link :href="route('setting.money')" class="hidden lg:block default text-sm text-white">
Баланс: {{ $page.props.balance }}
</inertia-link>
<button
type="button"
class="
flex
md:hidden
p-1
rounded-full
items-center
justify-center
text-white
focus:outline-none
"
@click="showSearch"
>
<svg
class="w-5 h-5 md:h-6 md:w-6"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<circle cx="11" cy="11"
r="8"
></circle>
<line x1="21" y1="21"
x2="16.65" y2="16.65"
></line>
</svg>
</button>
<div class="relative">
<div
v-if="$page.props.is_notify"
class="
animate-ping
absolute
top-0
right-1
w-2
h-2
md:w-3
md:h-3
bg-pink
rounded-full
"
></div>
<inertia-link
:href="route('setting.notify')"
class="
flex
p-1
rounded-full
items-center
justify-center
text-white
focus:outline-none
"
>
<svg
class="w-5 h-5 md:h-6 md:w-6"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"></path>
<path d="M13.73 21a2 2 0 0 1-3.46 0"></path>
</svg>
</inertia-link>
</div>
<dropdown class="flex md:hidden user-menu h-12 w-12 rounded-full items-center justify-center focus:outline-none" placement="bottom-start">
<svg class="h-6 w-6 text-white" aria-hidden="true"
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round"
stroke-linejoin="round"
> <line x1="3" y1="12"
x2="21" y2="12"
></line> <line x1="3" y1="6"
x2="21" y2="6"
></line> <line x1="3" y1="18"
x2="21" y2="18"
></line> </svg>
<template #dropdown>
<div class="mobile-user-menu z-50 bg-orange-dark rounded-b-lg">
<div
class="absolute z-50 right-0 top-[-16px] mr-[20px] sm:mr-[20px]"
>
</div>
<ul class="p-6 text-white space-y-6">
<li>
<inertia-link :href="route('setting.money')">
Баланс: {{ $page.props.balance }}
</inertia-link>
</li>
<li>
<inertia-link :href="route('profile.user', $page.props.auth.user.username)">
Профиль
</inertia-link>
</li>
<li>
<inertia-link :href="route('feeds.layoutsidebar')">
Новости
</inertia-link>
</li>
<li>
<inertia-link :href="route('messenger.index')">
<!-- <span class="text-xs rounded-full mr-1"></span> -->
Сообщения
</inertia-link>
</li>
<li>
<inertia-link :href="route('images.index')">
Изображения
</inertia-link>
</li>
<li>
<inertia-link :href="route('videos.index')">
Видео
</inertia-link>
</li>
<li>
<inertia-link :href="route('musics.index')">
Музыка
</inertia-link>
</li>
<li>
<inertia-link :href="route('users.index')">
Пользователи
</inertia-link>
</li>
<li>
<inertia-link :href="route('setting.index')">
Настройки
</inertia-link>
</li>
<li>
<inertia-link as="button" :href="route('logout')"
method="delete"
>
Выйти
</inertia-link>
</li>
</ul>
</div>
</template>
</dropdown>
<div class="hidden md:block relative flex-shrink-0">
<inertia-link
:href="route('profile.user', $page.props.auth.user.username)"
class="
bg-white
rounded-full
flex
text-sm
focus:outline-none
focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500
"
>
<user-avatar :user="$page.props.auth.user" size="small"
class="w-9 h-9 md:h-12 md:w-12 text-base"
/>
</inertia-link>
</div>
</div>
</div>
</div>
</header>
</template>
<script>
import Dropdown from '@/Shared/Form/Dropdown.vue'
import UserAvatar from '@/Shared/Misc/UserAvatar.vue'
import AudioPlayer from '@/Shared/AudioPlayer.vue'
import SearchHeader from '@/Shared/Search/SearchHeader.vue'
export default {
components: {
AudioPlayer,
UserAvatar,
Dropdown,
SearchHeader,
},
methods: {
showSearch(){
this.$refs.searchHeader.openModal()
},
setLogo() {
return '/image/logotype.svg'
},
}
}
</script>

View File

@@ -0,0 +1,107 @@
<template>
<div class="md:py-7 col-span-2 lg:col-span-1 border-r border-indigo-300">
<ul class="flex md:block text-base lg:text-xl xl:text-2xl text-gray-light overflow-x-auto">
<li
:class="[$page.component === 'Settings/SettingsProfile' ?
'md:border-l-4 md:border-b-0 border-b-2 border-pink text-white' :
'hover:text-white' ,'px-6 py-4']"
>
<inertia-link :href="route('setting.index')">
Профиль
</inertia-link>
</li>
<li
:class="[$page.url.startsWith('/settings/purchases') ?
'md:border-l-4 md:border-b-0 border-b-2 border-pink text-white' :
'hover:text-white' ,'px-6 py-4']"
>
<inertia-link :href="route('setting.purchases')">
Покупки
</inertia-link>
</li>
<li
:class="[$page.component === 'Settings/SettingsLikes' ?
'md:border-l-4 md:border-b-0 border-b-2 border-pink text-white' :
'hover:text-white' ,'px-6 py-4']"
>
<inertia-link :href="route('setting.likes')">
Понравилось
</inertia-link>
</li>
<li
:class="[$page.component === 'Settings/SettingsMoney' ?
'md:border-l-4 md:border-b-0 border-b-2 border-pink text-white' :
'hover:text-white' ,'px-6 py-4']"
>
<inertia-link :href="route('setting.money')">
Доход
</inertia-link>
</li>
<li
:class="[$page.component === 'Settings/SettingsTarif' ?
'md:border-l-4 md:border-b-0 border-b-2 border-pink text-white' :
'hover:text-white' ,'px-6 py-4']"
>
<inertia-link :href="route('setting.tarif')">
Тарифы
</inertia-link>
</li>
<li
:class="[$page.component === 'Settings/SettingsNotify' ?
'md:border-l-4 md:border-b-0 border-b-2 border-pink text-white' :
'hover:text-white' ,'px-6 py-4']"
>
<inertia-link :href="route('setting.notify')">
Оповещения
</inertia-link>
</li>
<li
:class="[$page.component === 'Settings/SettingsPayouts' ?
'md:border-l-4 md:border-b-0 border-b-2 border-pink text-white' :
'hover:text-white' ,'px-6 py-4']"
>
<inertia-link :href="route('setting.payouts')">
Выплаты
</inertia-link>
</li>
<li
:class="[$page.component === 'Settings/SettingsPacket' ?
'md:border-l-4 md:border-b-0 border-b-2 border-pink text-white' :
'hover:text-white' ,'px-6 py-4']"
>
<inertia-link :href="route('setting.packet')">
Платные подписки
</inertia-link>
</li>
<li
:class="[$page.component === 'Settings/SettingsVerification' ?
'md:border-l-4 md:border-b-0 border-b-2 border-pink text-white' :
'hover:text-white' ,'px-6 py-4']"
>
<inertia-link :href="route('setting.verification')">
Верификация
</inertia-link>
</li>
<li
:class="[$page.component === 'Settings/SettingsDocuments' ?
'md:border-l-4 md:border-b-0 border-b-2 border-pink text-white' :
'hover:text-white' ,'px-6 py-4']"
>
<inertia-link :href="route('setting.documents')">
Документы
</inertia-link>
</li>
<li
:class="[$page.component === 'Settings/SettingsWriteToUs' ?
'md:border-l-4 md:border-b-0 border-b-2 border-pink text-white' :
'hover:text-white' ,'px-6 py-4']"
>
<inertia-link :href="route('setting.write-to-us')">
Написать нам
</inertia-link>
</li>
</ul>
</div>
</template>

View File

@@ -0,0 +1,148 @@
<template>
<div class="main-sidebar-wrp hidden w-28 bg-indigo-200 overflow-y-auto md:block">
<div class="w-full py-6 flex flex-col items-center">
<div class="flex-1 w-full px-2 space-y-1">
<inertia-link :class="$page.url.startsWith('/profile') ? 'text-white group w-full p-3 rounded-md flex flex-col items-center text-xs font-medium' : 'text-gray-light hover:bg-orange hover:text-white group w-full p-3 rounded-md flex flex-col items-center text-xs' " :href="route('profile.user', $page.props.auth.user.username)">
<svg :class="$page.url.startsWith('/profile') ? 'text-white h-6 w-6' : 'text-indigo-300 group-hover:text-white h-6 w-6' " xmlns="http://www.w3.org/2000/svg"
fill="none" viewBox="0 0 24 24"
stroke="currentColor" aria-hidden="true"
>
<path stroke-linecap="round" stroke-linejoin="round"
stroke-width="2"
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"
/>
</svg>
<span class="mt-2">Профиль</span>
</inertia-link>
<inertia-link :href="route('setting.money')"
class="lg:hidden text-gray-light hover:bg-orange hover:text-white group w-full p-3 rounded-md flex flex-col items-center text-xs"
>
<svg class="text-green group-hover:text-white h-6 w-6" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 440 440" fill="currentColor"
stroke-width="2"
stroke-linecap="round" stroke-linejoin="round"
><path d="M232.522 242.428c63.913 0 115.91-54.382 115.91-121.227C348.432 54.37 296.435 0 232.522 0H120.568v282.428h-29v30h29V440h30V312.428h101.955v-30H150.568v-40h81.954zM150.568 30h81.955c47.371 0 85.91 40.912 85.91 91.201 0 50.303-38.539 91.227-85.91 91.227h-81.955V30z" /></svg>
<span class="mt-2">{{ $page.props.balance }}</span>
</inertia-link>
<inertia-link class="relative" :class="$page.component === 'Messenger/Index' ? 'text-white group w-full p-3 rounded-md flex flex-col items-center text-xs font-medium' : 'text-gray-light hover:bg-orange hover:text-white group w-full p-3 rounded-md flex flex-col items-center text-xs' "
:href="route('messenger.index')"
>
<div v-if="$page.props.message_reading_count" class="absolute top-1 right-1 w-2 h-2 md:w-5 md:h-5 bg-pink rounded-full text-xs flex justify-center items-center text-white">
{{ $page.props.message_reading_count }}
</div>
<svg :class="$page.component === 'Messenger/Index' ? 'text-white h-6 w-6' : 'text-indigo-300 group-hover:text-white h-6 w-6' " xmlns="http://www.w3.org/2000/svg"
width="24" height="24"
viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round"
><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path></svg>
<span class="mt-2">Сообщения</span>
</inertia-link>
<inertia-link :class="$page.component === 'Feed/Index' ? 'text-white group w-full p-3 rounded-md flex flex-col items-center text-xs font-medium' : 'text-gray-light hover:bg-orange hover:text-white group w-full p-3 rounded-md flex flex-col items-center text-xs' " :href="route('feeds.layoutsidebar')">
<svg :class="$page.component === 'Feed/Index' ? 'text-white h-6 w-6' : 'text-indigo-300 group-hover:text-white h-6 w-6' " xmlns="http://www.w3.org/2000/svg"
fill="none" viewBox="0 0 24 24"
stroke="currentColor" aria-hidden="true"
>
<path stroke-linecap="round" stroke-linejoin="round"
stroke-width="2"
d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z"
/>
</svg>
<span class="mt-2">Новости</span>
</inertia-link>
<inertia-link :class="$page.component === 'Image/Index' ? 'text-white group w-full p-3 rounded-md flex flex-col items-center text-xs font-medium' : 'text-gray-light hover:bg-orange hover:text-white group w-full p-3 rounded-md flex flex-col items-center text-xs' " :href="route('images.index')">
<svg :class="$page.component === 'Image/Index' ? 'text-white h-6 w-6' : 'text-indigo-300 group-hover:text-white h-6 w-6' " xmlns="http://www.w3.org/2000/svg"
fill="none" viewBox="0 0 24 24"
stroke="currentColor" aria-hidden="true"
>
<path stroke-linecap="round" stroke-linejoin="round"
stroke-width="2"
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
<span class="mt-2">Изображения</span>
</inertia-link>
<inertia-link :class="$page.component === 'Video/Feed' ? 'text-white group w-full p-3 rounded-md flex flex-col items-center text-xs font-medium' : 'text-gray-light hover:bg-orange hover:text-white group w-full p-3 rounded-md flex flex-col items-center text-xs' " :href="route('videos.index')">
<svg :class="$page.component === 'Video/Feed' ? 'text-white h-6 w-6' : 'text-indigo-300 group-hover:text-white h-6 w-6' " xmlns="http://www.w3.org/2000/svg"
fill="none" viewBox="0 0 24 24"
stroke="currentColor" aria-hidden="true"
>
<path stroke-linecap="round" stroke-linejoin="round"
stroke-width="2" d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"
/>
</svg>
<span class="mt-2">Видео</span>
</inertia-link>
<inertia-link :class="$page.component === 'Music/Feed' ? 'text-white group w-full p-3 rounded-md flex flex-col items-center text-xs font-medium' : 'text-gray-light hover:bg-orange hover:text-white group w-full p-3 rounded-md flex flex-col items-center text-xs' " :href="route('musics.index')">
<svg :class="$page.component === 'Music/Feed' ? 'text-white h-6 w-6' : 'text-indigo-300 group-hover:text-white h-6 w-6' " xmlns="http://www.w3.org/2000/svg"
fill="none" viewBox="0 0 24 24"
stroke="currentColor" aria-hidden="true"
>
<path stroke-linecap="round" stroke-linejoin="round"
stroke-width="2" d="M9 19V6l12-3v13M9 19c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zm12-3c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zM9 10l12-3"
/>
</svg>
<span class="mt-2">Музыка</span>
</inertia-link>
<inertia-link :class="$page.component === 'User/Index' ? 'text-white group w-full p-3 rounded-md flex flex-col items-center text-xs font-medium' : 'text-gray-light hover:bg-orange hover:text-white group w-full p-3 rounded-md flex flex-col items-center text-xs' " :href="route('users.index')">
<svg :class="$page.component === 'User/Index' ? 'text-white h-6 w-6' : 'text-indigo-300 group-hover:text-white h-6 w-6' " xmlns="http://www.w3.org/2000/svg"
fill="none" viewBox="0 0 24 24"
stroke="currentColor" aria-hidden="true"
>
<path stroke-linecap="round" stroke-linejoin="round"
stroke-width="2"
d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"
/>
</svg>
<span class="mt-2">Пользователи</span>
</inertia-link>
<inertia-link :class="$page.component.startsWith('Settings') ? 'text-white group w-full p-3 rounded-md flex flex-col items-center text-xs font-medium' : 'text-gray-light hover:bg-orange hover:text-white group w-full p-3 rounded-md flex flex-col items-center text-xs' " :href="route('setting.index')">
<svg :class="$page.component.startsWith('Settings') ? 'text-white h-6 w-6' : 'text-indigo-300 group-hover:text-white h-6 w-6' " xmlns="http://www.w3.org/2000/svg"
fill="none" viewBox="0 0 24 24"
stroke="currentColor" aria-hidden="true"
>
<path stroke-linecap="round" stroke-linejoin="round"
stroke-width="2"
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
/><path stroke-linecap="round" stroke-linejoin="round"
stroke-width="2"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
<span class="mt-2">Настройки</span>
</inertia-link>
<inertia-link
as="button"
:href="route('logout')" method="delete"
class="relative text-gray-light hover:bg-orange hover:text-white group w-full p-3 rounded-md flex flex-col items-center text-xs"
>
<svg class="text-indigo-300 group-hover:text-white h-6 w-6" xmlns="http://www.w3.org/2000/svg"
width="24" height="24"
viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round"
><path stroke-linecap="round" stroke-linejoin="round"
stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
/></svg>
<span class="mt-2">Выйти</span>
</inertia-link>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,45 @@
<template>
<div class="bg-indigo-200 shadow-classic rounded-md p-5">
<span class="block text-white font-medium">Лидеры</span>
<div v-if="leaderUsers.length" class="mt-5 space-y-5">
<div v-for="leaderUser in leaderUsers" :key="leaderUser.id"
class="flex items-center"
>
<div class="flex-shrink-0 mr-2 md:mr-4">
<inertia-link :href="route('profile.user', leaderUser.username)" class="flex-shrink-0 block mr-2 md:mr-4">
<user-avatar :user="leaderUser" size="small"
class="w-10 h-10 md:w-14 md:h-14 text-lg"
/>
</inertia-link>
</div>
<div class="flex flex-col">
<inertia-link :href="route('profile.user', leaderUser.username)" class="hover:underline text-sm md:text-base block text-white">
{{ leaderUser.name }}
</inertia-link>
<span class="text-xs text-gray-light">Кол. голосов: {{ leaderUser.countVote }}</span>
</div>
</div>
</div>
<div v-else class="text-center mt-3 text-lg text-gray-light">
Лидер не определен
</div>
</div>
</template>
<script>
import { usePage } from '@inertiajs/inertia-vue3'
import UserAvatar from '@/Shared/Misc/UserAvatar.vue'
export default {
components: {
UserAvatar,
},
computed: {
leaderUsers() {
return usePage().props.value.leaders
},
}
}
</script>

View File

@@ -0,0 +1,61 @@
<script setup>
import { useInfinityScroll } from '@/includes/composables/useInfinityScroll'
import MessangerModal from '@/Shared/Messanger/MessangerModal.vue'
import UserAvatar from '@/Shared/Misc/UserAvatar.vue'
import UserBanner from '@/Shared/Misc/UserBanner.vue'
import axios from 'axios'
import debounce from 'lodash/debounce'
import { ref, watch } from 'vue'
const search = ref('')
const refMessangerModal = ref()
const { domNodeFn, cursor, loading, error, callAPI, data: userLists } = useInfinityScroll(async () => {
const url = search.value ? `user-friend/?search=${search.value}&cursor=${cursor.value}` : `user-friend/?cursor=${cursor.value}`
const res = await axios.get(url)
return res.data
})
callAPI()
watch(search, debounce(() => {
callAPI()
}, 500))
function openModalMessanger(user) {
refMessangerModal.value.openAction(user)
}
</script>
<template>
<div v-if="userLists" class="mt-5 grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 cards-block rounded-md bg-indigo-200 grid gap-2 lg:gap-4 grid-cards ">
<messanger-modal ref="refMessangerModal" />
<div
v-for="user in userLists"
:ref="el => domNodeFn(el, user.id)"
:key="user.id"
class="user-card relative cursor-pointer"
@click="openModalMessanger(user)"
>
<div class="absolute inset-x-0 bottom-4 z-10 flex justify-center">
<div class="flex flex-col items-center">
<div class="flex-shrink-0">
<user-avatar :user="user" size="small"
class="border border-white shadow-classic h-20 w-20 text-lg"
/>
</div>
<div class="mt-2 text-white text-sm text-center">
<p class="">
{{ user.name }}
</p>
<p class="text-xs">
{{ user.username }}
</p>
</div>
</div>
</div>
<div class="gradient-profile relative overflow-hidden">
<user-banner class="h-72 bg-indigo-300" :user="user"
size="banner"
/>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,285 @@
<template>
<div v-if="user" class="flex flex-col h-full">
<board-top :user="user">
<template v-if="partnerIsBanned !== null" #menu>
<button class="default text-gray text-sm underline" @click="bannRequest">
{{ isBannedMessage }}
</button>
<!-- <button class="default mt-2 text-gray text-sm underline" @click="exitRoom">
Покинуть беседу
</button> -->
</template>
</board-top>
<div class="px-2 sm:px-5 xl:px-10 flex bg-indigo-200 border-b border-indigo-300">
<div class="flex-1 pt-5 pb-3">
<textarea ref="textarea" v-model="textMessage"
rows="3"
autofocus
name="comment" class="placeholder-gray bg-indigo-200 text-white max-h-56 block w-full py-3 border-0 resize-none focus:ring-0 text-sm sm:text-base"
placeholder="Напишите сообщение ..."
:disabled="frozen"
@keyup.ctrl.enter="createMessage"
/>
</div>
<div v-if="!frozen" class="mt-5 ml-3 flex-shrink-0 text-pink"
@click="createMessage"
>
<button class="default">
<svg class="w-8 h-8 flex-shrink-0">
<use xlink:href="#arrow-circle"></use>
</svg>
</button>
</div>
</div>
<div class="py-5 lg:py-10 px-2 sm:px-5 xl:px-10 space-y-5 flex-1 bg-indigo-400">
<div v-if="frozen" class="p-3 text-sm rounded-md bg-orange text-white text-center">
Вы не можете отправить сообщение пользователю
</div>
<div v-if="error" class="p-3 text-sm rounded-md bg-pink text-white text-center">
Ошибка загрузки данных
</div>
<div v-for="(messageCollection, key) in messages" :key="key"
class="space-y-5"
>
<div class="text-gray-light text-xs text-right">
{{ key }}
</div>
<board-message-item v-for="message in messageCollection"
:key="message.id"
:ref="el => domNodeFn(el, message.id)"
:profile-id="user.id"
:user-message="message"
@remove="removeMessage"
/>
</div>
<div v-if="loading" class="flex items-center justify-center">
<svg class="animate-spin h-8 w-8 text-white" xmlns="http://www.w3.org/2000/svg"
fill="none" viewBox="0 0 24 24"
>
<circle class="opacity-25" cx="12"
cy="12" r="10"
stroke="currentColor" stroke-width="4"
></circle>
<path class="opacity-75" fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
</div>
</div>
</div>
<div v-else class="p-10 text-center">
<h2 class="text-lg xl:text-3xl font-bold text-indigo-100">
Выберите пользователя
<board-friend-list />
</h2>
</div>
</template>
<script>
import { useAutoresizeTextarea } from '@/includes/composables'
import { useInfinityScroll } from '@/includes/composables/useInfinityScroll'
import { ref, watch, toRef, computed } from 'vue'
import { Inertia } from '@inertiajs/inertia'
import axios from 'axios'
import BoardMessageItem from './BoardMessageItem.vue'
import BoardTop from './BoardTop.vue'
import BoardFriendList from './BoardFriendList.vue'
export default {
components:{
BoardTop,
BoardMessageItem,
BoardFriendList
},
props: {
user: {
type: Object,
default: () => ({})
},
roomId: {
type: Number,
default: 0
},
frozen: {
type: Boolean,
default: false
},
search: {
type: String,
default: ''
}
},
emits: {
'disable': null,
'last-message': null,
'change-room': null,
'leave-room': null,
},
setup(props, {emit}){
const roomId = toRef(props, 'roomId')
const partnerIsBanned = ref(null)
const textarea = ref()
const textMessage = ref()
useAutoresizeTextarea(textarea)
const { domNodeFn, cursor, loading, error, refetchApi, data: messages } = useInfinityScroll(async () => {
const url = props.search ? `messenger/${roomId.value}?search=${props.search}&cursor=${cursor.value}` : `messenger/${roomId.value}?cursor=${cursor.value}`
const res = await axios.get(url)
return res.data
})
const checkRequestBanned = () => {
axios.post(route('messenger.banned.user'), {
params: {
user: props.user.id,
}
}).then(response => {
partnerIsBanned.value = response.data.banned
})
}
watch(roomId, (id) => {
if(id){
refetchApi()
checkRequestBanned()
partnerIsBanned.value = null
}
if(textarea.value){
textarea.value.focus()
}
}, { immediate: true })
watch(loading, (loading) => {
emit('disable', loading)
})
const createMessage = () => {
if(!textMessage.value.trim()){
alert('Введите сообщение')
return
}
axios.post('messenger', {
params: {
message: textMessage.value,
room: roomId.value,
}
}).then(response => {
if(!response.data){
alert('Вы не можете отправить сообщение данному пользователю')
return
}
textMessage.value = ''
const k = response.data.keyGroupDate // keyGroupDate - ключ, eg 28.11.21
if(messages.value[k]){
messages.value[k].unshift(response.data)
}else{
messages.value = { [k]: [ response.data ], ...messages.value }
}
emit('last-message', response.data)
})
}
const bannRequest = () => {
Inertia.post(route('messenger.banned.room'), { room: roomId.value },
{
preserveState: true,
onSuccess: () => {
checkRequestBanned()
emit('change-room', roomId.value)
}
})
}
const removeMessage = (message) => {
// console.log(id)
removeMessageRequest(message)
}
const removeMessageRequest = (message) => {
const group = messages.value[message.group]
const count = group.length
const index = messages.value[message.group].findIndex((item) => item.id === message.id)
axios.delete(route('messenger.message.remove', {'id': message.id})).then(response => {
if(count === 1){
delete messages.value[message.group]
}else{
// delete messages.value[message.group][index]
messages.value[message.group] = messages.value[message.group].filter((i) => i.id != messages.value[message.group][index].id)
}
})
}
// const removeMessageRequest = (message) => {
// // console.log(message)
// // console.log(messages.value)
// const group = messages.value[message.group]
// const count = group.length
// const index = messages.value[message.group].findIndex((item) => item.id === message.id)
// Inertia.delete(route('messenger.message.remove', {'id': message.id}),
// {
// preserveState: true,
// onSuccess: () => {
// if(count === 1){
// delete messages.value[message.group]
// }else{
// delete messages.value[message.group][index]
// }
// }
// })
// }
// const exitRoom = () => {
// Inertia.post(route('messenger.leave.room'), { room: roomId.value },
// {
// preserveState: true,
// onSuccess: () => {
// emit('leave-room')
// }
// })
// }
const isBannedMessage = computed(() => {
if(partnerIsBanned.value === null){
return false
}
if(partnerIsBanned.value){
return 'Убрать из черного списка'
}
return 'Добавить в черный список'
})
return {
textarea,
textMessage,
loading,
messages,
createMessage,
error,
domNodeFn,
bannRequest,
isBannedMessage,
partnerIsBanned,
removeMessage
}
}
}
</script>

View File

@@ -0,0 +1,63 @@
<template>
<div class="flex">
<user-avatar :user="userMessage.user" size="small"
class="flex-shrink-0 w-10 h-10 lg:w-12 lg:h-12 xl:w-14 xl:h-14 text-sm xl:text-lg"
/>
<div class="ml-5 flex bg-indigo-100 p-4 rounded shadow relative">
<div v-if="isAuthorMessage" class="flex absolute top-px right-px">
<button @click="remove">
<svg class="text-red w-3.5 h-3.5" xmlns="http://www.w3.org/2000/svg"
width="24"
height="24" viewBox="0 0 24 24"
fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round"
stroke-linejoin="round"
><line x1="18" y1="6"
x2="6" y2="18"
></line><line x1="6" y1="6"
x2="18" y2="18"
></line></svg>
</button>
</div>
<div class="text-sm text-white break-all">
<footer class="font-bold mb-2 text-gray-light flex items-center">
{{ userMessage.user.first_name }}
<span class="font-normal text-xs text-gray ml-4 lg:ml-8 flex-shrink-0">{{ userMessage.created }}</span>
</footer>
{{ userMessage.message }}
</div>
</div>
</div>
</template>
<script>
import UserAvatar from '@/Shared/Misc/UserAvatar.vue'
export default {
components:{
UserAvatar,
},
props: {
userMessage: {
type: Object,
default: () => {}
},
profileId: {
type: Number,
default: 0
},
},
emits: {
'remove': null
},
computed: {
isAuthorMessage() {
return this.profileId != this.userMessage.user.id
},
},
methods: {
remove(){
this.$emit('remove', this.userMessage)
}
}
}
</script>

View File

@@ -0,0 +1,81 @@
<template>
<div :class="{
'bg-purple': !room.is_my_message && !room.is_reading
}" class="relative flex items-center px-2 xl:px-8 hover:bg-indigo-300 cursor-pointer transition-colors"
@click="selectRoom(room)"
>
<div v-if="!room.is_reading && !room.is_my_message" class="absolute bottom-1 right-3 text-xxs text-white">
<span>не прочитано</span>
</div>
<div v-if="room.is_my_message" class="absolute bottom-0 right-3">
<div v-show="!room.is_reading" class="flex relative w-[30px] h-[24px] text-gray-light">
<svg xmlns="http://www.w3.org/2000/svg"
width="16"
height="16" viewBox="0 0 24 24"
fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" class="absolute top-0 right-0"
><polyline points="20 6 9 17 4 12"></polyline></svg>
</div>
<div v-show="room.is_reading" class="flex relative w-[30px] h-[24px] text-orange">
<svg xmlns="http://www.w3.org/2000/svg"
width="16"
height="16" viewBox="0 0 24 24"
fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" class="absolute top-0 right-[5px]"
><polyline points="20 6 9 17 4 12"></polyline></svg>
<svg xmlns="http://www.w3.org/2000/svg"
width="16"
height="16" viewBox="0 0 24 24"
fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" class="absolute top-0 right-0"
><polyline points="20 6 9 17 4 12"></polyline></svg>
</div>
</div>
<div class="absolute top-2 right-2 text-xxs lg:text-xs text-gray-light">
{{ room.updated_at_human }}
</div>
<user-avatar :user="room.correspond" size="small"
class="flex-shrink-0 w-10 h-10 lg:w-12 lg:h-12 xl:w-14 xl:h-14 text-sm xl:text-lg"
/>
<div class="ml-3 flex flex-col w-4/5 overflow-hidden">
<span class="text-sm lg:text-base block text-white">{{ room.correspond.name }}</span>
<span class="text-xs lg:text-sm truncate text-gray-light">
<span v-if="room.is_my_message" class="text-orange">Вы:</span>
{{ room.message }}
</span>
</div>
</div>
</template>
<script>
import UserAvatar from '@/Shared/Misc/UserAvatar.vue'
export default {
components: {
UserAvatar,
},
props: {
room: {
type: Object,
default: () => {}
},
},
emits:{
'select-room': null
},
setup(props, {emit}){
const selectRoom = (room) => {
emit('select-room', room)
}
return {
selectRoom
}
},
}
</script>

View File

@@ -0,0 +1,56 @@
<template>
<div>
<div class="gradient-profile relative border-b border-indigo-300">
<user-banner class="h-24 xl:h-40 bg-indigo-200" :user="user"
size="hero"
/>
</div>
<div class="-mt-14 xl:-mt-24 relative xl:container xl:mx-auto px-5 xl:px-10">
<div class="flex flex-col md:flex-row">
<div class="flex-shrink-0 self-center md:self-start md:mr-6 2xl:mr-10">
<user-avatar :user="user" size="medium"
class="shadow-classic object-cover w-28 h-28 text-xl xl:w-48 xl:h-48 xl:text-3xl"
/>
</div>
<div class="w-full">
<div class="h-12 xl:h-24 hidden md:block"></div>
<div class="mt-2 ">
<div class="-mx-2 -my-2 lg:-mx-4 lg:-my-4 flex flex-col md:flex-row flex-wrap xl:flex-nowrap justify-between">
<div class="max-w-[300px] text-center md:text-left mx-2 my-2 lg:mx-4 lg:my-4 flex flex-shrink-0 flex-col self-center md:self-start">
<inertia-link :href="route('profile.user', user.username)" class="text-lg xl:text-2xl block font-semibold text-white">
{{ user.name }}
</inertia-link>
<h2 class="text-sm xl:text-base text-gray-light">
@{{ user.username }}
</h2>
</div>
<div class="mx-2 my-2 lg:mx-4 lg:my-4 flex flex-col items-end">
<slot name="menu" />
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import UserAvatar from '@/Shared/Misc/UserAvatar.vue'
import UserBanner from '@/Shared/Misc/UserBanner.vue'
export default {
components:{
UserAvatar,
UserBanner
},
props: {
user: {
type: Object,
default: () => {}
}
}
}
</script>

View File

@@ -0,0 +1,187 @@
<template>
<TransitionRoot as="template" :show="open">
<Dialog as="div" static
class="fixed z-[1000] inset-0 overflow-y-auto" :open="open"
@close="closeAction"
>
<div class="flex items-center justify-center min-h-screen pt-4 px-4 pb-4 text-center sm:block sm:p-0">
<TransitionChild as="template" enter="ease-out duration-300"
enter-from="opacity-0" enter-to="opacity-100"
leave="ease-in duration-200" leave-from="opacity-100"
leave-to="opacity-0"
>
<DialogOverlay class="fixed inset-0 bg-indigo-200 bg-opacity-75 transition-opacity" />
</TransitionChild>
<span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">&#8203;</span>
<TransitionChild as="template" enter="ease-out duration-300"
enter-from="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" enter-to="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200" leave-from="opacity-100 translate-y-0 sm:scale-100"
leave-to="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<div class="inline-block align-bottom bg-indigo-300 rounded-lg px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle w-full sm:max-w-lg sm:w-full sm:p-6">
<div class="sm:flex sm:items-center">
<div class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-orange sm:mx-0 sm:h-10 sm:w-10">
<ChatIcon class="h-6 w-6 text-red-600" aria-hidden="true" />
</div>
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<DialogTitle as="h3" class="text-lg leading-6 font-medium text-gray-light">
Новое сообщение
</DialogTitle>
</div>
<div v-if="showDialogLink" class="ml-auto">
<a href="#" class="underline text-gray text-xs"
@click.prevent="linkTo"
>перейти в чат</a>
</div>
</div>
<div class="mt-5">
<textarea ref="textarea" v-model="form.message"
rows="3"
name="comment" class="outline-none focus:ring-orange-dark focus:border-transparent focus:ring-offset-0 bg-indigo-200 text-white max-h-56 block w-full py-3 border-0 resize-none sm:text-sm"
placeholder="Сообщение ..."
/>
</div>
<div class="mt-5 sm:mt-4 sm:flex sm:flex-row space-x-3 justify-end">
<button type="button"
class="my-1 transition shadow-none hover:shadow-classic inline-flex items-center px-6 py-2 justify-center text-base rounded-md text-white bg-indigo-300 focus:outline-none"
@click="closeAction"
>
Отменить
</button>
<button type="button"
class="my-1 transition shadow-none hover:shadow-classic inline-flex items-center px-6 py-2 justify-center text-base rounded-md text-white bg-green focus:outline-none"
@click="submit"
>
Отправить
</button>
</div>
</div>
</TransitionChild>
</div>
</Dialog>
</TransitionRoot>
</template>
<script>
import {
Dialog,
DialogOverlay,
DialogTitle,
TransitionChild,
TransitionRoot,
} from '@headlessui/vue'
import { ChatIcon } from '@heroicons/vue/outline'
import { ref } from 'vue'
import { useAutoresizeTextarea } from '@/includes/composables'
import axios from 'axios'
import { useForm } from '@inertiajs/inertia-vue3'
import { Inertia } from '@inertiajs/inertia'
export default {
components: {
Dialog,
DialogOverlay,
DialogTitle,
TransitionChild,
TransitionRoot,
ChatIcon,
},
setup(props, {expose}) {
const form = useForm({
message: null,
chat_room: null,
user_id: null,
})
const textarea = ref()
const user = ref()
const open = ref(false)
const chatCheck = ref(false)
const showDialogLink = ref(false)
const openAction = (extUser) => {
open.value = true
user.value = extUser
form.user_id = extUser.id
if(!chatCheck.value){
exsistChatRequest(user.value)
}
}
const closeAction = () => {
open.value = false
}
const exsistChatRequest = (user) => {
chatCheck.value = true
axios.post('/messenger-check-room', {
params: {
user: user.id,
}
}).then(response => {
if(response.data > 0){
showDialogLink.value = 1
form.chat_room = response.data
}
})
}
const submit = () => {
if(!form.message){
alert('Введите сообщение')
return
}
if(form.chat_room){
createMessage()
}else{
createRoom()
}
}
const createMessage = () => {
form.post(route('messenger.store.profile'), {
onSuccess: () => {
closeAction()
form.message = ''
linkTo()
},
})
}
const createRoom = () => {
form.post(route('messenger.create.room'), {
onSuccess: () => {
closeAction()
showDialogLink.value = 1
form.message = ''
linkTo()
},
})
}
const linkTo = () => {
Inertia.visit(`/messenger?user=${user.value.username}`, { method: 'get' })
}
useAutoresizeTextarea(textarea)
expose({
openAction
})
return {
form,
textarea,
open,
closeAction,
user,
showDialogLink,
submit,
linkTo,
}
},
}
</script>

View File

@@ -0,0 +1,18 @@
<template>
<Head :title="title ? `${title} - Тизер` : 'Тизер'">
<slot />
</Head>
</template>
<script>
import { Head } from "@inertiajs/inertia-vue3";
export default {
components: {
Head,
},
props: {
title: String,
},
};
</script>

View File

@@ -0,0 +1,20 @@
<template>
<div class="flex items-center text-gray-light">
<svg class="w-5 h-5 flex-shrink-0">
<use xlink:href="#message-circle"></use>
</svg>
<span v-show="comments" class="ml-2 text-sm">{{
comments
}}</span>
</div>
</template>
<script>
export default {
components: {
},
props: {
comments: Number,
},
};
</script>

View File

@@ -0,0 +1,19 @@
<template>
<div class="-mx-1">
<inertia-link
:href="route('feed.tags', tag.slug)"
class="inline-flex rounded-full items-center mx-1 py-0.5 px-2.5 text-sm font-medium bg-orange text-indigo-300"
v-for="tag in tags" :key="tag.id">
{{tag.name}}
</inertia-link>
</div>
</template>
<script>
export default {
components: {},
props: {
tags: Array,
},
};
</script>

View File

@@ -0,0 +1,25 @@
<template>
<inertia-link :href="route('feed.tags', slug)"
:class="[$page.component === 'Tag/Feed' ? 'bg-orange shadow-classic2' : 'shadow-classic bg-indigo-200 hover:bg-orange' , 'transition inline-flex items-center px-3 py-3 lg:px-6 lg:py-6 xl:px-10 text-sm lg:text-lg justify-center rounded-md text-white focus:outline-none']">
Все
</inertia-link>
<inertia-link :href="route('list.images.tag', slug)"
:class="[$page.component === 'Image/Index' ? 'bg-orange shadow-classic2' : 'shadow-classic bg-indigo-200 hover:bg-orange' , 'transition inline-flex items-center px-3 py-3 lg:px-6 lg:py-6 xl:px-10 text-sm lg:text-lg justify-center rounded-md text-white focus:outline-none']">
Изображения
</inertia-link>
<inertia-link :href="route('list.musics.tag', slug)"
:class="[$page.component === 'Music/Feed' ? 'bg-orange shadow-classic2' : 'shadow-classic bg-indigo-200 hover:bg-orange' , 'transition inline-flex items-center px-3 py-3 lg:px-6 lg:py-6 xl:px-10 text-sm lg:text-lg justify-center rounded-md text-white focus:outline-none']">
Музыка
</inertia-link>
<inertia-link :href="route('list.videos.tag', slug)"
:class="[$page.component === 'Video/Feed' ? 'bg-orange shadow-classic2' : 'shadow-classic bg-indigo-200 hover:bg-orange' , 'transition inline-flex items-center px-3 py-3 lg:px-6 lg:py-6 xl:px-10 text-sm lg:text-lg justify-center rounded-md text-white focus:outline-none']">
Видео
</inertia-link>
</template>
<script>
export default {
props: {
slug: String,
},
}
</script>

View File

@@ -0,0 +1,76 @@
<template>
<div class="absolute left-0 right-0 z-[100]">
<div v-if="$page.props.flash.success && show" class="mx-auto mt-4 mb-4 flex items-center justify-between bg-green rounded max-w-3xl">
<div class="flex items-center">
<svg class="ml-4 mr-2 flex-shrink-0 w-4 h-4 fill-white" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
><polygon points="0 11 2 9 7 14 18 3 20 5 7 18" /></svg>
<div class="py-4 text-white text-sm font-medium">
{{ $page.props.flash.success }}
</div>
</div>
<button type="button" class="group mr-2 p-2"
@click="show = false"
>
<svg class="block w-2 h-2 fill-green-800 group-hover:fill-white" xmlns="http://www.w3.org/2000/svg"
width="235.908" height="235.908"
viewBox="278.046 126.846 235.908 235.908"
><path d="M506.784 134.017c-9.56-9.56-25.06-9.56-34.62 0L396 210.18l-76.164-76.164c-9.56-9.56-25.06-9.56-34.62 0-9.56 9.56-9.56 25.06 0 34.62L361.38 244.8l-76.164 76.165c-9.56 9.56-9.56 25.06 0 34.62 9.56 9.56 25.06 9.56 34.62 0L396 279.42l76.164 76.165c9.56 9.56 25.06 9.56 34.62 0 9.56-9.56 9.56-25.06 0-34.62L430.62 244.8l76.164-76.163c9.56-9.56 9.56-25.06 0-34.62z" /></svg>
</button>
</div>
<div v-if="($page.props.flash.error || Object.keys($page.props.errors).length > 0) && show" class="mx-auto mt-4 mb-4 flex items-center justify-between bg-red rounded max-w-3xl">
<div class="flex items-center">
<svg class="ml-4 mr-2 flex-shrink-0 w-4 h-4 fill-white" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
><path d="M2.93 17.07A10 10 0 1 1 17.07 2.93 10 10 0 0 1 2.93 17.07zm1.41-1.41A8 8 0 1 0 15.66 4.34 8 8 0 0 0 4.34 15.66zm9.9-8.49L11.41 10l2.83 2.83-1.41 1.41L10 11.41l-2.83 2.83-1.41-1.41L8.59 10 5.76 7.17l1.41-1.41L10 8.59l2.83-2.83 1.41 1.41z" /></svg>
<div v-if="$page.props.flash.error" class="py-4 text-white text-sm font-medium">
{{ $page.props.flash.error }}
</div>
<div v-else class="py-4 text-white text-sm font-medium">
<span v-if="Object.keys($page.props.errors).length === 1">There is one form error.</span>
<span v-else>There are {{ Object.keys($page.props.errors).length }} form errors.</span>
</div>
</div>
<button type="button" class="group mr-2 p-2"
@click="show = false"
>
<svg class="block w-2 h-2 fill-red-800 group-hover:fill-white" xmlns="http://www.w3.org/2000/svg"
width="235.908" height="235.908"
viewBox="278.046 126.846 235.908 235.908"
><path d="M506.784 134.017c-9.56-9.56-25.06-9.56-34.62 0L396 210.18l-76.164-76.164c-9.56-9.56-25.06-9.56-34.62 0-9.56 9.56-9.56 25.06 0 34.62L361.38 244.8l-76.164 76.165c-9.56 9.56-9.56 25.06 0 34.62 9.56 9.56 25.06 9.56 34.62 0L396 279.42l76.164 76.165c9.56 9.56 25.06 9.56 34.62 0 9.56-9.56 9.56-25.06 0-34.62L430.62 244.8l76.164-76.163c9.56-9.56 9.56-25.06 0-34.62z" /></svg>
</button>
</div>
</div>
</template>
<script>
//import { ref, watch } from 'vue'
export default {
// setup() {
// const show = ref(true)
// watch('$page.props.flash', function(){
// this.show = true
// });
// return {
// show,
// };
// },
data() {
return {
show: true,
}
},
watch: {
'$page.props.flash': {
handler() {
this.show = true
},
deep: true,
},
},
}
</script>

View File

@@ -0,0 +1,126 @@
<template>
<slot v-bind="$attrs" />
<slot v-if="loading" name="loading"
:text="loadText"
>
<div v-if="loading" class="col-span-full flex justify-center">
<svg class="flex-shrink-0 animate-spin h-6 w-6 text-white" xmlns="http://www.w3.org/2000/svg"
fill="none" viewBox="0 0 24 24"
> <circle class="opacity-25" cx="12"
cy="12" r="10"
stroke="currentColor" stroke-width="4"
></circle><path class="opacity-75" fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path></svg>
</div>
</slot>
</template>
<script>
import { ref, onUpdated, onUnmounted, watch } from 'vue'
import axios from 'axios'
import { usePage } from '@inertiajs/inertia-vue3'
export default {
props: {
nodeElement: Object,
loadText: {
type: String,
default: 'Загрузка ...',
},
nextCursor: { type: String, default: '' },
},
emits: ['fromPagination'],
setup(props, { emit }) {
const cursor = ref(props.nextCursor)
const loading = ref(false)
let installObserver = false
let observer = null
// watch(props, (props) => {
// observer?.disconnect();
// createObserver();
// });
const loadMore = () => {
loading.value = true
if (!cursor.value) {
return Promise.resolve(null)
}
return new Promise((resolve) => {
axios
.get(usePage().url.value, { params: { cursor: cursor.value } })
.then(({ data }) => {
if (data.collections.length) {
cursor.value = data.next
emit('fromPagination', data.collections)
resolve(true)
}
resolve(false)
})
})
}
const createObserver = () => {
if (props.nodeElement) {
const intersectionCallback = (entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
console.log('isIntersecting')
console.log(props.nodeElement)
removeObserver(entry.target)
loadMore().then((exist) => {
loading.value = false
if (exist) {
createObserver()
}
})
}
})
}
observer = new IntersectionObserver(intersectionCallback, {})
observer.observe(props.nodeElement)
}
}
const removeObserver = (target) => {
observer.unobserve(target)
observer?.disconnect()
}
onUpdated(() => {
if (!installObserver) {
createObserver()
installObserver = true
}
})
onUnmounted(() => {
observer?.disconnect()
})
// onMounted(() => {
// window.addEventListener(
// "scroll",
// debounce((e) => {
// console.log(props.nodeElement);
// let pixelsFromBottom =
// document.documentElement.offsetHeight -
// document.documentElement.scrollTop -
// window.innerHeight;
// if (pixelsFromBottom < props.bottomLine && !loading.value) {
// loading.value = true;
// props.loadMore().finally(() => (loading.value = false));
// }
// }, 200)
// );
// });
return {
loading,
}
},
}
</script>

View File

@@ -0,0 +1,24 @@
<script>
export default {
emits: ["fromPagination"],
render() {
const { lists, addTag, deleteTag } = this;
return this.$slots.default({
tags,
addTag,
deleteTag,
});
},
props: {
nodeElement: Object,
loadText: {
type: String,
default: "Загрузка ...",
},
nextCursor: { type: String, default: "" },
},
setup(){
},
};
</script>

View File

@@ -0,0 +1,32 @@
<template>
<div
:class="[
liked ? 'text-red' : 'text-gray-light',
'flex items-center',
]"
>
<button class="default" @click.stop="likeFeed">
<svg class="w-5 h-5 flex-shrink-0">
<use xlink:href="#heart"></use>
</svg>
</button>
<span v-show="likes" class="ml-2 text-sm">{{
likes
}}</span>
</div>
</template>
<script>
export default {
props: {
likes: Number,
liked: Boolean,
},
emits: ['likeFeed'],
methods: {
likeFeed() {
this.$emit('likeFeed')
},
},
}
</script>

View File

@@ -0,0 +1,13 @@
<template>
<button type="button" @click="back"><slot /></button>
</template>
<script>
export default {
methods: {
back() {
window.history.back();
},
}
}
</script>

View File

@@ -0,0 +1,25 @@
<template>
<button @click.stop="addAudio" type="button" class="default flex-shrink-0"
:class="[
liked ? 'text-red' : 'text-gray-light'
]"
>
<svg class="w-4 h-4">
<use xlink:href="#heart"></use>
</svg>
</button>
</template>
<script>
export default {
emits: ["addAudio"],
props: {
media_id: Number,
liked: Boolean,
},
methods: {
addAudio() {
this.$emit("addAudio", this.media_id);
},
},
};
</script>

View File

@@ -0,0 +1,20 @@
<template>
<div class="cursor-pointer flex items-center text-gray-light">
<svg class="w-5 h-5 flex-shrink-0">
<use xlink:href="#share"></use>
</svg>
<span v-show="shares" class="ml-2 text-sm">{{
shares
}}</span>
</div>
</template>
<script>
export default {
components: {
},
props: {
shares: Number,
},
};
</script>

View File

@@ -0,0 +1,32 @@
<template>
<button :class="[ currentSong?.id === media_id ? '' : 'hidden', 'default' ]">
<svg :class="[ playing ? 'hidden' : 'block', 'w-6 h-6' ]">
<use xlink:href="#play"></use>
</svg>
<svg :class="[ playing ? 'block' : 'hidden', 'w-6 h-6' ]">
<use xlink:href="#pause"></use>
</svg>
</button>
<button :class="[ currentSong?.id !== media_id ? '' : 'hidden', 'default' ]">
<svg class="w-6 h-6">
<use xlink:href="#play"></use>
</svg>
</button>
</template>
<script>
import { mapState, mapGetters } from "vuex";
export default {
components: {
},
props: {
media_id: Number,
},
computed: {
...mapGetters(["playing"]),
...mapState({
currentSong: (state) => state.player.currentSong,
}),
},
};
</script>

View File

@@ -0,0 +1,34 @@
<template>
<div
v-if="user.photo_path"
class="rounded-full bg-cover bg-center"
:style="setBackground()"
></div>
<div
v-else
:style="`background-color:${user.color};`"
class="select-none rounded-full font-bold flex items-center justify-center text-white"
>
{{ user.user_char }}
</div>
</template>
<script>
export default {
props: {
user: {
type: Object,
required: true
},
size: {
type: String,
default: 'small',
},
},
methods: {
setBackground() {
return `background-image: url(/img/${this.user.photo_path}?p=${this.size});`
},
},
}
</script>

View File

@@ -0,0 +1,28 @@
<template>
<div
v-if="user.banner_path"
class="bg-cover bg-center"
:style="setBackground()"
></div>
<div
v-else
class="bg-cover bg-center"
></div>
</template>
<script>
export default {
props: {
user: Object,
size: {
type: String,
default: 'small',
},
},
methods: {
setBackground() {
return `background-image: url(/img/${this.user.banner_path}?p=${this.size});`;
},
},
};
</script>

View File

@@ -0,0 +1,22 @@
<template>
<div class="count-views">
<div v-show="count" class="flex items-center text-gray-light">
<svg class="w-5 h-5 flex-shrink-0">
<use xlink:href="#eye"></use>
</svg>
<span class="ml-2 text-sm">{{ count }}</span>
</div>
</div>
</template>
<script>
export default {
components: {},
props: {
count: {
type: Number,
default: 0
},
},
}
</script>

View File

@@ -0,0 +1,6 @@
<template>
<div class="mt-2 flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-orange flex-shrink-0 mr-2"><polygon points="7.86 2 16.14 2 22 7.86 22 16.14 16.14 22 7.86 22 2 16.14 2 7.86 7.86 2"></polygon><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg>
<div class="text-sm text-gray-light">контент будет участвовать в поиске, если заполните название (а еще лучше и описание и теги)</div>
</div>
</template>

View File

@@ -0,0 +1,44 @@
<template>
<component
:is="currentNotify"
></component>
</template>
<script>
import NotifyTextLike from '@/Shared/Notification/NotifyTextLike.vue'
import NotifyTextComment from '@/Shared/Notification/NotifyTextComment.vue'
import NotifyTextSubs from '@/Shared/Notification/NotifyTextSubs.vue'
import NotifyTextLeader from '@/Shared/Notification/NotifyTextLeader.vue'
import NotifyTextPurchase from '@/Shared/Notification/NotifyTextPurchase.vue'
import NotifyTextRemoveFeed from '@/Shared/Notification/NotifyTextRemoveFeed.vue'
import NotifyTextCustomPaidSubs from '@/Shared/Notification/NotifyTextCustomPaidSubs.vue'
import NotifyTextBannedMessageFeed from '@/Shared/Notification/NotifyTextBannedMessageFeed.vue'
export default {
components: {
NotifyTextLike,
NotifyTextComment,
NotifyTextSubs,
NotifyTextLeader,
NotifyTextPurchase,
NotifyTextRemoveFeed,
NotifyTextCustomPaidSubs,
NotifyTextBannedMessageFeed,
},
provide() {
return {
content: this.content
}
},
props: {
type: String,
content: Object,
},
computed: {
currentNotify() {
return 'notify-text-' + this.type
},
},
methods: {},
}
</script>

View File

@@ -0,0 +1,28 @@
<template>
<div v-if="content.success">
Контент одобрен и успешно опубликован
</div>
<div v-else>
<div>
Контент не может быть опубликован (причина):
</div>
<div class="text-base text-gray-light">
<ul>
<li v-for="textBreak in textsBreak" :key="textBreak">
{{ textBreak }}
</li>
</ul>
</div>
</div>
</template>
<script>
export default {
inject: ['content'],
computed: {
textsBreak() {
return this.content.text.split('\n')
}
}
}
</script>

View File

@@ -0,0 +1,10 @@
<template>
<div>оставил(а) комментарий:</div>
<div class="truncate text-base text-gray-light">{{content.comment}}</div>
</template>
<script>
export default {
inject: ["content"],
};
</script>

View File

@@ -0,0 +1,3 @@
<template>
<p>оформил платную подписку на вас</p>
</template>

View File

@@ -0,0 +1,3 @@
<template>
<p>проголосовал за вас</p>
</template>

View File

@@ -0,0 +1,3 @@
<template>
<p>поставил(а) лайк</p>
</template>

View File

@@ -0,0 +1,12 @@
<template>
<div>Купил товар на сумму:</div>
<div class="truncate text-base text-gray-light">
{{ content.price }}
</div>
</template>
<script>
export default {
inject: ['content'],
}
</script>

View File

@@ -0,0 +1,4 @@
<template>
<div>Ваш контент был удален за нарушение правил</div>
<div class="truncate text-base text-gray-light"></div>
</template>

View File

@@ -0,0 +1,3 @@
<template>
<p>подписался на вас</p>
</template>

View File

@@ -0,0 +1,150 @@
<template>
<TransitionRoot as="template" :show="open">
<Dialog as="div" static
class="fixed z-[1000] inset-0 overflow-y-auto" :open="open"
@close="staticCloseModal"
>
<div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<TransitionChild as="template" enter="ease-out duration-300"
enter-from="opacity-0" enter-to="opacity-100"
leave="ease-in duration-200" leave-from="opacity-100"
leave-to="opacity-0"
>
<DialogOverlay class="fixed inset-0 bg-indigo-200 bg-opacity-75 transition-opacity" />
</TransitionChild>
<span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">&#8203;</span>
<TransitionChild as="template" enter="ease-out duration-300"
enter-from="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" enter-to="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200" leave-from="opacity-100 translate-y-0 sm:scale-100"
leave-to="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<div class="inline-block align-bottom bg-indigo-300 md:rounded-md text-left overflow-hidden shadow-xl transform transition-all md:align-middle md:max-w-6xl w-full">
<div class="md:grid grid-cols-12">
<button class="flex md:hidden w-full items-center justify-center p-3 bg-indigo-300 text-gray-light text-sm border-b border-indigo-100" @click="staticCloseModal">
Закрыть
</button>
<modal-feed-media
:type="feed.type"
:feed_id="feed.id"
:title="feed.entity.title"
:preview="feed.entity.preview"
:medias="feed.entity.collection_medias"
/>
<modal-feed-body
:user="feed.user"
:feed_id="feed.id"
:entity="feed.entity"
@openShare="showShareModal"
@onRemoveFeed="onRemoveFeed"
@disableModal="checkCloseModal"
/>
</div>
</div>
</TransitionChild>
</div>
<modal-warning
:feed_id="modalFeedId"
:open="warningShow"
@action="deleteActionModal"
/>
<modal-share ref="shareModalNode" :entity="feed.entity" />
</Dialog>
</TransitionRoot>
</template>
<script>
import {
Dialog,
DialogOverlay,
DialogTitle,
TransitionChild,
TransitionRoot,
} from '@headlessui/vue'
import ModalWarning from '@/Shared/Overlay/ModalWarning.vue'
import ModalShare from '@/Shared/Overlay/ModalShare.vue'
import ModalFeedMedia from '@/Shared/Overlay/ModalFeedMedia.vue'
import ModalFeedBody from '@/Shared/Overlay/ModalFeedBody.vue'
export default {
components: {
Dialog,
DialogOverlay,
DialogTitle,
TransitionChild,
TransitionRoot,
ModalFeedMedia,
ModalFeedBody,
ModalWarning,
ModalShare,
},
provide() {
return {
is_exist_menu: this.is_exist_menu
}
},
props: {
modalFeed: Object,
is_exist_menu: {
type: Boolean,
default: true,
},
open: {
type: Boolean,
default: false,
},
},
emits: ['closeModal', 'destroyFeed'],
data() {
return {
disabled: 0,
modalFeedId: 0,
warningShow: false,
feed: {},
}
},
watch: {
open(open) {
if (open) {
this.feed = this.modalFeed
}
},
},
methods: {
deleteActionModal(removeFeed) {
if (removeFeed) {
this.$emit('destroyFeed')
this.$nextTick(() => {
this.staticCloseModal()
})
}
this.warningShow = false
},
showShareModal() {
this.$refs.shareModalNode.openModal()
},
onRemoveFeed(feed_id) {
this.modalFeedId = feed_id
this.warningShow = true
},
checkCloseModal(value) {
this.disabled = value
},
staticCloseModal() {
if (!this.disabled) {
this.$emit('closeModal', false)
}
},
},
}
</script>

View File

@@ -0,0 +1,361 @@
<template>
<div class="col-span-5 modal-body flex flex-col">
<div class="modal-head mb-3 px-2 py-4 md:py-4 md:px-6 flex justify-between items-center border-b border-indigo-100">
<div v-if="entity.is_ads" class="flex items-center space-x-4">
<user-avatar :user="user" size="small"
class="w-12 h-12 md:w-16 md:h-16 border-2 border-orange text-lg"
/>
<p class="text-base md:text-xl font-semibold text-gray">
{{ user.name }}
</p>
</div>
<div v-else class="flex items-center space-x-4">
<inertia-link :href="route('profile.user', user.username)" class="flex-shrink-0 block">
<user-avatar :user="user" size="small"
class="w-12 h-12 md:w-16 md:h-16 border-2 border-orange text-lg"
/>
</inertia-link>
<inertia-link :href="route('profile.user', user.username)" class="flex flex-col">
<p class="text-base md:text-xl font-semibold text-gray">
{{ user.name }}
</p>
<p class="min-h-[24px]">
<span v-show="user.count_posts" class="md:mt-1 text-sm text-gray-light">{{ user.count_posts }} {{ countPosts }}</span>
</p>
</inertia-link>
</div>
<div v-if="is_exist_menu && entity.is_ads == false" class="text-white">
<dropdown-menu-point v-if="$page.props.auth.user">
<MenuItems class="origin-top-right absolute right-0 mt-2 w-64 bg-indigo-200 shadow-lg max-h-60 rounded-md text-base ring-1 ring-indigo-100 overflow-auto focus:outline-none">
<MenuItem v-if="user.id === $page.props.auth.user.id">
<inertia-link :href="route(`${entity.type}.edit`, entity.slug)" class="group flex items-center px-4 py-2 text-base hover:bg-indigo-300 text-gray-light">
<PencilAltIcon class="mr-3 h-5 w-5 text-gray-400 group-hover:text-orange" aria-hidden="true" />
Редактировать
</inertia-link>
</MenuItem>
<MenuItem v-if="user.id === $page.props.auth.user.id">
<button class="w-full group flex items-center px-4 py-2 text-base hover:bg-indigo-300 text-gray-light" @click="onRemoveFeed()">
<MinusCircleIcon class="mr-3 h-5 w-5 text-gray-400 group-hover:text-orange" aria-hidden="true" />
Удалить
</button>
</MenuItem>
<MenuItem v-if="user.id !== $page.props.auth.user.id">
<inertia-link :href="route('complaint.reason', feed_id)" class="group flex items-center px-4 py-2 text-base hover:bg-indigo-300 text-gray-light">
<ExclamationIcon class="mr-3 h-5 w-5 text-gray-400 group-hover:text-orange" aria-hidden="true" />
Пожаловаться
</inertia-link>
</MenuItem>
</MenuItems>
</dropdown-menu-point>
</div>
</div>
<!-- -->
<!-- max-h-[20rem] h-[20rem] -->
<div data-simplebar class="modal-feed min-h-[4rem] md:min-h-[22rem] max-h-[22rem] overflow-auto">
<div class="p-2 md:py-4 md:px-6 space-y-3 md:space-y-6">
<div v-if="entity.tags.length" class="">
<feed-tags :tags="entity.tags" />
</div>
<div v-if="entity.body" class="comment-line comment-line--head flex flex-col text-gray text-sm">
<div v-if="entity.is_ads" class="prose-content"
v-html="entity.body"
></div>
<div v-else class="flex group">
<inertia-link :href="route('profile.user', user.username)" class="flex-shrink-0 block mr-3">
<user-avatar :user="user" size="small"
class="w-10 h-10 border border-orange text-xs"
/>
</inertia-link>
<div>
<inertia-link :href="route('profile.user', user.username)" class="font-semibold underline inline-block mr-2">
{{ user.username }}
</inertia-link>
{{ entity.body }}
<div class="transition-opacity opacity-0 group-hover:opacity-100 flex space-x-4">
<span class="text-xs text-gray-light">{{ entity.created_at_humans }}</span>
</div>
</div>
</div>
</div>
<div v-for="comment in comments" :key="comment.id"
class="comment-line flex flex-col text-gray text-sm"
>
<user-comment :creator_id="user.id" :comment="comment"
@showChildren="loadChildrenComment" @toAnswer="sendAnswerParent"
@removeCommentItem="removeCommentItem"
/>
</div>
<div v-if="cursor" class="flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" width="24"
height="24"
viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round"
class="cursor-pointer text-white" @click="loadComments"
><circle cx="12" cy="12"
r="10"
></circle><line x1="12" y1="8"
x2="12" y2="16"
></line><line x1="8" y1="12"
x2="16" y2="12"
></line></svg>
</div>
</div>
</div>
<div v-if="$page.props.auth.user" class="modal-footer mt-auto p-2 md:py-4 md:px-6 border-t border-indigo-100">
<feed-paid-block
:is_paid="entity.is_paid"
:user_id="user.id"
:price="entity.price"
:feed_id="feed_id"
:paid_open="entity.paid_open"
class="text-white"
@onReplaceFeed="onReplaceFeed"
/>
<div class="misc-info mt-2 md:mt-0 mb-2 flex flex-row-reverse">
<div class="flex space-x-4">
<template v-if="entity.is_ads == false">
<like-count :likes="entity.likes" :liked="entity.liked"
@likeFeed="likeFeed"
/>
<comment-count :comments="entity.comments" />
<share-count @click="openShareModal" />
</template>
<view-count :count="entity.views_count" />
</div>
</div>
<div v-if="entity.is_ads == false" class="modal-send flex">
<div class="flex-1">
<textarea v-model="new_comment"
class="pl-0 pr-2 resize-none bg-indigo-300 w-full outline-none focus:ring-transparent focus:border-transparent focus:ring-offset-0 text-base text-gray border-transparent" placeholder="Написать комментарий..."
> </textarea>
</div>
<div class="mt-1 flex-shrink-0 text-pink">
<button class="default" @click="addComment">
<svg class="w-8 h-8 flex-shrink-0">
<use xlink:href="#arrow-circle"></use>
</svg>
</button>
</div>
</div>
</div>
<div v-else class="modal-footer mt-auto p-2 md:py-4 md:px-6 border-t border-indigo-100 flex justify-center">
<a rel="nofollow" href="/login"
class="my-1 transition shadow-none hover:shadow-classic2 inline-flex items-center px-8 py-1 md:py-3 justify-center text-sm md:text-base rounded-md text-white bg-pink focus:outline-none"
>
Купить
</a>
</div>
</div>
</template>
<script>
import { Inertia } from '@inertiajs/inertia'
import axios from 'axios'
import { MenuItem, MenuItems } from '@headlessui/vue'
import {
MinusCircleIcon,
ExclamationIcon,
PencilAltIcon,
} from '@heroicons/vue/solid'
import helper from '@/includes/helper'
import DropdownMenuPoint from '@/Shared/Form/DropdownMenuPoint.vue'
import FeedPaidBlock from '@/Shared/FeedList/FeedPaidBlock.vue'
import UserAvatar from '@/Shared/Misc/UserAvatar.vue'
import LikeCount from '@/Shared/Misc/LikeCount.vue'
import FeedTags from '@/Shared/Misc/FeedTags.vue'
import CommentCount from '@/Shared/Misc/CommentCount.vue'
import ShareCount from '@/Shared/Misc/ShareCount.vue'
import UserComment from '@/Shared/Partials/UserComment.vue'
import ViewCount from '@/Shared/Misc/ViewCount.vue'
export default {
components: {
DropdownMenuPoint,
MenuItem,
MenuItems,
MinusCircleIcon,
ExclamationIcon,
PencilAltIcon,
UserAvatar,
LikeCount,
ShareCount,
CommentCount,
UserComment,
FeedTags,
FeedPaidBlock,
ViewCount,
},
inject: ['is_exist_menu'],
props: {
user: Object,
entity: Object,
feed_id: Number,
},
emits: ['disableModal', 'onRemoveFeed', 'openShare'],
data() {
return {
new_comment: null,
to_user: null,
parent_send_comment: null,
comments: [],
cursor: null,
}
},
computed: {
countPosts() {
return helper.declNumPosts(this.user.count_posts)
},
},
mounted() {
if(this.entity.is_ads == false){
this.loadComments()
this.loadCountPostsUser()
}
this.addView()
},
unmounted() {
this.comments = []
this.cursor = null
},
methods: {
openShareModal(){
this.$emit('openShare')
},
sendAnswerParent(id, to_user_id, username) {
this.parent_send_comment = id
this.to_user = to_user_id
if (this.new_comment) {
this.new_comment = `@${username} ${this.new_comment}`
} else {
this.new_comment = `@${username} `
}
},
onReplaceFeed(data) {
this.entity.collection_medias = data.collection
this.entity.preview = data.preview
this.entity.paid_open = 1
},
loadCountPostsUser() {
if (
typeof this.user.count_posts === 'undefined' ||
parseInt(this.user.count_posts, 10) === 0
) {
axios.post(route('user.posts.count', this.user.id)).then(({ data }) => {
this.user.count_posts = data
})
}
},
onRemoveFeed() {
this.$emit('onRemoveFeed', this.feed_id)
},
loadComments() {
this.$emit('disableModal', 1)
const that = this
axios.post(route('comments.show', this.feed_id), { cursor: this.cursor }).then(({ data }) => {
data.items.forEach(element => {
that.comments.push(element)
})
that.cursor = data.nextCursor
that.$emit('disableModal', 0)
})
},
loadChildrenComment(id, items) {
const index = this.comments.findIndex((comment) => comment.id === id)
this.comments[index].closed_children_comments = 0
items.forEach(element => {
this.comments[index].childrens.push(element)
})
},
removeCommentItem(id, remove_count = 1, parent_id = null) {
if(parent_id){
const index = this.comments.findIndex((comment) => comment.id === parent_id)
const indexChild = this.comments[index].childrens.findIndex((comment) => comment.id === id)
this.comments[index].childrens.splice(indexChild, 1)
}else{
const index = this.comments.findIndex((comment) => comment.id === id)
this.comments.splice(index, 1)
}
this.entity.comments = this.entity.comments - remove_count
},
addView() {
axios
.post(route('add.view.feed', this.feed_id))
.then(({ data }) => {
data && this.entity.views_count++
})
},
addComment() {
if (!this.new_comment) {
return false
}
this.$emit('disableModal', 1)
axios
.post(route('comments.store', this.feed_id), {
body: this.new_comment,
parent: this.parent_send_comment,
to: this.to_user,
})
.then(({ data }) => {
if (this.parent_send_comment) {
const index = this.comments.findIndex(
(comment) => comment.id === this.parent_send_comment
)
this.comments[index].childrens.push(data)
} else {
this.comments.push(data)
}
this.entity.comments++
this.parent_send_comment = null
this.to_user = null
this.$emit('disableModal', 0)
})
this.new_comment = null
},
likeFeed() {
axios
.post(route('feed.like', this.feed_id))
.then(() => {
if (this.entity.liked) {
this.entity.liked = false
this.entity.likes--
} else {
this.entity.liked = true
this.entity.likes++
}
})
// Inertia.post(
// route('feed.like', this.feed_id),
// {},
// {
// preserveScroll: true,
// preserveState: true,
// }
// )
// if (this.entity.liked) {
// this.entity.liked = false
// this.entity.likes--
// } else {
// this.entity.liked = true
// this.entity.likes++
// }
},
},
}
</script>

View File

@@ -0,0 +1,38 @@
<template>
<div class="col-span-7 modal-media modal-media-height border-r border-indigo-100">
<component
:title="title"
:feed_id="feed_id"
:preview="preview"
:medias="medias"
:is="currentModalMedia"
></component>
</div>
</template>
<script>
import ModalFeedMediaImages from "@/Shared/Overlay/ModalFeedMediaImages.vue";
import ModalFeedMediaVideos from "@/Shared/Overlay/ModalFeedMediaVideos.vue";
import ModalFeedMediaMusics from "@/Shared/Overlay/ModalFeedMediaMusics.vue";
export default {
components: {
ModalFeedMediaImages,
ModalFeedMediaVideos,
ModalFeedMediaMusics,
},
props: {
medias: Array,
type: String,
feed_id: Number,
title: String,
preview: String,
},
computed: {
currentModalMedia() {
return "modal-feed-media-" + this.type.toLowerCase();
},
},
methods: {},
};
</script>

View File

@@ -0,0 +1,33 @@
<template>
<swiper class="h-full" :grabCursor="true" navigation :pagination="{ clickable: true }">
<swiper-slide v-for="media in medias" :key="media.id">
<img class="w-full h-full object-contain" :src="media.url" alt="" />
</swiper-slide>
</swiper>
</template>
<script>
import SwiperCore, { Navigation, Pagination } from 'swiper/core';
import { Swiper, SwiperSlide } from "swiper/vue";
import "swiper/swiper-bundle.css";
// import "swiper/components/navigation/navigation.min.css";
// import "swiper/components/pagination/pagination.min.css";
SwiperCore.use([Navigation, Pagination]);
export default {
components: {
Swiper,
SwiperSlide,
},
props: {
medias: Array,
title: String,
preview: String,
},
methods: {},
};
</script>

View File

@@ -0,0 +1,110 @@
<template>
<div class="modal-media h-full">
<div class="flex items-center border-b border-indigo-100">
<div class="flex-shrink-0 mr-5">
<feed-preview
class="h-28 w-28 md:w-56 md:h-56 object-cover"
type="music"
:source="preview"
/>
</div>
<div class="flex flex-col">
<span class="text-base md:text-xl font-semibold text-gray">{{
title
}}</span>
</div>
</div>
<div data-simplebar class="max-h-96 overflow-auto">
<div class="divide-y divide-indigo-100">
<div
@click.prevent="startPlay(media)"
v-for="media in medias"
:key="media.id"
:class="[
currentSong?.id === media.id
? 'bg-indigo-200 bg-opacity-25'
: 'hover:bg-indigo-200 hover:bg-opacity-25',
'p-4 flex items-center space-x-4',
]"
>
<div class="flex">
<toggle-play-button :media_id="media.id" />
</div>
<div class="flex-1 text-sm text-gray-light">
{{ media.name }}
</div>
<div class="text-sm text-gray-light">
<div v-show="currentSong?.id === media.id">{{seek}}</div>
<div v-show="currentSong?.id !== media.id">{{media.time}}</div>
</div>
<music-add-count v-if="$page.props.auth.user" @addAudio="likeAudio" :media_id='media.id' :liked='media.liked' />
</div>
</div>
</div>
</div>
</template>
<script>
import { mapActions, mapState, mapGetters } from "vuex";
import { Inertia } from "@inertiajs/inertia";
import find from "lodash/find";
import FeedPreview from "@/Shared/Feed/FeedPreview.vue";
import TogglePlayButton from "@/Shared/Misc/TogglePlayButton.vue";
import MusicAddCount from "@/Shared/Misc/MusicAddCount.vue";
export default {
components: {
FeedPreview,
TogglePlayButton,
MusicAddCount,
},
props: {
medias: Array,
title: String,
preview: String,
feed_id: Number,
},
methods: {
...mapActions(["toggleAudio", "newCurrentPlaylist", "skipToIndexByMusic"]),
startPlay(music) {
if (this.currentSong?.id === music.id) {
this.toggleAudio();
return;
}
// if (this.playlist_id === this.feed_id) {
// this.skipToIndexByMusic(music);
// return;
// }
this.newCurrentPlaylist([music, this.medias, this.feed_id]);
},
likeAudio(media_id) {
Inertia.post(
route("feed.audio.like", media_id),
{},
{
preserveScroll: true,
preserveState: true,
}
);
let media = find(this.medias, (item) => item.id === media_id);
if(media){
media.liked = !media.liked;
}
},
},
computed: {
...mapGetters(["playing"]),
...mapState({
playlist_id: (state) => state.player.playlist_id,
seek: (state) => state.player.seek,
currentSong: (state) => state.player.currentSong,
}),
},
};
</script>

View File

@@ -0,0 +1,32 @@
<template>
<swiper class="h-full" :grabCursor="true" navigation :pagination="{ clickable: true }">
<swiper-slide v-for="media in medias" class="aspect-w-16 aspect-h-12 md:aspect-h-9" :key="media.id">
<video :src="media.url" controls></video>
</swiper-slide>
</swiper>
</template>
<script>
import SwiperCore, { Navigation, Pagination } from 'swiper/core';
import { Swiper, SwiperSlide } from "swiper/vue";
import "swiper/swiper-bundle.css";
// import "swiper/components/navigation/navigation.min.css";
// import "swiper/components/pagination/pagination.min.css";
SwiperCore.use([Navigation, Pagination]);
export default {
components: {
Swiper,
SwiperSlide,
},
props: {
medias: Array,
title: String,
preview: String,
},
methods: {},
};
</script>

View File

@@ -0,0 +1,175 @@
<template>
<TransitionRoot as="template" :show="open">
<Dialog as="div" static
class="fixed z-[1000] inset-0 overflow-y-auto" :open="open"
@close="staticCloseModal"
>
<div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<TransitionChild as="template" enter="ease-out duration-300"
enter-from="opacity-0" enter-to="opacity-100"
leave="ease-in duration-200" leave-from="opacity-100"
leave-to="opacity-0"
>
<DialogOverlay class="fixed inset-0 bg-indigo-200 bg-opacity-75 transition-opacity" />
</TransitionChild>
<span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">&#8203;</span>
<TransitionChild as="template" enter="ease-out duration-300"
enter-from="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" enter-to="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200" leave-from="opacity-100 translate-y-0 sm:scale-100"
leave-to="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<div class="inline-block align-bottom bg-indigo-300 rounded-lg px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full sm:p-6">
<div class="">
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<DialogTitle as="h3" class="text-lg leading-6 font-medium text-gray-light">
Поделиться
</DialogTitle>
<div class="my-4">
<input :value="route(`${entity.type}.show`, entity.slug)" class="js-input-text-share w-full focus:ring-4 focus:ring-offset-1 focus:ring-orange focus:ring-opacity-20 focus:ring-offset-orange focus:border-transparent text-gray border border-indigo-300 bg-indigo-200 rounded-md placeholder-gray-light"
type="text" readonly
>
</div>
<div class="flex items-center justify-end -mx-1">
<div class="transition-shadow shadow-none hover:shadow-classic2 p-2 bg-[#4C75A3] text-white mx-1 cursor-pointer" title="Скопировать текст"
@click="copyText"
>
<svg xmlns="http://www.w3.org/2000/svg" width="24"
height="24" viewBox="0 0 24 24"
fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" class="feather feather-copy"
><rect x="9" y="9"
width="13" height="13"
rx="2" ry="2"
></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>
</div>
<div class="transition-shadow shadow-none hover:shadow-classic2 p-2 bg-[#4C75A3] text-white mx-1 cursor-pointer" @click="shareVK">
<svg xmlns="http://www.w3.org/2000/svg" width="24"
height="24" viewBox="0 0 576 512"
><path fill="currentColor" d="M545 117.7c3.7-12.5 0-21.7-17.8-21.7h-58.9c-15 0-21.9 7.9-25.6 16.7 0 0-30 73.1-72.4 120.5-13.7 13.7-20 18.1-27.5 18.1-3.7 0-9.4-4.4-9.4-16.9V117.7c0-15-4.2-21.7-16.6-21.7h-92.6c-9.4 0-15 7-15 13.5 0 14.2 21.2 17.5 23.4 57.5v86.8c0 19-3.4 22.5-10.9 22.5-20 0-68.6-73.4-97.4-157.4-5.8-16.3-11.5-22.9-26.6-22.9H38.8c-16.8 0-20.2 7.9-20.2 16.7 0 15.6 20 93.1 93.1 195.5C160.4 378.1 229 416 291.4 416c37.5 0 42.1-8.4 42.1-22.9 0-66.8-3.4-73.1 15.4-73.1 8.7 0 23.7 4.4 58.7 38.1 40 40 46.6 57.9 69 57.9h58.9c16.8 0 25.3-8.4 20.4-25-11.2-34.9-86.9-106.7-90.3-111.5-8.7-11.2-6.2-16.2 0-26.2 .1-.1 72-101.3 79.4-135.6z"></path></svg>
</div>
</div>
</div>
</div>
<div class="mt-5 sm:mt-4 sm:flex">
<button type="button"
class="mx-3 my-1 transition shadow-none hover:shadow-classic inline-flex items-center px-8 py-3 justify-center text-base rounded-md text-white bg-indigo-300 focus:outline-none"
@click="staticCloseModal"
>
Отменить
</button>
</div>
</div>
</TransitionChild>
</div>
</Dialog>
</TransitionRoot>
</template>
<script>
import {
Dialog,
DialogOverlay,
DialogTitle,
TransitionChild,
TransitionRoot,
} from '@headlessui/vue'
export default {
components: {
Dialog,
DialogOverlay,
DialogTitle,
TransitionChild,
TransitionRoot,
},
// emits: {
// close: null,
// },
props: {
entity: Object,
},
data() {
return {
open: false,
}
},
methods: {
openModal() {
this.open = true
},
staticCloseModal() {
this.open = false
},
copyText(){
const text = document.querySelector('.js-input-text-share')
text.select()
text.setSelectionRange(0, 99999)
navigator.clipboard.writeText(text.value)
},
shareTwitter() {
const urlInner = route(`${this.entity.type}.show`, this.entity.slug)
const body = this.entity.body ?? ''
const url = `https://twitter.com/intent/tweet?text=${body}&url=${urlInner}`
this.openWindow(url)
},
shareFB() {
const urlInner = route(`${this.entity.type}.show`, this.entity.slug)
const body = this.entity.body ?? ''
const url = `https://www.facebook.com/sharer.php?u=${urlInner}&t=${body}`
this.openWindow(url)
},
shareVK() {
const urlInner = route(`${this.entity.type}.show`, this.entity.slug)
const title = this.entity.title ?? ''
const url = `https://vk.com/share.php?url=${urlInner}&title=${title}`
this.openWindow(url)
},
openWindow(url) {
this.popupCenter({url, title: 'share', w: 600, h: 400})
},
popupCenter({ url, title, w, h }) {
const dualScreenLeft =
window.screenLeft !== undefined ? window.screenLeft : window.screenX
const dualScreenTop =
window.screenTop !== undefined ? window.screenTop : window.screenY
const width = window.innerWidth
? window.innerWidth
: document.documentElement.clientWidth
? document.documentElement.clientWidth
: screen.width
const height = window.innerHeight
? window.innerHeight
: document.documentElement.clientHeight
? document.documentElement.clientHeight
: screen.height
const systemZoom = width / window.screen.availWidth
const left = (width - w) / 2 / systemZoom + dualScreenLeft
const top = (height - h) / 2 / systemZoom + dualScreenTop
const newWindow = window.open(
url,
title,
`
scrollbars=yes,
width=${w / systemZoom},
height=${h / systemZoom},
top=${top},
left=${left}
`
)
if (window.focus) newWindow.focus()
},
},
}
</script>

View File

@@ -0,0 +1,98 @@
<template>
<TransitionRoot as="template" :show="open">
<Dialog as="div" static
class="fixed z-[1000] inset-0 overflow-y-auto" :open="open"
@close="staticCloseModal"
>
<div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<TransitionChild as="template" enter="ease-out duration-300"
enter-from="opacity-0" enter-to="opacity-100"
leave="ease-in duration-200" leave-from="opacity-100"
leave-to="opacity-0"
>
<DialogOverlay class="fixed inset-0 bg-indigo-200 bg-opacity-75 transition-opacity" />
</TransitionChild>
<span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">&#8203;</span>
<TransitionChild as="template" enter="ease-out duration-300"
enter-from="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" enter-to="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200" leave-from="opacity-100 translate-y-0 sm:scale-100"
leave-to="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<div class="inline-block align-bottom bg-indigo-300 rounded-lg px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full sm:p-6">
<div class="sm:flex sm:items-start">
<div class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-orange sm:mx-0 sm:h-10 sm:w-10">
<ExclamationIcon class="h-6 w-6 text-red-600" aria-hidden="true" />
</div>
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<DialogTitle as="h3" class="text-lg leading-6 font-medium text-gray-light">
Удалить контент
</DialogTitle>
<div class="mt-2">
<p class="text-sm text-gray-light">
Вы уверены, что хотите удалить этот контент? Восстановить можно, но будет сложно!
</p>
</div>
</div>
</div>
<div class="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
<button type="button"
class="mx-3 my-1 transition shadow-none hover:shadow-pink inline-flex items-center px-8 py-3 justify-center text-base rounded-md text-white bg-pink focus:outline-none"
@click="remove"
>
Удалить
</button>
<button type="button"
class="mx-3 my-1 transition shadow-none hover:shadow-classic inline-flex items-center px-8 py-3 justify-center text-base rounded-md text-white bg-indigo-300 focus:outline-none"
@click="staticCloseModal"
>
Отменить
</button>
</div>
</div>
</TransitionChild>
</div>
</Dialog>
</TransitionRoot>
</template>
<script>
import {
Dialog,
DialogOverlay,
DialogTitle,
TransitionChild,
TransitionRoot,
} from '@headlessui/vue'
import { ExclamationIcon } from '@heroicons/vue/outline'
export default {
components: {
Dialog,
DialogOverlay,
DialogTitle,
TransitionChild,
TransitionRoot,
ExclamationIcon,
},
props: {
feed_id: Number,
open: {
type: Boolean,
default: false,
},
},
emits: ['action'],
methods: {
staticCloseModal() {
this.$emit('action', false)
},
remove() {
this.$emit('action', true)
},
},
}
</script>

View File

@@ -0,0 +1,161 @@
<template>
<div class="gradient-profile relative">
<user-banner class="h-56 xl:h-80 bg-indigo-200" :user="user"
size="hero"
/>
</div>
<messanger-modal ref="messangerModal" />
<div class="-mt-24 xl:-mt-32 relative xl:container xl:mx-auto px-2 md:px-3">
<div class="flex flex-col md:flex-row">
<div class="flex-shrink-0 self-center md:self-start md:mr-6 2xl:mr-10">
<user-avatar :user="user" size="medium"
class="shadow-classic object-cover w-48 h-48 xl:w-64 xl:h-64 text-5xl"
/>
</div>
<div class="w-full">
<div class="h-24 xl:h-32 hidden md:block"></div>
<div class="mt-2 ">
<div class="-mx-2 -my-2 lg:-mx-4 lg:-my-4 flex flex-col md:flex-row flex-wrap xl:flex-nowrap">
<div class="max-w-[350px] text-center md:text-left mx-2 my-2 lg:mx-4 lg:my-4 flex flex-shrink-0 flex-col self-center md:self-start">
<h1 class="md:mb-3 text-2xl xl:text-4xl font-semibold text-white">
{{ user.name }}
</h1>
<h2 class="text-base xl:text-xl text-gray-light">
@{{ user.username }}
</h2>
</div>
<!-- self-center -->
<div class="mx-2 my-2 lg:mx-4 lg:my-4 self-center md:self-start flex flex-1 flex-col">
<div class="md:mt-2">
<div class="flex 2xl:text-lg text-white -mx-4">
<inertia-link :href="route('profile.user', user.username)" class="block mx-4">
<span class="text-orange">{{ counts.feeds }}</span> {{ countPosts }}
</inertia-link>
<inertia-link :href="route('profile.subs', user.username)" class="block mx-4">
<span class="text-orange">{{ counts.subscribers }}</span> {{ countSubs }}
</inertia-link>
<inertia-link :href="route('profile.readers', user.username)" class="block mx-4">
<span class="text-orange">{{ counts.readers }}</span> {{ countReaders }}
</inertia-link>
</div>
<div class="mt-4 text-gray-light text-sm min-h-[20px]">
{{ user.about }}
</div>
</div>
</div>
<div class="mx-2 my-2 lg:mx-4 lg:my-4 2xl:flex-shrink-0 self-center text-center">
<inertia-link v-if="user.is_auth_user" class="inline-flex tracking-wide items-center px-4 py-3 border border-white text-sm 2xl:text-base text-white rounded-full bg-transparent hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2"
:href="route('setting.index')"
>
Редактировать профиль
</inertia-link>
<div v-else>
<button v-if="$page.props.auth.user && (user.private === false || packageCompleted)" class="pt-2 default flex items-center text-white"
@click="openModalMessanger"
>
<svg xmlns="http://www.w3.org/2000/svg" width="24"
height="24" viewBox="0 0 24 24"
fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" class="flex-shrink-0 mr-2"
><line x1="22" y1="2"
x2="11" y2="13"
></line><polygon points="22 2 15 22 11 13 2 9 22 2"></polygon></svg>
написать сообщение
</button>
<toggle
v-if="$page.props.auth.user && user.private === false"
:user_id="user.id"
class="mt-3"
:enabled="user.is_sub"
textin="Подписаться"
textout="Отписаться" @clicked="susbscribe"
/>
<div v-if="authUserActiveSubscription">
<toggle
v-if="user.is_sub && (user.private === false || packageCompleted)"
class="mt-3"
:user_id="user.id"
:enabled="is_leader"
:disabled="limitLeader === true && user.is_leader === false ? true : false"
textin="Сделать лидером"
textout="Убрать лидера" @clicked="leader"
/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import helper from '@/includes/helper'
import { Inertia } from '@inertiajs/inertia'
import UserAvatar from '@/Shared/Misc/UserAvatar.vue'
import UserBanner from '@/Shared/Misc/UserBanner.vue'
import Toggle from '@/Shared/Form/Toggle.vue'
import MessangerModal from '@/Shared/Messanger/MessangerModal.vue'
export default {
components: {
Toggle,
UserAvatar,
UserBanner,
MessangerModal,
},
props: {
user: Object,
counts: Object,
is_leader: Boolean,
authUserActiveSubscription: Boolean,
limitLeader: {
type: Boolean,
default: false
},
packageCompleted: {
type: Boolean,
default: false
},
},
computed: {
countPosts() {
return helper.declNumPosts(this.counts.feeds)
},
countSubs() {
return helper.declNumSubs(this.counts.subscribers)
},
countReaders() {
return helper.declNumReaders(this.counts.readers)
},
},
methods: {
openModalMessanger() {
this.$refs.messangerModal.openAction(this.user)
},
susbscribe(user_id) {
Inertia.post(
route('users.subs', user_id),
{},
{ preserveScroll: true, preserveState: true }
)
},
leader(user_id) {
Inertia.post(
route('users.leader', user_id),
{ vote: this.is_leader },
{ preserveScroll: true, preserveState: true }
)
},
},
}
</script>

View File

@@ -0,0 +1,98 @@
<template>
<div class="mt-12 xl:container xl:mx-auto px-2 md:px-3 buttons-filter-line">
<div class="flex">
<div class="flex flex-wrap -mx-2 -my-2 lg:-mx-4 lg:-my-4">
<inertia-link :href="route('profile.user', user.username)"
:class="[$page.component === 'Profile/Index' ? 'shadow-classic2 bg-orange text-white' : 'shadow-classic text-gray bg-indigo-200 hover:bg-orange hover:text-white', 'mx-2 my-2 lg:mx-4 lg:my-4 py-3 px-6 xl:px-10 transition inline-flex items-center justify-center text-sm xl:text-base rounded-md focus:outline-none']"
>
<svg class="-ml-1 mr-2 h-4 w-4 md:h-5 md:w-5 flex-shrink-0" fill="currentColor"
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"
><path fill-rule="evenodd" clip-rule="evenodd"
d="M4.167 3.333a.833.833 0 00-.833.834v11.666c0 .46.373.834.833.834h11.667c.46 0 .833-.373.833-.834V4.167a.833.833 0 00-.833-.834H4.167zm-2.5.834a2.5 2.5 0 012.5-2.5h11.667a2.5 2.5 0 012.5 2.5v11.666a2.5 2.5 0 01-2.5 2.5H4.167a2.5 2.5 0 01-2.5-2.5V4.167z"
/><path fill-rule="evenodd" clip-rule="evenodd"
d="M1.667 7.5c0-.46.373-.833.833-.833h15a.833.833 0 110 1.666h-15a.833.833 0 01-.833-.833z"
/><path fill-rule="evenodd" clip-rule="evenodd"
d="M7.5 6.667c.46 0 .834.373.834.833v10a.833.833 0 01-1.667 0v-10c0-.46.373-.833.833-.833z"
/></svg>
Публикации
</inertia-link>
<inertia-link :href="route('profile.readers', user.username)"
:class="[$page.component === 'Profile/Readers' ? 'shadow-classic2 bg-orange text-white' : 'shadow-classic text-gray bg-indigo-200 hover:bg-orange hover:text-white', 'mx-2 my-2 lg:mx-4 lg:my-4 py-3 px-6 xl:px-10 transition inline-flex items-center justify-center text-sm xl:text-base rounded-md focus:outline-none']"
>
<svg class="-ml-1 mr-2 h-4 w-4 md:h-5 md:w-5 flex-shrink-0" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round"
><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="8.5" cy="7"
r="4"
></circle><polyline points="17 11 19 13 23 9"></polyline></svg>
Подписки
</inertia-link>
<inertia-link :href="route('profile.subs', user.username)"
:class="[$page.component === 'Profile/Subs' ? 'shadow-classic2 bg-orange text-white' : 'shadow-classic text-gray bg-indigo-200 hover:bg-orange hover:text-white', 'mx-2 my-2 lg:mx-4 lg:my-4 py-3 px-6 xl:px-10 transition inline-flex items-center justify-center text-sm xl:text-base rounded-md focus:outline-none']"
>
<svg class="-ml-1 mr-2 h-4 w-4 md:h-5 md:w-5 flex-shrink-0" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round"
><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7"
r="4"
></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></svg>
Подписчики
</inertia-link>
</div>
<div v-if="user.is_auth_user" class="ml-auto">
<dropdown-menu>
<MenuItems class="origin-top-right absolute right-0 mt-2 w-64 bg-indigo-300 shadow-lg max-h-60 rounded-md text-base ring-1 ring-indigo-200 overflow-auto focus:outline-none">
<MenuItem>
<inertia-link :href="route('videos.create')" class="group flex items-center px-4 py-2 text-base hover:bg-indigo-200 text-gray-light">
<VideoCameraIcon class="mr-3 h-5 w-5 text-gray-400 group-hover:text-orange" aria-hidden="true" />
Загрузить видео
</inertia-link>
</MenuItem>
<MenuItem>
<inertia-link :href="route('images.create')" class="group flex items-center px-4 py-2 text-base hover:bg-indigo-200 text-gray-light">
<PhotographIcon class="mr-3 h-5 w-5 text-gray-400 group-hover:text-orange" aria-hidden="true" />
Загрузить ие
</inertia-link>
</MenuItem>
<MenuItem>
<inertia-link :href="route('musics.create')" class="group flex items-center px-4 py-2 text-base hover:bg-indigo-200 text-gray-light">
<MusicNoteIcon class="mr-3 h-5 w-5 text-gray-400 group-hover:text-orange" aria-hidden="true" />
Загрузить музыку
</inertia-link>
</MenuItem>
</MenuItems>
</dropdown-menu>
</div>
</div>
</div>
</template>
<script>
import {
PhotographIcon,
VideoCameraIcon,
MusicNoteIcon,
} from '@heroicons/vue/solid'
import { MenuItem, MenuItems } from '@headlessui/vue'
import DropdownMenu from '@/Shared/Form/DropdownMenu.vue'
export default {
components: {
DropdownMenu,
MenuItem,
MenuItems,
PhotographIcon,
MusicNoteIcon,
VideoCameraIcon,
},
props: {
user: Object,
},
}
</script>

View File

@@ -0,0 +1,153 @@
<template>
<div class="flex group">
<inertia-link :href="route('profile.user', comment.user.username)" class="block flex-shrink-0 mr-3">
<user-avatar
:user="comment.user"
size="small"
:class="[
creator_id === comment.user.id ? 'border border-orange' : '',
'text-xs w-10 h-10',
]"
/>
</inertia-link>
<div class="flex-1 mr-3">
<inertia-link :href="route('profile.user', comment.user.username)" class="font-semibold underline inline-block mr-2">
{{ comment.user.username }}
</inertia-link>
{{ comment.body }}
<div
class="
transition-opacity
opacity-0
group-hover:opacity-100
flex
space-x-3
"
>
<span class="text-xs text-gray-light">{{
comment.created_at_humans
}}</span>
<template v-if="$page.props.auth.user">
<span
class="
text-xs text-gray-light
font-semibold
hover:underline
cursor-pointer
"
@click="toAnswer(comment.id, comment.user.id, comment.user.username)"
>ответить</span>
<inertia-link
v-if="$page.props.auth.user.id !== comment.user?.id"
:href="route('complaint.reason.comment', comment.id)"
class="
text-xs text-orange
font-semibold
hover:underline
cursor-pointer
"
>
пожаловаться
</inertia-link>
<user-comment-remove v-if="$page.props.auth.user.id === comment.user.id || $page.props.auth.user.id === creator_id" :text="comment.children_count ? 'удалить ветку' : 'удалить'"
@remove="removeComment"
/>
</template>
</div>
</div>
<!-- <div class="flex-shrink-0">
<button class="button-default">
<svg class="w-3 h-3">
<use xlink:href="#heart"></use>
</svg>
</button>
</div> -->
</div>
<div v-if="comment.children_count && comment.closed_children_comments" class="test-box ml-14 mt-2 text-gray-light text-xs">
<button class="button-default" @click="showChildren">
--- Посмотреть ответы ({{ comment.children_count }})
</button>
</div>
<div v-if="comment.childrens" class="mt-2 space-y-3 ml-14 md:space-y-6">
<div v-for="children in comment.childrens" :key="children.id"
class="comment-line text-gray text-sm"
>
<user-comment-children :creator_id="creator_id" :comment="children"
@toAnswerChild="sendChild" @removeComment="removeCommentChildren"
/>
</div>
</div>
<div v-if="cursor" class="flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" width="24"
height="24"
viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round"
class="cursor-pointer text-white" @click="showChildren"
><circle cx="12" cy="12"
r="10"
></circle><line x1="12" y1="8"
x2="12" y2="16"
></line><line x1="8" y1="12"
x2="16" y2="12"
></line></svg>
</div>
</template>
<script>
import UserAvatar from '@/Shared/Misc/UserAvatar.vue'
import UserCommentChildren from '@/Shared/Partials/UserCommentChildren.vue'
import UserCommentRemove from './UserCommentRemove.vue'
import axios from 'axios'
export default {
components: {
UserAvatar,
UserCommentChildren,
UserCommentRemove,
},
props: {
comment: Object,
creator_id: Number,
},
emits: ['toAnswer', 'showChildren', 'removeCommentItem'],
data() {
return {
cursor: null,
}
},
methods: {
toAnswer(id, user_id, name) {
this.$emit('toAnswer', id, user_id, name)
},
sendChild(user_id, name){
this.toAnswer(this.comment.id, user_id, name)
},
showChildren(){
const that = this
axios.post(route('comments.children.show', this.comment.id), { cursor: this.cursor }).then(({ data }) => {
that.$emit('showChildren', that.comment.id, data.items)
that.cursor = data.nextCursor
})
},
removeComment() {
const that = this
axios.post(route('comments.remove', this.comment.id)).then(({ data }) => {
that.$emit('removeCommentItem', that.comment.id, data, null)
})
},
removeCommentChildren(id) {
const that = this
axios.post(route('comments.remove', id)).then(({ data }) => {
that.$emit('removeCommentItem', id, data, that.comment.id)
})
}
},
}
</script>

View File

@@ -0,0 +1,92 @@
<template>
<div class="flex group">
<inertia-link :href="route('profile.user', comment.user.username)" class="block flex-shrink-0 mr-3">
<user-avatar
:user="comment.user"
size="small"
:class="[
creator_id == comment.user.id ? 'border border-orange' : '',
'text-xs w-10 h-10',
]"
/>
</inertia-link>
<div class="flex-1 mr-3">
<inertia-link :href="route('profile.user', comment.user.username)" class="font-semibold underline inline-block mr-2">
{{ comment.user.username }}
</inertia-link>
<div v-show="comment.user.id != comment.answer_to.id">
<inertia-link :href="route('profile.user', comment.answer_to.username)" class="text-orange">
@{{ comment.answer_to.username }}
</inertia-link>
</div>
{{ comment.body }}
<div
class="
transition-opacity
opacity-0
group-hover:opacity-100
flex
space-x-4
"
>
<span class="text-xs text-gray-light">{{
comment.created_at_humans
}}</span>
<inertia-link
v-if="$page.props.auth.user.id !== comment.user.id"
:href="route('complaint.reason.comment', comment.id)"
class="
text-xs text-orange
font-semibold
hover:underline
cursor-pointer
"
>
пожаловаться
</inertia-link>
<span
class="
text-xs text-gray-light
font-semibold
hover:underline
cursor-pointer
"
@click="toAnswerChildren()"
>ответить</span>
<user-comment-remove v-if="$page.props.auth.user.id === comment.user.id || $page.props.auth.user.id === creator_id" @remove="removeComment" />
</div>
</div>
<!-- <div class="flex-shrink-0">
<button class="button-default">
<svg class="w-3 h-3">
<use xlink:href="#heart"></use>
</svg>
</button>
</div> -->
</div>
</template>
<script>
import UserAvatar from '@/Shared/Misc/UserAvatar.vue'
import UserCommentRemove from './UserCommentRemove.vue'
export default {
components: {
UserAvatar,
UserCommentRemove
},
props: {
comment: Object,
creator_id: Number,
},
emits: ['toAnswerChild', 'removeComment'],
methods: {
toAnswerChildren() {
this.$emit('toAnswerChild', this.comment.user.id, this.comment.user.username)
},
removeComment() {
this.$emit('removeComment', this.comment.id)
}
},
}
</script>

View File

@@ -0,0 +1,37 @@
<script setup>
import { ref } from 'vue'
defineProps({
text: {
type: String,
default: ''
}
})
const emit = defineEmits({
'remove': null
})
const show = ref(false)
function showPopupRemove() {
show.value = !show.value
}
function remove() {
show.value = false
emit('remove')
}
</script>
<template>
<div class="relative inline-flex">
<div v-if="show" class=" absolute text-xs px-1 py-1 w-[130px] leading-none rounded bg-pink top-[-35px]">
<span class="cursor-pointer" @click="remove">Нажмите для подтверждения</span>
</div>
<span
class="text-xs text-red font-semibold hover:underline cursor-pointer"
@click="showPopupRemove"
>
{{ show ? 'отменить' : text }}
</span>
</div>
</template>

View File

@@ -0,0 +1,38 @@
<template>
<section>
<h2 class="text-base text-gray font-semibold">Превью:</h2>
<div class="mt-3 grid grid-cols-2 sm:grid-cols-3 gap-2 lg:gap-4">
<div v-for="common_media in purchase.common_medias" :key="common_media.id">
<div data-fancybox="preview" :data-src="common_media.url" class="cursor-pointer">
<img :src="common_media.url" class="w-full h-36 md:h-72 object-cover" alt="">
</div>
<a :href="common_media.url" download class="hover:text-orange transition-colors mt-2 text-gray-light text-sm">Скачать</a>
</div>
</div>
</section>
<section class="mt-6">
<h2 class="text-base text-gray font-semibold">Медиа материалы:</h2>
<div class="mt-3 grid grid-cols-2 sm:grid-cols-3 gap-2 lg:gap-4">
<div v-for="paid_media in purchase.paid_medias" :key="paid_media.id">
<div data-fancybox="paid" :data-src="paid_media.url" class="cursor-pointer">
<img :src="paid_media.url" class="w-full h-36 md:h-72 object-cover" alt="">
</div>
<a :href="paid_media.url" download class="hover:text-orange transition-colors mt-2 text-gray-light text-sm">Скачать</a>
</div>
</div>
</section>
</template>
<script>
import { Fancybox } from "@fancyapps/ui/src/Fancybox/Fancybox.js";
import "@fancyapps/ui/dist/fancybox.css";
export default {
components: {},
props: {
purchase: Object,
},
};
</script>

View File

@@ -0,0 +1,38 @@
<template>
<section>
<h2 class="text-base text-gray font-semibold">Превью:</h2>
<div class="mt-3 ">
<feed-music-body
:feed_id='purchase.id'
:title='purchase.title'
:preview='purchase.preview'
:medias='purchase.common_medias'
/>
</div>
</section>
<section class="mt-6">
<h2 class="text-base text-gray font-semibold">Медиа материалы:</h2>
<div class="mt-3">
<feed-music-body
:feed_id='purchase.id'
:title='purchase.title'
:preview='purchase.preview'
:medias='purchase.paid_medias'
/>
</div>
</section>
</template>
<script>
import FeedMusicBody from "@/Shared/FeedList/FeedMusicBody.vue";
export default {
components: {
FeedMusicBody,
},
props: {
purchase: Object,
},
};
</script>

View File

@@ -0,0 +1,35 @@
<template>
<section>
<h2 class="text-base text-gray font-semibold">Превью:</h2>
<div class="mt-3 grid grid-cols-2 sm:grid-cols-3 gap-2 lg:gap-4">
<div v-for="common_media in purchase.common_medias" :key="common_media.id">
<div>
<video :src="common_media.url" controls></video>
</div>
<a :href="common_media.url" download class="hover:text-orange transition-colors mt-2 text-gray-light text-sm">Скачать</a>
</div>
</div>
</section>
<section class="mt-6">
<h2 class="text-base text-gray font-semibold">Медиа материалы:</h2>
<div class="mt-3 grid grid-cols-2 sm:grid-cols-3 gap-2 lg:gap-4">
<div v-for="paid_media in purchase.paid_medias" :key="paid_media.id">
<div>
<video :src="paid_media.url" controls></video>
</div>
<a :href="paid_media.url" download class="hover:text-orange transition-colors mt-2 text-gray-light text-sm">Скачать</a>
</div>
</div>
</section>
</template>
<script>
export default {
components: {},
props: {
purchase: Object,
},
};
</script>

View File

@@ -0,0 +1,238 @@
<template>
<div ref="topbar" class="col-span-4 h-12 hidden md:flex">
<form class="main-search w-full flex md:ml-0" action="#"
method="GET"
>
<label for="search_field" class="sr-only">Поиск контента</label>
<div
class="relative w-full text-gray-400 focus-within:text-gray-600"
>
<input
id="search_field"
v-model="search"
name="search_field"
class="
h-full
w-full
rounded-md
border-transparent
py-2
pr-11
pl-4
text-base
bg-orange-dark
text-white
placeholder-gray
focus:border-transparent
focus:outline-none
focus:ring-2 focus:ring-orange-dark
"
placeholder="Глобальный поиск..."
type="search"
/>
<div
class="pointer-events-none absolute inset-y-0 right-4 flex items-center text-gray"
>
<svg v-show="!loadead" class="flex-shrink-0 h-6 w-6"
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
fill="currentColor"
> <path fill-rule="evenodd" clip-rule="evenodd"
d="M11 4a7 7 0 100 14 7 7 0 000-14zm-9 7a9 9 0 1118 0 9 9 0 01-18 0z"
/> <path fill-rule="evenodd" clip-rule="evenodd"
d="M15.943 15.943a1 1 0 011.414 0l4.35 4.35a1 1 0 01-1.414 1.414l-4.35-4.35a1 1 0 010-1.414z"
/> </svg>
<svg v-show="loadead" class="flex-shrink-0 animate-spin h-6 w-6 text-white"
xmlns="http://www.w3.org/2000/svg" fill="none"
viewBox="0 0 24 24"
>
<circle class="opacity-25" cx="12"
cy="12" r="10"
stroke="currentColor" stroke-width="4"
></circle>
<path class="opacity-75" fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
</div>
</div>
</form>
</div>
<teleport to="body">
<div v-if="open_drop">
<div
style="position: fixed;top: 0;right: 0;left: 0;bottom: 0;z-index: 9998;background: black; opacity: 0.2;"
@click="open_drop = false"
/>
<div ref="dropdown" class="z-[9999] main-search-list shadow-classic rounded-md bg-indigo-200 ">
<div class="w-[580px] overflow-hidden pb-5">
<button class="text-white absolute top-2 right-2" @click.stop="jump">
<svg xmlns="http://www.w3.org/2000/svg" width="18"
height="18" viewBox="0 0 24 24"
fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" class="feather feather-x"
><line x1="18" y1="6"
x2="6" y2="18"
></line><line x1="6" y1="6"
x2="18" y2="18"
></line></svg>
</button>
<div data-simplebar class="mt-5 overflow-auto max-h-72">
<div class="block md:hidden mx-5 p-3">
<input v-model="search" type="text"
class="w-full
rounded-md
border-transparent
py-2
pr-11
pl-4
text-base
bg-indigo-400
text-white
placeholder-gray
focus:border-transparent
focus:outline-none
focus:ring-2 focus:ring-orange-dark" placeholder="Глобальный поиск..."
>
</div>
<div v-if="userSearch" class="text-gray-light divide-y divide-indigo-300"
@click.stop="jump"
>
<inertia-link v-for="user in userSearch" :key="user.id"
:href="route('profile.user', user.username)" class="flex cursor-pointer items-center p-3 hover:bg-indigo-300"
>
<div class="flex-shrink-0 mr-5">
<user-avatar :user="user" size="small"
class="w-8 h-8 md:h-10 md:w-10 text-md"
/>
</div>
<div class="flex flex-col">
<span class="hover:underline text-sm md:text-base block font-medium text-white">{{ user.name }}</span>
<span class="hover:underline text-xs text-gray-light">{{ user.username }}</span>
</div>
</inertia-link>
</div>
<div v-if="tagSearch" class="text-gray-light divide-y divide-indigo-300">
<inertia-link v-for="tag in tagSearch" :key="tag.id"
:href="route('feed.tags', tag.slug)" class="flex cursor-pointer items-center p-3 hover:bg-indigo-300"
>
<div class="flex items-center">
<span class="text-base md:text-lg block font-medium text-white">#{{ tag.name }}</span>
<span class="ml-5 text-xs text-gray-light">{{ tag.feeds_count }}</span>
</div>
</inertia-link>
</div>
</div>
</div>
</div>
</div>
</teleport>
</template>
<script>
import Popper from 'popper.js'
import throttle from 'lodash/throttle'
import axios from 'axios'
import UserAvatar from '@/Shared/Misc/UserAvatar.vue'
export default {
components: {
UserAvatar,
},
data() {
return {
search: '',
open_drop: false,
loadead: false,
popper: null,
data: [],
}
},
computed: {
userSearch() {
return this.data.users
},
tagSearch() {
return this.data.tags
},
},
watch: {
search: {
handler: throttle(function () {
this.loadead = true
axios
.post(route('user.tab.search'), {
search: this.search,
})
.then(({ data }) => {
this.data = data
this.loadead = false
if (data.tags.length || data.users.length) {
this.open_drop = true
}
})
}, 1000),
},
open_drop(open_drop) {
if (open_drop) {
this.$nextTick(() => {
if (window.matchMedia('(min-width: 1024px)').matches) {
this.popper = new Popper(this.$refs.topbar, this.$refs.dropdown, {
placement: 'bottom-start',
modifiers: {
preventOverflow: { boundariesElement: 'scrollParent' },
offset: {
enabled: true,
offset: '0, 10',
},
},
})
} else {
const mainHeader = document.querySelector('.main-header-wrp')
this.popper = new Popper(mainHeader, this.$refs.dropdown, {
placement: 'bottom-center',
positionFixed: true,
modifiers: {
preventOverflow: {
boundariesElement: 'scrollParent',
padding: 10,
},
offset: {
enabled: true,
offset: '0, 10',
},
},
})
}
})
} else if (this.popper) {
setTimeout(() => this.popper.destroy(), 100)
}
},
},
methods: {
jump() {
this.data = []
this.search = ''
this.loadead = false
this.open_drop = false
},
openModal(){
this.open_drop = true
}
},
}
</script>

View File

@@ -0,0 +1,142 @@
<template>
<div class="mt-2 p-2 lg:p-4">
<div class="relative">
<div class="absolute inset-y-0 left-3 flex items-center z-10">
<svg class="flex-shrink-0 h-5 w-5 text-gray-light" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24" fill="currentColor"
> <path fill-rule="evenodd" clip-rule="evenodd"
d="M11 4a7 7 0 100 14 7 7 0 000-14zm-9 7a9 9 0 1118 0 9 9 0 01-18 0z"
/> <path fill-rule="evenodd" clip-rule="evenodd"
d="M15.943 15.943a1 1 0 011.414 0l4.35 4.35a1 1 0 01-1.414 1.414l-4.35-4.35a1 1 0 010-1.414z"
/> </svg>
</div>
<input class=" relative w-full focus:ring-4 focus:ring-offset-1 focus:ring-orange focus:ring-opacity-20 focus:ring-offset-orange focus:border-transparent text-gray border border-indigo-300 bg-indigo-200 rounded-md placeholder-gray-light !pl-10 " placeholder="Поиск музыки"
type="search"
/>
</div>
</div>
<div data-simplebar class="max-h-72 overflow-auto ">
<div class="mt-2 divide-y divide-indigo-100">
<div
v-for="listsMusic in listsFavoriteMusicsState"
:ref="el => { if (el && listsMusic.id === lastElementIDGet) loadMore = el }"
:key="listsMusic.id"
class="
hover:bg-indigo-300 hover:bg-opacity-25 p-4 flex items-center space-x-4" @click="toggleAudioLocal(listsMusic)"
>
<div class="flex">
<button class="default">
<svg class="w-6 h-6">
<use v-show="!listsMusic.playing" xlink:href="#play"></use>
<use v-show="listsMusic.playing" xlink:href="#pause"></use>
</svg>
</button>
</div>
<div class="flex flex-1 text-sm text-gray-light">
{{ listsMusic.name }}
</div>
<music-add-count v-if="$page.props.auth.user" :media_id="listsMusic.id"
:liked="listsMusic.liked" @addAudio="likeAudio"
/>
<div class="text-sm text-gray-light">
<span v-show="!listsMusic.playing">{{ listsMusic.time }}</span>
<span v-show="listsMusic.playing">{{ seekState }}</span>
</div>
</div>
</div>
</div>
</template>
<script>
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { useStore } from 'vuex'
import find from 'lodash/find'
import { Inertia } from '@inertiajs/inertia'
import MusicAddCount from '@/Shared/Misc/MusicAddCount.vue'
export default {
components: {
MusicAddCount,
},
setup() {
const store = useStore()
const containerRef = ref(null)
let observer = null
const playingGet = computed(() => store.getters['playing'])
const lastElementIDGet = computed(() => store.getters['lastFavID'])
const seekState = computed(() => store.state.player.seek)
const currentSongState = computed(() => store.state.player.currentSong)
const listsFavoriteMusicsState = computed(
() => store.state.player.favorite_playlist
)
const toggleAudioLocal = (music) => {
if (playingGet.value || currentSongState.value?.id === music.id) {
store.dispatch('toggleAudio')
return
}
store.dispatch('skipToIndexByMusic', music)
}
onMounted(() => {
store.dispatch('initFavoritedPlaylist').then(() => {
createObserver()
})
})
const createObserver = () => {
if (containerRef.value) {
const intersectionCallback = (entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
observer.unobserve(entry.target)
store.dispatch('paginationFavPlaylist').then((exist) => {
if (exist) {
observer?.disconnect()
createObserver()
}
})
}
})
}
observer = new IntersectionObserver(intersectionCallback, {})
observer.observe(containerRef.value)
}
}
onUnmounted(() => {
observer?.disconnect()
})
const likeAudio = (media_id) => {
Inertia.post(
route('feed.audio.like', media_id),
{},
{
preserveScroll: true,
preserveState: true,
}
)
let media = find(
listsFavoriteMusicsState.value,
(item) => item.id === media_id
)
if (media) {
media.liked = !media.liked
}
}
return {
loadMore: containerRef,
toggleAudioLocal,
listsFavoriteMusicsState,
seekState,
lastElementIDGet,
likeAudio,
}
},
}
</script>

View File

@@ -0,0 +1,113 @@
<template>
<div class="mt-2 p-2 lg:p-4">
<div class="relative">
<div class="absolute inset-y-0 left-3 flex items-center z-10">
<svg class="flex-shrink-0 h-5 w-5 text-gray-light" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24" fill="currentColor"
> <path fill-rule="evenodd" clip-rule="evenodd"
d="M11 4a7 7 0 100 14 7 7 0 000-14zm-9 7a9 9 0 1118 0 9 9 0 01-18 0z"
/> <path fill-rule="evenodd" clip-rule="evenodd"
d="M15.943 15.943a1 1 0 011.414 0l4.35 4.35a1 1 0 01-1.414 1.414l-4.35-4.35a1 1 0 010-1.414z"
/> </svg>
</div>
<input class=" relative w-full focus:ring-4 focus:ring-offset-1 focus:ring-orange focus:ring-opacity-20 focus:ring-offset-orange focus:border-transparent text-gray border border-indigo-300 bg-indigo-200 rounded-md placeholder-gray-light !pl-10 " placeholder="Поиск музыки"
type="search"
/>
</div>
</div>
<div data-simplebar class="max-h-72 overflow-auto ">
<div class="mt-2 divide-y divide-indigo-100">
<div
v-for="listsMusic in listsLoadedMusicsState"
:ref="el => { if (el && listsMusic.id === lastElementIDGet) loadMore = el }"
:key="listsMusic.id"
class="
hover:bg-indigo-300 hover:bg-opacity-25 p-4 flex items-center space-x-4" @click="toggleAudioLocal(listsMusic)"
>
<div class="flex">
<button class="default">
<svg class="w-6 h-6">
<use v-show="!listsMusic.playing" xlink:href="#play"></use>
<use v-show="listsMusic.playing" xlink:href="#pause"></use>
</svg>
</button>
</div>
<div class="flex flex-1 text-sm text-gray-light">
{{ listsMusic.name }}
</div>
<div class="text-sm text-gray-light">
<span v-show="!listsMusic.playing">{{ listsMusic.time }}</span>
<span v-show="listsMusic.playing">{{ seekState }}</span>
</div>
</div>
</div>
</div>
</template>
<script>
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { useStore } from 'vuex'
export default {
setup() {
const store = useStore()
const containerRef = ref(null)
let observer = null
const playingGet = computed(() => store.getters['playing'])
const lastElementIDGet = computed(() => store.getters['lastLoadedID'])
const seekState = computed(() => store.state.player.seek)
const currentSongState = computed(() => store.state.player.currentSong)
const listsLoadedMusicsState = computed(
() => store.state.player.my_playlist
)
const toggleAudioLocal = (music) => {
if (playingGet.value || currentSongState.value?.id === music.id) {
store.dispatch('toggleAudio')
return
}
store.dispatch('skipToIndexByMusic', music)
}
onMounted(() => {
store.dispatch('initLoadedPlaylist').then(() => {
createObserver()
})
})
const createObserver = () => {
if (containerRef.value) {
const intersectionCallback = (entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
observer.unobserve(entry.target)
store.dispatch('paginationLoadedPlaylist').then((exist) => {
if (exist) {
observer?.disconnect()
createObserver()
}
})
}
})
}
observer = new IntersectionObserver(intersectionCallback, {})
observer.observe(containerRef.value)
}
}
onUnmounted(() => {
observer?.disconnect()
})
return {
loadMore: containerRef,
toggleAudioLocal,
listsLoadedMusicsState,
seekState,
lastElementIDGet,
}
},
}
</script>

View File

@@ -0,0 +1,71 @@
<template>
<div data-simplebar class="max-h-72 overflow-auto ">
<div class="mt-2 divide-y divide-indigo-100">
<div
v-for="listsMusic in listsMusics"
:key="listsMusic.id"
:class="[
currentSong?.id === listsMusic.id
? 'bg-indigo-300 bg-opacity-25'
: 'hover:bg-indigo-300 hover:bg-opacity-25',
'p-4 flex items-center space-x-4',
]"
@click="toggleAudioLocal(listsMusic)"
>
<div class="flex">
<toggle-play-button :media_id="listsMusic.id" />
</div>
<div class="flex flex-1 text-sm text-gray-light">
{{ listsMusic.name }}
</div>
<div class="text-sm text-gray-light">
<span v-show="!listsMusic.playing">{{ listsMusic.time }}</span>
<span v-show="listsMusic.playing">{{ seek }}</span>
</div>
</div>
</div>
</div>
</template>
<script>
import { mapActions, mapState } from 'vuex'
import TogglePlayButton from '@/Shared/Misc/TogglePlayButton.vue'
export default {
components: {
TogglePlayButton
},
data() {
return {
}
},
created(){
//console.log('loaded - now');
},
computed: {
//...mapGetters(["playing"]),
...mapState({
seek: (state) => state.player.seek,
currentSong: (state) => state.player.currentSong,
listsMusics: (state) => state.player.playlist,
}),
},
methods: {
...mapActions([
'toggleAudio',
'skipToIndexByMusic',
]),
toggleAudioLocal(music){
if (this.currentSong?.id === music.id) {
this.toggleAudio()
return
}
this.skipToIndexByMusic(music)
},
},
}
</script>

View File

@@ -0,0 +1,47 @@
<template>
<div class="flex">
<button class="default">
<svg class="w-6 h-6">
<use v-show="!listsMusic.playing" xlink:href="#play"></use>
<use v-show="listsMusic.playing" xlink:href="#pause"></use>
</svg>
</button>
</div>
<div class="flex flex-1 text-sm text-gray-light">
{{ listsMusic.name }}
</div>
<div ref="favplayer" class="text-sm text-gray-light">
<span v-show="!listsMusic.playing">{{ listsMusic.time }}</span>
<span v-show="listsMusic.playing">{{ seek }}</span>
</div>
</template>
<script>
import { ref, watch } from 'vue'
import { useIntersectionObserver } from '@/includes/composables'
export default {
props: {
listsMusic: Object,
seek: String,
},
setup() {
const containerRef = ref(null)
const { isIntersecting } = useIntersectionObserver(
containerRef,
{},
showDetail
)
const showDetail = (entry) => {
//console.log(entry)
}
watch(isIntersecting, (isIntersecting) => {
//console.log(isIntersecting)
})
return {
favplayer: containerRef,
showDetail,
}
},
}
</script>