feat: Add more pages
This commit is contained in:
89
src/pages/Media/MediaCreatePage/index.tsx
Normal file
89
src/pages/Media/MediaCreatePage/index.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import {
|
||||
Button,
|
||||
Paper,
|
||||
TextField,
|
||||
Select,
|
||||
MenuItem,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
} from "@mui/material";
|
||||
import { mediaStore, MEDIA_TYPE_LABELS } from "@shared";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { ArrowLeft, Loader2, Save } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
export const MediaCreatePage = observer(() => {
|
||||
const navigate = useNavigate();
|
||||
const [name, setName] = useState("");
|
||||
const [type, setType] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const handleCreate = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await mediaStore.createMedia(name, type);
|
||||
toast.success("Медиа успешно создано");
|
||||
navigate("/media");
|
||||
} catch (error) {
|
||||
toast.error("Ошибка при создании медиа");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Paper className="w-full h-full p-3 flex flex-col gap-10">
|
||||
<div className="flex justify-between items-center">
|
||||
<button
|
||||
className="flex items-center gap-2"
|
||||
onClick={() => navigate(-1)}
|
||||
>
|
||||
<ArrowLeft size={20} />
|
||||
Назад
|
||||
</button>
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold">Создание медиа</h1>
|
||||
<div className="flex flex-col gap-10 w-full items-end">
|
||||
<TextField
|
||||
className="w-full"
|
||||
label="Название"
|
||||
required
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
/>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>Тип</InputLabel>
|
||||
<Select
|
||||
value={type}
|
||||
label="Тип"
|
||||
onChange={(e) => setType(e.target.value)}
|
||||
required
|
||||
>
|
||||
{Object.entries(MEDIA_TYPE_LABELS).map(([value, label]) => (
|
||||
<MenuItem key={value} value={value}>
|
||||
{label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
className="w-min flex gap-2 items-center"
|
||||
startIcon={<Save size={20} />}
|
||||
onClick={handleCreate}
|
||||
disabled={isLoading || !name || !type}
|
||||
>
|
||||
{isLoading ? (
|
||||
<Loader2 size={20} className="animate-spin" />
|
||||
) : (
|
||||
"Создать"
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</Paper>
|
||||
);
|
||||
});
|
||||
295
src/pages/Media/MediaEditPage/index.tsx
Normal file
295
src/pages/Media/MediaEditPage/index.tsx
Normal file
@@ -0,0 +1,295 @@
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
CircularProgress,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
MenuItem,
|
||||
Paper,
|
||||
Select,
|
||||
TextField,
|
||||
Typography,
|
||||
Alert,
|
||||
Snackbar,
|
||||
} from "@mui/material";
|
||||
import { Save, ArrowLeft } from "lucide-react";
|
||||
import { authInstance, mediaStore, MEDIA_TYPE_LABELS } from "@shared";
|
||||
import { MediaViewer } from "@widgets";
|
||||
|
||||
export const MediaEditPage = observer(() => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [success, setSuccess] = useState(false);
|
||||
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const [newFile, setNewFile] = useState<File | null>(null);
|
||||
const [uploadDialogOpen, setUploadDialogOpen] = useState(false); // State for the upload dialog
|
||||
|
||||
const media = id ? mediaStore.media.find((m) => m.id === id) : null;
|
||||
const [mediaName, setMediaName] = useState(media?.media_name ?? "");
|
||||
const [mediaFilename, setMediaFilename] = useState(media?.filename ?? "");
|
||||
const [mediaType, setMediaType] = useState(media?.media_type ?? 1);
|
||||
const [availableMediaTypes, setAvailableMediaTypes] = useState<number[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
mediaStore.getOneMedia(id);
|
||||
}
|
||||
console.log(newFile);
|
||||
console.log(uploadDialogOpen);
|
||||
}, [id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (media) {
|
||||
setMediaName(media.media_name);
|
||||
setMediaFilename(media.filename);
|
||||
setMediaType(media.media_type);
|
||||
|
||||
// Set available media types based on current file extension
|
||||
const extension = media.filename.split(".").pop()?.toLowerCase();
|
||||
if (extension) {
|
||||
if (["glb", "gltf"].includes(extension)) {
|
||||
setAvailableMediaTypes([6]); // 3D model
|
||||
} else if (["jpg", "jpeg", "png", "gif"].includes(extension)) {
|
||||
setAvailableMediaTypes([1, 3, 4, 5]); // Photo, Icon, Watermark, Panorama
|
||||
} else if (["mp4", "webm", "mov"].includes(extension)) {
|
||||
setAvailableMediaTypes([2]); // Video
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [media]);
|
||||
|
||||
// const handleDrop = (e: DragEvent<HTMLDivElement>) => {
|
||||
// e.preventDefault();
|
||||
// e.stopPropagation();
|
||||
// setIsDragging(false);
|
||||
|
||||
// const files = Array.from(e.dataTransfer.files);
|
||||
// if (files.length > 0) {
|
||||
// setNewFile(files[0]);
|
||||
// setMediaFilename(files[0].name);
|
||||
// setUploadDialogOpen(true); // Open dialog on file drop
|
||||
// }
|
||||
// };
|
||||
|
||||
// const handleDragOver = (e: DragEvent<HTMLDivElement>) => {
|
||||
// e.preventDefault();
|
||||
// setIsDragging(true);
|
||||
// };
|
||||
|
||||
// const handleDragLeave = () => {
|
||||
// setIsDragging(false);
|
||||
// };
|
||||
|
||||
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = e.target.files;
|
||||
if (files && files.length > 0) {
|
||||
const file = files[0];
|
||||
setNewFile(file);
|
||||
setMediaFilename(file.name);
|
||||
|
||||
// Determine media type based on file extension
|
||||
const extension = file.name.split(".").pop()?.toLowerCase();
|
||||
if (extension) {
|
||||
if (["glb", "gltf"].includes(extension)) {
|
||||
setAvailableMediaTypes([6]); // 3D model
|
||||
setMediaType(6);
|
||||
} else if (["jpg", "jpeg", "png", "gif"].includes(extension)) {
|
||||
setAvailableMediaTypes([1, 3, 4, 5]); // Photo, Icon, Watermark, Panorama
|
||||
setMediaType(1); // Default to Photo
|
||||
} else if (["mp4", "webm", "mov"].includes(extension)) {
|
||||
setAvailableMediaTypes([2]); // Video
|
||||
setMediaType(2);
|
||||
}
|
||||
}
|
||||
|
||||
setUploadDialogOpen(true); // Open dialog on file selection
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!id) return;
|
||||
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
await authInstance.patch(`/media/${id}`, {
|
||||
media_name: mediaName,
|
||||
filename: mediaFilename,
|
||||
type: mediaType,
|
||||
});
|
||||
|
||||
// If a new file was selected, the actual file upload will happen
|
||||
// via the UploadMediaDialog. We just need to make sure the metadata
|
||||
// is updated correctly before or after.
|
||||
// Since the dialog handles the actual upload, we don't call updateMediaFile here.
|
||||
|
||||
setSuccess(true);
|
||||
handleUploadSuccess();
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Failed to save media");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUploadSuccess = () => {
|
||||
// After successful upload in the dialog, refresh media data if needed
|
||||
if (id) {
|
||||
mediaStore.getOneMedia(id);
|
||||
}
|
||||
setNewFile(null); // Clear the new file state after successful upload
|
||||
setUploadDialogOpen(false);
|
||||
setSuccess(true);
|
||||
};
|
||||
|
||||
if (!media && id) {
|
||||
// Only show loading if an ID is present and media is not yet loaded
|
||||
return (
|
||||
<Box className="flex justify-center items-center h-screen">
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box className="p-6 max-w-4xl mx-auto">
|
||||
<Box className="flex items-center gap-4 mb-6">
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<ArrowLeft size={20} />}
|
||||
onClick={() => navigate("/media")}
|
||||
>
|
||||
Назад
|
||||
</Button>
|
||||
<Typography variant="h5">Редактирование медиа</Typography>
|
||||
</Box>
|
||||
|
||||
<Paper className="p-6">
|
||||
<input
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
className="hidden"
|
||||
onChange={handleFileSelect}
|
||||
accept="image/*,video/*,.glb,.gltf"
|
||||
/>
|
||||
<Box className="flex flex-col gap-6">
|
||||
<Box className="flex gap-4">
|
||||
<TextField
|
||||
fullWidth
|
||||
value={mediaName}
|
||||
onChange={(e) => setMediaName(e.target.value)}
|
||||
label="Название медиа"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<TextField
|
||||
fullWidth
|
||||
value={mediaFilename}
|
||||
onChange={(e) => setMediaFilename(e.target.value)}
|
||||
label="Название файла"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<FormControl fullWidth sx={{ width: "50%" }}>
|
||||
<InputLabel>Тип медиа</InputLabel>
|
||||
<Select
|
||||
value={mediaType}
|
||||
label="Тип медиа"
|
||||
onChange={(e) => setMediaType(Number(e.target.value))}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{availableMediaTypes.length > 0
|
||||
? availableMediaTypes.map((type) => (
|
||||
<MenuItem key={type} value={type}>
|
||||
{
|
||||
MEDIA_TYPE_LABELS[
|
||||
type as keyof typeof MEDIA_TYPE_LABELS
|
||||
]
|
||||
}
|
||||
</MenuItem>
|
||||
))
|
||||
: Object.entries(MEDIA_TYPE_LABELS).map(([type, label]) => (
|
||||
<MenuItem key={type} value={Number(type)}>
|
||||
{label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<Box className="flex gap-6">
|
||||
<Paper
|
||||
elevation={2}
|
||||
sx={{
|
||||
flex: 1,
|
||||
p: 2,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
minHeight: 400,
|
||||
}}
|
||||
>
|
||||
<MediaViewer
|
||||
media={{
|
||||
id: id || "",
|
||||
media_type: mediaType,
|
||||
filename: mediaFilename,
|
||||
}}
|
||||
/>
|
||||
</Paper>
|
||||
|
||||
<Box className="flex flex-col gap-4 self-end">
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
startIcon={<Save size={20} />}
|
||||
onClick={handleSave}
|
||||
disabled={isLoading || (!mediaName && !mediaFilename)}
|
||||
>
|
||||
{isLoading ? <CircularProgress size={20} /> : "Сохранить"}
|
||||
</Button>
|
||||
{/* Only show "Replace file" button if no new file is currently selected */}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{error && (
|
||||
<Typography color="error" className="mt-2">
|
||||
{error}
|
||||
</Typography>
|
||||
)}
|
||||
{success && (
|
||||
<Typography color="success.main" className="mt-2">
|
||||
Медиа успешно сохранено
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
<Snackbar
|
||||
open={!!error}
|
||||
autoHideDuration={6000}
|
||||
onClose={() => setError(null)}
|
||||
>
|
||||
<Alert severity="error" onClose={() => setError(null)}>
|
||||
{error}
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
|
||||
<Snackbar
|
||||
open={success}
|
||||
autoHideDuration={2000}
|
||||
onClose={() => setSuccess(false)}
|
||||
>
|
||||
<Alert severity="success" onClose={() => setSuccess(false)}>
|
||||
Медиа успешно сохранено
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
110
src/pages/Media/MediaListPage/index.tsx
Normal file
110
src/pages/Media/MediaListPage/index.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
||||
import { languageStore, MEDIA_TYPE_LABELS, mediaStore } from "@shared";
|
||||
import { useEffect, useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Eye, Trash2 } from "lucide-react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { CreateButton, DeleteModal, LanguageSwitcher } from "@widgets";
|
||||
|
||||
export const MediaListPage = observer(() => {
|
||||
const { media, getMedia, deleteMedia } = mediaStore;
|
||||
const navigate = useNavigate();
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [rowId, setRowId] = useState<string | null>(null); // Lifted state
|
||||
const { language } = languageStore;
|
||||
|
||||
useEffect(() => {
|
||||
getMedia();
|
||||
}, [language]);
|
||||
|
||||
const columns: GridColDef[] = [
|
||||
{
|
||||
field: "media_name",
|
||||
headerName: "Название",
|
||||
flex: 1,
|
||||
},
|
||||
{
|
||||
field: "media_type",
|
||||
headerName: "Тип",
|
||||
width: 200,
|
||||
flex: 1,
|
||||
renderCell: (params: GridRenderCellParams) => {
|
||||
return (
|
||||
<p>
|
||||
{
|
||||
MEDIA_TYPE_LABELS[
|
||||
params.row.media_type as keyof typeof MEDIA_TYPE_LABELS
|
||||
]
|
||||
}
|
||||
</p>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: "actions",
|
||||
headerName: "Действия",
|
||||
flex: 1,
|
||||
align: "center",
|
||||
headerAlign: "center",
|
||||
|
||||
renderCell: (params: GridRenderCellParams) => {
|
||||
return (
|
||||
<div className="flex h-full gap-7 justify-center items-center">
|
||||
<button onClick={() => navigate(`/media/${params.row.id}`)}>
|
||||
<Eye size={20} className="text-green-500" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsDeleteModalOpen(true);
|
||||
setRowId(params.row.id);
|
||||
}}
|
||||
>
|
||||
<Trash2 size={20} className="text-red-500" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const rows = media.map((media) => ({
|
||||
id: media.id,
|
||||
media_name: media.media_name,
|
||||
media_type: media.media_type,
|
||||
}));
|
||||
|
||||
return (
|
||||
<>
|
||||
<LanguageSwitcher />
|
||||
|
||||
<div className="w-full">
|
||||
<div className="flex justify-between items-center mb-10">
|
||||
<h1 className="text-2xl">Медиа</h1>
|
||||
<CreateButton label="Создать медиа" path="/media/create" />
|
||||
</div>
|
||||
<DataGrid
|
||||
rows={rows}
|
||||
columns={columns}
|
||||
hideFooterPagination
|
||||
hideFooter
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DeleteModal
|
||||
open={isDeleteModalOpen}
|
||||
onDelete={async () => {
|
||||
if (rowId) {
|
||||
deleteMedia(rowId.toString());
|
||||
getMedia();
|
||||
}
|
||||
setIsDeleteModalOpen(false);
|
||||
setRowId(null);
|
||||
}}
|
||||
onCancel={() => {
|
||||
setIsDeleteModalOpen(false);
|
||||
setRowId(null);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
44
src/pages/Media/MediaPreviewPage/index.tsx
Normal file
44
src/pages/Media/MediaPreviewPage/index.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import { mediaStore } from "@shared";
|
||||
import { useEffect } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { MediaViewer } from "@widgets";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Download } from "lucide-react";
|
||||
import { Button } from "@mui/material";
|
||||
|
||||
export const MediaPreviewPage = observer(() => {
|
||||
const { id } = useParams();
|
||||
const { oneMedia, getOneMedia } = mediaStore;
|
||||
|
||||
useEffect(() => {
|
||||
getOneMedia(id!);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="w-full h-[80vh] flex flex-col justify-center items-center gap-4">
|
||||
<div className="w-full h-full flex justify-center items-center">
|
||||
<MediaViewer className="w-full h-full" media={oneMedia!} />
|
||||
</div>
|
||||
|
||||
{oneMedia && (
|
||||
<div className="flex flex-col items-center gap-4 bg-[#998879] p-4 rounded-md">
|
||||
<p className="text-white text-center">
|
||||
Чтобы скачать файл, нажмите на кнопку ниже
|
||||
</p>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
startIcon={<Download size={16} />}
|
||||
component="a"
|
||||
href={`${
|
||||
import.meta.env.VITE_KRBL_MEDIA
|
||||
}${id}/download?token=${localStorage.getItem("token")}`}
|
||||
target="_blank"
|
||||
>
|
||||
Скачать
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
3
src/pages/Media/index.ts
Normal file
3
src/pages/Media/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from "./MediaEditPage";
|
||||
export * from "./MediaListPage";
|
||||
export * from "./MediaPreviewPage";
|
||||
Reference in New Issue
Block a user