Add preliminary js upload

This commit is contained in:
2026-01-08 12:54:21 +01:00
parent e5de5b325d
commit 7c9f47cb1f
3 changed files with 156 additions and 0 deletions

View File

@@ -0,0 +1,51 @@
.modal {
display: flex;
justify-content: center;
align-items: center;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100vh;
background-color: rgba(0, 0, 0, 0.45);
}
.modal .modal-inner {
width: 1024px;
background-color: #0a0e1a;
border-radius: 8px;
padding: 12px;
color: white;
border: 1px solid #504768;
max-height: 90vh;
overflow: auto;
}
.modal .modal-inner form {
display: flex;
flex-direction: column;
gap: 12px;
}
.modal .modal-inner .form-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.modal .modal-inner .form-actions {
display: flex;
justify-content: space-between;
}
.modal .modal-inner textarea {
max-width: 100%;
resize: vertical;
}
.modal .modal-inner .form-error {
padding: 8px;
background-color: rgba(200, 0, 0, 0.3);
border: 1px solid #a00;
border-radius: 4px;
}

View File

@@ -0,0 +1,55 @@
const assetModal = document.getElementById("custom-asset-modal");
const userSourceTextArea = document.getElementById("custom-asset-code");
const formErrorWrapper = document.getElementById("custom-asset-error");
const jsErrorTitle = document.getElementById("js-error-title");
const jsErrorDetails = document.getElementById("js-error-details");
function toggleCustomAssetModal(event) {
if (event !== undefined && event.target !== event.currentTarget) {
return;
}
if (assetModal.classList.contains("hidden")) {
assetModal.classList.remove("hidden");
} else {
assetModal.classList.add("hidden");
}
}
function submitCodeAsset(formEvent) {
formEvent.preventDefault();
const src = userSourceTextArea.value;
const error = getUserJavaScriptSourceError(src);
if (error) {
jsErrorTitle.textContent = error.title;
jsErrorDetails.textContent = error.details;
formErrorWrapper.classList.remove("hidden");
return false;
}
formErrorWrapper.classList.add("hidden");
jsErrorTitle.textContent = "";
jsErrorDetails.textContent = "";
return false;
}
function getUserJavaScriptSourceError(src) {
let ast;
try {
ast = acorn.parse(src, {
ecmaVersion: "latest",
sourceType: "script",
});
} catch (e) {
return { title: "Syntax Error", details: e.message };
}
const functionNames = ast.body.filter((node) => node.type === "FunctionDeclaration").map((node) => node.id.name);
if (!functionNames.includes("init")) {
return { title: "Missing function: init", details: "Your code must include a function named 'init'." };
}
if (!functionNames.includes("tick")) {
return { title: "Missing function: tick", details: "Your code must include a function named 'tick'." };
}
return undefined;
}

View File

@@ -7,6 +7,7 @@
<meta name="_csrf_header" th:content="${_csrf.headerName}" /> <meta name="_csrf_header" th:content="${_csrf.headerName}" />
<link rel="icon" href="/favicon.ico" /> <link rel="icon" href="/favicon.ico" />
<link rel="stylesheet" href="/css/styles.css" /> <link rel="stylesheet" href="/css/styles.css" />
<link rel="stylesheet" href="/css/customAssets.css" />
<link <link
rel="stylesheet" rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css"
@@ -16,6 +17,7 @@
/> />
<script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/stompjs@2.3.3/lib/stomp.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/stompjs@2.3.3/lib/stomp.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/acorn@8.15.0/dist/acorn.min.js"></script>
<script src="/js/csrf.js"></script> <script src="/js/csrf.js"></script>
</head> </head>
<body class="admin-body"> <body class="admin-body">
@@ -60,6 +62,18 @@
</span> </span>
</label> </label>
</div> </div>
<div class="upload-row">
<label
class="file-input-trigger"
id="custom-asset-button"
onclick="toggleCustomAssetModal(undefined)"
>
<span class="file-input-icon"><i class="fa-solid fa-code"></i></span>
<span class="file-input-copy">
<strong>Create custom asset</strong>
</span>
</label>
</div>
<div class="rail-body"> <div class="rail-body">
<div class="rail-scroll"> <div class="rail-scroll">
<ul id="asset-list" class="asset-list"></ul> <ul id="asset-list" class="asset-list"></ul>
@@ -348,6 +362,41 @@
</section> </section>
</div> </div>
</div> </div>
<div id="custom-asset-modal" class="modal hidden" onclick="toggleCustomAssetModal(event)">
<section class="modal-inner">
<h1>Create Custom Asset</h1>
<form id="custom-asset-form" onsubmit="submitCodeAsset(event)">
<div class="form-group">
<label for="custom-asset-name">Asset name</label>
<input
id="custom-asset-name"
type="text"
class="text-input"
placeholder="Enter asset name"
required
/>
</div>
<div class="form-group">
<label for="custom-asset-type">Asset code</label>
<textarea
class="text-input"
id="custom-asset-code"
placeholder="function init({ surface, assets, channel }) {&#10;&#10;}&#10;&#10;function tick() {&#10;&#10;}"
rows="25"
required
></textarea>
</div>
<div class="form-error hidden" id="custom-asset-error">
<strong>JavaScript error: <span id="js-error-title"></span></strong>
<pre id="js-error-details"></pre>
</div>
<div class="form-actions">
<button type="button" class="secondary" onclick="toggleCustomAssetModal(event)">Cancel</button>
<button type="submit" class="primary">Test and save</button>
</div>
</form>
</section>
</div>
<script th:inline="javascript"> <script th:inline="javascript">
const broadcaster = /*[[${broadcaster}]]*/ ""; const broadcaster = /*[[${broadcaster}]]*/ "";
const username = /*[[${username}]]*/ ""; const username = /*[[${username}]]*/ "";
@@ -357,5 +406,6 @@
<script src="/js/cookie-consent.js"></script> <script src="/js/cookie-consent.js"></script>
<script src="/js/toast.js"></script> <script src="/js/toast.js"></script>
<script src="/js/admin.js"></script> <script src="/js/admin.js"></script>
<script src="/js/customAssets.js"></script>
</body> </body>
</html> </html>