Image Adder Adminpanel
This commit is contained in:
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
230
01-frontend/src/helper/adminpanel/ItemImageDialog.tsx
Normal file
230
01-frontend/src/helper/adminpanel/ItemImageDialog.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user