initial commit

This commit is contained in:
Inshal
2024-10-25 01:02:11 +05:00
commit 6e65bc3a62
1710 changed files with 273609 additions and 0 deletions

View File

@@ -0,0 +1,150 @@
<script setup>
import { PerfectScrollbar } from 'vue3-perfect-scrollbar'
import { useChat } from './useChat'
import { useChatStore } from '@/views/apps/chat/useChatStore'
import { avatarText } from '@core/utils/formatters'
const emit = defineEmits(['close'])
const store = useChatStore()
const { resolveAvatarBadgeVariant } = useChat()
</script>
<template>
<template v-if="store.activeChat">
<!-- Close Button -->
<div
class="pt-2 me-2"
:class="$vuetify.locale.isRtl ? 'text-left' : 'text-right'"
>
<IconBtn @click="$emit('close')">
<VIcon
icon="tabler-x"
color="disabled"
class="text-medium-emphasis"
/>
</IconBtn>
</div>
<!-- User Avatar + Name + Role -->
<div class="text-center px-6">
<VBadge
location="bottom right"
offset-x="7"
offset-y="4"
bordered
:color="resolveAvatarBadgeVariant(store.activeChat.contact.status)"
class="chat-user-profile-badge mb-5"
>
<VAvatar
size="80"
:variant="!store.activeChat.contact.avatar ? 'tonal' : undefined"
:color="!store.activeChat.contact.avatar ? resolveAvatarBadgeVariant(store.activeChat.contact.status) : undefined"
>
<VImg
v-if="store.activeChat.contact.avatar"
:src="store.activeChat.contact.avatar"
/>
<span
v-else
class="text-3xl"
>{{ avatarText(store.activeChat.contact.fullName) }}</span>
</VAvatar>
</VBadge>
<h5 class="text-h5">
{{ store.activeChat.contact.fullName }}
</h5>
<p class="text-capitalize text-medium-emphasis">
{{ store.activeChat.contact.role }}
</p>
</div>
<!-- User Data -->
<PerfectScrollbar
class="ps-chat-user-profile-sidebar-content text-medium-emphasis pb-5 px-5"
:options="{ wheelPropagation: false }"
>
<!-- About -->
<div class="my-8">
<span class="text-sm text-disabled">ABOUT</span>
<p class="mt-2">
{{ store.activeChat.contact.about }}
</p>
</div>
<!-- Personal Information -->
<div class="mb-8">
<span class="d-block text-sm text-disabled mb-3">PERSONAL INFORMATION</span>
<div class="d-flex align-center">
<VIcon
class="me-2"
icon="tabler-mail"
size="24"
/>
<span>lucifer@email.com</span>
</div>
<div class="d-flex align-center my-3">
<VIcon
class="me-2"
icon="tabler-phone-call"
size="24"
/>
<span>+1(123) 456 - 7890</span>
</div>
<div class="d-flex align-center">
<VIcon
class="me-2"
icon="tabler-clock"
size="24"
/>
<span>Mon - Fri 10AM - 8PM</span>
</div>
</div>
<!-- Options -->
<div>
<span class="d-block text-sm text-disabled mb-3">OPTIONS</span>
<div class="d-flex align-center">
<VIcon
class="me-2"
icon="tabler-badge"
size="24"
/>
<span>Add Tag</span>
</div>
<div class="d-flex align-center my-3">
<VIcon
class="me-2"
icon="tabler-star"
size="24"
/>
<span>Important Contact</span>
</div>
<div class="d-flex align-center mb-3">
<VIcon
class="me-2"
icon="tabler-photo"
size="24"
/>
<span>Shared Media</span>
</div>
<div class="d-flex align-center mb-3">
<VIcon
class="me-2"
icon="tabler-trash"
size="24"
/>
<span>Delete Contact</span>
</div>
<div class="d-flex align-center">
<VIcon
class="me-2 ms-1"
size="18"
icon="tabler-ban"
/>
<span>Block Contact</span>
</div>
</div>
</PerfectScrollbar>
</template>
</template>

