This commit is contained in:
2025-05-24 01:47:40 +09:00
commit 09d97cbb0b
1594 changed files with 184634 additions and 0 deletions

178
src/stores/chat.ts Normal file
View File

@@ -0,0 +1,178 @@
/**
* This is a store that hold the messaging-v1 state
* It uses the useFetch composition component to make the api calls
*
* @see /src/pages/messaging-v1.vue
* @see /src/composable/useFetch.ts
* @see /src/components/partials/chat/*.vue
* @see /src/utils/api/chat
*/
import { acceptHMRUpdate, defineStore } from 'pinia'
import type { $Fetch } from 'ofetch'
export interface Conversation {
id: number
name: string
lastMessage: string
unreadMessages: boolean
avatar: string
}
export interface Message {
id: number
conversationId: number
messageId: number
type: 'msg' | 'image' | 'imagelink' | 'system'
sender: string | null
avatar: string | null
content: {
time: string | null
text?: string
subtext?: string
image_url?: string
link_image?: string
link_badge?: string
}
}
const defaultConversation: Conversation = {
id: 0,
name: '',
lastMessage: '',
unreadMessages: false,
avatar: '/images/avatars/placeholder.jpg',
}
export const useChat = defineStore('chat', () => {
const $fetch = useApiFetch()
const conversations = ref<Conversation[]>([])
const messages = ref<Message[]>([])
const selectedConversationId = ref(0)
const addConversationOpen = ref(false)
const mobileConversationDetailsOpen = ref(false)
const loading = ref(false)
const selectedConversation = computed(() => {
const conversation = conversations.value?.find(
item => item.id === selectedConversationId.value,
)
if (!conversation) {
return defaultConversation
}
else {
return conversation
}
})
async function loadConversations(start = 0, limit = 10) {
if (loading.value) return
loading.value = true
try {
const response = await fetchConversations($fetch, start, limit)
conversations.value = response.conversations ?? []
}
finally {
loading.value = false
}
}
async function selectConversastion(conversationId: number) {
if (loading.value) return
loading.value = true
try {
const response = await fetchMessages($fetch, conversationId)
selectedConversationId.value = conversationId
messages.value = response.messages
}
finally {
loading.value = false
}
}
function unselectConversation() {
selectedConversationId.value = 0
messages.value = []
}
function setAddConversationOpen(value: boolean) {
addConversationOpen.value = value
}
function setMobileConversationDetailsOpen(value: boolean) {
mobileConversationDetailsOpen.value = value
}
return {
conversations,
messages,
selectedConversation,
selectedConversationId,
addConversationOpen,
mobileConversationDetailsOpen,
loading,
loadConversations,
setAddConversationOpen,
setMobileConversationDetailsOpen,
selectConversastion,
unselectConversation,
} as const
})
async function fetchConversations(
$fetch: $Fetch,
start = 0,
limit = 20,
): Promise<{ conversations: Conversation[], count: number }> {
let count = 0
const { _data: conversations = [], headers } = await $fetch.raw<Conversation[]>(
`/api/conversations`,
{
query: {
_start: start,
_limit: limit,
},
},
)
if (headers.has('X-Total-Count')) {
count = parseInt(headers.get('X-Total-Count') ?? '0')
}
return { conversations, count }
}
async function fetchMessages(
$fetch: $Fetch,
conversationId: number,
start = 0,
limit = 20,
): Promise<{ messages: Message[], count: number }> {
let count = 0
const { _data: messages = [], headers } = await $fetch.raw<Message[]>(
`/api/conversations/${conversationId}/messages?_start=${start}&_limit=${limit}`,
)
if (headers.has('X-Total-Count')) {
count = parseInt(headers.get('X-Total-Count') ?? '0')
}
return { messages, count }
}
/**
* Pinia supports Hot Module replacement so you can edit your stores and
* interact with them directly in your app without reloading the page.
*
* @see https://pinia.esm.dev/cookbook/hot-module-replacement.html
* @see https://vitejs.dev/guide/api-hmr.html
*/
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useChat, import.meta.hot))
}

View File

