This commit is contained in:
2026-03-11 17:56:17 +03:00
parent 24c935df5f
commit 5a188b80e3
32 changed files with 225 additions and 828 deletions

View File

@@ -8,7 +8,7 @@ const authStore = useAuthStore()
<template> <template>
<div class="h-screen bg-gray-500"> <div class="h-screen bg-gray-500">
<div class="h-full m-auto py-4 md:w-4/5 sm:w-full w-full"> <div class="h-full m-auto py-4 md:w-full sm:w-full w-full">
<MainView v-if="authStore.isAuth" /> <MainView v-if="authStore.isAuth" />
<SignInView v-else /> <SignInView v-else />
</div> </div>

View File

@@ -1,7 +1,7 @@
@import "tailwindcss"; @import "tailwindcss";
html { html {
font-size: 14px; font-size: 16px;
font-weight: 400; font-weight: 400;
font-family: "Montserrat", sans-serif; font-family: "Montserrat", sans-serif;
font-optical-sizing: auto; font-optical-sizing: auto;

View File

@@ -0,0 +1,11 @@
<script setup lang="ts">
import { useMenuStore } from '@/stores/menu.ts'
const menuStore = useMenuStore()
</script>
<template>
<VBtn icon="mdi-arrow-left" size="small" @click="menuStore.selected = 'chats'" />
</template>
<style scoped></style>

View File

@@ -1,52 +0,0 @@
<script setup lang="ts">
import { type Chat, useChatsStore } from '@/stores/chats.ts'
import { useAuthStore } from '@/stores/auth.ts'
import { computed } from 'vue'
interface Props {
chat: Chat
}
const { chat } = defineProps<Props>()
const authStore = useAuthStore()
const chatName = computed(() => {
switch (chat.type_id) {
case 1:
const otherUsers = chat.users.filter((user) => user.id !== authStore.me?.id)
return otherUsers[0]?.name ?? otherUsers[0]?.email ?? 'unknown'
case 2:
return chat.name
default:
return 'chat'
}
})
const avatarText = computed(() => {
return chatName.value.slice(0, 1).toUpperCase()
})
const lastMessageCreatedAt = computed(() => {
return new Date().toLocaleTimeString('ru-RU', { timeStyle: 'short' })
})
</script>
<template>
<VListItem :value="chat.id">
<template v-slot:prepend>
<VAvatar color="primary" :text="avatarText" />
</template>
<template v-slot:append>
<div class="flex flex-col justify-end">
<div class="text-xs">{{ lastMessageCreatedAt }}</div>
<VChip size="small">12</VChip>
</div>
</template>
<v-list-item-title>{{ chatName }}</v-list-item-title>
<v-list-item-subtitle>subtitle</v-list-item-subtitle>
</VListItem>
</template>
<style scoped></style>

View File

@@ -0,0 +1,75 @@
<script setup lang="ts">
import { type Chat } from '@/stores/chats.ts'
import { useAuthStore } from '@/stores/auth.ts'
import { computed } from 'vue'
interface Props {
chat: Chat
}
const { chat } = defineProps<Props>()
const authStore = useAuthStore()
const chatName = computed(() => {
switch (chat.typeId) {
case 1:
const otherUsers = chat.users.filter((user) => user.id !== authStore.me?.id)
return otherUsers[0]?.name ?? otherUsers[0]?.email ?? 'unknown'
case 2:
return chat.name
default:
return 'chat'
}
})
const lastMessage = computed(() => {
return chat.message?.message ?? ''
})
const avatarText = computed(() => {
return chatName.value.slice(0, 1).toUpperCase()
})
const lastMessageCreatedAt = computed(() => {
if (!chat.message) return ''
const date = new Date(chat.message.createdAt)
return date.toLocaleTimeString('ru-RU', { timeStyle: 'short' })
})
</script>
<template>
<VListItem :value="chat.id">
<template v-slot:prepend>
<VAvatar color="primary" :text="avatarText" />
</template>
<template #title>
<div class="flex justify-between">
<div class="font-medium truncate">
{{ chatName }}
</div>
<div class="text-xs">
{{ lastMessageCreatedAt }}
</div>
</div>
</template>
<template #subtitle>
<div class="flex justify-between">
<div>{{ lastMessage }}</div>
<div class="text-xs">
<!-- <VChip v-show="true" size="small" text="0" />-->
</div>
</div>
</template>
<!-- <template v-slot:append>-->
<!-- <div class="flex flex-col">-->
<!-- <div class="text-sm">{{ lastMessageCreatedAt }}</div>-->
<!-- <VChip v-show="true" size="small">0</VChip>-->
<!-- </div>-->
<!-- </template>-->
</VListItem>
</template>
<style scoped></style>

View File

@@ -1,9 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { useChatsStore } from '@/stores/chats.ts' import { useChatsStore } from '@/stores/chats.ts'
import { SocketDataReq, useSocketsStore } from '@/stores/sockets.ts' import { SocketDataReq, useSocketsStore } from '@/stores/sockets.ts'
import ChatListElement from '@/components/ChatListElement.vue'
import { onMounted, ref, watch, watchEffect } from 'vue' import { onMounted, ref, watch, watchEffect } from 'vue'
import { type SelectedMenu, useMenuStore } from '@/stores/menu.ts' import { type SelectedMenu, useMenuStore } from '@/stores/menu.ts'
import ChatListElement from '@/components/Chats/ChatListElement.vue'
const chatsStore = useChatsStore() const chatsStore = useChatsStore()
const socketsStore = useSocketsStore() const socketsStore = useSocketsStore()
@@ -17,7 +17,6 @@ watch(selected, (val) => {
}) })
watch(menu, (val) => { watch(menu, (val) => {
console.log(val)
if (val) menuStore.selected = val[0] as SelectedMenu if (val) menuStore.selected = val[0] as SelectedMenu
}) })
@@ -34,29 +33,23 @@ onMounted(() => console.log('CHAT LIST'))
<template> <template>
<div class="flex flex-col h-full"> <div class="flex flex-col h-full">
<VToolbar> <VToolbar class="px-2">
<VMenu transition="slide-y-transition"> <VMenu transition="slide-y-transition">
<template v-slot:activator="{ props }"> <template v-slot:activator="{ props }">
<VBtn icon="mdi-menu" v-bind="props" /> <VBtn size="small" icon="mdi-menu" class="mr-2" v-bind="props" />
</template> </template>
<VList class="top-1" density="compact" slim v-model:selected="menu"> <VList class="top-1" density="compact" slim v-model:selected="menu">
<VListItem density="compact" value="profile" prepend-gap="8" title="Profile">
<template #prepend>
<VAvatar color="primary" text="A" />
</template>
</VListItem>
<VDivider class="my-1" />
<VListItem value="users" title="Contacts" prepend-gap="8" prepend-icon="mdi-account" /> <VListItem value="users" title="Contacts" prepend-gap="8" prepend-icon="mdi-account" />
<VListItem value="settings" title="Settings" prepend-gap="8" prepend-icon="mdi-cog" /> <VListItem value="settings" title="Settings" prepend-gap="8" prepend-icon="mdi-cog" />
<VDivider class="my-1" /> <VDivider class="my-1" />
<VListItem value="logout" title="Log Out" /> <VListItem value="logout" title="Log Out" />
</VList> </VList>
</VMenu> </VMenu>
<VSheet class="w-full mx-2"> <VSheet class="w-full">
<VTextField prepend-inner-icon="mdi-magnify" label="search chat"></VTextField> <VTextField prepend-inner-icon="mdi-magnify" placeholder="search chat" />
</VSheet> </VSheet>
</VToolbar> </VToolbar>
<VList v-model:selected="selected" mandatory> <VList v-model:selected="selected" mandatory lines="one">
<ChatListElement v-for="chat in chatsStore.chats" :key="chat.id" v-bind="{ chat }" /> <ChatListElement v-for="chat in chatsStore.chats" :key="chat.id" v-bind="{ chat }" />
</VList> </VList>
</div> </div>

