Skip to content

Commit

Permalink
support media uploading
Browse files Browse the repository at this point in the history
  • Loading branch information
Kostya Bats committed Feb 7, 2024
1 parent 6b9f239 commit 9cd10fc
Show file tree
Hide file tree
Showing 9 changed files with 189 additions and 7 deletions.
38 changes: 37 additions & 1 deletion src/backend/src/main/kotlin/org/icpclive/admin/Routing.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.icpclive.admin

import io.ktor.http.*
import io.ktor.http.content.*
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.request.*
Expand All @@ -18,7 +19,8 @@ import org.icpclive.cds.tunning.toAdvancedProperties
import org.icpclive.data.Controllers
import org.icpclive.data.DataBus
import org.icpclive.util.sendFlow

import org.icpclive.util.sendJsonFlow
import java.nio.file.Files

fun Route.configureAdminApiRouting() {
authenticate("admin-api-auth") {
Expand Down Expand Up @@ -114,8 +116,42 @@ fun Route.configureAdminApiRouting() {

webSocket("/backendLog") { sendFlow(DataBus.loggerFlow) }
webSocket("/adminActions") { sendFlow(DataBus.adminActionsFlow) }

route("/media") {
get {
run {
val mediaDirectoryFile = Config.mediaDirectory.toFile()
call.respond(
mediaDirectoryFile.walkTopDown()
.filter { it.isFile }.map { it.relativeTo(mediaDirectoryFile).path }.toList()
)
}
}

post("/upload") {
call.adminApiAction {
var uploadedFileUrl: String? = null
val multipart = call.receiveMultipart()
multipart.forEachPart { partData ->
if (partData is PartData.FileItem) {
Files.write(
Config.mediaDirectory.resolve(partData.storeName),
partData.streamProvider().readBytes()
)
uploadedFileUrl = partData.storeName
}
}
uploadedFileUrl
}
}
}
}
route("/social") {
setupSocial()
}
}

private val PartData.FileItem.storeName: String
get() {
return this.originalFileName!!.replace("[^\\w.]".toRegex(), "_")
}
5 changes: 3 additions & 2 deletions src/frontend/admin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"dependencies": {
"@emotion/react": "^11.8.2",
"@emotion/styled": "^11.8.1",
"@monaco-editor/react": "^4.6.0",
"@mui/icons-material": "^5.5.1",
"@mui/material": "^5.5.3",
"@mui/styled-engine-sc": "^5.5.2",
Expand All @@ -13,10 +14,10 @@
"notistack": "^2.0.3",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-drag-drop-files": "^2.3.4",
"react-rnd": "^10.3.7",
"react-router-dom": "^6.2.2",
"react-scripts": "^5.0.1",
"@monaco-editor/react": "^4.6.0"
"react-scripts": "^5.0.1"
},
"scripts": {
"start": "react-scripts start",
Expand Down
4 changes: 4 additions & 0 deletions src/frontend/admin/src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,7 @@ body {
overflow-y: auto;
overflow-x: hidden;
}

.media-files-uploader {
max-width: 100% !important;
}
2 changes: 2 additions & 0 deletions src/frontend/admin/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import TeamSpotlight from "./components/TeamSpotlight";
import { useLocalStorageState } from "./utils";
import FullScreenClockManager from "./components/FullScreenClockManager";
import AdvancedJson from "./components/AdvancedJson";
import MediaFiles from "./components/MediaFiles";

const dashboard_elements = {
"Controls": <Controls/>,
Expand Down Expand Up @@ -60,6 +61,7 @@ function App() {
<Route path="/analytics" element={<Analytics/>}/>
<Route path="/teamSpotlight" element={<TeamSpotlight/>}/>
<Route path="/advancedJson" element={<AdvancedJson/>}/>
<Route path="/media" element={<MediaFiles/>}/>
</Routes>
<Overlay isOverlayPreviewShown={isOverlayPreviewShown}/>
</div>
Expand Down
6 changes: 2 additions & 4 deletions src/frontend/admin/src/AppNav.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import * as React from "react";
import React, { useState } from "react";
import { useNavigate } from "react-router-dom";
import AppBar from "@mui/material/AppBar";
import Box from "@mui/material/Box";
Expand All @@ -18,16 +18,14 @@ const pages = {
// "Title": "title",
// "Picture": "picture",
"TeamView": "teamview",
// "TeamPVP": "teampvp",
// "SplitScreen": "splitscreen",
"Scoreboard": "scoreboard",
"Ticker": "ticker",
"Dashboard": "dashboard",
"Backend Log": "log",
"Analytics": "analytics",
"Spotlight": "teamSpotlight",
"Advanced": "advancedJson",
// "Advanced Properties": "AdvancedProperties",
"Media": "media",
};


Expand Down
109 changes: 109 additions & 0 deletions src/frontend/admin/src/components/MediaFiles.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import React, { useState, useEffect } from "react";
import { styled } from "@mui/material/styles";
import { FileUploader } from "react-drag-drop-files";
import { Button, Container, Link, Paper, Stack } from "@mui/material";

Check warning on line 4 in src/frontend/admin/src/components/MediaFiles.js

View workflow job for this annotation

GitHub Actions / Lint admin

'Button' is defined but never used

Check warning on line 4 in src/frontend/admin/src/components/MediaFiles.js

View workflow job for this annotation

GitHub Actions / Lint admin

'Stack' is defined but never used

Check warning on line 4 in src/frontend/admin/src/components/MediaFiles.js

View workflow job for this annotation

GitHub Actions / Lint admin

'Button' is defined but never used

Check warning on line 4 in src/frontend/admin/src/components/MediaFiles.js

View workflow job for this annotation

GitHub Actions / Lint admin

'Stack' is defined but never used
import { errorHandlerWithSnackbar, useErrorHandlerWithSnackbar } from "../errors";

Check warning on line 5 in src/frontend/admin/src/components/MediaFiles.js

View workflow job for this annotation

GitHub Actions / Lint admin

'errorHandlerWithSnackbar' is defined but never used

Check warning on line 5 in src/frontend/admin/src/components/MediaFiles.js

View workflow job for this annotation

GitHub Actions / Lint admin

'errorHandlerWithSnackbar' is defined but never used
import { createApiGet } from "../utils";
import CloudUploadIcon from "@mui/icons-material/CloudUpload";

Check warning on line 7 in src/frontend/admin/src/components/MediaFiles.js

View workflow job for this annotation

GitHub Actions / Lint admin

'CloudUploadIcon' is defined but never used

Check warning on line 7 in src/frontend/admin/src/components/MediaFiles.js

View workflow job for this annotation

GitHub Actions / Lint admin

'CloudUploadIcon' is defined but never used
import AttachFileIcon from "@mui/icons-material/AttachFile";
import Box from "@mui/material/Box";
import { BASE_URL_BACKEND, MEDIAS_LOCATION } from "../config";
import { useSnackbar } from "notistack";

Check warning on line 11 in src/frontend/admin/src/components/MediaFiles.js

View workflow job for this annotation

GitHub Actions / Lint admin

'useSnackbar' is defined but never used

Check warning on line 11 in src/frontend/admin/src/components/MediaFiles.js

View workflow job for this annotation

GitHub Actions / Lint admin

'useSnackbar' is defined but never used
import "../App.css";

const VisuallyHiddenInput = styled("input")({

Check warning on line 14 in src/frontend/admin/src/components/MediaFiles.js

View workflow job for this annotation

GitHub Actions / Lint admin

'VisuallyHiddenInput' is assigned a value but never used

Check warning on line 14 in src/frontend/admin/src/components/MediaFiles.js

View workflow job for this annotation

GitHub Actions / Lint admin

'VisuallyHiddenInput' is assigned a value but never used
clip: "rect(0 0 0 0)",
clipPath: "inset(50%)",
height: 1,
overflow: "hidden",
position: "absolute",
bottom: 0,
left: 0,
whiteSpace: "nowrap",
width: 1,
});

const FileLink = styled(Link)(({ theme, highlight }) => ({
...theme.typography.body2,
padding: "8px",
display: "flex",
alignItems: "center",
flexWrap: "wrap",
color: highlight ? theme.palette.primary.main : null,
}));

const FileItem = ({ fileName, highlight }) => {

Check warning on line 35 in src/frontend/admin/src/components/MediaFiles.js

View workflow job for this annotation

GitHub Actions / Lint admin

'fileName' is missing in props validation

Check warning on line 35 in src/frontend/admin/src/components/MediaFiles.js

View workflow job for this annotation

GitHub Actions / Lint admin

'highlight' is missing in props validation

Check warning on line 35 in src/frontend/admin/src/components/MediaFiles.js

View workflow job for this annotation

GitHub Actions / Lint admin

'fileName' is missing in props validation

Check warning on line 35 in src/frontend/admin/src/components/MediaFiles.js

View workflow job for this annotation

GitHub Actions / Lint admin

'highlight' is missing in props validation
return (
<Paper>
<FileLink href={fileUrl(fileName)} highlight={highlight} target="_blank">
<AttachFileIcon fontSize="small"/>
<span>{highlight ? fileUrl(fileName) : fileName}</span>
</FileLink>
</Paper>
);
};

const fileUrl = (fileName) => {
return MEDIAS_LOCATION + "/" + fileName;
};

function AdvancedJson() {
const errorHandler = useErrorHandlerWithSnackbar();

const apiGet = createApiGet(BASE_URL_BACKEND + "/media");
const [mediaFiles, setMediaFiles] = useState([]);
const [uploadedFileUrl, setUploadedFileUrl] = useState(null);

const loadFiles = () => {
apiGet("").then(f => setMediaFiles(f));
};

useEffect(() => {
loadFiles();
}, []);

const uploadNewFile = (file) => {
// const files = e.target.files;
// if (files.length < 1) {
// errorHandler("No media file selected");
// return;
// }
const formData = new FormData();
formData.append("file", file);
fetch(BASE_URL_BACKEND + "/media/upload", {
method: "POST",
body: formData,
}).then(r => r.json()).then(r => {
if (r.status === "error") {
errorHandler("Failed to uplaod file: " + file);
} else if (r.status !== "ok" && !r.response) {
errorHandler("Failed to uplaod file");
}
setUploadedFileUrl(r.status === "ok" && r.response ? r.response : null);
loadFiles();
});
};

return (
<Container maxWidth="lg" sx={{ pt: 2 }}>
<FileUploader handleChange={uploadNewFile} name="file" classes="media-files-uploader" />

{uploadedFileUrl && (
<Box sx={{ pt: 1 }}>
<FileItem fileName={uploadedFileUrl} highlight />
</Box>
)}

<Box sx={{
pt: 1,
display: "grid",
gridTemplateColumns: { md: "1fr 1fr 1fr 1fr", sm: "1fr 1fr" },
gap: 1,
}}>
{mediaFiles.map(fileName => <FileItem key={fileName} fileName={fileName} />)}
</Box>
</Container>
);
}

export default AdvancedJson;
1 change: 1 addition & 0 deletions src/frontend/admin/src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const BACKEND_PORT = process.env.REACT_APP_BACKEND_PORT ?? window.location.port;
export const BASE_URL_BACKEND = process.env.REACT_APP_BACKEND_URL ?? (BACKEND_PROTO + window.location.hostname + ":" + BACKEND_PORT + "/api/admin");
export const OVERLAY_LOCATION = process.env.REACT_APP_OVERLAY_LOCATION ?? (BACKEND_PROTO + window.location.hostname + ":" + BACKEND_PORT + "/overlay");
export const SCHEMAS_LOCATION = process.env.REACT_APP_SCHEMAS_LOCATION ?? (BACKEND_PROTO + window.location.hostname + ":" + BACKEND_PORT + "/schemas");
export const MEDIAS_LOCATION = process.env.REACT_APP_MEDIAS_LOCATION ?? (BACKEND_PROTO + window.location.hostname + ":" + BACKEND_PORT + "/media");

const WS_PROTO = window.location.protocol === "https:" ? "wss://" : "ws://";
export const BASE_URL_WS = process.env.REACT_APP_WEBSOCKET_URL ?? (WS_PROTO + window.location.hostname + ":" + BACKEND_PORT + "/api/admin");
Expand Down
7 changes: 7 additions & 0 deletions src/frontend/admin/src/errors.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import { useSnackbar } from "notistack";

export const errorHandlerWithSnackbar = (snackBarEnqueue) =>
(cause) => {
return (error) => {
console.error(cause + ": " + error);
snackBarEnqueue(cause, { variant: "error" });
};
};

export const useErrorHandlerWithSnackbar = () => {
const { enqueueSnackbar } = useSnackbar();
return errorHandlerWithSnackbar(enqueueSnackbar);
};
24 changes: 24 additions & 0 deletions src/frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 9cd10fc

Please sign in to comment.