@@ -0,0 +1,223 @@
/**
* This store is used for the layout switcher.
* It's used on the demo page to allow user to change which component
* is used for the layout.
*
* We can import and set activeSidebar (or use toggleSidebar) anywhere in our project
* @see /src/pages/components.vue
* @see /src/pages/sidebar/dashboards.vue
* @see /src/layouts/layout-switcher/LayoutSwitcher.vue
*/
import { acceptHMRUpdate, defineStore } from 'pinia'
export const useLayoutSwitcher = defineStore('layoutSwitcher', () => {
const route = useRoute()
// utils
const isNavbarRoute = computed(() => route?.fullPath?.startsWith?.('/navbar/'))
const isSidebarRoute = computed(() => route?.fullPath?.startsWith?.('/sidebar/'))
const hasDynamicLayout = computed(() => isNavbarRoute.value || isSidebarRoute.value)
const navbarLayoutLink = computed(
() => route?.fullPath?.replace?.('sidebar', 'navbar') ?? '',
)
const sidebarLayoutLink = computed(
() => route?.fullPath?.replace?.('navbar', 'sidebar') ?? '',
)
// navbar
const NavbarLayout = defineAsyncComponent({
loader: () => import('/@src/layouts/navbar.vue'),
delay: 0,
suspensible: false,
})
const NavsearchLayout = defineAsyncComponent({
loader: () => import('/@src/layouts/navsearch.vue'),
delay: 0,
suspensible: false,
})
const navbarComponents = {
'navbar-default': NavbarLayout,
'navbar-fade': NavbarLayout,
'navbar-colored': NavbarLayout,
'navsearch-fixed': NavsearchLayout,
'navsearch-fixed-fade': NavsearchLayout,
'navsearch-shrink': NavsearchLayout,
'navsearch-reveal': NavsearchLayout,
} as const
type NavbarComponentsId = keyof typeof navbarComponents
const navbarComponentsIds = Object.keys(navbarComponents)
const navbarLayoutId = ref<NavbarComponentsId>('navbar-default')
const navbarLayoutComponent = computed(() => {
return navbarComponents[navbarLayoutId.value] || NavbarLayout
})
const navbarLayoutTheme = computed(() => {
switch (navbarLayoutId.value) {
case 'navbar-fade':
case 'navsearch-fixed-fade':
return 'fade'
case 'navbar-colored':
return 'colored'
case 'navsearch-fixed':
case 'navsearch-shrink':
case 'navsearch-reveal':
default:
return 'default'
}
})
// sidebar
const SidebarLayout = defineAsyncComponent({
loader: () => import('/@src/layouts/sidebar.vue'),
delay: 0,
suspensible: false,
})
const SideblockLayout = defineAsyncComponent({
loader: () => import('/@src/layouts/sideblock.vue'),
delay: 0,
suspensible: false,
})
const sidebarComponents = {
'sidebar-default': SidebarLayout,
'sidebar-color': SidebarLayout,
'sidebar-color-curved': SidebarLayout,
'sidebar-curved': SidebarLayout,
'sidebar-float': SidebarLayout,
'sidebar-labels': SidebarLayout,
'sidebar-labels-hover': SidebarLayout,
'sideblock-default': SideblockLayout,
'sideblock-color': SideblockLayout,
'sideblock-color-curved': SideblockLayout,
'sideblock-curved': SideblockLayout,
} as const
type SidebarComponentsId = keyof typeof sidebarComponents
const sidebarComponentsIds = Object.keys(sidebarComponents)
const sidebarLayoutId = ref<SidebarComponentsId>('sideblock-default')
const sidebarLayoutComponent = computed(() => {
return sidebarComponents[sidebarLayoutId.value] || SidebarLayout
})
const sidebarLayoutTheme = computed(() => {
switch (sidebarLayoutId.value) {
case 'sidebar-float':
return 'float'
case 'sidebar-labels':
return 'labels'
case 'sidebar-labels-hover':
return 'labels-hover'
case 'sidebar-color':
case 'sideblock-color':
return 'color'
case 'sidebar-curved':
case 'sideblock-curved':
return 'curved'
case 'sideblock-color-curved':
case 'sidebar-color-curved':
return 'color-curved'
case 'sidebar-default':
case 'sideblock-default':
default:
return 'default'
}
})
// dynamic layout
const dynamicLayoutId = computed<NavbarComponentsId | SidebarComponentsId>({
get: () => {
if (isNavbarRoute.value) {
return navbarLayoutId.value
}
else {
return sidebarLayoutId.value
}
},
set: (value) => {
if (navbarComponentsIds.includes(value)) {
navbarLayoutId.value = value as NavbarComponentsId
return
}
if (sidebarComponentsIds.includes(value)) {
sidebarLayoutId.value = value as SidebarComponentsId
}
},
})
const dynamicLayoutComponent = computed(() => {
if (isNavbarRoute.value) {
return navbarLayoutComponent.value
}
else {
return sidebarLayoutComponent.value
}
})
const contentSize = ref<'default' | 'large' | 'wide' | 'full'>('large')
const dynamicLayoutProps = computed(() => {
if (isNavbarRoute.value) {
return {
theme: navbarLayoutTheme.value,
key: navbarLayoutId.value,
size: contentSize.value,
scrollBehavior:
navbarLayoutId.value === 'navsearch-shrink'
? 'shrink'
: navbarLayoutId.value === 'navsearch-reveal'
? 'reveal'
: undefined,
}
}
else {
return {
theme: sidebarLayoutTheme.value,
size: contentSize.value,
key: sidebarLayoutId.value,
}
}
})
function setDynamicLayoutId(theme: NavbarComponentsId | SidebarComponentsId) {
dynamicLayoutId.value = theme
}
return {
contentSize,
dynamicLayoutComponent,
dynamicLayoutProps,
dynamicLayoutId,
setDynamicLayoutId,
sidebarLayoutId,
sidebarLayoutComponent,
sidebarLayoutTheme,
navbarLayoutId,
navbarLayoutComponent,
navbarLayoutTheme,
isNavbarRoute,
isSidebarRoute,
navbarLayoutLink,
sidebarLayoutLink,
hasDynamicLayout,
} as const
})
/**
* Pinia supports Hot Module replacement so you can edit your stores and
* interact with them directly in your app without reloading the page.
*
* @see https://pinia.esm.dev/cookbook/hot-module-replacement.html
* @see https://vitejs.dev/guide/api-hmr.html
*/
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useLayoutSwitcher, import.meta.hot))
}