View File

@@ -1,46 +1,30 @@
<script setup lang="ts"> <script setup lang="ts">
import ChatToolbar from '@/components/ChatToolbar.vue' import { useMenuStore } from '@/stores/menu.ts'
import ChatList from '@/components/ChatList.vue' import { computed } from 'vue'
import { useAuthStore } from '@/stores/auth.ts' import ChatsList from '@/components/Chats/ChatsList.vue'
import { type Chat, useChatsStore } from '@/stores/chats.ts' import UsersList from '@/components/Users/UsersList.vue'
import { SocketDataReq, useSocketsStore } from '@/stores/sockets.ts' import SettingsList from '@/components/Settings/SettingsList.vue'
import UserList from '@/components/Users/UserList.vue'
const authStore = useAuthStore() const menuStore = useMenuStore()
const chatsStore = useChatsStore()
const socketsStore = useSocketsStore()
interface Props { const component = computed(() => {
selected: string console.log(menuStore.selected)
} switch (menuStore.selected) {
case 'chats':
const props = defineProps() return ChatsList
case 'users':
function onSelect(value: { id: unknown }) { return UsersList
const chatId = value.id as string case 'settings':
chatsStore.selected = chatId return SettingsList
default:
socketsStore.send({ return ChatsList
type: SocketDataReq.GET_MESSAGES,
data: { chat_id: chatId },
})
}
function getChatName(chat: Chat) {
if (chat.type_id === 2) {
return chat.name
} else if (chat.type_id === 1) {
const otherUsers = chat.users.filter((user) => user.id !== authStore.me?.id)
return otherUsers[0]?.name ?? otherUsers[0]?.email ?? 'unknown'
} }
})
return 'unknown ID'
}
</script> </script>
<template> <template>
<div class="flex flex-col h-full gap-2"> <div class="flex flex-col h-full gap-2">
<slot /> <slot :component="component" />
</div> </div>
</template> </template>

