From 444685b1018b15b5037467e7d6a8fab4801a236e Mon Sep 17 00:00:00 2001 From: "oussama.aftys" <oussama.aftys@marketingconfort.com> Date: Mon, 29 Jul 2024 18:13:44 +0100 Subject: [PATCH] optimize live product timestamps management --- .dockerignore | 46 +++ .env | 4 +- next.config.mjs | 14 +- package-lock.json | 45 +++ package.json | 1 + src/components/copy-button.tsx | 7 +- src/config-global.ts | 2 + src/hooks/use-async.ts | 27 ++ src/hooks/use-live-comments.ts | 5 +- src/shared/api/live.ts | 233 ++++++++----- src/shared/api/server.ts | 61 +++- .../notification-item.tsx | 2 +- .../lives/add-edit-live/add-live-view.tsx | 52 +-- .../lives/all-lives/live-table-row.tsx | 256 +++++++-------- src/shared/sections/lives/details/view.tsx | 137 ++++---- .../supervision/details-comments-input.tsx | 2 +- .../supervision/details-comments-item.tsx | 26 +- .../supervision/details-comments-list.tsx | 73 +++-- .../supervision/details-comments-section.tsx | 5 +- .../supervision/details-player-section.tsx | 60 +++- .../supervision/details-product-card.tsx | 306 +++++++++++------- .../supervision/details-products-section.tsx | 4 +- .../sections/lives/supervision/view.tsx | 8 +- src/shared/types/live.ts | 11 +- 24 files changed, 881 insertions(+), 506 deletions(-) create mode 100644 .dockerignore create mode 100644 src/hooks/use-async.ts diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..96f888bb --- /dev/null +++ b/.dockerignore @@ -0,0 +1,46 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +.env + +### VS Code ### +.vscode/ diff --git a/.env b/.env index c20eeb49..8b729873 100644 --- a/.env +++ b/.env @@ -1,8 +1,8 @@ # HOST NEXT_PUBLIC_HOST_API=https://api-dev-minimal-v510.vercel.app -NEXT_PUBLIC_HOST_API_URL=https://mydressin-stream-service.mc-test.xyz -NEXT_PUBLIC_WEB_SOCKET_URL=https://mydressin-stream-service.mc-test.xyz/ws +NEXT_PUBLIC_HOST_API_URL=http://15.237.175.171:8080 +NEXT_PUBLIC_WEB_SOCKET_URL=http://15.237.175.171:8080/ws # ASSETS NEXT_PUBLIC_ASSETS_API=https://api-dev-minimal-v510.vercel.app diff --git a/next.config.mjs b/next.config.mjs index c3bec89a..58f9c664 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,6 +1,18 @@ /** @type {import('next').NextConfig} */ +import { resolve } from 'path'; + const nextConfig = { - + webpack: (config, { isServer }) => { + // Ensure inherits is properly resolved + config.resolve.fallback = { + ...config.resolve.fallback, + inherits: resolve('node_modules/inherits/inherits_browser.js'), + }; + + // Additional custom webpack configurations can go here + + return config; + }, }; export default nextConfig; diff --git a/package-lock.json b/package-lock.json index 8d5b4bf2..bce6958a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -76,6 +76,7 @@ }, "devDependencies": { "@types/autosuggest-highlight": "^3.2.3", + "@types/json2csv": "^5.0.7", "@types/node": "20.11.30", "@types/nprogress": "^0.2.3", "@types/react": "18.2.70", @@ -1859,6 +1860,11 @@ "resolved": "https://registry.npmjs.org/@stomp/stompjs/-/stompjs-7.0.0.tgz", "integrity": "sha512-fGdq4wPDnSV/KyOsjq4P+zLc8MFWC3lMmP5FBgLWKPJTYcuCbAIrnRGjB7q2jHZdYCOD5vxLuFoKIYLy5/u8Pw==" }, + "node_modules/@streamparser/json": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@streamparser/json/-/json-0.0.6.tgz", + "integrity": "sha512-vL9EVn/v+OhZ+Wcs6O4iKE9EUpwHUqHmCtNUMWjqp+6dr85+XPOSGTEsqYNq1Vn04uk9SWlOVmx9J48ggJVT2Q==" + }, "node_modules/@swc/helpers": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.2.tgz", @@ -1956,6 +1962,15 @@ "hoist-non-react-statics": "^3.3.0" } }, + "node_modules/@types/json2csv": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/@types/json2csv/-/json2csv-5.0.7.tgz", + "integrity": "sha512-Ma25zw9G9GEBnX8b12R4EYvnFT6dBh8L3jwsN5EUFXa+fl2dqmbLDbNWN0XuQU3rSXdsbBeCYjI9uHU2PUBxhA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/lodash": { "version": "4.14.202", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.202.tgz", @@ -4890,6 +4905,31 @@ "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" }, + "node_modules/json2csv": { + "version": "6.0.0-alpha.2", + "resolved": "https://registry.npmjs.org/json2csv/-/json2csv-6.0.0-alpha.2.tgz", + "integrity": "sha512-nJ3oP6QxN8z69IT1HmrJdfVxhU1kLTBVgMfRnNZc37YEY+jZ4nU27rBGxT4vaqM/KUCavLRhntmTuBFqZLBUcA==", + "dependencies": { + "@streamparser/json": "^0.0.6", + "commander": "^6.2.0", + "lodash.get": "^4.4.2" + }, + "bin": { + "json2csv": "bin/json2csv.js" + }, + "engines": { + "node": ">= 12", + "npm": ">= 6.13.0" + } + }, + "node_modules/json2csv/node_modules/commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "engines": { + "node": ">= 6" + } + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -5054,6 +5094,11 @@ "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==" }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==" + }, "node_modules/lodash.groupby": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/lodash.groupby/-/lodash.groupby-4.6.0.tgz", diff --git a/package.json b/package.json index e819a22e..07547707 100644 --- a/package.json +++ b/package.json @@ -77,6 +77,7 @@ }, "devDependencies": { "@types/autosuggest-highlight": "^3.2.3", + "@types/json2csv": "^5.0.7", "@types/node": "20.11.30", "@types/nprogress": "^0.2.3", "@types/react": "18.2.70", diff --git a/src/components/copy-button.tsx b/src/components/copy-button.tsx index f6ae035a..00ad37f6 100644 --- a/src/components/copy-button.tsx +++ b/src/components/copy-button.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { IconButton, SxProps, Theme, Tooltip } from '@mui/material'; import ContentCopyIcon from '@mui/icons-material/ContentCopy'; +import { useSnackbar } from '@/shared/components/snackbar'; interface CopyButtonProps { value: string; @@ -8,13 +9,15 @@ interface CopyButtonProps { } const CopyButton: React.FC<CopyButtonProps> = ({ value, sx }) => { + const {enqueueSnackbar} = useSnackbar(); const handleCopy = () => { navigator.clipboard.writeText(value) .then(() => { - console.log('Copied to clipboard'); + enqueueSnackbar('Copied to clipboard', {variant: 'success'}); }) .catch(err => { - console.error('Failed to copy: ', err); + console.error(err); + enqueueSnackbar('Failed to copy to clipboard', {variant: 'error'}); }); }; diff --git a/src/config-global.ts b/src/config-global.ts index 8964baca..e833250b 100644 --- a/src/config-global.ts +++ b/src/config-global.ts @@ -5,6 +5,8 @@ import { paths } from '@/routes/paths'; export const HOST_API = process.env.NEXT_PUBLIC_HOST_API; export const ASSETS_API = process.env.NEXT_PUBLIC_ASSETS_API; +export const HOST_API_URL = process.env.NEXT_PUBLIC_HOST_API_URL; +export const WEB_SOCKET_URL = process.env.NEXT_PUBLIC_WEB_SOCKET_URL; export const GATEWAY_API_URL = process.env.NEXT_PUBLIC_MYDRESSIN_GATEWAY_API_URL ; diff --git a/src/hooks/use-async.ts b/src/hooks/use-async.ts new file mode 100644 index 00000000..c30e75b4 --- /dev/null +++ b/src/hooks/use-async.ts @@ -0,0 +1,27 @@ +//@hook/use-async + +import { useState, useCallback } from 'react'; + +type AsyncFunction = (...args: any[]) => Promise<void>; + +const useAsync = (asyncFunction: AsyncFunction) => { + const [loading, setLoading] = useState<boolean>(false); + + const execute = useCallback( + async (...args: any[]) => { + setLoading(true); + try { + await asyncFunction(...args); + } catch (error) { + console.error('Error executing async function:', error); + } finally { + setLoading(false); + } + }, + [asyncFunction] + ); + + return { loading, execute }; +}; + +export default useAsync; diff --git a/src/hooks/use-live-comments.ts b/src/hooks/use-live-comments.ts index d6740040..64577430 100644 --- a/src/hooks/use-live-comments.ts +++ b/src/hooks/use-live-comments.ts @@ -7,13 +7,14 @@ import { ILive, ILiveComment } from "@/shared/types/live"; import { useGetCommentsLive } from "@/shared/api/live"; import { mutate } from "swr"; import { endpoints } from "@/shared/api/server"; +import { WEB_SOCKET_URL } from "@/config-global"; type LiveCommentsContextType = { comments: ILiveComment[]; pinnedComments: ILiveComment[]; }; -const WEB_SOCKET_URL = process.env.WEB_SOCKET_URL || "http://15.237.175.171:8080/ws"; +; const useLiveComments = ({ id, status }: ILive): LiveCommentsContextType => { const [comments, setComments] = useState<ILiveComment[]>([]); @@ -75,7 +76,7 @@ const useLiveComments = ({ id, status }: ILive): LiveCommentsContextType => { useEffect(() => { if (!clientRef.current) { - socketRef.current = new SockJS(WEB_SOCKET_URL); + socketRef.current = new SockJS(WEB_SOCKET_URL as string); clientRef.current = Stomp.over(socketRef.current); clientRef.current.connect({}, () => { diff --git a/src/shared/api/live.ts b/src/shared/api/live.ts index dd6f81da..dadc2904 100644 --- a/src/shared/api/live.ts +++ b/src/shared/api/live.ts @@ -1,8 +1,9 @@ -import { AllLivesAnalytics, ILive, ILiveComment, ILiveItem, ILiveProduct, ILiveStatistic, ILiveStatisticsSummary, LiveProductStatus, LiveStatus } from "@/shared/types/live"; +import { AllLivesAnalytics, ILive, ILiveComment, ILiveItem, ILiveProduct, ILiveStatistic, ILiveStatisticsSummary, LiveProductStatus, LiveStatus, TimeStampProduct } from "@/shared/types/live"; import { generateStatisticsForLives } from "../_mock"; import useSWR, { mutate } from 'swr'; import { useMemo } from 'react'; import axiosInstance, { fetcher, endpoints } from './server'; +import { update } from "lodash"; const options = { revalidateIfStale: false, @@ -40,16 +41,16 @@ function sortLivesByStatus(lives: ILiveItem[]): ILiveItem[] { export async function addLive(data: Record<string, any>, image: File) { const formData = new FormData(); formData.append('image', image); - formData.append('data',new Blob([JSON.stringify(data)], { type: 'application/json' })); // Serialize the data object + formData.append('data', new Blob([JSON.stringify(data)], { type: 'application/json' })); - const response = await axiosInstance.post('/live', formData, { + const response = await axiosInstance.post(endpoints.live.create, formData, { headers: { 'Content-Type': 'multipart/form-data' } }); const newData = response.data; - mutate([endpoints.live.stream], (lives: ILiveItem[] = []) => [newData, ...lives], false); + mutate([endpoints.live.all], (lives: ILiveItem[] = []) => [newData, ...lives], false); return newData; } @@ -60,16 +61,16 @@ export async function editLive(liveId: string, data: Record<string, any>, image? if (image) { formData.append('image', image); } - formData.append('data', new Blob([JSON.stringify(data)], { type: 'application/json' })); + formData.append('data', new Blob([JSON.stringify(data)], { type: 'application/json' })); - const response = await axiosInstance.put(`${endpoints.live.stream}/${liveId}`, formData, { + const response = await axiosInstance.put(endpoints.live.update(liveId), formData, { headers: { 'Content-Type': 'multipart/form-data' } }); const newData = response.data; - mutate([endpoints.live.stream], (lives: ILiveItem[] = []) => { + mutate([endpoints.live.all], (lives: ILiveItem[] = []) => { const index = lives.findIndex((live) => live.id === liveId); if (index > -1) { lives[index] = { ...lives[index], ...newData }; @@ -78,7 +79,7 @@ export async function editLive(liveId: string, data: Record<string, any>, image? }, false); mutate( - [`${endpoints.live.stream}/${liveId}`], + [endpoints.live.get(liveId)], (live: ILive | null = null) => { if (live) { live = { ...live, ...newData }; @@ -95,11 +96,14 @@ export async function editLive(liveId: string, data: Record<string, any>, image? } } + + + export async function deleteLive(liveId: string) { - await axiosInstance.delete(`${endpoints.live.stream}/${liveId}`); + await axiosInstance.delete(endpoints.live.delete(liveId)); mutate( - [endpoints.live.stream], + [endpoints.live.all], (lives: ILiveItem[] = []) => { const index = lives.findIndex((live) => live.id === liveId); if (index > -1) { @@ -111,9 +115,10 @@ export async function deleteLive(liveId: string) { ); } + export async function archiveLive(liveId: string) { - await axiosInstance.post(`${endpoints.live.stream}/${liveId}/archive`); - mutate([endpoints.live.stream], (lives: ILiveItem[] = []) => { + await axiosInstance.post(endpoints.live.archive(liveId)); + mutate([endpoints.live.all], (lives: ILiveItem[] = []) => { const index = lives.findIndex((live) => live.id === liveId); if (index > -1) { lives[index].status = LiveStatus.ARCHIVED; @@ -122,21 +127,22 @@ export async function archiveLive(liveId: string) { }, true); mutate( - [`${endpoints.live.stream}/${liveId}`], + [endpoints.live.get(liveId)], (live: ILive | null = null) => { if (live) { live.status = LiveStatus.ARCHIVED; } return live; }, - true + false ); } + export async function restoreLive(liveId: string) { - await axiosInstance.post(`${endpoints.live.stream}/${liveId}/restore`); - mutate([endpoints.live.stream], (lives: ILiveItem[] = []) => { + await axiosInstance.post(endpoints.live.restore(liveId)); + mutate([endpoints.live.all], (lives: ILiveItem[] = []) => { const index = lives.findIndex((live) => live.id === liveId); if (index > -1) { lives[index].status = LiveStatus.REVIEW; @@ -145,23 +151,23 @@ export async function restoreLive(liveId: string) { }, true); mutate( - [`${endpoints.live.stream}/${liveId}`], + [endpoints.live.get(liveId)], (live: ILive | null = null) => { if (live) { live.status = LiveStatus.REVIEW; } return live; }, - true + false ); } + export async function publishLive(liveId: string) { - const URL = `${endpoints.live.stream}/${liveId}/publish`; - await axiosInstance.post(URL); + await axiosInstance.post(endpoints.live.publish(liveId)); mutate( - [`${endpoints.live.stream}/${liveId}`], + [endpoints.live.get(liveId)], (live: ILive | null = null) => { if (live) { live.status = LiveStatus.REPLAY; @@ -172,7 +178,7 @@ export async function publishLive(liveId: string) { ); mutate( - [endpoints.live.stream], + [endpoints.live.all], (lives: ILiveItem[] = []) => { const index = lives.findIndex((live) => live.id === liveId); if (index > -1) { @@ -186,7 +192,7 @@ export async function publishLive(liveId: string) { export function useGetLives() { - const URL = [endpoints.live.stream]; + const URL = [endpoints.live.all]; const { data, isLoading, error, isValidating } = useSWR<ILiveItem[]>(URL, fetcher, options); @@ -202,26 +208,29 @@ export function useGetLives() { } export function useGetLive(liveId: string) { - const URL = [`${endpoints.live.stream}/${liveId}`]; + const URL = [endpoints.live.get(liveId)]; const { data, isLoading, error } = useSWR(URL, fetcher, options); return { liveData: data as ILive, liveIsLoading: isLoading, liveError: error }; } + /////////////////////////////////Live Statistics API////////////////////////////// ////////////////////////////////////////////////////////////////////////////////// export async function getRealTimeStats(id: string): Promise<ILiveStatistic> { - const response = await axiosInstance.get<ILiveStatistic>(endpoints.live.stats.realTime(id)); + const response = await axiosInstance.get<ILiveStatistic>(endpoints.stats.getStreamStats(id)); return response.data; } + export async function getLiveStatistics(liveId: string) { - const response = await axiosInstance.get<ILiveStatisticsSummary>(endpoints.live.stats.summary(liveId)); + const response = await axiosInstance.get<ILiveStatisticsSummary>(endpoints.stats.getStreamSummary(liveId)); return response.data; } + export async function getLiveStatisticsForAllLives() { - const response = await axiosInstance.get<AllLivesAnalytics>(endpoints.live.stats.summaryAll); + const response = await axiosInstance.get<AllLivesAnalytics>(endpoints.stats.getAllLivesSummary); return response.data; } @@ -260,68 +269,53 @@ export function useGetCommentsLive(liveId: string) { ); } -export async function deleteCommentLive(commentId: string | undefined, liveId: string | undefined) { + +export async function deleteCommentLive(commentId: string, liveId: string) { await axiosInstance.delete(endpoints.comments.delete(commentId, liveId)); } -export async function pinCommentLive(commentId: string | undefined, liveId: string | undefined) { +export async function pinCommentLive(commentId: string, liveId: string) { await axiosInstance.post(endpoints.comments.pin(commentId, liveId)); } -export async function unpinCommentLive(commentId: string | undefined, liveId: string | undefined) { + +export async function unpinCommentLive(commentId: string, liveId: string) { await axiosInstance.post(endpoints.comments.unpin(commentId, liveId)); } + /////////////////////////////////Live Product API//////////////////////////////// ////////////////////////////////////////////////////////////////////////////////// -export async function addProductLive(liveId: string, productId: string, startTime: number, endTime: number) { - await axiosInstance.post(`${endpoints.live.products}/${productId}/stream/${liveId}`, { startTime, endTime }); - - mutate( - [`${endpoints.live.stream}/${liveId}/products`], - (products: ILiveProduct[] = []) => { - const index = products.findIndex((product) => product.id === productId); - if (index > -1) {addLive - products[index] = { ...products[index], startTime, endTime, status: LiveProductStatus.PRESENTED }; - } - return [...products]; - }, - false - ); -} export async function searchProductLive(sku: string) { - return await axiosInstance.get(`${endpoints.product}/${sku}`); + return await axiosInstance.get(endpoints.product.search(sku)); } export async function addProductToLive(liveId: string, productId: string) { - const response = await axiosInstance.post<ILiveProduct>(`${endpoints.live.products}/add`, null, { - params: { liveId, productId } - }); + const response = await axiosInstance.post<ILiveProduct>(endpoints.liveProducts.add(productId, liveId)); - mutate([`${endpoints.live.stream}/${liveId}/products`], (products: ILiveProduct[] = []) => { - products.push(response.data); + mutate([endpoints.liveProducts.all(liveId)], (products: ILiveProduct[] = []) => { + products.push({...response.data, timeStampProduct: []}); return [...products]; - }, false - ); + }, false); } export function useGetProducts() { - const URL = [`${endpoints.product}?size=100&page=1`]; + const URL = [endpoints.product.all(1, 100)]; const { data, isLoading } = useSWR(URL, fetcher, options); return { productsData: data as ILiveProduct[], productsLoading: isLoading }; } -export async function deleteProductLive(productId: string, liveId: string) { - await axiosInstance.delete(`${endpoints.live.products}/${productId}/stream/${liveId}`); +export async function deleteProductLive(liveProductId: string, liveId: string) { + await axiosInstance.delete(endpoints.liveProducts.delete(liveId, liveProductId)); mutate( - [`${endpoints.live.stream}/${liveId}/products`], + [endpoints.liveProducts.all(liveId)], (products: ILiveProduct[] = []) => { - const index = products.findIndex((product) => product.id === productId); + const index = products.findIndex((product) => product.id == liveProductId); if (index > -1) { products.splice(index, 1); } @@ -330,9 +324,8 @@ export async function deleteProductLive(productId: string, liveId: string) { false ); } - export function getProductsLive(liveId: string) { - const URL = [`${endpoints.live.stream}/${liveId}/products`]; + const URL = [endpoints.liveProducts.all(liveId)]; const { data, isLoading, error, isValidating } = useSWR<ILiveProduct[]>(URL, fetcher, options); return useMemo( @@ -346,17 +339,66 @@ export function getProductsLive(liveId: string) { ); } + +export async function addTimeStampProduct(liveId: string, productId: string, startTime: number, endTime: number) { + const response = await axiosInstance.post<TimeStampProduct>(endpoints.liveProducts.addTimeStamp(productId, startTime, endTime)); + + mutate( + [endpoints.liveProducts.all(liveId)], + (products: ILiveProduct[] = []) => { + const index = products.findIndex((product) => product.id === productId); + if (index > -1) { + products[index] = { ...products[index], timeStampProduct: [...products[index].timeStampProduct, response.data] }; + } + return [...products]; + }, + false + ); +} + +export async function deleteTimeStampProduct(id: string) { + await axiosInstance.delete(endpoints.liveProducts.deleteTimeStamp(id)); + mutate( + [endpoints.liveProducts.all(id)], + (products: ILiveProduct[] = []) => { + const productIndex = products.findIndex((product) => product.id == id); + if (productIndex > -1) { + products[productIndex].timeStampProduct = products[productIndex].timeStampProduct.filter((timeStamp) => timeStamp.id != id); + console.log('products[productIndex].timeStampProduct', products[productIndex].timeStampProduct); + } + return [...products]; + }, + true + ); +} + + + +export async function presentProductLive(productId: string, liveId: string, startTime: number) { + await axiosInstance.post(endpoints.liveProducts.present(liveId, productId, startTime)); + mutate( + [endpoints.liveProducts.all(liveId)], + (products: ILiveProduct[] = []) => { + const index = products.findIndex((product) => product.id === productId); + if (index > -1) { + products[index].status = LiveProductStatus.IN_PRESENTATION; + } + return [...products]; + }, + false + ); +} + + export async function pushProductLive(productId: string, liveId: string, startTime: number) { - await axiosInstance.post(`${endpoints.live.products}/push`, null, { - params: { productId, liveId, startTime } - }); + const response = await axiosInstance.post<TimeStampProduct>(endpoints.liveProducts.push(productId, startTime)); mutate( - [`${endpoints.live.stream}/${liveId}/products`], + [endpoints.liveProducts.all(liveId)], (products: ILiveProduct[] = []) => { const index = products.findIndex((product) => product.id === productId); if (index > -1) { - products[index] = { ...products[index], startTime, status: LiveProductStatus.IN_PRESENTATION }; + products[index] = { ...products[index], timeStampProduct: [...products[index].timeStampProduct, response.data], status: LiveProductStatus.IN_PRESENTATION }; } return [...products]; }, @@ -366,32 +408,58 @@ export async function pushProductLive(productId: string, liveId: string, startTi export async function hideProductLive(productId: string, liveId: string, endTime: number) { try { - await axiosInstance.post(`${endpoints.live.products}/hide`, null, { - params: { productId, liveId, endTime } - }); + const response = await axiosInstance.post<TimeStampProduct>(endpoints.liveProducts.hide(productId, endTime)); mutate( - [`${endpoints.live.stream}/${liveId}/products`], + [endpoints.liveProducts.all(liveId)], (products: ILiveProduct[] = []) => { - const index = products.findIndex((product) => product.id === productId); - if (index > -1) { - products[index] = { ...products[index], endTime, status: LiveProductStatus.PRESENTED }; + const productIndex = products.findIndex((product) => product.id === productId); + if (productIndex > -1) { + products[productIndex].status = LiveProductStatus.PRESENTED + const timeStampIndex = products[productIndex].timeStampProduct.findIndex((timeStamp) => timeStamp.id === response.data.id); + if (timeStampIndex > -1) { + products[productIndex].timeStampProduct[timeStampIndex] = response.data; + } else { + products[productIndex].timeStampProduct.push(response.data); + } + } return [...products]; }, false ); } catch (e) { + console.error('Error hiding product:', e); } } + + +export async function updateTimeStampProduct(id: string, startTime: number, endTime: number) { + const response = await axiosInstance.put<TimeStampProduct>(endpoints.liveProducts.updateTimeStamp(id, startTime, endTime)); + mutate( + [endpoints.liveProducts.all(id)], + (products: ILiveProduct[] = []) => { + const productIndex = products.findIndex((product) => product.id === id); + if (productIndex > -1) { + const timeStampIndex = products[productIndex].timeStampProduct.findIndex((timeStamp) => timeStamp.id === response.data.id); + if (timeStampIndex > -1) { + products[productIndex].timeStampProduct[timeStampIndex] = response.data; + } else { + products[productIndex].timeStampProduct.push(response.data); + } + } + return [...products]; + }, + false + ); +} /////////////////////////////////Live State API//////////////////////////////// ////////////////////////////////////////////////////////////////////////////// export async function stopLive(liveId: string) { - const URL = `${endpoints.live.stream}/${liveId}/stop`; - await axiosInstance.post(URL); + await axiosInstance.post(endpoints.live.stop(liveId)); mutate( - [`${endpoints.live.stream}/${liveId}`], + [endpoints.live.get(liveId)], (live: ILive | null = null) => { if (live) { live.status = LiveStatus.REVIEW; @@ -402,7 +470,7 @@ export async function stopLive(liveId: string) { ); mutate( - [endpoints.live.stream], + [endpoints.live.all], (lives: ILiveItem[] = []) => { const index = lives.findIndex((live) => live.id === liveId); if (index > -1) { @@ -415,10 +483,9 @@ export async function stopLive(liveId: string) { } export async function startLive(liveId: string) { - const URL = `${endpoints.live.stream}/${liveId}/start`; - await axiosInstance.post(URL); + await axiosInstance.post(endpoints.live.start(liveId)); mutate( - [`${endpoints.live.stream}/${liveId}`], + [endpoints.live.get(liveId)], (live: ILive | null = null) => { if (live) { live.status = LiveStatus.ONGOING; @@ -430,7 +497,7 @@ export async function startLive(liveId: string) { ); mutate( - [endpoints.live.stream], + [endpoints.live.all], (lives: ILiveItem[] = []) => { const index = lives.findIndex((live) => live.id === liveId); if (index > -1) { @@ -440,8 +507,4 @@ export async function startLive(liveId: string) { }, false ); -} - - - - +} \ No newline at end of file diff --git a/src/shared/api/server.ts b/src/shared/api/server.ts index 61ecc60a..20918f8e 100644 --- a/src/shared/api/server.ts +++ b/src/shared/api/server.ts @@ -1,10 +1,13 @@ +import { HOST_API_URL } from '@/config-global'; import axios, { AxiosRequestConfig } from 'axios'; import { comment } from 'postcss'; import { shippingClassesList } from '../_mock/_shipping'; +import { HOST_API } from '@/config-global'; + + -export const HOST_API = 'http://15.237.175.171:8080'; @@ -30,22 +33,51 @@ export const fetcher = async (args: string | [string, AxiosRequestConfig]) => { export const endpoints = { comments: { add: "/chat/message", - get: (liveId: string) => `/chat/stream/${liveId}`, - delete: (commentId: string | undefined, liveId: string | undefined) => `/chat/stream/${liveId}/message/${commentId}`, - pin: (commentId: string | undefined, liveId: string | undefined) => `/chat/stream/${liveId}/pin/${commentId}`, - unpin: (commentId: string | undefined, liveId: string | undefined) => `/chat/stream/${liveId}/unpin/${commentId}`, + get: (liveId: string) => `/chat/live?streamId=${liveId}`, + delete: (commentId: string, liveId: string) => `/chat/live/delete?streamId=${liveId}&messageId=${commentId}`, + pin: (commentId: string, liveId: string) => `/chat/live/pin?streamId=${liveId}&messageId=${commentId}`, + unpin: (commentId: string, liveId: string) => `/chat/live/unpin?streamId=${liveId}&messageId=${commentId}`, + }, + product: { + all: (page: number, size: number) => `/products/all?page=${page}&size=${size}`, + search: (sku: string) => `/products/search?sku=${sku}`, }, - product: "/products", live: { - stream: '/live', - products : '/live-product', - stats : { - summaryAll : '/api/stats/summary', - summary : (liveId:string) => `/api/stats/${liveId}/summary`, - realTime : (keyStream:string) => `/api/stats/${keyStream}`, - } + all: '/live/all', + client: '/live/client', + get: (liveId: string) => `/live?liveId=${liveId}`, + create: '/live/create', + update: (liveId: string) => `/live/update?liveId=${liveId}`, + start: (liveId: string) => `/live/start?liveId=${liveId}`, + stop: (liveId: string) => `/live/stop?liveId=${liveId}`, + publish: (liveId: string) => `/live/publish?liveId=${liveId}`, + delete: (liveId: string) => `/live/delete?liveId=${liveId}`, + archive: (liveId: string) => `/live/archive?liveId=${liveId}`, + restore: (liveId: string) => `/live/restore?liveId=${liveId}`, + ongoing: '/live/ongoing' }, - + cards: { + add: (userId: string, productId: string) => `/cards/add?userId=${userId}&productId=${productId}`, + all: (userId: string) => `/cards/all?userId=${userId}` + }, + liveProducts: { + presented: (liveId: string) => `/live-products/presented?liveId=${liveId}`, + all: (liveId: string) => `/live-products/all?liveId=${liveId}`, + add: (liveProductId: string, liveId: string) => `/live-products/add?liveProductId=${liveProductId}&liveId=${liveId}`, + delete: (liveId: string, liveProductId:string) => `/live-products/delete?liveId=${liveId}&liveProductId=${liveProductId}`, + present: (liveId: string, liveProductId: string, startTime: number) => `/live-products/present?liveId=${liveId}&liveProductId=${liveProductId}&startTime=${startTime}`, + push: (liveProductId: string, startTime: number) => `/live-products/push?liveProductId=${liveProductId}&startTime=${startTime}`, + hide: (liveProductId: string, endTime: number) => `/live-products/hide?liveProductId=${liveProductId}&endTime=${endTime}`, + addTimeStamp: (liveProductId: string, startTime: number, endTime: number) => `/live-products/add-time-stamp?liveProductId=${liveProductId}&startTime=${startTime}&endTime=${endTime}`, + deleteTimeStamp: (id: string) => `/live-products/delete-time-stamp?id=${id}`, + updateTimeStamp: (id: string, startTime: number, endTime: number) => `/live-products/update-time-stamp?id=${id}&startTime=${startTime}&endTime=${endTime}` + }, + stats: { + getStats:`/api/stats`, + getStreamStats: (liveId: string) => `/api/stats?liveId=${liveId}`, + getStreamSummary: (liveId: string) => `/api/stats/summary?liveId=${liveId}`, + getAllLivesSummary: `/api/stats/summary/all`, + },, shipping:{ shippingClass:{ getAll: `/shipping/shipping-class`, @@ -135,3 +167,4 @@ export const endpoints = { }; + diff --git a/src/shared/layouts/common/notifications-popover/notification-item.tsx b/src/shared/layouts/common/notifications-popover/notification-item.tsx index ec055419..d86fbc00 100644 --- a/src/shared/layouts/common/notifications-popover/notification-item.tsx +++ b/src/shared/layouts/common/notifications-popover/notification-item.tsx @@ -145,7 +145,7 @@ export default function NotificationItem({ notification }: NotificationItemProps }} > <FileThumbnail - file="http://15.237.175.171:8080/httpsdesign-suriname-2015.mp3" + file="http://localhost:8080/httpsdesign-suriname-2015.mp3" sx={{ width: 40, height: 40 }} /> diff --git a/src/shared/sections/lives/add-edit-live/add-live-view.tsx b/src/shared/sections/lives/add-edit-live/add-live-view.tsx index 02cc7c3c..d0f1a23b 100644 --- a/src/shared/sections/lives/add-edit-live/add-live-view.tsx +++ b/src/shared/sections/lives/add-edit-live/add-live-view.tsx @@ -1,3 +1,4 @@ + "use client"; import * as Yup from 'yup'; @@ -30,14 +31,14 @@ import { ILiveProduct } from '@/shared/types/live'; import { addLive } from '@/shared/api/live'; import { useRouter } from 'next/navigation'; import ImageCrop from '@/components/image-crop'; +import useAsync from '@/hooks/use-async'; export default function AddLiveView() { const mdUp = useResponsive('up', 'md'); const { enqueueSnackbar } = useSnackbar(); const settings = useSettingsContext(); const router = useRouter(); - const [addLoading, setAddLoading] = useState(false); - + const NewLiveSchema = Yup.object().shape({ title: Yup.string().required('Title is required'), chatName: Yup.string().required('Moderator is required'), @@ -91,28 +92,29 @@ export default function AddLiveView() { setProducts(products.filter((p) => p.id !== product.id)); }; + const asyncAddLive = useAsync(async (formData, image) => { + await addLive(formData, image); + }); + const onSubmit = handleSubmit((data) => { - if(addLoading) return; - setAddLoading(true); - const formData = { ...data, products: products.map((product) => product.id) } - if (image) addLive(formData, image) - .then(() => { - enqueueSnackbar('live crée avec succés', { variant: 'success' }); - reset(); - setProducts([]); - setImage(null); - router.push(paths.dashboard.live.root) - setAddLoading(false); - - }) - .catch((err) => { - enqueueSnackbar(err.message, { variant: 'error' }); - console.error(err); - setAddLoading(false); - }) - else enqueueSnackbar('Veuillez ajouter une image de couverture', { variant: 'error' }); - - + if (asyncAddLive.loading) return; + const formData = { ...data, products: products.map((product) => product.id) }; + if (image) { + asyncAddLive.execute(formData, image) + .then(() => { + enqueueSnackbar('Live créé avec succès', { variant: 'success' }); + reset(); + setProducts([]); + setImage(null); + router.push(paths.dashboard.live.root); + }) + .catch((err) => { + enqueueSnackbar(err.message, { variant: 'error' }); + console.error(err); + }); + } else { + enqueueSnackbar('Veuillez ajouter une image de couverture', { variant: 'error' }); + } }); const renderDetails = ( @@ -338,8 +340,8 @@ export default function AddLiveView() { type="submit" variant="contained" size="large" - loading={addLoading} - disabled={addLoading} + loading={asyncAddLive.loading} + disabled={asyncAddLive.loading} sx={{ ml: 2 }} > Enregistrer diff --git a/src/shared/sections/lives/all-lives/live-table-row.tsx b/src/shared/sections/lives/all-lives/live-table-row.tsx index 3aa9bc08..2ed9c4b5 100644 --- a/src/shared/sections/lives/all-lives/live-table-row.tsx +++ b/src/shared/sections/lives/all-lives/live-table-row.tsx @@ -1,10 +1,6 @@ - -import Button from '@mui/material/Button'; import Avatar from '@mui/material/Avatar'; -import Divider from '@mui/material/Divider'; import MenuItem from '@mui/material/MenuItem'; import TableRow from '@mui/material/TableRow'; -import Checkbox from '@mui/material/Checkbox'; import TableCell from '@mui/material/TableCell'; import IconButton from '@mui/material/IconButton'; import ListItemText from '@mui/material/ListItemText'; @@ -20,8 +16,7 @@ import CustomPopover, { usePopover } from '@/shared/components/custom-popover'; import { ILiveItem, LiveStatus } from '@/shared/types/live'; import { archiveLive, deleteLive, publishLive, restoreLive } from '@/shared/api/live'; import LoadingButton from '@mui/lab/LoadingButton'; -import { useState } from 'react'; - +import useAsync from '@/hooks/use-async'; type Props = { row: ILiveItem; @@ -29,7 +24,7 @@ type Props = { onViewRow: VoidFunction; onDetails: VoidFunction; onEditRow: VoidFunction; - onViewStats: VoidFunction + onViewStats: VoidFunction; }; export default function LiveTableRow({ @@ -43,72 +38,74 @@ export default function LiveTableRow({ const popover = usePopover(); const openDetails = useBoolean(); const deleteConfirm = useBoolean(); - const publishConirm = useBoolean(); - const archiveConirm = useBoolean(); - const restoreConirm = useBoolean(); - const [loading, setLoading] = useState(false); + const publishConfirm = useBoolean(); + const archiveConfirm = useBoolean(); + const restoreConfirm = useBoolean(); const { enqueueSnackbar } = useSnackbar(); - const handleDelete = async () => { - try { - setLoading(true); - await deleteLive(row.id); - deleteConfirm.onFalse(); - enqueueSnackbar('live supprimé avec succés', { variant: 'success' }); - } catch (error) { - enqueueSnackbar('erreur lors de la supression', { variant: 'error' }); - } finally { - setLoading(false); - } - } - - const handlePublish = async () => { - try { - setLoading(true); - await publishLive(row.id); - publishConirm.onFalse(); - enqueueSnackbar('live publié avec succés', { variant: 'success' }); - } catch (error) { - enqueueSnackbar("erreur lors de la publication", { variant: 'error' }); - } finally { - setLoading(false); - } - } - - const handleArchive = async () => { - try { - setLoading(true); - await archiveLive(row.id); - archiveConirm.onFalse(); - enqueueSnackbar('live archivé avec succés', { variant: 'success' }); - } catch (error) { - enqueueSnackbar("erreur lors de l'archivation", { variant: 'error' }); - } finally { - setLoading(false); - } - } - - const handleUnarchive = async () => { - try { - setLoading(true); - await restoreLive(row.id); - restoreConirm.onFalse(); - enqueueSnackbar('live désarchivé avec succés', { variant: 'success' }); - } catch (error) { - enqueueSnackbar("erreur lors de la désarchivation", { variant: 'error' }); - } finally { - setLoading(false); - } - } - - - + const asyncDeleteLive = useAsync(async () => { + await deleteLive(row.id); + }); + + const asyncPublishLive = useAsync(async () => { + await publishLive(row.id); + }); + + const asyncArchiveLive = useAsync(async () => { + await archiveLive(row.id); + }); + + const asyncRestoreLive = useAsync(async () => { + await restoreLive(row.id); + }); + + const handleDelete = () => { + asyncDeleteLive.execute() + .then(() => { + deleteConfirm.onFalse(); + enqueueSnackbar('live supprimé avec succès', { variant: 'success' }); + }) + .catch(() => { + enqueueSnackbar('erreur lors de la suppression', { variant: 'error' }); + }); + }; + + const handlePublish = () => { + asyncPublishLive.execute() + .then(() => { + publishConfirm.onFalse(); + enqueueSnackbar('live publié avec succès', { variant: 'success' }); + }) + .catch(() => { + enqueueSnackbar('erreur lors de la publication', { variant: 'error' }); + }); + }; + + const handleArchive = () => { + asyncArchiveLive.execute() + .then(() => { + archiveConfirm.onFalse(); + enqueueSnackbar('live archivé avec succès', { variant: 'success' }); + }) + .catch(() => { + enqueueSnackbar('erreur lors de l\'archivation', { variant: 'error' }); + }); + }; + + const handleUnarchive = () => { + asyncRestoreLive.execute() + .then(() => { + restoreConfirm.onFalse(); + enqueueSnackbar('live désarchivé avec succès', { variant: 'success' }); + }) + .catch(() => { + enqueueSnackbar('erreur lors de la désarchivation', { variant: 'error' }); + }); + }; return ( <> - - <TableRow hover > - + <TableRow hover> <TableCell sx={{ cursor: 'pointer' }} onClick={onDetails}> <Avatar alt={title} @@ -118,7 +115,7 @@ export default function LiveTableRow({ /> </TableCell> - <TableCell sx={{ cursor: 'pointer' }} onClick={onDetails} > + <TableCell sx={{ cursor: 'pointer' }} onClick={onDetails}> {title} </TableCell> @@ -146,7 +143,7 @@ export default function LiveTableRow({ /> </TableCell> - <TableCell sx={{ cursor: 'pointer' }} onClick={onDetails} >{description}</TableCell> + <TableCell sx={{ cursor: 'pointer' }} onClick={onDetails}>{description}</TableCell> <TableCell sx={{ cursor: 'pointer' }} onClick={onDetails}> <Label variant="soft" @@ -173,7 +170,6 @@ export default function LiveTableRow({ <Iconify icon="eva:more-vertical-fill" /> </IconButton> </TableCell> - </TableRow> <CustomPopover @@ -185,38 +181,35 @@ export default function LiveTableRow({ <MenuItem onClick={() => { onViewRow(); - openDetails.onTrue() + openDetails.onTrue(); popover.onClose(); - }} - > - <Iconify icon="solar:eye-bold" /> voir </MenuItem> <MenuItem onClick={() => { onDetails(); - openDetails.onTrue() + openDetails.onTrue(); popover.onClose(); - }} - > <Iconify icon="gg:details-more" /> détails </MenuItem> - {status === LiveStatus.REVIEW && <MenuItem - onClick={() => { - popover.onClose(); - publishConirm.onTrue(); - }} - style={{ outline: 'none', color: 'blue' }} - > - <Iconify icon="ic:baseline-publish" /> - publier - </MenuItem>} + {status === LiveStatus.REVIEW && ( + <MenuItem + onClick={() => { + popover.onClose(); + publishConfirm.onTrue(); + }} + style={{ outline: 'none', color: 'blue' }} + > + <Iconify icon="ic:baseline-publish" /> + publier + </MenuItem> + )} <MenuItem onClick={() => { onEditRow(); @@ -224,20 +217,21 @@ export default function LiveTableRow({ }} style={{ outline: 'none' }} > - <Iconify icon="solar:pen-bold" /> Modifier </MenuItem> - {row.status != LiveStatus.COMING && <MenuItem - onClick={() => { - onViewStats(); - popover.onClose(); - }} - style={{ outline: 'none' }} - > - <Iconify icon="icomoon-free:stats-bars" /> - Stats - </MenuItem>} + {row.status !== LiveStatus.COMING && ( + <MenuItem + onClick={() => { + onViewStats(); + popover.onClose(); + }} + style={{ outline: 'none' }} + > + <Iconify icon="icomoon-free:stats-bars" /> + Stats + </MenuItem> + )} <MenuItem onClick={() => { popover.onClose(); @@ -249,25 +243,31 @@ export default function LiveTableRow({ Supprimer </MenuItem> - {(status === LiveStatus.REPLAY || status === LiveStatus.REVIEW) && <MenuItem - onClick={() => { - popover.onClose(); - archiveConirm.onTrue(); - }} - style={{ outline: 'none', color: 'orange' }} - > <Iconify icon="system-uicons:archive" /> - archiver - </MenuItem>} - - {status === LiveStatus.ARCHIVED && <MenuItem - onClick={() => { - popover.onClose(); - restoreConirm.onTrue(); - }} - style={{ outline: 'none', color: 'orange' }} - > <Iconify icon="system-uicons:archive" /> - restorer - </MenuItem>} + {(status === LiveStatus.REPLAY || status === LiveStatus.REVIEW) && ( + <MenuItem + onClick={() => { + popover.onClose(); + archiveConfirm.onTrue(); + }} + style={{ outline: 'none', color: 'orange' }} + > + <Iconify icon="system-uicons:archive" /> + archiver + </MenuItem> + )} + + {status === LiveStatus.ARCHIVED && ( + <MenuItem + onClick={() => { + popover.onClose(); + restoreConfirm.onTrue(); + }} + style={{ outline: 'none', color: 'orange' }} + > + <Iconify icon="system-uicons:archive" /> + restorer + </MenuItem> + )} </CustomPopover> <ConfirmDialog open={deleteConfirm.value} @@ -275,41 +275,41 @@ export default function LiveTableRow({ title="Supprimer" content="Voulez-vous vraiment supprimer ce live?" action={ - <LoadingButton loading={loading} variant="contained" color="error" onClick={handleDelete}> + <LoadingButton loading={asyncDeleteLive.loading} variant="contained" color="error" onClick={handleDelete}> Supprimer </LoadingButton> } /> <ConfirmDialog - open={publishConirm.value} - onClose={publishConirm.onFalse} + open={publishConfirm.value} + onClose={publishConfirm.onFalse} title="publier" content="Voulez-vous vraiment publier ce live?" action={ - <LoadingButton loading={loading} variant="contained" color="info" onClick={handlePublish}> + <LoadingButton loading={asyncPublishLive.loading} variant="contained" color="info" onClick={handlePublish}> publier </LoadingButton> } /> <ConfirmDialog - open={archiveConirm.value} - onClose={archiveConirm.onFalse} + open={archiveConfirm.value} + onClose={archiveConfirm.onFalse} title="archiver" content="Voulez-vous vraiment archiver ce live?" action={ - <LoadingButton loading={loading} variant="contained" color="warning" onClick={handleArchive}> + <LoadingButton loading={asyncArchiveLive.loading} variant="contained" color="warning" onClick={handleArchive}> archiver </LoadingButton> } /> <ConfirmDialog - open={restoreConirm.value} - onClose={restoreConirm.onFalse} + open={restoreConfirm.value} + onClose={restoreConfirm.onFalse} title="restorer" content="Voulez-vous vraiment restorer ce live?" action={ - <LoadingButton loading={loading} variant="contained" color="warning" onClick={handleUnarchive}> + <LoadingButton loading={asyncRestoreLive.loading} variant="contained" color="warning" onClick={handleUnarchive}> restorer </LoadingButton> } diff --git a/src/shared/sections/lives/details/view.tsx b/src/shared/sections/lives/details/view.tsx index e039c975..2fc0f8ca 100644 --- a/src/shared/sections/lives/details/view.tsx +++ b/src/shared/sections/lives/details/view.tsx @@ -20,9 +20,9 @@ import { VideoFormatsField } from './video-format'; import CustomBreadcrumbs from '@/components/custom-breadcrumbs'; import { paths } from '@/routes/paths'; import { useSettingsContext } from '@/shared/components/settings'; -import { Avatar, Button, CardMedia, Checkbox, Container, List, ListItem, ListItemAvatar, ListItemText, Paper } from '@mui/material'; +import { Avatar, Button, Container, List, ListItem, ListItemAvatar, ListItemText, Paper } from '@mui/material'; import { ILiveProduct, LiveStatus } from '@/shared/types/live'; -import { deleteLive, editLive, getProductsLive, publishLive, restoreLive , archiveLive} from '@/shared/api/live'; +import { deleteLive, editLive, getProductsLive, publishLive, restoreLive, archiveLive } from '@/shared/api/live'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { useLiveData } from '@/contexts/live-stats'; import LoadingButton from '@mui/lab/LoadingButton'; @@ -33,6 +33,7 @@ import { styled } from '@mui/material/styles'; import CopyButton from '@/components/copy-button'; import { useBoolean } from '@/hooks'; import { ConfirmDialog } from '@/shared/components/custom-dialog'; +import useAsync from '@/hooks/use-async'; const StyledList = styled(List)({ overflowY: 'auto', @@ -56,8 +57,7 @@ const StyledList = styled(List)({ const ProductCard = ({ product }: any) => ( <Card variant="outlined" key={product.id} sx={{ marginBottom: 2 }}> - <ListItem - > + <ListItem> <ListItemAvatar> <Avatar variant="rounded" alt={product.title} sx={{ height: 100, width: 100 }} src={product.image} /> </ListItemAvatar> @@ -116,47 +116,73 @@ export default function LiveDetailsView() { const archiveConfirm = useBoolean(); const restoreConfirm = useBoolean(); - - const handleDelete = async () => { + const asyncDeleteLive = useAsync(async () => { await deleteLive(liveData.id); deleteConfirm.onFalse(); router.push(paths.dashboard.live.all_lives); + }); - } + const asyncPublishLive = useAsync(async () => { + await publishLive(liveData.id); + }); - const handlePublishLive = async () => { - try { - await publishLive(liveData.id); - enqueueSnackbar('Le live a été publié', { variant: 'success' }); - } catch (err: any) { - enqueueSnackbar(err.message as string, { variant: 'error' }); - } finally { - publishConfirm.onFalse(); - } - } + const asyncArchiveLive = useAsync(async () => { + await archiveLive(liveData.id); + }); - const handleArchiveLive = async () => { - try { - await archiveLive(liveData.id); - enqueueSnackbar('Le live a été archivé', { variant: 'success' }); - } catch (err: any) { - enqueueSnackbar(err.message as string, { variant: 'error' }); - } finally { - archiveConfirm.onFalse(); - } - } + const asyncRestoreLive = useAsync(async () => { + await restoreLive(liveData.id); + }); + const handleDelete = () => { + asyncDeleteLive.execute() + .then(() => { + enqueueSnackbar('Live supprimé avec succès', { variant: 'success' }); + }) + .catch(() => { + enqueueSnackbar('Erreur lors de la suppression', { variant: 'error' }); + }); + }; + + const handlePublishLive = () => { + asyncPublishLive.execute() + .then(() => { + enqueueSnackbar('Le live a été publié', { variant: 'success' }); + }) + .catch((err) => { + enqueueSnackbar(err.message, { variant: 'error' }); + }) + .finally(() => { + publishConfirm.onFalse(); + }); + }; + + const handleArchiveLive = () => { + asyncArchiveLive.execute() + .then(() => { + enqueueSnackbar('Le live a été archivé', { variant: 'success' }); + }) + .catch((err) => { + enqueueSnackbar(err.message, { variant: 'error' }); + }) + .finally(() => { + archiveConfirm.onFalse(); + }); + }; + + const handleRestoreLive = () => { + asyncRestoreLive.execute() + .then(() => { + enqueueSnackbar('Le live a été restauré', { variant: 'success' }); + }) + .catch((err) => { + enqueueSnackbar(err.message, { variant: 'error' }); + }) + .finally(() => { + restoreConfirm.onFalse(); + }); + }; - const handleRestoreLive = async () => { - try { - await restoreLive(liveData.id); - enqueueSnackbar('Le live a été restauré', { variant: 'success' }); - } catch (err: any) { - enqueueSnackbar(err.message as string, { variant: 'error' }); - } finally { - restoreConfirm.onFalse(); - } - } const methods = useForm({ resolver: yupResolver(NewLiveSchema), defaultValues, @@ -188,12 +214,14 @@ export default function LiveDetailsView() { }, [router] ); + const handleViewStats = useCallback( (id: string) => { router.push(paths.dashboard.live.statistics.single(id)); }, [router] ); + const handleViewInfo = useCallback( (id: string) => { router.push(paths.dashboard.live.details(id)); @@ -210,7 +238,7 @@ export default function LiveDetailsView() { }; editLive(liveData.id, formData) .then(() => { - enqueueSnackbar('Live Updated successfully', { variant: 'success' }); + enqueueSnackbar('Live mis à jour avec succès', { variant: 'success' }); setImage(null); }) .catch((err) => { @@ -514,7 +542,6 @@ export default function LiveDetailsView() { (status == LiveStatus.ONGOING && 'warning') || (status == LiveStatus.COMING && 'error') || (status == LiveStatus.REVIEW && 'info') || 'default' - } > { @@ -553,7 +580,8 @@ export default function LiveDetailsView() { loading={isSubmitting} sx={{ ml: 2 }} onClick={() => handleViewStats(liveData.id)} - ><Iconify sx={{ mr: 2 }} icon="icomoon-free:stats-bars" /> + > + <Iconify sx={{ mr: 2 }} icon="icomoon-free:stats-bars" /> Statistiques </LoadingButton> )} @@ -564,7 +592,7 @@ export default function LiveDetailsView() { onClick={publishConfirm.onTrue} sx={{ ml: 2, backgroundColor: 'blue' }} > - <Iconify sx={{ mr: 2 }} icon="ic:baseline-publish" /> + <Iconify sx={{ mr: 2 }} icon="ic:baseline-publish" /> Publier </LoadingButton> )} @@ -574,9 +602,9 @@ export default function LiveDetailsView() { size="large" color='warning' onClick={archiveConfirm.onTrue} - sx={{ ml: 2}} + sx={{ ml: 2 }} > - <Iconify sx={{ mr: 2 }} icon="dashicons:archive" /> + <Iconify sx={{ mr: 2 }} icon="dashicons:archive" /> archiver </LoadingButton> )} @@ -588,7 +616,7 @@ export default function LiveDetailsView() { onClick={restoreConfirm.onTrue} sx={{ ml: 2 }} > - <Iconify sx={{ mr: 2 }} icon="mdi:archive-off" /> + <Iconify sx={{ mr: 2 }} icon="mdi:archive-off" /> restorer </LoadingButton> )} @@ -606,8 +634,6 @@ export default function LiveDetailsView() { </Grid> ); - - return ( <Container maxWidth={settings.themeStretch ? false : 'lg'}> <CustomBreadcrumbs @@ -636,9 +662,9 @@ export default function LiveDetailsView() { title="Supprimer" content="Voulez-vous vraiment supprimer ce live?" action={ - <Button variant="contained" color="error" onClick={handleDelete}> + <LoadingButton loading={asyncDeleteLive.loading} variant="contained" color="error" onClick={handleDelete}> Supprimer - </Button> + </LoadingButton> } /> <ConfirmDialog @@ -647,9 +673,9 @@ export default function LiveDetailsView() { title="Publier" content="Voulez-vous publier le live?" action={ - <Button variant="contained" color="info" onClick={handlePublishLive}> + <LoadingButton loading={asyncPublishLive.loading} variant="contained" color="info" onClick={handlePublishLive}> Publier - </Button> + </LoadingButton> } /> @@ -659,9 +685,9 @@ export default function LiveDetailsView() { title="Archiver" content="Voulez-vous archiver le live?" action={ - <Button variant="contained" color="warning" onClick={handleArchiveLive}> + <LoadingButton loading={asyncArchiveLive.loading} variant="contained" color="warning" onClick={handleArchiveLive}> Archiver - </Button> + </LoadingButton> } /> @@ -671,12 +697,11 @@ export default function LiveDetailsView() { title="Restorer" content="Voulez-vous restorer le live?" action={ - <Button variant="contained" color="warning" onClick={handleRestoreLive}> + <LoadingButton loading={asyncRestoreLive.loading} variant="contained" color="warning" onClick={handleRestoreLive}> Restorer - </Button> + </LoadingButton> } - /> + /> </Container> ); } - diff --git a/src/shared/sections/lives/supervision/details-comments-input.tsx b/src/shared/sections/lives/supervision/details-comments-input.tsx index 6deaf53c..cd6babf0 100644 --- a/src/shared/sections/lives/supervision/details-comments-input.tsx +++ b/src/shared/sections/lives/supervision/details-comments-input.tsx @@ -85,7 +85,7 @@ export default function CommentsInput({ <Typography variant="body1" > {comment?.content} </Typography> - <IconButton onClick={async () => await unpinCommentLive(comment.id, liveId)} sx={{ position: "absolute", right: 0, padding: 1 }} size="small"> + <IconButton onClick={async () => await unpinCommentLive(comment.id as string, liveId)} sx={{ position: "absolute", right: 0, padding: 1 }} size="small"> <Iconify style={{ color: 'gray', marginBottom: 2 }} width={20} icon="ic:baseline-pin-off" /> </IconButton> </Card> diff --git a/src/shared/sections/lives/supervision/details-comments-item.tsx b/src/shared/sections/lives/supervision/details-comments-item.tsx index 400f06d2..935c43bd 100644 --- a/src/shared/sections/lives/supervision/details-comments-item.tsx +++ b/src/shared/sections/lives/supervision/details-comments-item.tsx @@ -1,34 +1,23 @@ -import { formatDistanceToNowStrict } from 'date-fns'; - import Stack from '@mui/material/Stack'; import IconButton from '@mui/material/IconButton'; import Typography from '@mui/material/Typography'; - - import Iconify from '@/components/iconify'; - import { ILiveComment } from '@/shared/types/live'; import { Card, useTheme, alpha } from '@mui/material'; -import { bgGradient } from '@/shared/theme/css'; import { useResponsive } from '@/hooks'; import { deleteCommentLive, pinCommentLive } from '@/shared/api/live'; - - type Props = { comment: ILiveComment; changeParentComment: (comment: ILiveComment) => void; - }; export default function LiveCommentItem({ comment, changeParentComment }: Props) { - - const { content, senderName, parent , admin} = comment; + const { content, senderName, parent, admin } = comment; const theme = useTheme(); const mdUp = useResponsive('up', 'md'); const bgStyle = mdUp ? { - - backgroundColor: !admin ? alpha(theme.palette['primary'].main, 0.2) : alpha(theme.palette.secondary.dark, 0.2) , + backgroundColor: !admin ? alpha(theme.palette['primary'].main, 0.2) : alpha(theme.palette.secondary.dark, 0.2), width: "100%", p: .5, fontSize: theme.typography.pxToRem(12), @@ -41,9 +30,6 @@ export default function LiveCommentItem({ comment, changeParentComment }: Props) p: .5, }; - - - const renderInfo = ( <Typography noWrap @@ -54,7 +40,6 @@ export default function LiveCommentItem({ comment, changeParentComment }: Props) }} > {senderName} - </Typography> ); @@ -111,7 +96,6 @@ export default function LiveCommentItem({ comment, changeParentComment }: Props) </Card> ); - const renderActions = ( <Stack direction="row" @@ -131,10 +115,10 @@ export default function LiveCommentItem({ comment, changeParentComment }: Props) <IconButton onClick={() => changeParentComment(comment)} size="small"> <Iconify icon="solar:reply-bold" width={16} /> </IconButton> - <IconButton onClick={() => deleteCommentLive(comment.id, comment.liveStream)} size="small"> + <IconButton onClick={() => deleteCommentLive(comment.id as string, comment.liveStream)} size="small"> <Iconify icon="material-symbols:delete" width={16} /> </IconButton> - <IconButton onClick={() => pinCommentLive(comment.id, comment.liveStream)} size="small"> + <IconButton onClick={() => pinCommentLive(comment.id as string, comment.liveStream)} size="small"> <Iconify icon="mdi:pin" width={16} /> </IconButton> </Stack> @@ -144,8 +128,6 @@ export default function LiveCommentItem({ comment, changeParentComment }: Props) <Stack direction="column" justifyContent={'unset'} sx={{ mb: 2 }}> <Stack alignItems="flex-end"> {renderInfo} - - </Stack> <Stack direction="row" diff --git a/src/shared/sections/lives/supervision/details-comments-list.tsx b/src/shared/sections/lives/supervision/details-comments-list.tsx index 80a1c989..0e8d375c 100644 --- a/src/shared/sections/lives/supervision/details-comments-list.tsx +++ b/src/shared/sections/lives/supervision/details-comments-list.tsx @@ -1,18 +1,40 @@ "use client"; import React, { useEffect, useRef } from 'react'; -import { Box } from '@mui/material'; +import { Box, IconButton } from '@mui/material'; import Scrollbar from '@/shared/components/scrollbar'; import LiveCommentItem from './details-comments-item'; import { ILiveComment } from '@/shared/types/live'; import useLiveComments from '@/hooks/use-live-comments'; import { useLiveData } from '@/contexts/live-stats'; +import { useResponsive } from '@/hooks'; +import Iconify from '@/components/iconify'; +import { parse } from 'json2csv'; export default function CommentsList({ changeParentComment }: { changeParentComment: (comment: ILiveComment | null) => void }) { const liveData = useLiveData(); - const { comments, pinnedComments } = useLiveComments(liveData); + const { comments } = useLiveComments(liveData); const latestCommentRef = useRef<HTMLDivElement | null>(null); const scrollbarRef = useRef<HTMLDivElement>(); + const mdUp = useResponsive('up', 'md'); + + const handleExportComments = () => { + const fields = ['senderName', 'content']; // specify the fields you want in the CSV + const opts = { fields }; + try { + const csv = parse(comments, opts); + const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'comments.csv'; + document.body.appendChild(a); + a.click(); + a.remove(); + } catch (err) { + console.error('Error converting JSON to CSV', err); + } + }; useEffect(() => { if (latestCommentRef.current && scrollbarRef.current) { @@ -21,31 +43,28 @@ export default function CommentsList({ changeParentComment }: { changeParentComm }, [comments]); return ( - <Scrollbar sx={{ px: 2, py: 5, height: 1, color: 'transparent' }}> - <Box sx={{ position: 'relative' }}> - {/* <Scrollbar sx={{ px: 2, py: 5, height: 1, color: 'transparent' }}> - <Box sx={{ height: 300, zIndex: 4, overflow: 'hidden', position: 'absolute', left: 0, right: 0, top: 0, bottom: 0 }}> - {pinnedComments.map((comment) => ( - <LiveCommentItem - changeParentComment={changeParentComment} - key={comment.id} - comment={comment} - /> - )) - } - </Box> - </Scrollbar> */} - - {comments.map((comment) => ( - <LiveCommentItem - changeParentComment={changeParentComment} - key={comment.id} - comment={comment} + <> + {mdUp && ( + <IconButton + onClick={handleExportComments} + sx={{ position: 'absolute', color: 'white', bgcolor: 'black', zIndex: 2, top: 20, right: 20 }} + > + <Iconify icon="solar:download-bold" /> + </IconButton> + )} + <Scrollbar sx={{ px: 2, height: 1, position: 'relative', color: 'transparent' }}> - /> - ))} - <div ref={latestCommentRef} /> - </Box> - </Scrollbar> + <Box sx={{ position: 'relative', py: 10 }}> + {comments.map((comment) => ( + <LiveCommentItem + changeParentComment={changeParentComment} + key={comment.id} + comment={comment} + /> + ))} + <div ref={latestCommentRef} /> + </Box> + </Scrollbar> + </> ); } diff --git a/src/shared/sections/lives/supervision/details-comments-section.tsx b/src/shared/sections/lives/supervision/details-comments-section.tsx index 1e6c86ff..3668b41d 100644 --- a/src/shared/sections/lives/supervision/details-comments-section.tsx +++ b/src/shared/sections/lives/supervision/details-comments-section.tsx @@ -1,9 +1,10 @@ import { useResponsive } from "@/hooks/use-responsive"; -import { Box, Card, Grid } from "@mui/material"; +import { Box, Button, Card, Grid } from "@mui/material"; import CommentsInput from "./details-comments-input"; import CommentsList from "./details-comments-list"; import { ILiveComment } from "@/shared/types/live"; import { useState } from "react"; +import useLiveComments from "@/hooks/use-live-comments"; type Props = { liveId: string, @@ -12,6 +13,8 @@ type Props = { export default function CommentSection({ liveId }: Props) { const mdUp = useResponsive('up', 'md'); const [parentComment, setParentComment] = useState<ILiveComment | null>(null); + + return ( <> diff --git a/src/shared/sections/lives/supervision/details-player-section.tsx b/src/shared/sections/lives/supervision/details-player-section.tsx index 9d033cfd..111fc343 100644 --- a/src/shared/sections/lives/supervision/details-player-section.tsx +++ b/src/shared/sections/lives/supervision/details-player-section.tsx @@ -5,7 +5,8 @@ import { ConfirmDialog } from "@/shared/components/custom-dialog"; import { LiveStatus } from "@/shared/types/live"; import LoadingButton from "@mui/lab/LoadingButton"; import { Card, Grid } from "@mui/material"; -import ReactPlayer from 'react-player' +import ReactPlayer from 'react-player'; +import { useRef, useEffect } from 'react'; type Props = { keyStream: string | undefined; @@ -17,10 +18,10 @@ type Props = { }; export default function VideoPlayerSection({ keyStream, hlsServerAddress, status, liveId, videoLink }: Props) { - const liveUrl = status!= LiveStatus.ONGOING ? videoLink : `${hlsServerAddress}/${keyStream}.m3u8`; + const liveUrl = status != LiveStatus.ONGOING ? videoLink : `${hlsServerAddress}/${keyStream}.m3u8`; const startConfirm = useBoolean(); - console.log( "status != LiveStatus.COMING =",status != LiveStatus.COMING) - console.log(liveUrl); + const playerRef = useRef<ReactPlayer>(null); + const handleStart = async () => { try { await startLive(liveId); @@ -28,30 +29,63 @@ export default function VideoPlayerSection({ keyStream, hlsServerAddress, status } catch (error) { console.error(error); } - } + }; + + const handleEnablePiP = () => { + if (playerRef.current) { + const internalPlayer = playerRef.current.getInternalPlayer(); + if (internalPlayer && internalPlayer.requestPictureInPicture) { + internalPlayer.requestPictureInPicture(); + } + } + }; + + useEffect(() => { + const interval = setInterval(() => { + const pipButton = document.querySelector('button[aria-label="Picture-in-Picture"]'); + if (pipButton) { + pipButton.textContent = 'Réduire'; + clearInterval(interval); + } + }, 1000); + + return () => clearInterval(interval); + }, []); + return ( <Grid item lg={4} md={8} xs={12}> <Card sx={{ position: "relative", height: 600, zIndex: 0, p: 0, bgcolor: 'black', display: 'flex', alignItems: 'center', justifyContent: 'center' }}> - {status != LiveStatus.COMING - && <ReactPlayer playing={true} + {status != LiveStatus.COMING && + <ReactPlayer + ref={playerRef} + playing={true} controls={true} + pip={true} width="100%" - height="100%" url={liveUrl} /> + height="100%" + url={liveUrl} + /> + } + {status == LiveStatus.COMING && + <Iconify + color='white' + sx={{ position: "absolute", margin: "auto", width: 100, height: 100, cursor: 'pointer' }} + onClick={startConfirm.onTrue} + icon="solar:play-bold" + /> } - {status == LiveStatus.COMING && <Iconify color='white' sx={{ position: "absolute", margin: "auto", width: 100, height: 100, cursor: 'pointer' }} onClick={startConfirm.onTrue} icon="solar:play-bold" />} </Card> <ConfirmDialog open={startConfirm.value} onClose={startConfirm.onFalse} title="démarrer" - content="Voulez-vous démarrer ce live?" + content="Voulez-vous démarrer ce live?" action={ <LoadingButton variant="contained" color="warning" onClick={handleStart}> démarrer </LoadingButton> } /> - </Grid> - ) -} \ No newline at end of file + ); +} diff --git a/src/shared/sections/lives/supervision/details-product-card.tsx b/src/shared/sections/lives/supervision/details-product-card.tsx index ecf39a2c..a8ff0938 100644 --- a/src/shared/sections/lives/supervision/details-product-card.tsx +++ b/src/shared/sections/lives/supervision/details-product-card.tsx @@ -1,22 +1,27 @@ "use client"; -import React, { useState, useCallback } from 'react'; +import React, { useState, useCallback, useEffect } from 'react'; import { Avatar, Box, Button, Card, Grid, + IconButton, ListItemText, + MenuItem, useTheme, } from '@mui/material'; import { TimeField } from '@mui/x-date-pickers'; import Iconify from '@/components/iconify'; import { - addProductLive, + addTimeStampProduct, deleteProductLive, + deleteTimeStampProduct, hideProductLive, + presentProductLive, pushProductLive, + updateTimeStampProduct, } from '@/shared/api/live'; import { calculateSeconds, @@ -24,10 +29,18 @@ import { secondsToTime, } from '@/utils/format-time'; import { useLiveData } from '@/contexts/live-stats'; -import { LiveProductStatus, LiveStatus } from '@/shared/types/live'; +import { LiveProductStatus, LiveStatus, TimeStampProduct } from '@/shared/types/live'; import { useSnackbar } from '@/components/snackbar'; +import EditIcon from '@mui/icons-material/Edit'; +import DeleteIcon from '@mui/icons-material/Delete'; +import BookmarkIcon from '@mui/icons-material/Bookmark'; +import AddIcon from '@mui/icons-material/Add'; +import useAsync from '@/hooks/use-async'; +import LoadingButton from '@mui/lab/LoadingButton'; +import CustomPopover, { usePopover } from '@/shared/components/custom-popover'; -type Props = { + +type ProductCardProps = { liveId: string; id: string; title: string; @@ -35,60 +48,174 @@ type Props = { sku: string; image: string; price: number; - startTime?: number | undefined; - endTime?: number | undefined; + timeStampProduct: TimeStampProduct[]; +}; + +type TimeStampProductFiledProps = { + liveProductId: string; + liveId: string; + id?: string; + initialStartTime?: number; + initialEndTime?: number; + status: LiveProductStatus; +}; + +const TimeStampProductFiled = ({ id, initialStartTime, initialEndTime, status, liveId, liveProductId }: TimeStampProductFiledProps) => { + const [startTime, setStartTime] = useState<number | undefined>(initialStartTime); + const [endTime, setEndTime] = useState<number | undefined>(initialEndTime); + const [modify, setModify] = useState<boolean>(false); + const popover = usePopover(); + + useEffect(() => { + console.log('initialStartTime', { id, initialStartTime, initialEndTime, status, liveId, liveProductId }); + }, []); + + const { loading: deleting, execute: handleDeleteTimeStampProduct } = useAsync(async () => { + await deleteTimeStampProduct(id as string); + }); + + const { loading: updating, execute: handleUpdateTimeStampProduct } = useAsync(async () => { + await updateTimeStampProduct(id as string, startTime as number, endTime as number); + setModify(false); + }); + + const { loading: adding, execute: handleAddTimeStampProduct } = useAsync(async () => { + await addTimeStampProduct(liveId, liveProductId, startTime as number, endTime as number); + setModify(false); + setStartTime(undefined); + setEndTime(undefined); + }); + + return ( + <Box sx={{ display: 'flex', width: '100%', justifyContent: 'start', gap: 2 }}> + <TimeField + value={startTime && secondsToTime(startTime)} + onChange={(date) => setStartTime(calculateSeconds(date as Date))} + disabled={id != undefined && !modify} + fullWidth + + size="small" + label="Start Time" + InputLabelProps={{ + shrink: true, + style: { fontSize: '0.8rem', } + }} + inputProps={{ + + step: 1, + style: { fontSize: '0.8rem', textAlign: 'center', height: 10, width:200 } + }} + sx={{ width: 200 }} + ampm={false} + format="HH:mm:ss" + /> + + <TimeField + fullWidth + size="small" + label="End Time" + onChange={(date) => setEndTime(calculateSeconds(date as Date))} + value={endTime && secondsToTime(endTime)} + disabled={id != undefined && !modify} + InputLabelProps={{ + shrink: true, + style: { fontSize: '0.8rem' }, + }} + inputProps={{ + step: 1, + style: { fontSize: '0.8rem', textAlign: 'center', height: 10 , width:200 } + }} + sx={{ width: 200 }} + ampm={false} + format="HH:mm:ss" + /> + + <CustomPopover + open={popover.open} + onClose={popover.onClose} + arrow="right-top" + sx={{ width: 160 }} + > + <MenuItem + onClick={() => { + setModify(true); + popover.onClose(); + + }} + + > + <LoadingButton loading={updating} > + <EditIcon /> modifier + </LoadingButton> + </MenuItem> + <MenuItem + onClick={() => { + handleDeleteTimeStampProduct(); + }} + + > + <LoadingButton loading={deleting} > + <DeleteIcon /> supprimer + </LoadingButton> + </MenuItem> + </CustomPopover> + + + {id != undefined ? ( + <> + {modify ? ( + <LoadingButton loading={updating} onClick={handleUpdateTimeStampProduct} disabled={updating}> + <Iconify icon="dashicons:saved" /> + </LoadingButton> + ) : ( + <IconButton color={popover.open ? 'inherit' : 'default'} onClick={popover.onOpen}> + <Iconify icon="eva:more-vertical-fill" /> + </IconButton> + )} + </> + ) : ( + <LoadingButton onClick={handleAddTimeStampProduct} loading={adding} disabled={adding}> + <Iconify icon="carbon:add-filled" /> + </LoadingButton> + )} + </Box> + ); }; -const ProductCard: React.FC<Props> = ({ +const ProductCard: React.FC<ProductCardProps> = ({ liveId, id, title, status, sku, image, - price, - startTime: initialStartTime, - endTime: initialEndTime, + timeStampProduct = [], }) => { const theme = useTheme(); const { enqueueSnackbar } = useSnackbar(); const { startedAt, status: liveStatus } = useLiveData(); - const [startTime, setStartTime] = useState<number | undefined>(initialStartTime); - const [endTime, setEndTime] = useState<number | undefined>(initialEndTime); - const [modify, setModify] = useState<boolean>(false); - const handleAddProduct = useCallback(() => { - addProductLive(liveId, id, startTime as number, endTime as number); - setModify(false); - }, [liveStatus, status, liveId, id, startTime, endTime, enqueueSnackbar]); - - const handlePushProduct = useCallback(() => { + const { loading: pushing, execute: handlePushProduct } = useAsync(async () => { if (status === LiveProductStatus.NOT_PRESENTED) { - pushProductLive( - id, - liveId, - differenceInSeconds(new Date(), new Date(startedAt as Date)) - ); + await presentProductLive(id, liveId, differenceInSeconds(new Date(), new Date(startedAt as Date))); + } else if (status === LiveProductStatus.PRESENTED) { + await pushProductLive(id, liveId, differenceInSeconds(new Date(), new Date(startedAt as Date))); } else { enqueueSnackbar('Produit déjà présenté', { variant: 'warning' }); } - }, [status, id, liveId, startedAt, enqueueSnackbar]); + }); - const handleHideProduct = useCallback(() => { + const { loading: hiding, execute: handleHideProduct } = useAsync(async () => { if (status === LiveProductStatus.IN_PRESENTATION) { - hideProductLive( - id, - liveId, - differenceInSeconds(new Date(), new Date(startedAt as Date)) - ); + await hideProductLive(id, liveId, differenceInSeconds(new Date(), new Date(startedAt as Date))); } else { enqueueSnackbar('Produit déjà caché', { variant: 'warning' }); } - }, [status, id, liveId, startedAt, enqueueSnackbar]); + }); - const handleDeleteProduct = useCallback(() => { - deleteProductLive(id, liveId); - }, [id, liveId]); + const { loading: deleting, execute: handleDeleteProduct } = useAsync(async () => { + await deleteProductLive(id, liveId); + }); return ( <Card variant="outlined" sx={{ marginBottom: 2, bgcolor: theme.palette.background.neutral }}> @@ -115,103 +242,50 @@ const ProductCard: React.FC<Props> = ({ <Box sx={{ display: 'flex', width: '100%', justifyContent: 'space-between', gap: 2 }}> {liveStatus === LiveStatus.ONGOING && ( <> - {status === LiveProductStatus.NOT_PRESENTED && ( - <Button variant="contained" fullWidth onClick={handlePushProduct}> + {(status === LiveProductStatus.NOT_PRESENTED || status === LiveProductStatus.PRESENTED) && ( + <LoadingButton loading={pushing} variant="contained" fullWidth onClick={handlePushProduct} disabled={pushing}> Pousser - </Button> + </LoadingButton> )} {status === LiveProductStatus.IN_PRESENTATION && ( - <Button variant="contained" color="info" fullWidth onClick={handleHideProduct}> + <LoadingButton loading={hiding} variant="contained" color="info" fullWidth onClick={handleHideProduct} disabled={hiding}> Cacher - </Button> - )} - {status === LiveProductStatus.PRESENTED && ( - <Button variant="contained" color="success" fullWidth disabled> - Présenté - </Button> - )} - </> - )} - {(liveStatus === LiveStatus.REVIEW || liveStatus === LiveStatus.REPLAY) && ( - <> - {status !== LiveProductStatus.PRESENTED && ( - <Button variant="contained" color="info" fullWidth onClick={handleAddProduct}> - Ajouter - </Button> - )} - {status === LiveProductStatus.PRESENTED && ( - <> - {modify ? ( - <Button - variant="contained" - color="success" - fullWidth - onClick={handleAddProduct} - > - Approuver - </Button> - ) : ( - <Button - variant="contained" - color="error" - fullWidth - onClick={() => setModify(true)} - > - Modifier - </Button> - )} - </> + </LoadingButton> )} </> )} - <Button + <LoadingButton onClick={handleDeleteProduct} variant="contained" fullWidth startIcon={<Iconify icon="ic:round-delete" />} + disabled={deleting} + loading={deleting} + > Supprimer - </Button> + </LoadingButton> </Box> {(liveStatus === LiveStatus.REVIEW || liveStatus === LiveStatus.REPLAY) && ( - <Box sx={{ display: 'flex', width: '100%', justifyContent: 'space-between', gap: 2 }}> - <TimeField - value={startTime && secondsToTime(startTime)} - onChange={(date) => setStartTime(calculateSeconds(date as Date))} - disabled={ - (status === LiveProductStatus.PRESENTED && !modify) - } - fullWidth - size="small" - label="Start Time" - InputLabelProps={{ - shrink: true, - }} - inputProps={{ - step: 1, - }} - ampm={false} - format="HH:mm:ss" - /> - <TimeField - fullWidth - size="small" - label="End Time" - onChange={(date) => setEndTime(calculateSeconds(date as Date))} - value={endTime && secondsToTime(endTime)} - disabled={ - (status === LiveProductStatus.PRESENTED && !modify) - } - InputLabelProps={{ - shrink: true, - }} - inputProps={{ - step: 1, - }} - ampm={false} - format="HH:mm:ss" + <> + {timeStampProduct.map((timeStamp) => ( + <TimeStampProductFiled + key={timeStamp.id} + id={timeStamp.id} + initialStartTime={timeStamp.startTime} + initialEndTime={timeStamp.endTime} + status={status} + liveId={liveId} + liveProductId={id} + /> + ))} + <TimeStampProductFiled + key={-1} + status={status} + liveId={liveId} + liveProductId={id} /> - </Box> + </> )} </Grid> </Grid> diff --git a/src/shared/sections/lives/supervision/details-products-section.tsx b/src/shared/sections/lives/supervision/details-products-section.tsx index 57330f9e..fc28a95e 100644 --- a/src/shared/sections/lives/supervision/details-products-section.tsx +++ b/src/shared/sections/lives/supervision/details-products-section.tsx @@ -7,6 +7,7 @@ import { useState } from "react"; import { addProductToLive, getProductsLive, searchProductLive } from "@/shared/api/live"; import { ILiveProduct } from "@/shared/types/live"; import LiveSearch from "./details-add-product"; +import { usePopover } from "@/shared/components/custom-popover"; type Props = { liveId: string; @@ -16,6 +17,7 @@ export default function ProductsSection({ liveId }: Props) { const lgUp = useResponsive('up', 'lg'); const { products } = getProductsLive(liveId); + const popover = usePopover(); const { endRef: productsEndRef } = useScroll(products); const [open, setOpen] = useState(false); @@ -47,7 +49,7 @@ export default function ProductsSection({ liveId }: Props) { return ( <> {lgUp ? ( - <Grid spacing={0} item lg={4} md={6} xs={12}> + <Grid item lg={4} md={6} xs={12}> <LiveSearch query={query} results={searchResults} diff --git a/src/shared/sections/lives/supervision/view.tsx b/src/shared/sections/lives/supervision/view.tsx index 708a0b5c..ca5b75d3 100644 --- a/src/shared/sections/lives/supervision/view.tsx +++ b/src/shared/sections/lives/supervision/view.tsx @@ -1,17 +1,13 @@ 'use client'; -import { useSettingsContext } from '@/shared/components/settings'; -import { Grid, Container, Card } from '@mui/material'; +import { Grid, Container } from '@mui/material'; import CommentSection from './details-comments-section'; import ProductsSection from './details-products-section'; import VideoPlayerSection from './details-player-section'; import HeadSection from './details-head-section'; -import { useGetLive } from '@/shared/api/live'; import { useLiveData } from '@/contexts/live-stats'; -type Props = { - liveId: string; -} + export default function LiveDetailsView() { const liveData = useLiveData(); diff --git a/src/shared/types/live.ts b/src/shared/types/live.ts index 7a7e01f0..0a7f9b62 100644 --- a/src/shared/types/live.ts +++ b/src/shared/types/live.ts @@ -70,7 +70,12 @@ export type ILiveComment = { } - +export type TimeStampProduct = { + id: string; + startTime?: number; + endTime?: number; + liveProductId?: string; +} export type ILiveProduct = { id: string; @@ -78,9 +83,9 @@ export type ILiveProduct = { sku: string; image: string; price: number; + isPresented: boolean; status: LiveProductStatus; - startTime?: number; - endTime?: number; + timeStampProduct: TimeStampProduct[]; } export type ILiveStatistic = { -- GitLab