Image Adder Adminpanel

This commit is contained in:
Tim
2025-06-18 22:34:53 +02:00
parent 5e4d06ff06
commit 8d3596c677
4 changed files with 290 additions and 18 deletions

View File

@@ -142,5 +142,12 @@
"DELIVERED": "Zugesendet",
"CANCELLED": "Storniert",
"fsImage": "FS-Bild",
"changeCustomer": "Kundendaten ändern"
"changeCustomer": "Kundendaten ändern",
"uploadImage": "Bild Hochladen",
"imageUploadNotice": "Lade das Bild in .webp-Format hoch, damit die Webseite schneller lädt",
"selectImage": "Bildauswahl",
"imageUploadedSuccessfully": "Bild hochgeladen",
"uploading": "Lädt hoch...",
"upload": "Hochladen",
"imageUploadNoticeFs": "Die Auflösung der Farming Station beträgt 720 x 960 px"
}

View File

@@ -142,5 +142,12 @@
"DELIVERED": "Delivered",
"CANCELLED": "Cancelled",
"fsImage": "FS-Image",
"changeCustomer": "Change Customer Data"
"changeCustomer": "Change Customer Data",
"uploadImage": "Upload Image",
"imageUploadNotice": "Upload the image in .webp format for better loading performance",
"selectImage": "Select Image",
"imageUploadedSuccessfully": "Uploaded Image Successfully",
"uploading": "Uploading...",
"upload": "Upload",
"imageUploadNoticeFs": "The Resolution of the Farming Station is 720 x 960 px"
}

View File

@@ -0,0 +1,230 @@
import React, { useState } from 'react';
import {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Button,
Box,
Typography,
Alert,
CircularProgress,
IconButton,
} from '@mui/material';
import CloudUploadIcon from '@mui/icons-material/CloudUpload';
import CloseIcon from '@mui/icons-material/Close';
import { useTranslation } from 'react-i18next';
import { Item } from '../../components/Item.tsx';
interface ItemImageDialogProps {
open: boolean;
onClose: () => void;
item: Item;
onSuccess?: () => void;
isFarmStationImage: boolean;
}
export default function ItemImageDialog({ open, onClose, item, onSuccess, isFarmStationImage }: ItemImageDialogProps) {
const { t } = useTranslation();
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const [preview, setPreview] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState(false);
const handleFileSelect = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (file) {
// Validate file type
if (!file.type.startsWith('image/')) {
setError('Please select a valid image file');
return;
}
// Validate file size (5MB limit)
if (file.size > 5 * 1024 * 1024) {
setError('File size must be less than 5MB');
return;
}
setSelectedFile(file);
setError(null);
// Create preview
const reader = new FileReader();
reader.onload = (e) => {
setPreview(e.target?.result as string);
};
reader.readAsDataURL(file);
}
};
const convertFileToBase64 = (file: File): Promise<string> => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
const result = reader.result as string;
// Remove the data URL prefix (e.g., "data:image/jpeg;base64,")
const base64 = result.split(',')[1];
resolve(base64);
};
reader.onerror = reject;
reader.readAsDataURL(file);
});
};
const handleUpload = async () => {
if (!selectedFile) {
setError('Please select an image file');
return;
}
setLoading(true);
setError(null);
try {
const base64Image = await convertFileToBase64(selectedFile);
const response = await fetch((isFarmStationImage ? 'http://localhost:8085/farm' : 'http://localhost:8085/image') + '?uuid=' + item.uuid , {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: base64Image,
});
if (!response.ok) {
console.error('Failed to upload image:', await response.text());
}
setSuccess(true);
onSuccess?.();
// Auto-close dialog after 1.5 seconds
setTimeout(() => {
handleClose();
}, 1500);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to upload image');
} finally {
setLoading(false);
}
};
const handleClose = () => {
setSelectedFile(null);
setPreview(null);
setError(null);
setSuccess(false);
setLoading(false);
onClose();
};
return (
<Dialog
open={open}
onClose={handleClose}
maxWidth="sm"
fullWidth
>
<DialogTitle sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
{t('uploadImage')} - {item.name}
<IconButton onClick={handleClose} size="small">
<CloseIcon />
</IconButton>
</DialogTitle>
<DialogContent>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, py: 1 }}>
{/* Item Info */}
<Box>
<Typography variant="body2" color="textSecondary">
{t('item')}: {item.name}
</Typography>
<Typography variant="body2" color="textSecondary">
UUID: {item.uuid}
</Typography>
<Typography variant="body2" color="error">
{t('imageUploadNotice')}
</Typography>
{isFarmStationImage && <Typography variant="body2" color="error">
{t('imageUploadNoticeFs')}
</Typography>}
</Box>
{/* File Upload */}
<Box>
<input
accept="image/*"
style={{ display: 'none' }}
id="image-upload"
type="file"
onChange={handleFileSelect}
/>
<label htmlFor="image-upload">
<Button
variant="outlined"
component="span"
startIcon={<CloudUploadIcon />}
fullWidth
sx={{ mb: 2 }}
>
{selectedFile ? selectedFile.name : t('selectImage')}
</Button>
</label>
</Box>
{/* Image Preview */}
{preview && (
<Box sx={{ textAlign: 'center' }}>
<img
src={preview}
alt="Preview"
style={{
maxWidth: '100%',
maxHeight: '300px',
objectFit: 'contain',
border: '1px solid #ddd',
borderRadius: '4px',
}}
/>
<Typography variant="caption" display="block" sx={{ mt: 1 }}>
{selectedFile?.name} ({(selectedFile?.size || 0 / 1024).toFixed(1)} KB)
</Typography>
</Box>
)}
{/* Error Message */}
{error && (
<Alert severity="error" onClose={() => setError(null)}>
{error}
</Alert>
)}
{/* Success Message */}
{success && (
<Alert severity="success">
{t('imageUploadedSuccessfully')}
</Alert>
)}
</Box>
</DialogContent>
<DialogActions>
<Button onClick={handleClose} disabled={loading}>
{t('cancel')}
</Button>
<Button
onClick={handleUpload}
variant="contained"
disabled={!selectedFile || loading}
startIcon={loading ? <CircularProgress size={20} /> : undefined}
>
{loading ? t('uploading') : t('upload')}
</Button>
</DialogActions>
</Dialog>
);
}