View File

@@ -1,10 +0,0 @@
<script setup lang="ts"></script>
<template>
<div class="d-flex ga-2 pa-4 border">
<div>V</div>
<div>name</div>
</div>
</template>
<style scoped></style>

View File

@@ -0,0 +1,10 @@
<script setup lang="ts"></script>
<template>
<VToolbar class="px-2">
<VAvatar text="A" color="primary" />
<VToolbarTitle text="Message title" />
</VToolbar>
</template>
<style scoped></style>

View File

@@ -0,0 +1,33 @@
<script setup lang="ts">
import { computed, nextTick, useTemplateRef, watch } from 'vue'
import { useScroll } from '@vueuse/core'
const area = useTemplateRef('messageArea')
const { y, arrivedState } = useScroll(area)
const messages = computed(() => {
// return [...messagesStore.messages]
})
async function scrollToBottom() {
await nextTick()
if (area.value) y.value = area.value?.scrollHeight
}
</script>
<template>
<div class="flex h-full flex-col overflow-hidden">
<div class="grow-0">
<slot name="toolbar" />
</div>
<div class="px-8 gap-2 grow flex flex-col-reverse overflow-y-auto" ref="messageArea">
<slot />
</div>
<div class="grow-0">
<slot name="input" />
</div>
</div>
</template>
<style scoped></style>

View File

@@ -1,54 +1,41 @@
<script setup lang="ts"> <script setup lang="ts">
import MessageToolbar from '@/components/MessageToolbar.vue'
import MessageForm from '@/components/MessageForm.vue'
import MessageData from '@/components/MessageData.vue'
import { useMessagesStore } from '@/stores/messages.ts'
import { useAuthStore } from '@/stores/auth.ts' import { useAuthStore } from '@/stores/auth.ts'
import { computed, nextTick, useTemplateRef, watch } from 'vue' import MessagesForm from '@/components/Messages/MessagesForm.vue'
import { useScroll } from '@vueuse/core' import MessageToolbar from '@/components/Messages/MessageToolbar.vue'
import { useChatsStore } from '@/stores/chats.ts' import MessageInput from '@/components/Messages/MessageInput.vue'
import MessageData from '@/components/Messages/MessageData.vue'
import { ref } from 'vue'
import type { Message } from '@/stores/messages.ts'
const authStore = useAuthStore() const authStore = useAuthStore()
const messagesStore = useMessagesStore()
const chatsStore = useChatsStore()
const area = useTemplateRef('messageArea') // watch(messages, async () => {
const { y, arrivedState } = useScroll(area) // await scrollToBottom()
// })
const messages = computed(() => { const messages = ref<Message[]>([])
return [...messagesStore.messages]
})
async function scrollToBottom() {
await nextTick()
if (area.value) y.value = area.value?.scrollHeight
}
watch(messages, async () => {
await scrollToBottom()
})
</script> </script>
<template> <template>
<div class="h-full"> <div class="h-full bg-gray-300">
<!-- <button class="position-absolute scroll-down" @click="scrollToBottom">UP</button>-->
<div class="flex flex-col h-full"> <div class="flex flex-col h-full">
<div class="flex h-full flex-col overflow-hidden"> <MessagesForm>
<message-toolbar class="grow-0" /> <template #toolbar>
<MessageToolbar />
<div class="px-8 gap-2 grow flex flex-col-reverse overflow-y-auto" ref="messageArea"> </template>
<message-data <template #default>
<MessageData
v-for="message in messages" v-for="message in messages"
:key="message.id" :key="message.id"
:text="message.text" :text="message.message"
:my="authStore.me?.id === message.user_id" :my="authStore.me?.id === message.userId"
:created-at="message.created_at" :created-at="message.createdAt"
></message-data> />
<!-- <div class="p-4 border" v-for="v in 101" :key="v">text {{ v }}</div>--> </template>
</div> <template #input>
<MessageInput />
<message-form class="grow-0 bg-green-200" /> </template>
</div> </MessagesForm>
</div> </div>
</div> </div>
</template> </template>

View File