View File

@@ -0,0 +1,91 @@
<script setup>
import { useChat } from '@/views/apps/chat/useChat'
import { useChatStore } from '@/views/apps/chat/useChatStore'
import {
avatarText,
formatDateToMonthShort,
} from '@core/utils/formatters'
const props = defineProps({
isChatContact: {
type: Boolean,
required: false,
default: false,
},
user: {
type: null,
required: true,
},
})
const store = useChatStore()
const { resolveAvatarBadgeVariant } = useChat()
const isChatContactActive = computed(() => {
const isActive = store.activeChat?.contact.id === props.user.id
if (!props.isChatContact)
return !store.activeChat?.chat && isActive
return isActive
})
</script>
<template>
<li :key="store.chatsContacts.length" class="chat-contact cursor-pointer d-flex align-center"
:class="{ 'chat-contact-active': isChatContactActive }">
<VBadge dot location="bottom right" offset-x="3" offset-y="0"
:color="resolveAvatarBadgeVariant(props.user.status)" bordered :model-value="props.isChatContact">
<VAvatar size="38" :variant="!props.user.avatar ? 'tonal' : undefined"
:color="!props.user.avatar ? resolveAvatarBadgeVariant(props.user.status) : undefined">
<VImg v-if="props.user.avatar" :src="props.user.avatar" alt="John Doe" />
<span v-else>{{ avatarText(user.fullName) }}</span>
</VAvatar>
</VBadge>
<div class="flex-grow-1 ms-4 overflow-hidden">
<p class="text-base text-high-emphasis mb-0">
{{ props.user.fullName }}
</p>
<p class="mb-0 text-truncate text-body-2">
{{ props.isChatContact && 'chat' in props.user ? props.user.chat.lastMessage.message : props.user.about
}}
</p>
</div>
<div v-if="props.isChatContact && 'chat' in props.user" class="d-flex flex-column align-self-start">
<span class="text-body-2 text-disabled whitespace-no-wrap">{{
formatDateToMonthShort(props.user.chat.lastMessage.time) }}</span>
<VBadge v-if="props.user.chat.unseenMsgs" color="error" inline :content="props.user.chat.unseenMsgs"
class="ms-auto" />
</div>
</li>
</template>
<style lang="scss">
@use "@styles/variables/_vuetify.scss";
@use "@core/scss/base/mixins";
@use "vuetify/lib/styles/tools/states" as vuetifyStates;
.chat-contact {
border-radius: vuetify.$border-radius-root;
padding-block: 8px;
padding-inline: 12px;
@include mixins.before-pseudo;
@include vuetifyStates.states($active: false);
&.chat-contact-active {
background: linear-gradient(72.47deg, rgb(var(--v-theme-primary)) 0%, #fff 300%);
color: #fff;
--v-theme-on-background: #fff;
.v-avatar {
background: #fff;
outline: 2px solid #fff;
}
}
.v-badge--bordered .v-badge__badge::after {
color: #fff;
}
}
</style>

View File

@@ -0,0 +1,96 @@
<script setup>
import { PerfectScrollbar } from 'vue3-perfect-scrollbar'
import { useChat } from './useChat'
import ChatContact from '@/views/apps/chat/ChatContact.vue'
import { useChatStore } from '@/views/apps/chat/useChatStore'
import AppTextField from '@/@core/app-form-elements/AppTextField.vue'
const props = defineProps({
search: {
type: String,
required: true,
},
isDrawerOpen: {
type: Boolean,
required: true,
},
})
const emit = defineEmits([
'openChatOfContact',
'showUserProfile',
'close',
])
const { resolveAvatarBadgeVariant } = useChat()
const search = useVModel(props, 'search')
const store = useChatStore()
</script>
<template>
<!-- 👉 Chat list header -->
<div v-if="store.profileUser" class="chat-list-header">
<VBadge dot location="bottom right" offset-x="3" offset-y="3"
:color="resolveAvatarBadgeVariant(store.profileUser.status)" bordered>
<VAvatar size="38" class="cursor-pointer" @click="$emit('showUserProfile')">
<VImg :src="store.profileUser.avatar" alt="John Doe" />
</VAvatar>
</VBadge>
<AppTextField v-model="search" placeholder="Search..." class="app-text-field flex-grow-1 ms-4 chat-list-search"
density="compact">
<template #prepend-inner>
<VIcon size="22" icon="tabler-search" />
</template>
</AppTextField>
<IconBtn v-if="$vuetify.display.smAndDown" @click="$emit('close')">
<VIcon icon="tabler-x" class="text-medium-emphasis" />
</IconBtn>
</div>
<VDivider />
<PerfectScrollbar tag="ul" class="d-flex flex-column gap-y-1 chat-contacts-list px-3 list-none"
:options="{ wheelPropagation: false }">
<li>
<span class="chat-contact-header d-block text-primary text-xl font-weight-medium">Chats</span>
</li>
<ChatContact v-for="contact in store.chatsContacts" :key="`chat-${contact.id}`" :user="contact" is-chat-contact
@click="$emit('openChatOfContact', contact.id)" />
<span v-show="!store.chatsContacts.length" class="no-chat-items-text text-disabled">No chats found</span>
<li>
<span class="chat-contact-header d-block text-primary text-xl font-weight-medium">Contacts</span>
</li>
<ChatContact v-for="contact in store.contacts" :key="`chat-${contact.id}`" :user="contact"
@click="$emit('openChatOfContact', contact.id)" />
<span v-show="!store.contacts.length" class="no-chat-items-text text-disabled">No contacts found</span>
</PerfectScrollbar>
</template>
<style lang="scss">
.chat-contacts-list {
--chat-content-spacing-x: 16px;
padding-block-end: 0.75rem;
.chat-contact-header {
margin-block-end: 0.625rem;
margin-block-start: 1.25rem;
}
.chat-contact-header,
.no-chat-items-text {
margin-inline: var(--chat-content-spacing-x);
}
}
.chat-list-search {
.v-field--focused {
box-shadow: none !important;
}
}
</style>

View File

@@ -0,0 +1,125 @@
<script setup>
import { useChatStore } from '@/views/apps/chat/useChatStore'
import { formatDate } from '@core/utils/formatters'
import { PerfectScrollbar } from 'vue3-perfect-scrollbar'
const store = useChatStore()
const contact = computed(() => ({
id: store.activeChat?.contact.id,
avatar: store.activeChat?.contact.avatar,
}))
const resolveFeedbackIcon = feedback => {
if (feedback.isSeen)
return {
icon: 'tabler-checks',
color: 'success',
}
else if (feedback.isDelivered)
return {
icon: 'tabler-checks',
color: undefined,
}
else
return {
icon: 'tabler-check',
color: undefined,
}
}
const msgGroups = computed(() => {
let messages = []
const _msgGroups = []
if (store.activeChat.chat) {
messages = store.activeChat.chat.messages
let msgSenderId = messages[0].senderId
let msgGroup = {
senderId: msgSenderId,
messages: [],
}
messages.forEach((msg, index) => {
if (msgSenderId === msg.senderId) {
msgGroup.messages.push({
message: msg.message,
time: msg.time,
feedback: msg.feedback,
})
} else {
msgSenderId = msg.senderId
_msgGroups.push(msgGroup)
msgGroup = {
senderId: msg.senderId,
messages: [{
message: msg.message,
time: msg.time,
feedback: msg.feedback,
}],
}
}
if (index === messages.length - 1)
_msgGroups.push(msgGroup)
})
}
return _msgGroups
})
</script>
<template>
<div class="chat-log pa-5">
<div v-for="(msgGrp, index) in msgGroups" :key="msgGrp.senderId + String(index)"
class="chat-group d-flex align-start" :class="[{
'flex-row-reverse': msgGrp.senderId !== contact.id,
'mb-4': msgGroups.length - 1 !== index,
}]">
<div class="chat-avatar" :class="msgGrp.senderId !== contact.id ? 'ms-4' : 'me-4'">
<VAvatar size="32">
<VImg :src="msgGrp.senderId === contact.id ? contact.avatar : store.profileUser?.avatar" />
</VAvatar>
</div>
<div class="chat-body d-inline-flex flex-column"
:class="msgGrp.senderId !== contact.id ? 'align-end' : 'align-start'">
<div v-for="(msgData, msgIndex) in msgGrp.messages" :key="msgData.time"
class="chat-content py-2 px-4 elevation-1" style="background-color: rgb(var(--v-theme-surface));"
:class="[
msgGrp.senderId === contact.id ? 'chat-left' : 'bg-primary text-white chat-right',
msgGrp.messages.length - 1 !== msgIndex ? 'mb-3' : 'mb-1',
]">
<p class="mb-0 text-base"> {{ msgData.message }}</p>
</div>
<div :class="{ 'text-right': msgGrp.senderId !== contact.id }">
<VIcon v-if="msgGrp.senderId !== contact.id" size="18"
:color="resolveFeedbackIcon(msgGrp.messages[msgGrp.messages.length - 1].feedback).color">
{{ resolveFeedbackIcon(msgGrp.messages[msgGrp.messages.length - 1].feedback).icon }}
</VIcon>
<span class="text-sm ms-1 text-disabled">{{ formatDate(msgGrp.messages[msgGrp.messages.length -
1].time, { hour: 'numeric', minute: 'numeric' }) }}</span>
</div>
</div>
</div>
</div>
</template>
<style lang=scss>
.chat-log {
.chat-content {
border-end-end-radius: 6px;
border-end-start-radius: 6px;
&.chat-left {
border-start-end-radius: 6px;
}
&.chat-right {
border-start-start-radius: 6px;
}
}
}
.text-base {
font-size: .9375rem !important;
line-height: 1.375rem !important;
}
</style>

