0% found this document useful (0 votes)
2 views

import FuseLoading from

The document contains code for handling image uploads and managing lottery themes in a React application. It includes functions for validating image types, uploading images, and managing lottery theme data using React Query. Additionally, it features a table component for displaying lottery themes and a form for creating new themes, with error handling and state management implemented throughout.
Copyright
© © All Rights Reserved
Available Formats
Download as TXT, PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
2 views

import FuseLoading from

The document contains code for handling image uploads and managing lottery themes in a React application. It includes functions for validating image types, uploading images, and managing lottery theme data using React Query. Additionally, it features a table component for displaying lottery themes and a form for creating new themes, with error handling and state management implemented throughout.
Copyright
© © All Rights Reserved
Available Formats
Download as TXT, PDF, TXT or read online on Scribd
You are on page 1/ 16

const isValidImage = (file: File): boolean => {

const validTypes = ['image/jpeg', 'image/png', 'image/gif']; // Adjust as


needed
return validTypes.includes(file.type);
};

const handleImageUpload = async (file: File, type: 'heroBanner' |


'detailsImage') => {
if (!file) return;

if (!isValidImage(file)) {
setToastMessage(t('invalidImage'));
return;
}

try {
const uploadedUrl = await uploadImage({rawFile: file});
if (type === 'heroBanner') {
setUploadedImageUrlHero(uploadedUrl);
} else {
setUploadedImageUrlDetails(uploadedUrl);
}
} catch (error) {
setToastMessage(t('uploadError'));
// Log más detalles sobre el error
logger.error('Error uploading image:', error, {
file,
type,
});
}
};

onChange={async (e) => {


const file = e?.target?.files?.
[0];
if (!file) return;

const uploadedHeroUrl = await


uploadImage({
rawFile: file,
title: 'heroBannerImage',
});

setUploadedImageUrlHero(uploadedHeroUrl);
}}

+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+++++++++++++++++++++++++++++++++++++++++++++++++++

TABLELIST

import {useMemo, useState, useEffect} from 'react';


import {type MRT_ColumnDef} from 'material-react-table';
import DataTable from 'app/shared-components/data-table/DataTable';
import FuseLoading from '@fuse/core/FuseLoading';
import {Box, Button, Paper} from '@mui/material';
import Typography from '@mui/material/Typography';
import {useTranslation} from 'react-i18next';
//import _ from 'lodash';
import {fetchLotteryThemeByName} from '@lottob/graphql/queries/lottery-value-theme-
by-name';
import {useQuery, useQueryClient} from '@tanstack/react-query';
import NavLinkAdapter from '@fuse/core/NavLinkAdapter';
import {fetchLotteryById} from '@lottob/graphql/queries/lotteries-id';
export interface ILotteryThemeDatum {
lotteryId: string;
name: string;
desktop: {
heroBanner?: {
src: string | null;
title?: string;
};
detailsImage?: {
src: string | null;
title?: string;
};
};
mobile: {
heroBanner?: {
src: string | null;
title?: string;
};
detailsImage?: {
src: string | null;
title?: string;
};
};
}