@@ -1,270 +0,0 @@
<template>
<v-container class="d-flex" style="height: 600px; gap: 16px">
<!-- Постоянный rail drawer на v-card -->
<v-card width="72" height="100%" elevation="3" class="rail-drawer" rounded="lg">
<!-- Логотип/бренд -->
<div class="d-flex justify-center py-4">
<v-avatar color="primary" size="40" class="brand-avatar">
<span class="text-white font-weight-bold">V</span>
</v-avatar>
</div>
<v-divider></v-divider>
<!-- Основная навигация -->
<v-list density="compact" nav class="px-2 py-2">
<v-list-item
v-for="item in mainMenu"
:key="item.title"
:value="item.title"
:active="activeItem === item.title"
@click="activeItem = item.title"
class="rail-list-item mb-1"
:class="{ 'active-item': activeItem === item.title }"
>
<template v-slot:prepend>
<v-badge
v-if="item.badge"
:content="item.badge"
color="error"
dot
location="bottom end"
offset-x="2"
offset-y="2"
>
<v-icon :icon="item.icon" size="24"></v-icon>
</v-badge>
<v-icon v-else :icon="item.icon" size="24"></v-icon>
</template>
<!-- Тултип с названием -->
<v-tooltip
activator="parent"
location="right"
transition="slide-x-transition"
:open-delay="300"
>
{{ item.title }}
</v-tooltip>
</v-list-item>
</v-list>
<v-divider></v-divider>
<!-- Нижняя группа иконок -->
<v-list density="compact" nav class="px-2 py-2" style="margin-top: auto">
<v-list-item
v-for="item in bottomMenu"
:key="item.title"
:value="item.title"
:active="activeItem === item.title"
@click="activeItem = item.title"
class="rail-list-item mb-1"
>
<template v-slot:prepend>
<v-icon :icon="item.icon" size="24"></v-icon>
</template>
<v-tooltip
activator="parent"
location="right"
transition="slide-x-transition"
:open-delay="300"
>
{{ item.title }}
</v-tooltip>
</v-list-item>
</v-list>
<!-- Профиль пользователя внизу -->
<div class="mt-auto">
<v-divider></v-divider>
<div class="d-flex justify-center py-3">
<v-menu location="top right" offset="15">
<template v-slot:activator="{ props }">
<v-avatar
size="44"
color="grey-lighten-2"
v-bind="props"
class="profile-avatar cursor-pointer"
>
<v-img src="https://randomuser.me/api/portraits/women/44.jpg"></v-img>
<v-tooltip activator="parent" location="right" :open-delay="300">
Профиль
</v-tooltip>
</v-avatar>
</template>
<!-- Меню профиля -->
<v-card min-width="200" rounded="lg">
<v-list>
<v-list-item
prepend-avatar="https://randomuser.me/api/portraits/women/44.jpg"
title="Анна Петрова"
subtitle="anna@email.com"
></v-list-item>
</v-list>
<v-divider></v-divider>
<v-list>
<v-list-item
prepend-icon="mdi-account"
title="Мой профиль"
value="profile"
></v-list-item>
<v-list-item
prepend-icon="mdi-cog"
title="Настройки"
value="settings"
></v-list-item>
<v-list-item prepend-icon="mdi-logout" title="Выйти" value="logout"></v-list-item>
</v-list>
</v-card>
</v-menu>
</div>
</div>
</v-card>
<!-- Основной контент -->
<v-card class="flex-grow-1 content-area" elevation="2" rounded="lg">
<!-- Хедер контента -->
<div class="d-flex align-center pa-4">
<div class="text-h5 font-weight-medium">
{{ getActiveTitle }}
</div>
<v-spacer></v-spacer>
<v-btn icon="mdi-bell-outline" variant="text" size="small"></v-btn>
<v-btn icon="mdi-magnify" variant="text" size="small" class="mr-2"></v-btn>
</div>
<v-divider></v-divider>
<!-- Контент -->
<div class="pa-6">
<div class="text-h6 mb-4">Добро пожаловать в раздел "{{ getActiveTitle }}"</div>
<v-row>
<v-col v-for="n in 6" :key="n" cols="12" md="6" lg="4">
<v-card elevation="1" rounded="lg" class="content-card">
<v-card-item>
<v-card-title>Карточка {{ n }}</v-card-title>
<v-card-subtitle>Описание карточки {{ n }}</v-card-subtitle>
</v-card-item>
<v-card-text>
Контент карточки {{ n }}. Здесь может быть любая информация.
</v-card-text>
</v-card>
</v-col>
</v-row>
</div>
</v-card>
</v-container>
</template>
<script setup>
import { ref, computed } from 'vue'
const activeItem = ref('Главная')
const mainMenu = [
{ title: 'Главная', icon: 'mdi-home-outline', activeIcon: 'mdi-home' },
{ title: 'Профиль', icon: 'mdi-account-outline', activeIcon: 'mdi-account' },
{ title: 'Сообщения', icon: 'mdi-message-outline', activeIcon: 'mdi-message', badge: '3' },
{ title: 'Файлы', icon: 'mdi-folder-outline', activeIcon: 'mdi-folder' },
{ title: 'Аналитика', icon: 'mdi-chart-line', activeIcon: 'mdi-chart-line' },
{ title: 'Календарь', icon: 'mdi-calendar-outline', activeIcon: 'mdi-calendar' },
]
const bottomMenu = [
{ title: 'Настройки', icon: 'mdi-cog-outline', activeIcon: 'mdi-cog' },
{ title: 'Помощь', icon: 'mdi-help-circle-outline', activeIcon: 'mdi-help-circle' },
]
const getActiveTitle = computed(() => {
const found = [...mainMenu, ...bottomMenu].find((item) => item.title === activeItem.value)
return found ? found.title : 'Главная'
})
</script>
<style scoped>
.rail-drawer {
display: flex;
flex-direction: column;
overflow-y: auto;
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE/Edge */
}
.rail-drawer::-webkit-scrollbar {
display: none; /* Chrome/Safari */
}
.rail-list-item {
justify-content: center !important;
min-height: 48px !important;
padding: 0 8px !important;
border-radius: 12px !important;
transition: all 0.2s ease;
}
.rail-list-item:hover {
background-color: rgba(var(--v-theme-primary), 0.08) !important;
transform: scale(1.05);
}
.rail-list-item.active-item {
background-color: rgb(var(--v-theme-primary)) !important;
}
.rail-list-item.active-item .v-icon {
color: white !important;
}
.rail-list-item .v-icon {
transition: transform 0.2s ease;
}
.rail-list-item:hover .v-icon {
transform: scale(1.1);
}
.brand-avatar {
transition: all 0.3s ease;
}
.brand-avatar:hover {
transform: rotate(10deg) scale(1.1);
}
.profile-avatar {
transition: all 0.2s ease;
border: 2px solid transparent;
}
.profile-avatar:hover {
border-color: rgb(var(--v-theme-primary));
transform: scale(1.05);
}
.content-area {
overflow-y: auto;
background-color: rgb(var(--v-theme-background)) !important;
}
.content-card {
transition: all 0.2s ease;
}
.content-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1) !important;
}
/* Анимация для тултипов */
:deep(.v-tooltip .v-overlay__content) {
background-color: rgb(var(--v-theme-primary)) !important;
color: white !important;
font-weight: 500;
font-size: 0.85rem;
padding: 6px 12px !important;
border-radius: 8px !important;
}
</style>

