Skip to content
Extraits de code Groupes Projets
Valider 09ac78bf rédigé par oussama aftys's avatar oussama aftys Validation de oussama aftys
Parcourir les fichiers

test streaming

parent 6b951fd2
Branches
1 requête de fusion!143Update 2 files
Affichage de
avec 380 ajouts et 290 suppressions
# HOST
NEXT_PUBLIC_HOST_API=https://api-dev-minimal-v510.vercel.app
NEXT_PUBLIC_HOST_API_URL=http://localhost:8080
NEXT_PUBLIC_WEB_SOCKET_URL="http://localhost:8080/ws"
# ASSETS
NEXT_PUBLIC_ASSETS_API=https://api-dev-minimal-v510.vercel.app
......
......@@ -6,9 +6,10 @@ import { ICroppedImage } from 'mui-image-crop/es/types';
type Props = {
image: File | undefined,
setImage: (image: File) => void;
disabled?: boolean;
};
const CustomImageCrop = ({ image, setImage }: Props) => {
const CustomImageCrop = ({ image, setImage, disabled = false }: Props) => {
const imageCropProps: ImageCropProps = {
value: image as ICroppedImage,
onChange: (value) => setImage(value as File),
......@@ -55,7 +56,7 @@ const CustomImageCrop = ({ image, setImage }: Props) => {
return (
<Card sx={{ display: 'flex', p: 4, flexDirection: 'column', alignItems: 'center', justifyContent: 'center' }}>
<ImageCrop {...imageCropProps} />
<ImageCrop disabled={disabled} {...imageCropProps} />
<Stack spacing={1} sx={{ textAlign: 'center' }}>
<Typography variant="h6">Déposer ou sélectionner une image</Typography>
</Stack>
......
"use client";
import React, { createContext, useContext, ReactNode, useState, useEffect, useRef } from "react";
import React, { createContext, useContext, ReactNode, useState, useEffect, useRef, useCallback } from "react";
import Stomp from "stompjs";
import SockJS from "sockjs-client";
import { ILive, ILiveComment, LiveStatus } from "@/shared/types/live";
import { useGetCommentsLive } from "@/shared/api/live";
type LiveCommentsContextType = {
comments: ILiveComment[];
pinnedComments: ILiveComment[];
};
const WEB_SOCKET_URL = process.env.WEB_SOCKET_URL || "http://localhost:8080/ws";
const useLiveComments = ({ id, status }: ILive): ILiveComment[] => {
const useLiveComments = ({ id, status }: ILive): LiveCommentsContextType => {
const [comments, setComments] = useState<ILiveComment[]>([]);
const [pinnedComments, setPinnedComments] = useState<ILiveComment[]>([]);
const { commentsData, commentsLoading } = useGetCommentsLive(id);
const socketRef = useRef<any>(null);
const clientRef = useRef<any>(null);
useEffect(() => {
setComments(commentsData);
}, [commentsLoading]);
if (!commentsLoading) {
const newComments = commentsData.reduce<{ comments: ILiveComment[], pinnedComments: ILiveComment[] }>(
(acc, comment) => {
if (comment.pinned) {
acc.pinnedComments.push(comment);
} else {
acc.comments.push(comment);
}
return acc;
},
{ comments: [], pinnedComments: [] }
);
setComments(newComments.comments);
setPinnedComments(newComments.pinnedComments);
}
}, [commentsData, commentsLoading]);
const deleteComment = useCallback((id: string | undefined) => {
setComments((prev) => prev.filter((comment) => comment.id !== id));
}, []);
const addComment = useCallback((comment: ILiveComment) => {
setComments((prev) => [...prev, comment]);
}, []);
const pinComment = useCallback((id: string | undefined) => {
const comment = comments.find((comment) => comment.id === id);
if (comment) {
setPinnedComments((prev) => [...prev, { ...comment, pinned: true }]);
setComments((prev) => prev.filter((comment) => comment.id !== id));
}
}, [comments]);
const unpinComment = useCallback((id: string | undefined) => {
const comment = pinnedComments.find((comment) => comment.id === id);
if (comment) {
setComments((prev) => [...prev, { ...comment, pinned: false }]);
setPinnedComments((prev) => prev.filter((comment) => comment.id !== id));
}
}, [pinnedComments]);
useEffect(() => {
if (status === LiveStatus.ONGOING && !clientRef.current) {
socketRef.current = new SockJS("http://localhost:8080/ws");
if (!clientRef.current) {
socketRef.current = new SockJS(WEB_SOCKET_URL);
clientRef.current = Stomp.over(socketRef.current);
clientRef.current.connect({}, () => {
console.log("Connected to LiveWebSocket");
clientRef.current.subscribe(`/topic/messages/${id}`, (message: any) => {
const comment: ILiveComment = JSON.parse(message.body);
setComments((prev) => [...prev, comment]);
switch (comment.type) {
case "DELETE":
deleteComment(comment.id);
break;
case "CHAT":
addComment(comment);
break;
case "PIN":
pinComment(comment.id);
break;
case "UNPIN":
unpinComment(comment.id);
break;
default:
break;
}
});
});
}
......@@ -42,9 +99,9 @@ const useLiveComments = ({ id, status }: ILive): ILiveComment[] => {
clientRef.current = null;
}
};
}, [id, status]);
}, [id, status, deleteComment, addComment, pinComment, unpinComment]);
return comments;
return { comments, pinnedComments };
};
export default useLiveComments;
\ No newline at end of file
export default useLiveComments;
import { AllLivesAnalytics, ILive, ILiveComment, ILiveItem, ILiveProduct, ILiveStatistic, ILiveStatisticsSummary, LiveStatus } from "@/shared/types/live";
import { _lives, generateStatisticsForLives } from "../_mock";
import { generateStatisticsForLives } from "../_mock";
import useSWR, { mutate } from 'swr';
import { useMemo } from 'react';
import axiosInstance, { fetcher, endpoints } from './server';
......@@ -10,25 +10,14 @@ const options = {
revalidateOnReconnect: false,
};
/////////////////////////////////////Live API/////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////
export async function addLive(data: any) {
const formData = {
title: data.title,
chatName: data.chatName,
description: data.description,
startDate: data.startDate,
keyWord: data.keyWord,
staging: data.staging.toString(),
enableChat: data.enableChat.toString(),
enableLive: data.enableLive.toString(),
videoLink: "dede",
...data,
status: LiveStatus.COMING,
image: "https://dailystar.com.au/wp-content/uploads/2015/12/Mode-presents-100-years-Of-Mens-New-Years-Eve-Fashion.png",
videoFormat: data.videoFormat,
products: data.products,
}
const response = await axiosInstance.post('/live', formData);
......@@ -39,8 +28,8 @@ export async function addLive(data: any) {
export async function editLive(liveId: string, data: any) {
try {
const response = await axiosInstance.put(`${endpoints.live.stream}/${liveId}`, {...data, status:LiveStatus.COMING});
const newData = await response.data;
const response = await axiosInstance.put(`${endpoints.live.stream}/${liveId}`, { ...data, status: LiveStatus.COMING });
const newData = response.data;
mutate([endpoints.live.stream, { params: [] }], (lives: ILiveItem[] = []) => {
const index = lives.findIndex((live) => live.id === liveId);
if (index > -1) {
......@@ -60,21 +49,19 @@ export function useGetLives() {
const { data, isLoading, error, isValidating } = useSWR<ILiveItem[]>(URL, fetcher, options);
const memoizedValue = useMemo(
return useMemo(
() => ({
livesData: (data as ILiveItem[]) || [],
livesData: data || [],
livesLoading: isLoading,
livesError: error,
livesValidating: isValidating,
}),
[data, error, isLoading, isValidating]
);
return memoizedValue;
}
export function useGetLive(liveId: string) {
const URL = [`${endpoints.live.stream}/${liveId}`]
const URL = [`${endpoints.live.stream}/${liveId}`];
const { data, isLoading, error } = useSWR(URL, fetcher, options);
return { liveData: data as ILive, liveIsLoading: isLoading, liveError: error };
}
......@@ -84,7 +71,7 @@ export function useGetLive(liveId: string) {
export async function getRealTimeStats(id: string): Promise<ILiveStatistic> {
const response = await axiosInstance.get<ILiveStatistic>(endpoints.live.stats.realTime(id));
return response.data ;
return response.data;
}
export async function getLiveStatistics(liveId: string) {
......@@ -92,88 +79,70 @@ export async function getLiveStatistics(liveId: string) {
return response.data;
}
export async function getLiveStatisticsForAllLives() {
const response = await axiosInstance.get<AllLivesAnalytics>(endpoints.live.stats.summaryAll);
return response.data;
}
export function useGetLivesStatistic(startDate: Date, endDate: Date, lives: string[]) {
const memoizedValue = useMemo(
() => (
{
statsLoading: false,
statsError: false,
statsValidating: false,
statsData: generateStatisticsForLives(2)
}
),
return useMemo(
() => ({
statsLoading: false,
statsError: false,
statsValidating: false,
statsData: generateStatisticsForLives(2),
}),
[startDate, endDate, lives]
);
return memoizedValue;
}
/////////////////////////////////Live Comment API/wwwwwwww///////////////////////////////
/////////////////////////////////Live Comment API/////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////
export async function addCommentLive(commentData: ILiveComment) {
const response = await axiosInstance.post(endpoints.comments.add, commentData);
return response.data as ILiveComment;
}
export function useGetCommentsLive(liveId: string) {
const URL = [endpoints.comments.get(liveId)];
const { data, isLoading, error, isValidating } = useSWR<ILiveComment[]>(URL, fetcher, options);
const { data, isLoading, error, isValidating } = useSWR(URL, fetcher, options);
const memoizedValue = useMemo(
return useMemo(
() => ({
commentsData: (data as ILiveComment[]) || [],
commentsData: data || [],
commentsLoading: isLoading,
commentsError: error,
commentsValidating: isValidating,
}),
[data, error, isLoading, isValidating]
);
return memoizedValue;
}
export async function deleteCommentLive(commentId: string | undefined, liveId: string | undefined) {
await axiosInstance.delete(endpoints.comments.delete(commentId, liveId));
}
export async function pinCommentLive(commentId: string | undefined, liveId: string | undefined) {
await axiosInstance.post(endpoints.comments.pin(commentId, liveId));
}
export async function deleteCommentLive(commentId: string) {
export async function unpinCommentLive(commentId: string | undefined, liveId: string | undefined) {
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}`, {
startTime,
endTime
});
await axiosInstance.post(`${endpoints.live.products}/${productId}`, { startTime, endTime });
mutate(
[endpoints.live.stream + '/' + liveId + '/products'],
[`${endpoints.live.stream}/${liveId}/products`],
(products: ILiveProduct[] = []) => {
const index = products.findIndex((product) => product.id === productId);
if (index > -1) {
const updatedProduct = {
...products[index],
startTime: startTime,
endTime: endTime,
enabled: true,
};
products[index] = updatedProduct;
products[index] = { ...products[index], startTime, endTime, enabled: true };
}
return [...products];
},
......@@ -183,84 +152,95 @@ export async function addProductLive(liveId: string, productId: string, startTim
export function useGetProducts() {
const URL = [`${endpoints.product}?size=100&page=1`];
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}`);
mutate([endpoints.live.stream + '/' + liveId + '/products'], (products: ILiveProduct[] = []) => {
const index = products.findIndex((product) => product.id === productId);
if (index > -1) {
products.splice(index, 1);
}
return [...products];
}, true);
mutate(
[`${endpoints.live.stream}/${liveId}/products`],
(products: ILiveProduct[] = []) => {
const index = products.findIndex((product) => product.id === productId);
if (index > -1) {
products.splice(index, 1);
}
return [...products];
},
true
);
}
export function getProductsLive(liveId: string) {
const URL = [endpoints.live.stream + '/' + liveId + '/products'];
const URL = [`${endpoints.live.stream}/${liveId}/products`];
const { data, isLoading, error, isValidating } = useSWR(URL, fetcher, options);
const memoizedValue = useMemo(
return useMemo(
() => ({
products: (data as ILiveProduct[]) || [],
products: data || [],
productsLoading: isLoading,
productsError: error,
productsValidating: isValidating,
}),
[data, error, isLoading, isValidating]
);
return memoizedValue;
}
/////////////////////////////////Live State API////////////////////////////////
//////////////////////////////////////////////////////////////////////////////
export function stopLive(liveId: string) {
export async function stopLive(liveId: string) {
const URL = `${endpoints.live.stream}/${liveId}/stop`;
axiosInstance.post(URL);
mutate([`${endpoints.live.stream}/${liveId}`], (live: ILive | null = null) => {
if (live) {
live.status = LiveStatus.REPLAY
}
return live;
}, true);
mutate([endpoints.live.stream], (lives: ILiveItem[] = []) => {
const index = lives.findIndex((live) => live.id === liveId);
if (index > -1) {
lives[index].status = LiveStatus.REPLAY;
}
return [...lives];
}, true);
await axiosInstance.post(URL);
mutate(
[`${endpoints.live.stream}/${liveId}`],
(live: ILive | null = null) => {
if (live) {
live.status = LiveStatus.REPLAY;
}
return live;
},
true
);
mutate(
[endpoints.live.stream],
(lives: ILiveItem[] = []) => {
const index = lives.findIndex((live) => live.id === liveId);
if (index > -1) {
lives[index].status = LiveStatus.REPLAY;
}
return [...lives];
},
true
);
}
export function startLive(liveId: string) {
export async function startLive(liveId: string) {
const URL = `${endpoints.live.stream}/${liveId}/start`;
axiosInstance.post(URL);
mutate([`${endpoints.live.stream}/${liveId}`], (live: ILive | null = null) => {
if (live) {
live.status = LiveStatus.ONGOING
live.startedAt = new Date();
}
return live;
}, true);
mutate([endpoints.live.stream], (lives: ILiveItem[] = []) => {
const index = lives.findIndex((live) => live.id === liveId);
if (index > -1) {
lives[index].status = LiveStatus.ONGOING;
}
return [...lives];
}, true);
}
\ No newline at end of file
console.log('url', URL)
await axiosInstance.post(URL);
mutate(
[`${endpoints.live.stream}/${liveId}`],
(live: ILive | null = null) => {
if (live) {
live.status = LiveStatus.ONGOING;
live.startedAt = new Date();
}
return live;
},
true
);
mutate(
[endpoints.live.stream],
(lives: ILiveItem[] = []) => {
const index = lives.findIndex((live) => live.id === liveId);
if (index > -1) {
lives[index].status = LiveStatus.ONGOING;
}
return [...lives];
},
true
);
}
......@@ -3,7 +3,7 @@ import { comment } from 'postcss';
import { shippingClassesList } from '../_mock/_shipping';
export const HOST_API = 'http://localhost:8081';
export const HOST_API = process.env.HOST_API_URL ||'http://localhost:8080';
export const axiosInstance = axios.create({ baseURL: HOST_API });
......@@ -27,10 +27,13 @@ export const fetcher = async (args: string | [string, AxiosRequestConfig]) => {
export const endpoints = {
comments: {
add: "/chat/message",
get: (liveId:string) =>`/chat/stream/${liveId}`,
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}`,
},
product: "/product",
live : {
product: "/products",
live: {
stream: '/live',
products : '/live-product',
stats : {
......
......@@ -9,9 +9,10 @@ type Props = {
onToggle: (event: React.ChangeEvent<HTMLInputElement>) => void;
title: string;
description: string;
disabled?: boolean;
};
export default function OptionItem({title, description, enabled, onToggle }:Props){
export default function OptionItem({title, description, enabled, onToggle, disabled=false }:Props){
return (
<Card sx={{p:2, }}>
<Typography variant="subtitle1" gutterBottom>
......@@ -20,7 +21,7 @@ export default function OptionItem({title, description, enabled, onToggle }:Prop
<Typography variant="body2" gutterBottom>
{description}
</Typography>
<Switch checked={enabled} onChange={onToggle} />
<Switch disabled={disabled} checked={enabled} onChange={onToggle} />
</Card>
);
};
......
......@@ -29,6 +29,7 @@ type Props = {
addProduct: (product: ILiveProduct) => void;
removeProduct: (product: ILiveProduct) => void;
selectedProducts: ILiveProduct[];
disabled?: boolean;
};
const ProductCard = ({ product, isChecked, handleToggle }: any) => (
......@@ -56,7 +57,7 @@ const ProductCard = ({ product, isChecked, handleToggle }: any) => (
</Card>
);
export default function AddProductPopup({ addProduct, removeProduct, selectedProducts }: Props) {
export default function AddProductPopup({ addProduct, removeProduct, selectedProducts, disabled = false }: Props) {
const [open, setOpen] = React.useState(false);
const [searchTerm, setSearchTerm] = React.useState('');
......@@ -87,13 +88,13 @@ export default function AddProductPopup({ addProduct, removeProduct, selectedPro
return (
<>
<Button
{!disabled && <Button
onClick={handleOpen}
variant="contained"
startIcon={<Iconify icon="mingcute:add-line" />}
>
Ajouter produits
</Button>
</Button>}
<Modal
open={open}
onClose={handleClose}
......
"use client"
"use client";
import * as Yup from 'yup';
import { Controller, useForm } from 'react-hook-form';
......@@ -12,10 +12,7 @@ import Typography from '@mui/material/Typography';
import { useResponsive } from '@/hooks/use-responsive';
import { useSnackbar } from '@/components/snackbar';
import FormProvider, {
RHFTextField,
RHFAutocomplete,
} from '@/components/hook-form';
import FormProvider, { RHFTextField, RHFAutocomplete } from '@/components/hook-form';
import { _tags } from '@/shared/_mock';
import { TimePicker } from '@mui/x-date-pickers/TimePicker';
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
......@@ -28,16 +25,12 @@ import CustomBreadcrumbs from '@/components/custom-breadcrumbs';
import { paths } from '@/routes/paths';
import { useSettingsContext } from '@/shared/components/settings';
import { Container } from '@mui/material';
import { ILive, ILiveProduct } from '@/shared/types/live';
import { ILive, ILiveProduct, LiveStatus } from '@/shared/types/live';
import { addLive, editLive, getProductsLive } from '@/shared/api/live';
import { useEffect, useMemo, useState } from 'react';
import { useLiveData } from '@/contexts/live-stats';
export default function EditLiveView() {
const liveData = useLiveData();
const mdUp = useResponsive('up', 'md');
......@@ -78,14 +71,11 @@ export default function EditLiveView() {
videoFormat: liveData.videoFormat,
}), [liveData]);
const methods = useForm({
resolver: yupResolver(NewLiveSchema),
defaultValues,
});
const {
watch,
control,
......@@ -98,15 +88,19 @@ export default function EditLiveView() {
const addProduct = (product: ILiveProduct) => {
setLiveProducts((prev) => [...prev, product]);
}
};
const removeProduct = (product: ILiveProduct) => {
setLiveProducts((prev) => prev.filter((p) => p.id !== product.id));
}
};
const onSubmit = handleSubmit((data) => {
const formData = { ...data, image: image as File, staging: false, products: products.map((product) => product.id) }
const formData = {
...data,
image: image as File,
staging: false,
products: products.map((product: ILiveProduct) => product.id)
};
editLive(liveData.id, formData)
.then(() => {
enqueueSnackbar('Live Updated successfully', { variant: 'success' });
......@@ -171,9 +165,6 @@ export default function EditLiveView() {
</>
);
const renderDetails = (
<>
{mdUp && (
......@@ -190,17 +181,11 @@ export default function EditLiveView() {
<Grid xs={12} md={8}>
<Card>
{!mdUp && <CardHeader title="Details" />}
<Stack spacing={3} sx={{ p: 3 }}>
<RHFTextField name="title" label="titre du Live" />
<RHFTextField name="description" label="Description" placeholder="Description..." multiline rows={4} />
<RHFTextField name="chatName" label="Nom affiché dans le chat" multiline />
<Stack
spacing={2}
direction={{ xs: 'column', sm: 'row' }}
>
<RHFTextField name="title" label="titre du Live" disabled={liveData.status !== LiveStatus.COMING} />
<RHFTextField name="description" label="Description" placeholder="Description..." multiline rows={4} disabled={liveData.status !== LiveStatus.COMING} />
<RHFTextField name="chatName" label="Nom affiché dans le chat" multiline disabled={liveData.status !== LiveStatus.COMING} />
<Stack spacing={2} direction={{ xs: 'column', sm: 'row' }}>
<Controller
name="startDate"
control={control}
......@@ -218,10 +203,10 @@ export default function EditLiveView() {
helperText: error?.message,
},
}}
disabled={liveData.status !== LiveStatus.COMING}
/>
)}
/>
<Controller
name="startDate"
control={control}
......@@ -239,6 +224,7 @@ export default function EditLiveView() {
helperText: error?.message,
},
}}
disabled={liveData.status !== LiveStatus.COMING}
/>
)}
/>
......@@ -269,6 +255,7 @@ export default function EditLiveView() {
/>
))
}
disabled={liveData.status !== LiveStatus.COMING}
/>
</Stack>
</Card>
......@@ -281,24 +268,31 @@ export default function EditLiveView() {
title: "Source de vidéo externe",
description: "Génère un lien pour votre logiciel de diffusion. Le lien sera disponible une fois le live crée",
enabled: values.externalVideo,
onToggle: () => setValue('externalVideo', !values.externalVideo as never)
}, {
onToggle: () => setValue('externalVideo', !values.externalVideo as never),
disabled: liveData.status !== LiveStatus.COMING,
},
{
title: "Staging",
description: "Ce live sera uniquement visible sur votre site de staging",
enabled: values.staging,
onToggle: () => setValue("staging", !values.staging as never)
}, {
onToggle: () => setValue("staging", !values.staging as never),
disabled: liveData.status !== LiveStatus.COMING,
},
{
title: "Désactiver le chat",
description: "Le chat sera accessible aux modérateur uniquement",
enabled: values.enableChat,
onToggle: () => setValue('enableChat', !values.enableChat as never)
}, {
onToggle: () => setValue('enableChat', !values.enableChat as never),
disabled: liveData.status !== LiveStatus.COMING,
},
{
title: "Cacher ce live",
description: "Ce live ne sera pas visible sur votre site",
enabled: values.enableLive,
onToggle: () => setValue('enableLive', !values.enableLive as never)
}
]
onToggle: () => setValue('enableLive', !values.enableLive as never),
disabled: liveData.status !== LiveStatus.COMING,
},
];
const renderOptions = (
<>
......@@ -312,7 +306,6 @@ export default function EditLiveView() {
</Typography>
</Grid>
)}
<Grid xs={12} md={8}>
<Card>
{!mdUp && <CardHeader title="Options" />}
......@@ -324,6 +317,7 @@ export default function EditLiveView() {
description={item.description}
onToggle={item.onToggle}
enabled={item.enabled}
disabled={item.disabled}
/>
</Grid>
))}
......@@ -345,17 +339,16 @@ export default function EditLiveView() {
</Typography>
</Grid>
)}
<Grid xs={12} md={8}>
<Card>
{!mdUp && <CardHeader title="Products" />}
<Stack spacing={2} sx={{ p: 3 }}>
<AddProduct addProduct={addProduct} removeProduct={removeProduct} selectedProducts={liveProducts} />
<AddProduct addProduct={addProduct} removeProduct={removeProduct} selectedProducts={liveProducts} disabled={liveData.status !== LiveStatus.COMING} />
</Stack>
</Card>
</Grid>
</>
)
);
const renderVisuels = (
<>
......@@ -369,43 +362,43 @@ export default function EditLiveView() {
</Typography>
</Grid>
)}
<Grid xs={12} md={8}>
<Card>
{!mdUp && <CardHeader title="Visuel" />}
<Stack spacing={2} sx={{ p: 3 }}>
<Typography variant="subtitle1">Format vidéo</Typography>
<Controller
name="videoFormat"
control={control}
render={({ field }) => (
<VideoFormatsField field={field} />
<VideoFormatsField field={field} disabled={liveData.status !== LiveStatus.COMING} />
)}
/>
</Stack>
<Stack spacing={2} sx={{ p: 3 }}>
<Typography variant="subtitle1">Photo de couverture</Typography>
<CustumImageCrop image={image as File} setImage={(image: File) => setImage(image)} />
<CustumImageCrop image={image as File} setImage={(image: File) => setImage(image)} disabled={liveData.status !== LiveStatus.COMING} />
</Stack>
</Card>
</Grid>
</>
);
const renderActions = (
<>
{mdUp && <Grid md={4} />}
<Grid xs={12} md={8} sx={{ display: 'flex', justifyContent: 'end', py: 4 }}>
<LoadingButton
type="submit"
variant="contained"
size="large"
loading={isSubmitting}
sx={{ ml: 2 }}
>Enregistrer</LoadingButton>
{liveData.status === LiveStatus.COMING && (
<LoadingButton
type="submit"
variant="contained"
size="large"
loading={isSubmitting}
sx={{ ml: 2 }}
>
Enregistrer
</LoadingButton>
)}
</Grid>
</>
);
......@@ -415,19 +408,10 @@ export default function EditLiveView() {
<CustomBreadcrumbs
heading={`Modifier live #${liveData.id}`}
links={[
{
name: 'dashboard',
href: paths.dashboard.root,
},
{
name: 'lives',
href: paths.dashboard.admin.live.all_lives,
},
{
name: 'création des live',
},
{ name: 'dashboard', href: paths.dashboard.root },
{ name: 'lives', href: paths.dashboard.live.all_lives },
{ name: 'création des live' },
]}
sx={{ mb: { xs: 3, md: 5 } }}
/>
<FormProvider methods={methods} onSubmit={onSubmit}>
......
......@@ -16,7 +16,7 @@ const videoFormats = [
export const VideoFormatsField = ({ field }: { field: any }) => {
export const VideoFormatsField = ({ field, disabled = false }: { field: any, disabled?: boolean }) => {
return (
<Box gap={2} display="grid" gridTemplateColumns="repeat(2, 1fr)">
{videoFormats.map((item) => (
......@@ -24,7 +24,7 @@ export const VideoFormatsField = ({ field }: { field: any }) => {
component={ButtonBase}
variant="outlined"
key={item.name}
onClick={() => field.onChange(item.name)}
onClick={() => { !disabled && field.onChange(item.name) }}
sx={{
p: 2.5,
borderRadius: 1,
......
......@@ -3,9 +3,11 @@ import { useState } from 'react';
import { useResponsive } from '@/hooks';
import InputEmoji from 'react-input-emoji'
import Iconify from '@/components/iconify';
import { addCommentLive } from '@/shared/api/live';
import { addCommentLive, unpinCommentLive } from '@/shared/api/live';
import { Card, IconButton, Paper, Typography, useTheme } from '@mui/material';
import { ILiveComment } from '@/shared/types/live';
import { useLiveData } from '@/contexts/live-stats';
import useLiveComments from '@/hooks/use-live-comments';
type Props = {
......@@ -18,13 +20,11 @@ export default function CommentsInput({
liveId, parentComment, changeParentComment
}: Props) {
const [comment, setComment] = useState('');
const liveData = useLiveData();
const { pinnedComments } = useLiveComments(liveData);
const theme = useTheme();
const inputStyle = parentComment ? {
background: theme.palette.background.default,
} : {};
const handleSendComment = async () => {
try {
......@@ -56,58 +56,90 @@ export default function CommentsInput({
alignItems: 'center',
justifyContent: 'center',
width: '98%',
gap: 4,
left: '50%',
borderRadius: '20px',
...inputStyle,
background: 'transparent',
borderRadius: '40px',
position: 'absolute',
transform: 'translateX(-50%)',
bottom: 1,
paddingRight: 4,
}}>
{parentComment && <Paper style={{
{pinnedComments.length != 0 && <Paper style={{
display: 'flex',
flexDirection: 'column',
background: theme.palette.background.default,
gap:3,
alignItems: 'center',
justifyContent: 'start',
gap: 4,
justifyContent: 'center',
borderRadius: '10px',
width: '100%',
padding: 10,
marginTop: 2,
borderRadius: 2,
background: 'transparent',
}}>
<Card sx={{ position: "relative", width: "100%", display: "flex", alignItems: "center",wordWrap: 'break-word', borderRadius: "40px", gap: 2, paddingX: 1, }}>
<Typography variant="body2" sx={{ color: 'gray' }}>
{parentComment?.senderName}:
</Typography>
<Typography variant="body1" >
{parentComment?.content}
</Typography>
<IconButton onClick={() => changeParentComment(null)} sx={{ position: "absolute", right: 0, padding: 1 }} size="small">
<Iconify style={{ color: 'gray', marginBottom: 2 }} width={20} icon="ic:sharp-cancel" />
</IconButton>
</Card>
{pinnedComments.map((comment: ILiveComment) => (
<Card sx={{ position: "relative", width: "100%", display: "flex", alignItems: "center", wordWrap: 'break-word', borderRadius: "40px", gap: 2, paddingX: 1, }}>
<Typography variant="body2" sx={{ color: 'gray' }}>
{comment?.senderName}:
</Typography>
<Typography variant="body1" >
{comment?.content}
</Typography>
<IconButton onClick={async () => await unpinCommentLive(comment.id, 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>
))}
</Paper>}
<Paper style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '100%',
padding: 0,
background: 'transparent',
width: "100%",
background: theme.palette.background.default,
borderRadius: '10px',
}}>
<InputEmoji
value={comment}
cleanOnEnter
onEnter={handleSendComment}
onChange={(value) => setComment(value)}
placeholder="Type a message"
height={20}
/>
{parentComment && <Paper style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'start',
gap: 4,
width: '100%',
padding: 10,
marginTop: 2,
borderRadius: 2,
background: 'transparent',
}}>
<Card sx={{ position: "relative", width: "100%", display: "flex", alignItems: "center", wordWrap: 'break-word', borderRadius: "40px", gap: 2, paddingX: 1, }}>
<Typography variant="body2" sx={{ color: 'gray' }}>
{parentComment?.senderName}:
</Typography>
<Typography variant="body1" >
{parentComment?.content}
</Typography>
<IconButton onClick={() => changeParentComment(null)} sx={{ position: "absolute", right: 0, padding: 1 }} size="small">
<Iconify style={{ color: 'gray', marginBottom: 2 }} width={20} icon="ic:sharp-cancel" />
</IconButton>
</Card>
</Paper>}
<Paper style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '100%',
padding: 0,
background: 'transparent',
}}>
<InputEmoji
value={comment}
cleanOnEnter
onEnter={handleSendComment}
onChange={(value) => setComment(value)}
placeholder="Type a message"
height={20}
/>
<Iconify onClick={handleSendComment} style={{ height: 30, width: 30, color: 'gray', marginBottom: 2 }} icon="iconamoon:send-fill" />
<Iconify onClick={handleSendComment} style={{ height: 30, width: 30, color: 'gray', marginBottom: 2 }} icon="iconamoon:send-fill" />
</Paper>
</Paper>
</Paper>
);
......
......@@ -11,6 +11,7 @@ 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';
......@@ -26,18 +27,19 @@ export default function LiveCommentItem({ comment, changeParentComment }: Props)
const theme = useTheme();
const mdUp = useResponsive('up', 'md');
const bgStyle = mdUp ? {
...bgGradient({
// ...bgGradient({
direction: '135deg',
startColor: alpha(theme.palette['primary'].light, 0.2),
endColor: alpha(theme.palette['primary'].main, 0.2),
}),
// direction: '135deg',
// startColor: alpha(theme.palette['primary'].light, 0.2),
// endColor: alpha(theme.palette['primary'].main, 0.2),
// }),
backgroundColor: alpha(theme.palette['primary'].main, 0.2),
width: "100%",
p: .5,
fontSize: theme.typography.pxToRem(12),
} : {
fontSize: theme.typography.pxToRem(12),
backgroundColor: 'rgb(0 0 0 / 50%)',
// backgroundColor: 'rgb(0 0 0 / 50%)',
color: 'white',
borderColor: 'gray',
textDecorationColor: 'gray',
......@@ -45,6 +47,9 @@ export default function LiveCommentItem({ comment, changeParentComment }: Props)
p: .5,
};
const renderInfo = (
<Typography
noWrap
......@@ -54,10 +59,8 @@ export default function LiveCommentItem({ comment, changeParentComment }: Props)
color: 'grey'
}}
>
{`${senderName},`} &nbsp;
{mdUp && formatDistanceToNowStrict(new Date(), {
addSuffix: true,
})}
{senderName}
</Typography>
);
......@@ -83,12 +86,12 @@ export default function LiveCommentItem({ comment, changeParentComment }: Props)
pl: 1,
mb: 1,
width: '100%',
flexWrap: 'wrap',
flexWrap: 'wrap',
}}
>
<Typography
variant="caption"
sx={{ color: 'primary.main', flexShrink: 0 }}
sx={{ color: 'primary.main', flexShrink: 0 }}
>
{parent.senderName}
</Typography>
......@@ -99,9 +102,9 @@ export default function LiveCommentItem({ comment, changeParentComment }: Props)
marginLeft: 1,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'normal',
whiteSpace: 'normal',
flexGrow: 1,
wordBreak: 'break-word',
wordBreak: 'break-word',
}}
>
{parent.content}
......@@ -134,10 +137,10 @@ export default function LiveCommentItem({ comment, changeParentComment }: Props)
<IconButton onClick={() => changeParentComment(comment)} size="small">
<Iconify icon="solar:reply-bold" width={16} />
</IconButton>
<IconButton size="small">
<IconButton onClick={() => deleteCommentLive(comment.id, comment.liveStream)} size="small">
<Iconify icon="material-symbols:delete" width={16} />
</IconButton>
<IconButton size="small">
<IconButton onClick={() => pinCommentLive(comment.id, comment.liveStream)} size="small">
<Iconify icon="mdi:pin" width={16} />
</IconButton>
</Stack>
......
......@@ -8,9 +8,9 @@ import { ILiveComment } from '@/shared/types/live';
import useLiveComments from '@/hooks/use-live-comments';
import { useLiveData } from '@/contexts/live-stats';
export default function CommentsList({ changeParentComment }: { changeParentComment: (comment: ILiveComment | null) => void }){
export default function CommentsList({ changeParentComment }: { changeParentComment: (comment: ILiveComment | null) => void }) {
const liveData = useLiveData();
const comments = useLiveComments(liveData);
const { comments, pinnedComments } = useLiveComments(liveData);
const latestCommentRef = useRef<HTMLDivElement | null>(null);
const scrollbarRef = useRef<HTMLDivElement>();
......@@ -22,13 +22,26 @@ export default function CommentsList({ changeParentComment }: { changeParentComm
return (
<Scrollbar sx={{ px: 2, py: 5, height: 1, color: 'transparent' }}>
<Box>
{comments.map((comment, index) => (
<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}
/>
))}
<div ref={latestCommentRef} />
......
......@@ -24,7 +24,7 @@ export default function CommentSection({ liveId }: Props) {
</Grid>
) : (
<>
<Box sx={{ height: 500, zIndex: 2, overflow: 'hidden', position: 'absolute', width: '30%', left: 2, minWidth: 200, bottom: 0, paddingBottom: 200 }}>
<Box sx={{ height: 500, zIndex: 4, overflow: 'hidden', position: 'absolute', width: '30%', left: 2, minWidth: 200, bottom: 0, paddingBottom: 200 }}>
<CommentsList changeParentComment={setParentComment} />
</Box>
......
import { useCallback, useEffect, useState } from "react";
import { useCallback, useEffect } from "react";
import { Button, IconButton, Paper, Stack, TextField, Typography } from "@mui/material";
import Iconify from "@/components/iconify";
import Label from "@/shared/components/label";
......@@ -7,10 +7,8 @@ import { paths } from "@/routes/paths";
import RouterLink from "@/routes/router-link";
import { fDateTime } from "@/utils/format-time";
import { LiveStatus } from "@/shared/types/live";
import { getRealTimeStats, startLive, stopLive } from "@/shared/api/live";
import { startLive, stopLive } from "@/shared/api/live";
import useTimer from "@/hooks/use-timer";
import usePolling from "@/hooks/use-polling";
import { useLiveStatistics } from "@/contexts/live-stats";
import { useGetLiveNumberOfviewers } from "@/contexts/live-stats";
import { ConfirmDialog } from "@/shared/components/custom-dialog";
......@@ -23,7 +21,7 @@ interface HeadSectionProps {
export default function HeadSection({ liveId, startedAt, status }: HeadSectionProps) {
const smUp = useResponsive('up', 'sm');
const [elapsedTime, handleStart] = useTimer(new Date());
const [elapsedTime, handleStart] = useTimer(startedAt ? new Date(startedAt) : new Date());
const numberOfViewers = useGetLiveNumberOfviewers();
useEffect(() => {
......@@ -40,6 +38,17 @@ export default function HeadSection({ liveId, startedAt, status }: HeadSectionPr
router.push(paths.dashboard.admin.live.root);
}, [router]);
const handleStartLive = async () => {
await startLive(liveId);
handleStart();
startConfirm.onFalse();
};
const handleStopLive = async () => {
await stopLive(liveId);
stopConfirm.onFalse();
};
return (
<>
<Paper
......@@ -62,7 +71,7 @@ export default function HeadSection({ liveId, startedAt, status }: HeadSectionPr
<Stack spacing={0.5}>
<Stack spacing={1} direction="row" alignItems="center">
<Typography variant="h4"> Live #{liveId} </Typography>
<Typography variant="h4">Live #{liveId}</Typography>
<Label variant="soft" color="success">
{status}
</Label>
......@@ -81,7 +90,7 @@ export default function HeadSection({ liveId, startedAt, status }: HeadSectionPr
direction="row"
height="100%"
>
{status == LiveStatus.ONGOING && (
{status === LiveStatus.ONGOING && (
<>
<TextField
label="Spectateur"
......@@ -113,7 +122,7 @@ export default function HeadSection({ liveId, startedAt, status }: HeadSectionPr
)}
{status === LiveStatus.COMING && (
<Button
onClick={() => { startConfirm.onTrue(); }}
onClick={startConfirm.onTrue}
variant="contained"
startIcon={<Iconify icon="material-symbols:not-started-rounded" />}
>
......@@ -122,7 +131,7 @@ export default function HeadSection({ liveId, startedAt, status }: HeadSectionPr
)}
{status === LiveStatus.ONGOING && (
<Button
onClick={() => { stopConfirm.onTrue(); }}
onClick={stopConfirm.onTrue}
variant="contained"
sx={{ color: 'error.main' }}
startIcon={<Iconify icon="fluent:record-stop-24-regular" />}
......@@ -144,9 +153,9 @@ export default function HeadSection({ liveId, startedAt, status }: HeadSectionPr
open={stopConfirm.value}
onClose={stopConfirm.onFalse}
title="Arrêter"
content="voulez vous arréter le live?"
content="Voulez-vous arrêter le live?"
action={
<Button variant="contained" color="error" onClick={() => { stopLive(liveId); stopConfirm.onFalse(); }}>
<Button variant="contained" color="error" onClick={handleStopLive}>
Arrêter
</Button>
}
......@@ -155,9 +164,9 @@ export default function HeadSection({ liveId, startedAt, status }: HeadSectionPr
open={startConfirm.value}
onClose={startConfirm.onFalse}
title="Démarrer"
content="voulez vous démarrer le live?"
content="Voulez-vous démarrer le live?"
action={
<Button variant="contained" color="success" onClick={() => { startLive(liveId); handleStart(); startConfirm.onFalse();}}>
<Button variant="contained" color="success" onClick={handleStartLive}>
Démarrer
</Button>
}
......@@ -165,4 +174,3 @@ export default function HeadSection({ liveId, startedAt, status }: HeadSectionPr
</>
);
}
......@@ -7,16 +7,17 @@ import useScroll from "@/hooks/use-scroll";
import { useState } from "react";
import { getProductsLive } from "@/shared/api/live";
import Iconify from "@/components/iconify";
import { ILiveProduct } from "@/shared/types/live";
type Props = {
liveId: string;
}
export default function ProductsSection({ liveId }: Props) {
export default function ProductsSection({ liveId }: Props) {
const lgUp = useResponsive('up', 'lg');
const { products } = getProductsLive(liveId);
const { products } = getProductsLive(liveId);
const { endRef: productsEndRef } = useScroll(products);
const [open, setOpen] = useState(false)
......@@ -45,7 +46,7 @@ export default function ProductsSection({ liveId }: Props) {
/>
<Card sx={{ height: 532 }}>
<Scrollbar ref={productsEndRef} sx={{ px: 2, py: 5, height: 1 }}>
{products.map((product) => (
{products.map((product: ILiveProduct) => (
<ProductCard enabled={false} key={product.id} liveId={liveId} {...product} />
))}
</Scrollbar>
......@@ -53,7 +54,7 @@ export default function ProductsSection({ liveId }: Props) {
</Grid>
) : (
<>
<Button variant="outlined" onClick={handleOpen} sx={{ position: 'absolute', color: 'white', zIndex: 2, top: 20, left: 20 , height:20, maxWidth:20, borderRadius: 2, border:2 , padding:1 }}>produits</Button>
<Button variant="outlined" onClick={handleOpen} sx={{ position: 'absolute', color: 'white', zIndex: 2, top: 20, left: 20 }}>Produits</Button>
<Modal
open={open}
onClose={handleClose}
......@@ -87,7 +88,7 @@ export default function ProductsSection({ liveId }: Props) {
/>
<Scrollbar ref={productsEndRef} sx={{ px: 2, py: 5, height: 1, overflowY: 'auto' }}>
<Box>
{products.map((product) => (
{products.map((product: ILiveProduct) => (
<ProductCard enabled={false} liveId={liveId} {...product} />
))}
</Box>
......
......@@ -53,18 +53,21 @@ export type ILiveComment = {
id?: string;
content: string;
type?: string;
pinned ?: boolean;
senderName: string;
sender: string;
liveStream: string;
}
export type ILiveProduct = {
id: string;
title: string;
sku: string;
image: string;
price: number;
enabled?: boolean;
startTime?: number;
endTime?: number;
}
......
0% ou .
You are about to add 0 people to the discussion. Proceed with caution.
Terminez d'abord l'édition de ce message.
Veuillez vous inscrire ou vous pour commenter