function LotteryThemesTable() {
const {t, i18n} = useTranslation('lotteryThemePage');
const queryClient = useQueryClient();

const fetchTheme = async () => await fetchLotteryThemeByName({dto: {name:


'lb_lottery_themes'}});

const {data, isLoading} = useQuery({


queryKey: ['lotteryTheme'],
queryFn: fetchTheme,
select: (data) => {
// Parse values here
return JSON.parse(data.configuration.value) as ILotteryThemeDatum[];
},
});

const [lotteryThemes, setLotteryThemes] = useState<ILotteryThemeDatum[]>([]);

useEffect(() => {
if (data) {
// Process data asynchronously
const processData = async () => {
const processedValues = await Promise.all(
data.map(async (value) => {
const lotteryLabel = await fetchLotteryById({
dto: {lotteryId: value.lotteryId},
});
return {...value, name: lotteryLabel.lottery.label};
})
);
setLotteryThemes(processedValues);
};
processData();
}
}, [data]);

useEffect(() => {
const handleInvalidateOnFocus = () => {
queryClient.invalidateQueries({
queryKey: ['lotteryTheme'], // Clave para filtrar consultas
});
};

window.addEventListener('focus', handleInvalidateOnFocus);
return () => window.removeEventListener('focus', handleInvalidateOnFocus);
}, [queryClient]);

const columns = useMemo<MRT_ColumnDef<ILotteryThemeDatum>[]>(


() => [
{
accessorKey: 'id',
header: 'Id',
Cell: ({row}) => <Typography
color="primary">{row.original.lotteryId}</Typography>,
},
{
accessorKey: 'name',
header: t('name'),
Cell: ({row}) => <Typography
color="primary">{row.original.name}</Typography>,
},
{
accessorKey: 'actions',
header: t('actions'),
enableSorting: false,
Cell: ({row}) => (
<Box sx={{display: 'flex', gap: 1}}>
<Button variant="text" color="secondary"
sx={{textTransform: 'uppercase'}} onClick={() => null}>
{t('copyTheme')}
</Button>
<Button
variant="text"
color="secondary"
sx={{textTransform: 'uppercase'}}
component={NavLinkAdapter}
to={`edit/${row.original.lotteryId}`}
>
{t('updateTheme')}
</Button>
<Button variant="text" color="secondary"
sx={{textTransform: 'uppercase'}} onClick={() => null}>
{t('updateJSON')}
</Button>
</Box>
),
},
],
[i18n.language, t]
);

if (isLoading || !lotteryThemes) {
return <FuseLoading />;
}

return (
<Paper
className="flex flex-col flex-auto shadow-3 rounded-t-16 overflow-
hidden rounded-b-0 w-full h-full"
elevation={0}
>
<DataTable
initialState={{
density: 'spacious',
columnPinning: {
left: ['mrt-row-expand', 'mrt-row-select'],
right: ['mrt-row-actions'],
},
}}
data={lotteryThemes || []}
columns={columns}
enableRowSelection={false}
enableColumnDragging={false}
rowCount={lotteryThemes.length || 0}
enableRowActions={false}
enableColumnActions={false}
enableToolbarInternalActions={false}
renderTopToolbarCustomActions={() => (
<Box
sx={{
display: 'flex',
justifyContent: 'flex-end',
gap: '1rem',
padding: '8px 16px',
width: '100%',
}}
>
<Button variant="text" color="secondary"
sx={{textTransform: 'uppercase'}} onClick={() => null}>
{t('createJSON')}
</Button>
<Button
variant="text"
color="secondary"
sx={{textTransform: 'uppercase'}}
component={NavLinkAdapter}
to="new"
>
{t('create')}
</Button>
</Box>
)}
/>
</Paper>
);
}

export default LotteryThemesTable;

+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+++++++++++++++++++++++++++++++++++++++++++++++

CreatePage

import FuseLoading from '@fuse/core/FuseLoading';


import FusePageCarded from '@fuse/core/FusePageCarded';
import {useForm, FormProvider, Controller} from 'react-hook-form';
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query';
import {useTranslation} from 'react-i18next';
import {Snackbar, Box, RadioGroup, FormControlLabel, Radio, Button, Select,
MenuItem, Typography} from '@mui/material';
import {useState, useMemo, useEffect} from 'react';
import {zodResolver} from '@hookform/resolvers/zod';
import {lotteryThemeSchema, TUpdateConfigurationForm} from './lottery-theme-create-
schema';
import {fetchLotteries} from '@lottob/graphql/queries/lotteries-id';
import {createTheme} from '@lottob/graphql/mutations/create-lottery-theme';
import {UpdateConfigurationDto} from '@lottob/gql/graphql';
import {LotteryThemesCreateHeader} from './components/LotteryThemesCreateHeader';
import {fetchLotteryThemeByName} from '@lottob/graphql/queries/lottery-value-theme-
by-name';
import {ILotteryThemeDatum} from '../list/LotteryThemesTable';
import {useLogger} from '@lottob/utils/logging';
import {CircularProgress} from '@mui/material';
import {useNavigate} from 'react-router';
import {uploadImageBase64} from '@lottob/utils/upload-image';
import {readFileAsync, TUploadedImage} from '@lottob/utils/convertFileToBase64';