View File

@@ -1,72 +0,0 @@
<template>
<v-container class="d-flex ga-4" style="height: 500px">
<!-- Простой rail drawer -->
<v-card width="72" height="100%" elevation="0" class="simple-rail">
<div class="d-flex flex-column align-center py-4">
<v-avatar color="primary" size="40" class="mb-4">
<v-icon icon="mdi-vuetify" color="white"></v-icon>
</v-avatar>
<v-divider class="w-100 mb-2"></v-divider>
<v-list density="compact" nav class="w-100 px-1">
<v-list-item
v-for="icon in icons"
:key="icon.name"
:value="icon.name"
:active="active === icon.name"
@click="active = icon.name"
class="simple-item"
>
<template v-slot:prepend>
<v-icon :icon="icon.icon"></v-icon>
</template>
</v-list-item>
</v-list>
<v-spacer></v-spacer>
<v-divider class="w-100 my-2"></v-divider>
<v-avatar size="44" class="mt-2 cursor-pointer" color="grey-lighten-3">
<v-img src="https://randomuser.me/api/portraits/men/32.jpg"></v-img>
<v-tooltip activator="parent" location="right">Профиль</v-tooltip>
</v-avatar>
</div>
</v-card>
</v-container>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const active = ref('Главная')
const icons = [
{ name: 'Главная', icon: 'mdi-home' },
{ name: 'Профиль', icon: 'mdi-account' },
{ name: 'Сообщения', icon: 'mdi-message' },
{ name: 'Настройки', icon: 'mdi-cog' },
]
</script>
<style scoped>
.simple-rail {
background-color: rgb(var(--v-theme-surface)) !important;
}
.simple-item {
justify-content: center !important;
min-height: 44px !important;
border-radius: 10px !important;
margin: 2px 0 !important;
}
.simple-item:hover {
background-color: rgba(var(--v-theme-primary), 0.1) !important;
}
.simple-item.v-list-item--active {
background-color: rgb(var(--v-theme-primary)) !important;
color: white !important;
}
</style>