44
src/stores/panels.ts Normal file
View File

@@ -0,0 +1,44 @@
/**
* This is a store that hold left panel state
* It could be one of the ActivePanelId
*
* Using useStorage from @vueuse/core allow persistance storage accross tabs/sessions
*
* We can import and set activePanel anywhere in our project
* @see /src/components/panels/PanelSearch.vue
* @see /src/components/panels/PanelActivity.vue
*/
import { acceptHMRUpdate, defineStore } from 'pinia'
import { useStorage } from '@vueuse/core'
export type ActivePanelId = 'none' | 'search' | 'languages' | 'activity' | 'task'
export const usePanels = defineStore('panels', () => {
const active = useStorage<ActivePanelId>('active-panel', 'none')
function setActive(panelId: ActivePanelId) {
active.value = panelId
}
function close() {
active.value = 'none'
}
return {
active,
setActive,
close,
} as const
})
/**
* Pinia supports Hot Module replacement so you can edit your stores and
* interact with them directly in your app without reloading the page.
*
* @see https://pinia.esm.dev/cookbook/hot-module-replacement.html
* @see https://vitejs.dev/guide/api-hmr.html
*/
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(usePanels, import.meta.hot))
}

View File

@@ -0,0 +1,37 @@
import { acceptHMRUpdate, defineStore } from 'pinia'
import { ref, computed } from 'vue'
import type { UserData } from '/@src/utils/types'
export const useUserSession = defineStore('userSession', () => {
const user = ref<UserData>()
const isLoggedIn = computed(() => user.value !== undefined)
function setUser(newUser: UserData) {
user.value = newUser
}
async function logoutUser() {
const token = useUserToken()
token.value = undefined
user.value = undefined
}
return {
user,
isLoggedIn,
logoutUser,
setUser,
} as const
})
/**
* Pinia supports Hot Module replacement so you can edit your stores and
* interact with them directly in your app without reloading the page.
*
* @see https://pinia.esm.dev/cookbook/hot-module-replacement.html
* @see https://vitejs.dev/guide/api-hmr.html
*/
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useUserSession, import.meta.hot))
}