type TMutationError = {
response?: {
errors: {message: string}[];
};
};
interface IThemeDeviceConfig {
heroBanner: {
src: string | {base64: string; fileName: string} | null;
title: string;
};
detailsImage: {
src: string | {base64: string; fileName: string} | null;
title: string;
};
}
interface ITheme {
lotteryId: string;
desktop?: IThemeDeviceConfig;
mobile?: IThemeDeviceConfig;
}

export default function CreateThemePage() {


const {t} = useTranslation('lotteryThemePage');
const logger = useLogger();
const queryClient = useQueryClient();
const navigate = useNavigate();

const [toastMessage, setToastMessage] = useState<string | null>(null);


const [lotteryId, setLotteryId] = useState<string>('');
const [deviceType, setDeviceType] = useState<'desktop' | 'mobile'>('desktop');
const [uploadedImageUrlHeroDesktop, setUploadedImageUrlHeroDesktop] =
useState<TUploadedImage | null>(null);
const [uploadedImageUrlDetailsDesktop, setUploadedImageUrlDetailsDesktop] =
useState<TUploadedImage | null>(null);
const [uploadedImageUrlHeroMobile, setUploadedImageUrlHeroMobile] =
useState<TUploadedImage | null>(null);
const [uploadedImageUrlDetailsMobile, setUploadedImageUrlDetailsMobile] =
useState<TUploadedImage | null>(null);
const [isSaved, setIsSaved] = useState(false);

const methods = useForm<TUpdateConfigurationForm>({


mode: 'onSubmit',
resolver: zodResolver(lotteryThemeSchema),
});

const {control} = methods;

const getFileExtensionFromBase64 = (base64Data: string): string | null => {


const regex = /^data:([a-zA-Z+/]*);base64,/;
const [, mimeType = ''] = base64Data.match(regex) ?? [];

// Map of MIME types to extensions


const mimeToExtension = {
'image/jpeg': 'jpg',
'image/png': 'png',
'image/gif': 'gif',
'image/svg+xml': 'svg',
'image/webp': 'webp',
'image/bmp': 'bmp',
'image/tiff': 'tiff',
} as const;

return mimeToExtension[mimeType] ?? null;


};

useEffect(() => {
if (deviceType === 'desktop') {
setUploadedImageUrlHeroDesktop(uploadedImageUrlHeroDesktop);
setUploadedImageUrlDetailsDesktop(uploadedImageUrlDetailsDesktop);
} else {
setUploadedImageUrlHeroMobile(uploadedImageUrlHeroMobile);
setUploadedImageUrlDetailsMobile(uploadedImageUrlDetailsMobile);
}
}, [deviceType]);

// Fetch themes for the select dropdown


const {data: lotteries, isLoading: loadingLotteries} = useQuery({
queryKey: ['lotteries', {isMini: false, perPage: 100}],
queryFn: async () => {
const response = await fetchLotteries({
dto: {
isMini: false,
perPage: 100,
},
});
return response;
},
});

// Mutation to create a theme


const {mutateAsync: createThemeMutation, isPending: isLoading} = useMutation({
mutationFn: async (variables: UpdateConfigurationDto) => await
createTheme({dto: variables}),
onSuccess: () => {
queryClient.invalidateQueries({queryKey: ['lotteryTheme']});
},
onError: (error: TMutationError) => {
const message = (error?.response?.errors[0]?.message as string) ||
t('somethingWrong');
setToastMessage(message);
},
});

const fetchTheme = async () => await fetchLotteryThemeByName({dto: {name:


'lottery_themes'}});

const {data: lotteryThemes} = useQuery({


queryKey: ['lotteryTheme'],
queryFn: () => fetchTheme(),
select: (data) => {
const values = JSON.parse(data.configuration.value) as
ILotteryThemeDatum[];
return values.map((value) => {
return {configurationName: data.configuration.name, ...value};
});
},
});

if (!lotteryThemes || lotteryThemes.length === 0) {


logger.warn('No themes found or query failed', {lotteryThemes});
} else {
logger.log('lotteryThemes: ', {lotteryThemes});
}

const handleThemeImageUpload = async (


lotteryId: string,
base64: string,
deviceType: 'desktop' | 'mobile',
imageType: 'hero' | 'details'
): Promise<{url: string; deviceType: 'desktop' | 'mobile'; imageType: 'hero' |
'details'}> => {
const extension = getFileExtensionFromBase64(base64);
if (!extension) {
throw new Error(`Invalid file extension for ${imageType} image`);
}
const fileNamePath = `/lotteries/${lotteryId}/assets/theme/${deviceType}_$
{imageType}.${extension}`;
const url = await uploadImageBase64(base64, fileNamePath);

return {
url,
deviceType,
imageType,
};
};

const handleCreateTheme = async () => {


if (!lotteryId) {
setToastMessage(t('lotterySelect'));
return;
}

// Check if the images are available


if (
!uploadedImageUrlHeroDesktop &&
!uploadedImageUrlDetailsDesktop &&
!uploadedImageUrlHeroMobile &&
!uploadedImageUrlDetailsMobile
) {
setToastMessage(t('uploadOneImage'));
return;
}

try {
const imagesToUpload: Array<Parameters<typeof handleThemeImageUpload>>
= [];

if (uploadedImageUrlHeroDesktop?.base64) {
imagesToUpload.push([lotteryId,
uploadedImageUrlHeroDesktop?.base64, 'desktop', 'hero']);
}
if (uploadedImageUrlDetailsDesktop?.base64) {
imagesToUpload.push([lotteryId,
uploadedImageUrlDetailsDesktop?.base64, 'desktop', 'details']);
}
if (uploadedImageUrlHeroMobile?.base64) {
imagesToUpload.push([lotteryId, uploadedImageUrlHeroMobile?.base64,
'mobile', 'hero']);
}
if (uploadedImageUrlDetailsMobile?.base64) {
imagesToUpload.push([lotteryId,
uploadedImageUrlDetailsMobile?.base64, 'mobile', 'details']);
}
console.log('imagesToUpload: ', uploadedImageUrlHeroDesktop);

const uploadedImages = await Promise.all(imagesToUpload.map((params) =>


handleThemeImageUpload(...params)));

// Fetch the current themes to check if one already exists for the
selected lottery
const configuration = await fetchTheme();
const rawConfigurationValue: string = configuration.configuration.value
|| '[]';
let lotteriesThemesList: ITheme[];
try {
lotteriesThemesList = JSON.parse(rawConfigurationValue);
if (!Array.isArray(lotteriesThemesList)) {
//console.error('lotteriesThemesList is not an array:',
lotteriesThemesList);
lotteriesThemesList = [];
}
} catch (parseError) {
//console.error('Error parsing configuration value:', parseError,
rawConfigurationValue);
lotteriesThemesList = [];
}

// Check if the theme for lotteryId already exist


let currentTheme = lotteriesThemesList.find((theme) => theme.lotteryId
=== lotteryId);

if (currentTheme) {
// If a theme already exists for this lottery, prevent creating a
new one
setToastMessage(t('themeAlreadyExists'));
return;
}
if (!currentTheme) {
currentTheme = {
lotteryId,
desktop: {
heroBanner: {
src: '',
title: '',
},
detailsImage: {
src: '',
title: '',
},
},
mobile: {
heroBanner: {
src: '',
title: '',
},
detailsImage: {
src: '',
title: '',
},
},
};

// Structure added for new theme


lotteriesThemesList.push(currentTheme);
}

// Keep existing images for both devices


//TODO Change this updating versioning with a file hash instead
const uploadVersion = Date.now();

const desktopImages: IThemeDeviceConfig = uploadedImages


.filter((uploadResult) => uploadResult.deviceType === 'desktop')
.reduce(
(accumulator, currentValue) => {
switch (currentValue.imageType) {
case 'hero':
accumulator.heroBanner.src = `${currentValue.url}?
v=${uploadVersion}`;
break;

case 'details':
accumulator.detailsImage.src = `$
{currentValue.url}?v=${uploadVersion}`;
break;

default:
break;
}

return accumulator;
},
{...currentTheme.desktop}
);

const mobileImages: IThemeDeviceConfig = uploadedImages


.filter((uploadResult) => uploadResult.deviceType === 'mobile')
.reduce(
(accumulator, currentValue) => {
switch (currentValue.imageType) {
case 'hero':
accumulator.heroBanner.src = `${currentValue.url}?
v=${uploadVersion}`;
break;

case 'details':
accumulator.detailsImage.src = `$
{currentValue.url}?v=${uploadVersion}`;
break;

default:
break;
}

return accumulator;
},
{...currentTheme.mobile}
);

// Update the theme with new images


currentTheme.desktop = desktopImages;
currentTheme.mobile = mobileImages;

// Create the payload with current configurations


const payload: UpdateConfigurationDto = {
configurationName: 'lottery_themes',
payload: {
value: JSON.stringify(lotteriesThemesList),
},
};

//console.log('Payload being sent to createThemeMutation:', payload);


await createThemeMutation(payload);
logger.log('Payload sent to createThemeMutation:', {
configurationName: 'lottery_themes',
payload: {
value: JSON.stringify(lotteriesThemesList),
},
});

await createThemeMutation(payload);

setToastMessage(t('createSuccessfully'));

setIsSaved(true);
} catch (error) {
logger.error('Error saving theme:', error);

setToastMessage(t('somethingWrong'));
}
};

