generated from Flycro/laravel-nuxt
feat: Realtime Functionality
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
@@ -6,6 +6,8 @@ import NewBookRecommendation from '~/components/modal/NewBookRecommendation.vue'
|
||||
import CastVote from '~/components/modal/CastVote.vue'
|
||||
|
||||
const dayjs = useDayjs()
|
||||
const { $echo } = useNuxtApp()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const bookRecommendationStore = useBookRecommendationStore()
|
||||
bookRecommendationStore.fetchRecommendations()
|
||||
@@ -68,6 +70,20 @@ const sort = ref({
|
||||
function resolveStatus(status: string) {
|
||||
return bookRecommendationStore.statusOptions.find(option => option.value === status)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
$echo.private(`BookRecommendation`)
|
||||
.listen('.BookRecommendationUpdated', (e) => {
|
||||
bookRecommendationStore.updateRecommendationWS(e.bookRecommendation)
|
||||
})
|
||||
.listen('.BookRecommendationDeleted', (e) => {
|
||||
bookRecommendationStore.deleteRecommendationWS(e.bookRecommendation)
|
||||
})
|
||||
.listen('.BookRecommendationCreated', (e) => {
|
||||
bookRecommendationStore.createRecommendationWS(e.bookRecommendation)
|
||||
})
|
||||
authStore.socketId = $echo.socketId()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -4,8 +4,24 @@ import BookInfoCard from '~/components/dashboard/BookInfoCard.vue'
|
||||
definePageMeta({ middleware: ['auth'] })
|
||||
|
||||
const bookRecommendationStore = useBookRecommendationStore()
|
||||
const authStore = useAuthStore()
|
||||
const { $echo } = useNuxtApp()
|
||||
|
||||
bookRecommendationStore.fetchActiveRecommendations()
|
||||
|
||||
onMounted(() => {
|
||||
$echo.private(`BookRecommendation`)
|
||||
.listen('.BookRecommendationUpdated', (e) => {
|
||||
bookRecommendationStore.updateRecommendationWS(e.bookRecommendation)
|
||||
})
|
||||
.listen('.BookRecommendationDeleted', (e) => {
|
||||
bookRecommendationStore.deleteRecommendationWS(e.bookRecommendation)
|
||||
})
|
||||
.listen('.BookRecommendationCreated', (e) => {
|
||||
bookRecommendationStore.createRecommendationWS(e.bookRecommendation)
|
||||
})
|
||||
authStore.socketId = $echo.socketId()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ofetch } from 'ofetch'
|
||||
import type { FetchOptions } from 'ofetch';
|
||||
import type { FetchOptions } from 'ofetch'
|
||||
|
||||
export default defineNuxtPlugin({
|
||||
name: 'app',
|
||||
@@ -10,10 +10,10 @@ export default defineNuxtPlugin({
|
||||
const auth = useAuthStore()
|
||||
|
||||
nuxtApp.provide('storage', (path: string): string => {
|
||||
if (!path) return ''
|
||||
if (!path) { return '' }
|
||||
|
||||
return path.startsWith('http://') || path.startsWith('https://') ?
|
||||
path
|
||||
return path.startsWith('http://') || path.startsWith('https://')
|
||||
? path
|
||||
: config.public.storageBase + path
|
||||
})
|
||||
|
||||
@@ -21,45 +21,54 @@ export default defineNuxtPlugin({
|
||||
// Initial headers with Accept
|
||||
const initialHeaders = {
|
||||
...headers,
|
||||
'Accept': 'application/json',
|
||||
};
|
||||
Accept: 'application/json',
|
||||
}
|
||||
|
||||
// Conditionally add server-specific headers
|
||||
if (process.server) {
|
||||
const serverHeaders = {
|
||||
'referer': useRequestURL().toString(),
|
||||
referer: useRequestURL().toString(),
|
||||
...useRequestHeaders(['x-forwarded-for', 'user-agent', 'referer']),
|
||||
};
|
||||
Object.assign(initialHeaders, serverHeaders);
|
||||
}
|
||||
Object.assign(initialHeaders, serverHeaders)
|
||||
}
|
||||
|
||||
// Conditionally add authorization header if logged in
|
||||
if (auth.isLoggedIn) {
|
||||
const authHeaders = {
|
||||
'Authorization': `Bearer ${auth.token}`,
|
||||
};
|
||||
Object.assign(initialHeaders, authHeaders);
|
||||
Authorization: `Bearer ${auth.token}`,
|
||||
}
|
||||
Object.assign(initialHeaders, authHeaders)
|
||||
}
|
||||
|
||||
return initialHeaders;
|
||||
// Conditionally add X-Socket-ID header if socket is connected
|
||||
if (auth.isLoggedIn && auth.socketId) {
|
||||
console.log('auth.socketId', auth.socketId)
|
||||
const socketHeaders = {
|
||||
'X-Socket-ID': auth.socketId,
|
||||
}
|
||||
Object.assign(initialHeaders, socketHeaders)
|
||||
}
|
||||
|
||||
return initialHeaders
|
||||
}
|
||||
|
||||
function buildBaseURL(baseURL: string): string {
|
||||
if (baseURL) return baseURL;
|
||||
if (baseURL) { return baseURL }
|
||||
|
||||
return process.server ?
|
||||
config.apiLocal + config.public.apiPrefix
|
||||
: config.public.apiBase + config.public.apiPrefix;
|
||||
return process.server
|
||||
? config.apiLocal + config.public.apiPrefix
|
||||
: config.public.apiBase + config.public.apiPrefix
|
||||
}
|
||||
|
||||
function buildSecureMethod(options: FetchOptions): void {
|
||||
if (process.server) return;
|
||||
if (process.server) { return }
|
||||
|
||||
const method = options.method?.toLowerCase() ?? 'get'
|
||||
|
||||
if (options.body instanceof FormData && method === 'put') {
|
||||
options.method = 'POST';
|
||||
options.body.append('_method', 'PUT');
|
||||
options.method = 'POST'
|
||||
options.body.append('_method', 'PUT')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,27 +76,27 @@ export default defineNuxtPlugin({
|
||||
return !baseURL
|
||||
&& !path.startsWith('/_nuxt')
|
||||
&& !path.startsWith('http://')
|
||||
&& !path.startsWith('https://');
|
||||
&& !path.startsWith('https://')
|
||||
}
|
||||
|
||||
globalThis.$fetch = ofetch.create({
|
||||
retry: false,
|
||||
|
||||
onRequest({ request, options }) {
|
||||
if (!isRequestWithAuth(options.baseURL ?? '', request.toString())) return
|
||||
if (!isRequestWithAuth(options.baseURL ?? '', request.toString())) { return }
|
||||
|
||||
options.credentials = 'include';
|
||||
options.credentials = 'include'
|
||||
|
||||
options.baseURL = buildBaseURL(options.baseURL ?? '');
|
||||
options.headers = buildHeaders(options.headers);
|
||||
options.baseURL = buildBaseURL(options.baseURL ?? '')
|
||||
options.headers = buildHeaders(options.headers)
|
||||
|
||||
buildSecureMethod(options);
|
||||
buildSecureMethod(options)
|
||||
},
|
||||
|
||||
onRequestError({ error }) {
|
||||
if (process.server) return;
|
||||
if (process.server) { return }
|
||||
|
||||
if (error.name === 'AbortError') return;
|
||||
if (error.name === 'AbortError') { return }
|
||||
|
||||
useToast().add({
|
||||
icon: 'i-heroicons-exclamation-circle-solid',
|
||||
@@ -110,7 +119,8 @@ export default defineNuxtPlugin({
|
||||
color: 'primary',
|
||||
})
|
||||
}
|
||||
} else if (response.status !== 422) {
|
||||
}
|
||||
else if (response.status !== 422) {
|
||||
if (process.client) {
|
||||
useToast().add({
|
||||
icon: 'i-heroicons-exclamation-circle-solid',
|
||||
@@ -119,11 +129,11 @@ export default defineNuxtPlugin({
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
} as FetchOptions)
|
||||
|
||||
if (auth.isLoggedIn) {
|
||||
await auth.fetchUser();
|
||||
await auth.fetchUser()
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
55
nuxt/plugins/echo.client.ts
Normal file
55
nuxt/plugins/echo.client.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import Echo from 'laravel-echo'
|
||||
import Pusher from 'pusher-js'
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
Pusher: typeof Pusher
|
||||
}
|
||||
}
|
||||
|
||||
window.Pusher = Pusher
|
||||
|
||||
export default defineNuxtPlugin(() => {
|
||||
const runtimeConfig = useRuntimeConfig()
|
||||
const authStore = useAuthStore()
|
||||
const echo = new Echo({
|
||||
broadcaster: runtimeConfig.public.echo.broadcaster,
|
||||
key: runtimeConfig.public.echo.key,
|
||||
cluster: 'mt1',
|
||||
wsHost: runtimeConfig.public.echo.wsHost,
|
||||
wsPort: runtimeConfig.public.echo.wsPort,
|
||||
wssPort: runtimeConfig.public.echo.wsPort,
|
||||
forceTLS: false,
|
||||
encrypted: runtimeConfig.public.echo.encrypted,
|
||||
disableStats: runtimeConfig.public.echo.disableStats,
|
||||
enabledTransports: ['ws', 'wss'],
|
||||
authorizer: (channel: { name: any }) => {
|
||||
return {
|
||||
authorize: async (socketId: string, callback: (error: any, respnse?: any) => void) => {
|
||||
try {
|
||||
const response = await $fetch(`/broadcasting/auth`, {
|
||||
method: 'POST',
|
||||
body: {
|
||||
socket_id: socketId,
|
||||
channel_name: channel.name,
|
||||
},
|
||||
})
|
||||
|
||||
callback(null, response)
|
||||
}
|
||||
catch (error) {
|
||||
callback(error)
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
authStore.socketId = echo.socketId()
|
||||
|
||||
// Make Echo instance available through the Nuxt app
|
||||
return {
|
||||
provide: {
|
||||
echo,
|
||||
},
|
||||
}
|
||||
})
|
||||
@@ -24,6 +24,7 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
maxAge: 60 * 60 * 24 * 365,
|
||||
})
|
||||
const isLoggedIn = computed(() => !!token.value)
|
||||
const socketId = ref('')
|
||||
|
||||
const { refresh: logout } = useFetch<any>('logout', {
|
||||
method: 'POST',
|
||||
@@ -49,5 +50,5 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
},
|
||||
})
|
||||
|
||||
return { user, isLoggedIn, logout, fetchUser, token }
|
||||
return { user, isLoggedIn, socketId, logout, fetchUser, token }
|
||||
})
|
||||
|
||||
@@ -21,11 +21,20 @@ export interface BookRecommendation {
|
||||
email: string
|
||||
avatar: string
|
||||
}
|
||||
votes?: Vote[]
|
||||
status: BookRecommendationStatusEnum
|
||||
cover_image?: string
|
||||
published_at?: string
|
||||
}
|
||||
|
||||
export interface Vote {
|
||||
book_recommendation_id: number
|
||||
id: number
|
||||
user_id: number
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export const useBookRecommendationStore = defineStore('bookRecommendations', () => {
|
||||
const recommendations = ref<BookRecommendation[]>([])
|
||||
|
||||
@@ -72,6 +81,48 @@ export const useBookRecommendationStore = defineStore('bookRecommendations', ()
|
||||
},
|
||||
})
|
||||
|
||||
const updateRecommendationWS = async (data: Partial<BookRecommendation>) => {
|
||||
// This data can be Partial, the id should always be present. We need to only update the properties that are present in the data object.
|
||||
// We also have a special case for activeRecommendations, in this case we could have a new recommendation that needs to be added to the list. This should only happen if the status is ACTIVE.
|
||||
// If the Status is not ACTIVE, we need to remove the recommendation from the list.
|
||||
const index = recommendations.value.findIndex(r => r.id === data.id)
|
||||
if (index !== -1) {
|
||||
recommendations.value[index] = { ...recommendations.value[index], ...data }
|
||||
}
|
||||
switch (data.status) {
|
||||
case BookRecommendationStatusEnum.ACTIVE:
|
||||
const activeIndex = recommendations.value.findIndex(r => r.id === data.id)
|
||||
if (activeIndex === -1) {
|
||||
await createRecommendationWS(data)
|
||||
}
|
||||
break
|
||||
default:
|
||||
const inactiveIndex = recommendations.value.findIndex(r => r.id === data.id)
|
||||
if (inactiveIndex !== -1) {
|
||||
recommendations.value.splice(inactiveIndex, 1)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const deleteRecommendationWS = async (data: Partial<BookRecommendation>) => {
|
||||
const index = recommendations.value.findIndex(r => r.id === data.id)
|
||||
if (index !== -1) {
|
||||
recommendations.value.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
const createRecommendationWS = async (data: Partial<BookRecommendation>) => {
|
||||
// Here we need to get the missing with data from the server
|
||||
await useFetch<BookRecommendation>(`book-recommendations/${data.id}?with=recommender,votes`, {
|
||||
onResponse({ response }) {
|
||||
if (response.status === 200) {
|
||||
recommendations.value.push(response._data)
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function resetRecommendations() {
|
||||
recommendations.value = []
|
||||
}
|
||||
@@ -79,6 +130,9 @@ export const useBookRecommendationStore = defineStore('bookRecommendations', ()
|
||||
return {
|
||||
recommendations,
|
||||
resetRecommendations,
|
||||
updateRecommendationWS,
|
||||
deleteRecommendationWS,
|
||||
createRecommendationWS,
|
||||
statusOptions,
|
||||
fetchRecommendations,
|
||||
fetchRecommendationsStatus,
|
||||
|
||||
Reference in New Issue
Block a user