View File

@@ -0,0 +1,111 @@
<script setup>
import { PerfectScrollbar } from 'vue3-perfect-scrollbar'
import { useChat } from './useChat'
import { useChatStore } from '@/views/apps/chat/useChatStore'
import { avatarText } from '@core/utils/formatters'
import AppTextarea from '@/@core/app-form-elements/AppTextarea.vue'
const emit = defineEmits(['close'])
// composables
const store = useChatStore()
const { resolveAvatarBadgeVariant } = useChat()
const userStatusRadioOptions = [
{
title: 'Online',
value: 'online',
color: 'success',
},
{
title: 'Away',
value: 'away',
color: 'warning',
},
{
title: 'Do not disturb',
value: 'busy',
color: 'error',
},
{
title: 'Offline',
value: 'offline',
color: 'secondary',
},
]
</script>
<template>
<template v-if="store.profileUser">
<!-- Close Button -->
<div class="pt-2 me-2 text-end">
<IconBtn @click="$emit('close')">
<VIcon class="text-medium-emphasis" color="disabled" icon="tabler-x" />
</IconBtn>
</div>
<!-- User Avatar + Name + Role -->
<div class="text-center px-6">
<VBadge location="bottom right" offset-x="7" offset-y="4" bordered
:color="resolveAvatarBadgeVariant(store.profileUser.status)" class="chat-user-profile-badge mb-3">
<VAvatar size="84" :variant="!store.profileUser.avatar ? 'tonal' : undefined"
:color="!store.profileUser.avatar ? resolveAvatarBadgeVariant(store.profileUser.status) : undefined">
<VImg v-if="store.profileUser.avatar" :src="store.profileUser.avatar" />
<span v-else class="text-3xl">{{ avatarText(store.profileUser.fullName) }}</span>
</VAvatar>
</VBadge>
<h5 class="text-h5">
{{ store.profileUser.fullName }}
</h5>
<p class="text-capitalize text-medium-emphasis">
{{ store.profileUser.role }}
</p>
</div>
<!-- User Data -->
<PerfectScrollbar class="ps-chat-user-profile-sidebar-content pb-5 px-5" :options="{ wheelPropagation: false }">
<!-- About -->
<div class="my-5 text-medium-emphasis">
<span for="textarea-user-about" class="text-sm text-disabled">ABOUT</span>
<AppTextarea id="textarea-user-about" v-model="store.profileUser.about" auto-grow class="mt-1"
rows="4" />
</div>
<!-- Status -->
<div class="mb-5">
<span class="text-sm text-disabled">STATUS</span>
<VRadioGroup v-model="store.profileUser.status" class="mt-1">
<VRadio v-for="radioOption in userStatusRadioOptions" :key="radioOption.title"
:label="radioOption.title" :value="radioOption.value" :color="radioOption.color" />
</VRadioGroup>
</div>
<!-- Settings -->
<div class="text-medium-emphasis">
<span class="text-sm text-disabled">SETTINGS</span>
<div class="d-flex align-center my-3">
<VIcon class="me-2" icon="tabler-message-dots" size="22" />
<span class="text-high-emphasis">Two-step Verification</span>
</div>
<div class="d-flex align-center mb-3">
<VIcon class="me-2" icon="tabler-bell" size="22" />
<span class="text-high-emphasis">Notification</span>
</div>
<div class="d-flex align-center mb-3">
<VIcon class="me-2" icon="tabler-user-plus" size="22" />
<span class="text-high-emphasis">Invite Friends</span>
</div>
<div class="d-flex align-center">
<VIcon class="me-2" icon="tabler-trash" size="22" />
<span class="text-high-emphasis">Delete Account</span>
</div>
</div>
<!-- Logout Button -->
<VBtn color="primary" class="mt-11">
Logout
</VBtn>
</PerfectScrollbar>
</template>
</template>