useEffect(() => {
if (isSaved) {
setTimeout(() => {
navigate('/lottery-theme');
}, 500);
}
}, [isSaved]);

const renderLotteries = useMemo(() => {


if (!lotteries?.lotteries?.data || lotteries.lotteries.data.length === 0) {
return (
<MenuItem disabled value="">
{t('noLotteriesAvailable')}
</MenuItem>
);
}

return lotteries?.lotteries.data.map((theme: {id: string; label: string})


=> (
<MenuItem key={theme.id} value={theme.id}>
{theme.label}
</MenuItem>
));
}, [lotteries, t]);

if (loadingLotteries) {
return <FuseLoading />;
}

return (
<FormProvider {...methods}>
<FusePageCarded
header={<LotteryThemesCreateHeader />}
content={
<Box className="p-16 sm:p-24 max-w-lg">
<Select
name="lotteryId"
value={lotteryId}
labelId="lottery-select-label"
onChange={(e) => {
const selectedValue = e.target.value;
setLotteryId(selectedValue || '');
}}
displayEmpty
sx={{width: '400px', mb: '30px'}}
>
<MenuItem id="lottery-select-label" value="">
{t('lottery')}
</MenuItem>

{renderLotteries}
</Select>
<RadioGroup
name="deviceType"
value={deviceType}
onChange={(e) => setDeviceType(e.target.value as
'desktop' | 'mobile')}
sx={{flexDirection: 'row', m: '0px'}}
>
<FormControlLabel
value="desktop"
control={<Radio />}
label={t('desktop')}
labelPlacement="start"
/>
<FormControlLabel
value="mobile"
control={<Radio />}
label={t('mobile')}
labelPlacement="start"
/>
</RadioGroup>

<Typography className="text-16 sm:text-20 font-semibold"


sx={{mt: '30px'}}>
{t('imageAsset')}
</Typography>
<Box sx={{display: 'flex', flexDirection: 'row', gap:
'40px'}}>
<Box sx={{mt: '30px'}}>
<Typography className="text-12" color="primary">
{t('heroBanner')}
</Typography>
<Controller
name="imageHeroBanner"
control={control}
render={() => (
<Button
variant="outlined"
component="label"
sx={{
mt: 2,
borderRadius: '5px',
border: 'none',
backgroundColor: '#f5f5f5',
pt: (
deviceType === 'desktop'
?
uploadedImageUrlHeroDesktop
:
uploadedImageUrlHeroMobile
)
? '90px'
: '30px',
pb: (
deviceType === 'desktop'
?
uploadedImageUrlHeroDesktop
:
uploadedImageUrlHeroMobile
)
? '90px'
: '30px',
width: '300px',
height: '150px',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
overflow: 'hidden',
...(deviceType === 'desktop' &&
uploadedImageUrlHeroDesktop &&
{
width: '100%',
height: '100%',
}),
...(deviceType === 'mobile' &&
uploadedImageUrlHeroMobile && {
width: '100%',
height: '100%',
}),
}}
>
{(
deviceType === 'desktop'
? uploadedImageUrlHeroDesktop
: uploadedImageUrlHeroMobile
) ? (
<img
src={
deviceType === 'desktop'
?
uploadedImageUrlHeroDesktop.url
:
uploadedImageUrlHeroMobile.url
}
alt="Uploaded preview"
style={{
maxWidth: '100%',
maxHeight: '100%',
objectFit: 'contain',
borderRadius: '8px',
minWidth: '150px',
minHeight: '150px',
}}
/>
) : (
t('uploadPicture')
)}
<input
accept="image/*"
className="hidden"
id="button-file"
type="file"
onChange={async (e) => {
const file = e.target.files?.
[0];
if (file) {
const uploadedImageUrl =
await readFileAsync(file);
if (deviceType ===
'desktop') {

setUploadedImageUrlHeroDesktop(uploadedImageUrl);
} else {

setUploadedImageUrlHeroMobile(uploadedImageUrl);
}
}
}}
/>
</Button>
)}
/>
</Box>
<Box sx={{mt: '30px'}}>
<Typography className="text-12" color="primary">
{t('details')}
</Typography>
<Controller
name="imageDetails"
control={control}
render={() => (
<Button
variant="outlined"
component="label"
sx={{
mt: 2,
borderRadius: '5px',
border: 'none',
backgroundColor: '#f5f5f5',
pt: (
deviceType === 'desktop'
?
uploadedImageUrlDetailsDesktop
:
uploadedImageUrlDetailsMobile
)
? '90px'
: '30px',
pb: (
deviceType === 'desktop'
?
uploadedImageUrlDetailsDesktop
:
uploadedImageUrlDetailsMobile
)
? '90px'
: '30px',
width: '300px',
height: '150px',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
overflow: 'hidden',
...(deviceType === 'desktop' &&
uploadedImageUrlDetailsDesktop
&& {
width: '100%',
height: '100%',
}),
...(deviceType === 'mobile' &&
uploadedImageUrlDetailsMobile
&& {
width: '100%',
height: '100%',
}),
}}
>
{(
deviceType === 'desktop'
?
uploadedImageUrlDetailsDesktop
: uploadedImageUrlDetailsMobile
) ? (
<img
src={
deviceType === 'desktop'
?
uploadedImageUrlDetailsDesktop.url
:
uploadedImageUrlDetailsMobile.url
}
alt="Uploaded preview"
style={{
maxWidth: '100%',
maxHeight: '100%',
objectFit: 'contain',
borderRadius: '8px',
minWidth: '150px',
minHeight: '150px',
}}
/>
) : (
t('uploadPicture')
)}
<input
accept="image/*"
className="hidden"
id="button-file"
type="file"
onChange={async (e) => {
const file = e.target.files?.
[0];
if (file) {
const uploadedImageUrl =
await readFileAsync(file);
if (deviceType ===
'desktop') {

setUploadedImageUrlDetailsDesktop(uploadedImageUrl);
} else {

setUploadedImageUrlDetailsMobile(uploadedImageUrl);
}
}
}}
/>
</Button>
)}
/>
</Box>
</Box>

<Button
className="mt-40"
variant="contained"
color="primary"
onClick={handleCreateTheme}
disabled={isLoading}
sx={{borderRadius: '5px'}}
>
{isLoading ? <CircularProgress size={24}
color="inherit" /> : t('save')}
</Button>
</Box>
}
/>
<Snackbar
anchorOrigin={{vertical: 'bottom', horizontal: 'center'}}
open={!!toastMessage}
autoHideDuration={3000}
onClose={() => setToastMessage(null)}
message={toastMessage}
/>
</FormProvider>
);
}

You might also like