View File

@@ -4,6 +4,7 @@ import { onMounted } from 'vue'
import { useUsersStore } from '@/stores/users.ts' import { useUsersStore } from '@/stores/users.ts'
import UsersListElement from '@/components/Users/UsersListElement.vue' import UsersListElement from '@/components/Users/UsersListElement.vue'
import { useAuthStore } from '@/stores/auth.ts' import { useAuthStore } from '@/stores/auth.ts'
import BackToChats from '@/components/BackToChats.vue'
const socketsStore = useSocketsStore() const socketsStore = useSocketsStore()
const usersStore = useUsersStore() const usersStore = useUsersStore()
@@ -31,21 +32,21 @@ function onStartChat(userId: number) {
<template> <template>
<div class="flex flex-col h-full"> <div class="flex flex-col h-full">
<VToolbar> <VToolbar class="px-2">
<VBtn icon="mdi-arrow-left"></VBtn> <BackToChats class="mr-2" />
<VSheet class="w-full mx-2"> <VSheet class="w-full">
<VTextField prepend-inner-icon="mdi-magnify" label="search contacts"></VTextField> <VTextField prepend-inner-icon="mdi-magnify" placeholder="search contacts" />
</VSheet> </VSheet>
</VToolbar> </VToolbar>
<div class="flex flex-col gap-2 h-full overflow-y-auto"> <VList>
<UsersListElement <UsersListElement
v-for="user in usersStore.users" v-for="user in usersStore.users"
:key="user.id" :key="user.id"
v-bind:user="user" v-bind:user="user"
@click="onStartChat" @click="onStartChat"
/> />
</div> </VList>
</div> </div>
</template> </template>

View File

@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import type { User } from '@/stores/users.ts' import type { User } from '@/stores/users.ts'
import { JustButton } from '@/components/simple' import { computed } from 'vue'
interface Props { interface Props {
user: User user: User
@@ -10,16 +10,32 @@ const { user } = defineProps<Props>()
const emit = defineEmits<{ const emit = defineEmits<{
click: [id: number] click: [id: number]
}>() }>()
const avatarText = computed(() => {
return user.name.slice(0, 1).toUpperCase() ?? ':)'
})
</script> </script>
<template> <template>
<div class="border p-2"> <VListItem>
<!-- <div>log: {{ user }}</div>--> <template v-slot:prepend>
<div>id:{{ user.id }}</div> <VAvatar color="primary" :text="avatarText" />
<div>name:{{ user.name }}</div> </template>
<div>email:{{ user.email }}</div>
<JustButton @click="emit('click', user.id)">start chat</JustButton> <template #title>
<div>
{{ user.name }}
</div> </div>
</template>
<template #subtitle>
<div>last seen never recently</div>
</template>
<template #append>
<VBtn icon="mdi-message" color="default" slim size="small" @click="emit('click', user.id)" />
</template>
</VListItem>
</template> </template>
<style scoped></style> <style scoped></style>

View File

@@ -1,9 +0,0 @@
<script setup lang="ts"></script>
<template>
<button class="border border-gray-700 rounded-md text-gray-700 px-1.5 py-1.5 flex justify-center">
<slot></slot>
</button>
</template>
<style scoped></style>

View File

@@ -1,12 +0,0 @@
<script setup lang="ts">
const model = defineModel()
</script>
<template>
<input
v-model="model"
class="border px-2 py-1.5 rounded-md bg-white outline-none focus:outline-none"
/>
</template>
<style scoped></style>

View File

@@ -1,23 +0,0 @@
<script setup lang="ts">
import { ref, useSlots } from 'vue'
const model = defineModel('selected')
function checkSlots() {
const slots = useSlots()
console.log('slots')
if (slots.default) {
// console.log('default slot', slots.default())
const a = slots.default().map((el) => el)
console.log('a', a)
}
}
</script>
<template>
<div class="flex flex-col gap-2">
<slot />
</div>
</template>
<style scoped></style>

View File

@@ -1,22 +0,0 @@
<script setup lang="ts">
import JustButton from '@/components/simple/JustButton.vue'
import type { SelectedMenu } from '@/stores/menu.ts'
interface Props {
icon: string
value: SelectedMenu
}
const { icon, value } = defineProps<Props>()
const emit = defineEmits<{
click: [value: SelectedMenu]
}>()
</script>
<template>
<JustButton @click="emit('click', value)">
<span class="mdi text-2xl" :class="icon"></span>
</JustButton>
</template>
<style scoped></style>

View File

@@ -1,9 +0,0 @@
<script setup lang="ts"></script>
<template>
<div class="flex flex-col gap-2 items-stretch p-1 border">
<slot></slot>
</div>
</template>
<style scoped></style>

View File

@@ -1,22 +0,0 @@
<script setup lang="ts">
interface Props {
text: string
value: string
}
const { text, value } = defineProps<Props>()
const emit = defineEmits<{
click: [value: string]
}>()
function onClick() {
emit('click', value)
}
</script>
<template>
<div class="flex flex-col gap-2 border" @click="onClick">{{ text }} - {{ value }}</div>
</template>
<style scoped></style>

View File

@@ -1,8 +0,0 @@
import JustInput from '@/components/simple/JustInput.vue'
import JustButton from '@/components/simple/JustButton.vue'
import JustList from '@/components/simple/JustList.vue'
import JustListItem from '@/components/simple/JustListItem.vue'
import JustNav from '@/components/simple/JustNav.vue'
import JustTest from '@/components/simple/JustTest.vue'
export { JustInput, JustButton, JustList, JustListItem, JustNav, JustTest }

View File

@@ -15,7 +15,7 @@ export const useAuthStore = defineStore('auth', () => {
}, },
body: JSON.stringify({ email: email }), body: JSON.stringify({ email: email }),
} }
const res = await fetch('http://localhost:5173/login', options) const res = await fetch('http://localhost:5173/api/login', options)
if (res.ok) { if (res.ok) {
const data: { accessToken: string; refreshToken: string } = await res.json() const data: { accessToken: string; refreshToken: string } = await res.json()

View File

@@ -1,12 +1,14 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { ref } from 'vue' import { ref } from 'vue'
import type { User } from '@/stores/users.ts' import type { User } from '@/stores/users.ts'
import type { Message } from '@/stores/messages.ts'
export interface Chat { export interface Chat {
id: string id: string
type_id: number typeId: number
name: string name: string
users: User[] users: User[]
message?: Message
} }
export const useChatsStore = defineStore('chats', () => { export const useChatsStore = defineStore('chats', () => {

View File

@@ -3,9 +3,9 @@ import { ref } from 'vue'
export interface Message { export interface Message {
id: number id: number
text: string message: string
user_id: number userId: number
created_at: string createdAt: string
} }
export const useMessagesStore = defineStore('messages', () => { export const useMessagesStore = defineStore('messages', () => {

View File

@@ -1,29 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted } from 'vue' import { onMounted } from 'vue'
import { useSocketsStore } from '@/stores/sockets.ts' import { useSocketsStore } from '@/stores/sockets.ts'
import LeftPane from '@/components/LeftPane.vue' import LeftPane from '@/components/LeftPane.vue'
import RightPane from '@/components/RightPane.vue' import RightPane from '@/components/RightPane.vue'
import UsersList from '@/components/Users/UsersList.vue'
import SettingsList from '@/components/Settings/SettingsList.vue'
import ChatsList from '@/components/Chats/ChatsList.vue'
import { useMenuStore } from '@/stores/menu.ts'
const socketsStore = useSocketsStore() const socketsStore = useSocketsStore()
const menuStore = useMenuStore()
const component = computed(() => {
console.log(menuStore.selected)
switch (menuStore.selected) {
case 'chats':
return ChatsList
case 'users':
return UsersList
case 'settings':
return SettingsList
default:
return ChatsList
}
})
onMounted(() => { onMounted(() => {
socketsStore.init() socketsStore.init()
@@ -34,7 +15,9 @@ onMounted(() => {
<div class="flex h-full"> <div class="flex h-full">
<div class="md:w-1/3 sm:w-1/2 w-1/2 bg-white"> <div class="md:w-1/3 sm:w-1/2 w-1/2 bg-white">
<LeftPane> <LeftPane>
<template #default="{ component }">
<component :is="component" /> <component :is="component" />
</template>
</LeftPane> </LeftPane>
</div> </div>
<div class="md:w-2/3 sm:w-1/2 w-1/2"> <div class="md:w-2/3 sm:w-1/2 w-1/2">

View File

@@ -1,32 +0,0 @@
<script setup lang="ts">
import { ref } from 'vue'
import { useAuthStore } from '@/stores/auth.ts'
const authStore = useAuthStore()
const email = ref('vadim.olonin@gmail.com')
const name = ref('')
async function onSubmit() {
await authStore.login(email.value)
}
</script>
<template>
<div class="h-100 d-flex flex-column align-center justify-center red">
<div class="w-20r border pa-4 blue">
<form @submit.prevent="onSubmit">
<div class="d-flex flex-column ga-4">
<div class="">Sign in</div>
<input v-model="email" placeholder="email" type="email" class="border" />
<input v-if="false" v-model="name" placeholder="name" />
<div class="d-flex ga-4 align-center">
<input type="checkbox" v-if="false" />
<button type="submit">login</button>
</div>
</div>
</form>
</div>
</div>
</template>
<style scoped></style>

View File

@@ -1,157 +0,0 @@
<script setup lang="ts">
import { ref } from 'vue'
import { JustInput } from '@/components/simple'
import {
ButtonGroup,
Button,
InputText,
Card,
ScrollPanel,
InputOtp,
ToggleSwitch,
Divider,
Toolbar,
IconField,
InputIcon,
SplitButton,
Menu,
} from 'primevue'
const email = ref('test')
const value = ref('1234')
const checked = ref(true)
const items = ref([
{
label: 'Update',
icon: 'pi pi-refresh',
},
{
label: 'Delete',
icon: 'pi pi-times',
},
])
const menu = ref()
const toggle = (event: Event) => {
menu.value.toggle(event)
}
</script>
<template>
<div class="flex flex-col gap-2 w-full bg-white p-4">
<div class="card flex justify-center">
<Button
type="button"
icon="pi pi-ellipsis-v"
@click="toggle"
aria-haspopup="true"
aria-controls="overlay_menu"
/>
<Menu ref="menu" id="overlay_menu" :model="items" :popup="true" />
</div>
<div class="card">
<Toolbar>
<template #start>
<Button icon="pi pi-plus" class="mr-2" severity="secondary" text />
<Button icon="pi pi-print" class="mr-2" severity="secondary" text />
<Button icon="pi pi-upload" severity="secondary" text />
</template>
<template #center>
<IconField>
<InputIcon>
<i class="pi pi-search" />
</InputIcon>
<InputText placeholder="Search" />
</IconField>
</template>
<template #end> <SplitButton label="Save" :model="items"></SplitButton></template>
</Toolbar>
</div>
<Divider />
<div class="flex gap-2 items-start">
<Button label="Submit" />
<Button icon="pi pi-home" aria-label="Save" />
<Button label="Profile" icon="pi pi-user" />
<Button label="Save" icon="pi pi-check" iconPos="right" />
</div>
<div>
<Button label="Secondary" severity="secondary" />
<Button label="Success" severity="success" />
<Button label="Info" severity="info" />
<Button label="Warn" severity="warn" />
<Button label="Help" severity="help" />
<Button label="Danger" severity="danger" />
</div>
<div>
<ButtonGroup>
<Button label="Save" icon="pi pi-check" />
<Button label="Delete" icon="pi pi-trash" />
<Button label="Cancel" icon="pi pi-times" />
</ButtonGroup>
</div>
<div>
<InputOtp v-model="value" />
</div>
<div>
<ToggleSwitch v-model="checked" />
</div>
<div>
<InputText type="text" placeholder="Normal" />
</div>
<Card>
<template #title>Simple Card</template>
<template #content>
<p class="m-0">
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Inventore sed consequuntur error
repudiandae numquam deserunt quisquam repellat libero asperiores earum nam nobis, culpa
ratione quam perferendis esse, cupiditate neque quas!
</p>
</template>
</Card>
<Card>
<template #content>
<ScrollPanel style="width: 100%; height: 100px">
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud
exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure
dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt
mollit anim id est laborum.
</p>
<Divider />
<p>
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque
laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi
architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas
sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione
voluptatem sequi nesciunt. Consectetur, adipisci velit, sed quia non numquam eius modi.
</p>
<p>
At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium
voluptatum deleniti atque corrupti quos dolores et quas molestias excepturi sint
occaecati cupiditate non provident, similique sunt in culpa qui officia deserunt
mollitia animi, id est laborum et dolorum fuga. Et harum quidem rerum facilis est et
expedita distinctio. Nam libero tempore, cum soluta nobis est eligendi optio cumque
nihil impedit quo minus.
</p>
<p class="m-0">
Quod maxime placeat facere possimus, omnis voluptas assumenda est, omnis dolor
repellendus. Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus
saepe eveniet ut et voluptates repudiandae sint et molestiae non recusandae. Itaque
earum rerum hic tenetur a sapiente delectus, ut aut reiciendis voluptatibus maiores
alias consequatur aut perferendis doloribus asperiores repellat
</p>
</ScrollPanel>
</template>
</Card>
</div>
</template>
<style scoped></style>

View File

@@ -16,7 +16,7 @@ export default defineConfig({
server: { server: {
host: '0.0.0.0', host: '0.0.0.0',
proxy: { proxy: {
'/login': 'http://localhost:3000', '/api': 'http://localhost:3000',
}, },
}, },
}) })