View File

@@ -10,24 +10,40 @@ import {useAccount} from "../AccountProvider.tsx";
import {useQuery} from "@tanstack/react-query";
import {fetchItems} from "../query/Queries.tsx";
import { mapValueToColor } from "../../util/ColorUtil.tsx";
import ItemImageDialog from "./ItemImageDialog.tsx";
export default function ItemsInfo() {
const theme = useTheme();
const {t} = useTranslation();
function handleIconEdit(item: Item) {
//TODO: implement
console.log("IconEdit", item);
}
function handleFarmStationEdit(item: Item) {
//TODO: implement
console.log("FsEdit", item);
}
const [rows, setRows] = useState<Item[]>([]);
const [selectedRows, setSelectedRows] = useState<Set<GridRowId>>(new Set());
const [editImageDialog, setEditImageDialog] = useState(false);
const [selectedItem, setSelectedItem] = useState<Item | null>(null);
const [isFarmStationImage, setIsFarmStationImage] = useState(false);
function handleImageEdit(item: Item) {
setIsFarmStationImage(false);
setSelectedItem(item);
setEditImageDialog(true);
console.log("IconEdit", item);
}
function handleFarmImageEdit(item: Item) {
setIsFarmStationImage(true);
setSelectedItem(item);
setEditImageDialog(true);
console.log("IconEdit", item);
}
function handleAddItem() {
//TODO: flsp
}
const {user: loginData} = useAccount();
const { data } = useQuery({
@@ -121,9 +137,9 @@ export default function ItemsInfo() {
[`& .${gaugeClasses.valueArc}`]: {
fill: () => {return mapValueToColor(0, 10, params.row.rating)},
},
}} text={() => `${params.row.rating}`} />
}} text={() => `${params.row.rating.toFixed(2)}`} />
},
{ //edit billing information button
{
field: "actualPrice",
headerName: t('actualPrice'),
width: 90,
@@ -135,14 +151,14 @@ export default function ItemsInfo() {
headerName: t('images'),
width: 90,
editable: false,
renderCell: params => <IconButton onClick={() => handleIconEdit(params.row)}> <EditIcon/> </IconButton>,
renderCell: params => <IconButton onClick={() => handleImageEdit(params.row)}> <EditIcon/> </IconButton>,
},
{
field: 'farmImage',
headerName: t('fsImage'),
width: 90,
editable: false,
renderCell: params => <IconButton onClick={() => handleFarmStationEdit(params.row)}> <EditIcon/> </IconButton>,
renderCell: params => <IconButton onClick={() => handleFarmImageEdit(params.row)}> <EditIcon/> </IconButton>,
}
];
@@ -179,7 +195,7 @@ export default function ItemsInfo() {
variant="contained"
color="primary"
startIcon={<DeleteIcon/>}
onClick={handleDeleteSelected}
onClick={handleAddItem}
disabled={selectedRows.size === 0}
sx={{
marginRight: 1
@@ -196,6 +212,18 @@ export default function ItemsInfo() {
return updatedRow;
}}
/>
{selectedItem && (
<ItemImageDialog
open={editImageDialog}
onClose={() => setEditImageDialog(false)}
item={selectedItem}
onSuccess={() => {
// Refresh data or update UI
console.log('Image uploaded successfully');
}}
isFarmStationImage={isFarmStationImage}
/>
)}
</Box>
);
}