View File

@@ -0,0 +1,16 @@
export const useChat = () => {
const resolveAvatarBadgeVariant = status => {
if (status === 'online')
return 'success'
if (status === 'busy')
return 'error'
if (status === 'away')
return 'warning'
return 'secondary'
}
return {
resolveAvatarBadgeVariant,
}
}

View File

@@ -0,0 +1,74 @@
import axios from '@axios';
export const useChatStore = defineStore('chat', {
// arrow function recommended for full type inference
state: () => ({
contacts: [],
chatsContacts: [],
profileUser: undefined,
activeChat: null,
}),
actions: {
async fetchChatsAndContacts(q) {
const { data } = await axios.get('/apps/chat/chats-and-contacts', {
params: { q },
})
const { chatsContacts, contacts, profileUser } = data
this.chatsContacts = chatsContacts
this.contacts = contacts
this.profileUser = profileUser
},
async getChat(userId) {
const { data } = await axios.get(`/apps/chat/chats/${userId}`)
this.activeChat = data
},
async sendMsg(message) {
const senderId = this.profileUser?.id
const { data } = await axios.post(`/apps/chat/chats/${this.activeChat?.contact.id}`, { message, senderId })
const { msg, chat } = data
console.log(this.activeChat?.contact.id,senderId)
console.log(data)
// ? If it's not undefined => New chat is created (Contact is not in list of chats)
if (chat !== undefined) {
const activeChat = this.activeChat
this.chatsContacts.push({
...activeChat.contact,
chat: {
id: chat.id,
lastMessage: [],
unseenMsgs: 0,
messages: [msg],
},
})
if (this.activeChat) {
this.activeChat.chat = {
id: chat.id,
messages: [msg],
unseenMsgs: 0,
userId: this.activeChat?.contact.id,
}
}
}
else {
this.activeChat?.chat?.messages.push(msg)
}
// Set Last Message for active contact
const contact = this.chatsContacts.find(c => {
if (this.activeChat)
return c.id === this.activeChat.contact.id
return false
})
contact.chat.lastMessage = msg
},
},
})