hmn/src/templates/src/project_edit.html

416 lines
14 KiB
HTML

{{ template "base.html" . }}
{{ define "extrahead" }}
{{ template "markdown_previews.html" .TextEditor }}
<script src="{{ static "js/tabs.js" }}"></script>
<script src="{{ static "js/image_selector.js" }}"></script>
<script src="{{ static "js/templates.js" }}"></script>
<script src="{{ static "js/base64.js" }}"></script>
<script src="{{ static "js/markdown_upload.js" }}"></script>
<style>
#desc-preview:empty::after {
content: 'A preview of your description will appear here.';
color: var(--dimmer-color);
font-style: italic;
}
#full_description.drop {
box-shadow: inset 0px 0px 5px yellow;
}
</style>
{{ end }}
{{ define "content" }}
<div class="ph3 ph0-ns">
{{ if .Editing }}
<h1>Edit {{ .ProjectSettings.Name }}</h1>
{{ else }}
<h1>Create a new {{ if .ProjectSettings.JamParticipation }}jam {{ end }}project</h1>
{{ end }}
<form id="project_form" class="tabbed edit-form" method="POST" enctype="multipart/form-data">
{{ csrftoken .Session }}
<div class="tab" data-name="General" data-slug="general">
<div class="edit-form-row">
<div class="pt-input-ns">Project name:</div>
<div>
<input required type="text" name="project_name" maxlength="255" class="textbox" value="{{ .ProjectSettings.Name }}">
<span class="note">* Required</span>
</div>
</div>
<div class="edit-form-row">
<div class="pt-input-ns">Status:</div>
<div>
<select name="lifecycle">
<option value="active" {{ if eq .ProjectSettings.Lifecycle "active" }}selected{{ end }}>Active</option>
<option value="hiatus" {{ if eq .ProjectSettings.Lifecycle "hiatus" }}selected{{ end }}>On Hiatus</option>
<option value="done" {{ if eq .ProjectSettings.Lifecycle "done" }}selected{{ end }}>Completed</option>
<option value="dead" {{ if eq .ProjectSettings.Lifecycle "dead" }}selected{{ end }}>Abandoned</option>
</select>
</div>
</div>
<div class="edit-form-row">
<div class="pt-input-ns">Owners:</div>
<div>
<input id="owner_name" form="" type="text" placeholder="Enter another user's username" />
<a href="javascript:;" id="owner_add" class="">Add</a>
<span id="owners_error" class="note"></span>
<div id="owner_list" class="pt1">
<template id="owner_row">
<div class="owner_row flex flex-row bg--card w5 pv1 ph2" data-tmpl="root">
<input type="hidden" name="owners" data-tmpl="input" />
<span class="flex-grow-1" data-tmpl="name"></span>
<a class="remove_owner" href="javascript:;">X</a>
</div>
</template>
{{ range .ProjectSettings.Owners }}
<div class="owner_row flex flex-row bg--card w5 pv1 ph2">
<input type="hidden" name="owners" value="{{ .Username }}" />
<span class="flex-grow-1">{{ .Username }}</span>
{{ if (or $.User.IsStaff (ne .ID $.User.ID)) }}
<a class="remove_owner" href="javascript:;">X</a>
{{ end }}
</div>
{{ end }}
</div>
</div>
</div>
<div class="edit-form-row">
<div class="pt-input-ns">Tag:</div>
<div>
<input
id="tag" name="tag" type="text"
pattern="^[a-z0-9]+(-[a-z0-9]+)*$" maxlength="20"
value="{{ .ProjectSettings.Tag }}"
/>
<div class="c--dim f7 mt1">e.g. "imgui" or "text-editor". Tags must be all lowercase, and can use hyphens to separate words.</div>
<div class="c--dim f7" id="tag-discord-info">If you have linked your Discord account, any #project-showcase messages with the tag "&amp;<span id="tag-preview"></span>" will automatically be associated with this project.</div>
</div>
</div>
{{ if .ProjectSettings.JamParticipation }}
<div class="edit-form-row">
<div class="pt-input-ns">Jam Participation</div>
<div class="pt-input-ns">
{{ range .ProjectSettings.JamParticipation }}
<div class="pb1">
<input id="jam_{{ .JamSlug }}" type="checkbox" name="jam_participation" value="{{ .JamSlug }}" {{ if .Participating }}checked{{ end }} />
<label for="jam_{{ .JamSlug }}">{{ .JamName }}</label>
</div>
{{ end }}
</div>
</div>
{{ end }}
{{ if and .Editing .User.IsStaff }}
<div class="edit-form-row">
<div class="pt-input-ns">Admin settings</div>
</div>
<div class="edit-form-row">
<div>Official:</div>
<div>
<input id="official" type="checkbox" name="official" {{ if not .ProjectSettings.Personal }}checked{{ end }} />
<label for="official">Official HMN project</label>
</div>
</div>
<div class="edit-form-row">
<div>Hidden:</div>
<div>
<input id="hidden" type="checkbox" name="hidden" {{ if .ProjectSettings.Hidden }}checked{{ end }} />
<label for="hidden">Hide</label>
</div>
</div>
<div class="edit-form-row">
<div class="pt-input-ns">Slug:</div>
<div>
<input type="text" name="slug" maxlength="255" class="textbox" value="{{ .ProjectSettings.Slug }}">
<div class="c--dim f7 mt1">Has no effect for personal projects. Personal projects have a slug derived from the title.</div>
<div class="c--dim f7">If you change this, make sure to change DNS too!</div>
</div>
</div>
<div class="edit-form-row">
<div>Featured:</div>
<div>
<input id="featured" type="checkbox" name="featured" {{ if .ProjectSettings.Featured }}checked{{ end }} />
<label for="featured">Featured</label>
<div class="c--dim f7">Bump to the top of the project index and show in the carousel. Has no effect for personal projects.</div>
</div>
</div>
{{ end }}
<div class="edit-form-row">
<div></div>
<div>
{{ if .Editing }}
<input type="submit" value="Save" />
{{ else }}
<a class="button submit" href="javascript:;" onclick="gotoTab('description');">Next</a>
{{ end }}
</div>
</div>
</div>
<div class="tab" data-name="Description" data-slug="description">
<div class="edit-form-row">
<div class="pt-input-ns">Short description:</div>
<div>
<textarea id="description" required maxlength="140" name="shortdesc">
{{- .ProjectSettings.Blurb -}}
</textarea>
<div class="c--dim f7">Plaintext only. No links or markdown.</div>
</div>
</div>
<div class="edit-form-row">
<div class="pt-input-ns">Project links:</div>
<div>
<textarea class="links" name="links" id="links" maxlength="2048" data-max-chars="2048">
{{- .ProjectSettings.LinksText -}}
</textarea>
<div class="c--dim f7">
<div>Relevant links to put on the project page.</div>
<div>Format: url [Title] (e.g. <code>http://example.com/ Example Site</code>)</div>
<div>(1 per line, 10 max)</div>
</div>
</div>
</div>
<div class="edit-form-row">
<div class="pt-input-ns">Full description:</div>
<div>
<textarea id="full_description" class="w-100 h5 minh-5 mono lh-copy" name="full_description">
{{- .ProjectSettings.Description -}}
</textarea>
<div class="flex justify-end items-center mt2">
<div class="upload_bar flex-grow-1">
<div class="instructions">
Upload files by dragging & dropping, pasting, or <label class="pointer link" for="file_input">selecting</label> them.
</div>
<div class="progress flex">
<div class="progress_text mr3"></div>
<div class="progress_bar flex-grow-1 flex-shrink-1 pa1"><div class=""></div></div>
</div>
</div>
</div>
<div class="b mt3 mb2">Preview:</div>
<div id="desc-preview" class="w-100"></div>
</div>
</div>
<div class="edit-form-row">
<div></div>
<div>
{{ if .Editing }}
<input type="submit" value="Save" />
{{ else }}
<a class="button submit" href="javascript:;" onclick="gotoTab('assets');">Next</a>
{{ end }}
</div>
</div>
</div>
<div class="tab" data-name="Assets" data-slug="assets">
<div class="edit-form-row">
<div>Light theme logo:</div>
<div class="light_logo">
{{ template "image_selector.html" imageselectordata "light_logo" .ProjectSettings.LightLogo false }}
</div>
</div>
<div class="edit-form-row">
<div>Dark theme logo:</div>
<div class="dark_logo">
{{ template "image_selector.html" imageselectordata "dark_logo" .ProjectSettings.DarkLogo false }}
</div>
</div>
<div class="edit-form-row">
<div></div>
<div>
{{ if .Editing }}
<input type="submit" value="Save" />
{{ else }}
<input type="submit" value="Create project" />
{{ end }}
</div>
</div>
</div>
</form>
<input type="file" multiple name="file_input" id="file_input" class="dn" />{{/* NOTE(mark): copied NOTE(asaf): Placing this outside the form to avoid submitting it to the server by accident */}}
</div>
<script>
let csrf = JSON.parse({{ csrftokenjs .Session }});
let projectForm = document.querySelector("#project_form");
projectForm.addEventListener("invalid", function(ev) {
switchToTabOfElement(document.body, ev.target);
}, true);
function gotoTab(tabName) {
switchTab(document.body, tabName);
}
//////////
// Tags //
//////////
const tag = document.querySelector('#tag');
const tagPreview = document.querySelector('#tag-preview');
function updateTagPreview() {
tagPreview.innerText = tag.value;
document.querySelector('#tag-discord-info').classList.toggle('dn', tag.value.length === 0);
}
updateTagPreview();
tag.addEventListener('input', () => updateTagPreview());
////////////////////////////
// Description management //
////////////////////////////
{{ if .Editing }}
const projectName = "new-project";
{{ else }}
const projectName = "{{ .Project.Name }}";
{{ end }}
const description = document.querySelector('#full_description');
const descPreview = document.querySelector('#desc-preview');
const { clear: clearDescription } = autosaveContent({
inputEl: description,
storageKey: `project-description/${projectName}`,
});
projectForm.addEventListener('submit', () => clearDescription());
let doMarkdown = initLiveMarkdown({ inputEl: description, previewEl: descPreview });
//////////////////////
// Owner management //
//////////////////////
const OWNER_QUERY_STATE_IDLE = 0;
const OWNER_QUERY_STATE_QUERYING = 1;
const MAX_OWNERS = {{ .MaxOwners }};
let ownerCheckUrl = "{{ .APICheckUsernameUrl }}";
let ownerQueryState = OWNER_QUERY_STATE_IDLE;
let addOwnerInput = document.querySelector("#owner_name");
let addOwnerButton = document.querySelector("#owner_add");
let ownersError = document.querySelector("#owners_error");
let ownerList = document.querySelector("#owner_list");
let ownerTemplate = makeTemplateCloner("owner_row");
addOwnerInput.addEventListener("keypress", function(ev) {
if (ev.which == 13) {
startAddOwner();
ev.preventDefault();
ev.stopPropagation();
}
});
addOwnerButton.addEventListener("click", function(ev) {
startAddOwner();
});
function updateAddOwnerStyles() {
const numOwnerRows = document.querySelectorAll('.owner_row').length;
addOwnerInput.disabled = numOwnerRows >= MAX_OWNERS;
}
updateAddOwnerStyles();
function startAddOwner() {
if (ownerQueryState == OWNER_QUERY_STATE_QUERYING) {
return;
}
let newOwner = addOwnerInput.value.trim().toLowerCase();
if (newOwner.length == 0) {
return;
}
let ownerEls = ownerList.querySelectorAll(".owner_row input[name='owners']");
for (let i = 0; i < ownerEls.length; ++i) {
let existingOwner = ownerEls[i].value.toLowerCase();
if (newOwner == existingOwner) {
return;
}
}
ownersError.textContent = "";
let xhr = new XMLHttpRequest();
xhr.withCredentials = true;
xhr.open("POST", ownerCheckUrl);
xhr.responseType = "json";
xhr.addEventListener("load", function(ev) {
let result = xhr.response;
if (result) {
if (result.found) {
addOwner(result.canonical);
addOwnerInput.value = "";
} else {
ownersError.textContent = "Username not found";
}
} else {
ownersError.textContent = "There was an issue validating this username";
}
setOwnerQueryState(OWNER_QUERY_STATE_IDLE);
if (document.activeElement == addOwnerButton) {
addOwnerInput.focus();
}
});
xhr.addEventListener("error", function(ev) {
ownersError.textContent = "There was an issue validating this username";
setOwnerQueryState(OWNER_QUERY_STATE_IDLE);
});
let data = new FormData();
data.append(csrf.field, csrf.token);
data.append("username", newOwner);
xhr.send(data);
setOwnerQueryState(OWNER_QUERY_STATE_QUERYING);
}
function setOwnerQueryState(state) {
ownerQueryState = state;
querying = (ownerQueryState == OWNER_QUERY_STATE_QUERYING);
addOwnerInput.disabled = querying;
addOwnerButton.disabled = querying;
updateAddOwnerStyles();
}
function addOwner(username) {
let ownerEl = ownerTemplate();
ownerEl.input.value = username;
ownerEl.name.textContent = username;
ownerList.appendChild(ownerEl.root);
updateAddOwnerStyles();
}
ownerList.addEventListener("click", function(ev) {
if (ev.target.classList.contains("remove_owner")) {
ev.target.parentElement.remove();
}
updateAddOwnerStyles();
});
/////////////////////
// Logo management //
/////////////////////
const logoMaxFileSize = {{ .LogoMaxFileSize }};
let lightLogoSelector = new ImageSelector(
document.querySelector("#project_form"),
logoMaxFileSize,
document.querySelector(".light_logo")
);
let darkLogoSelector = new ImageSelector(
document.querySelector("#project_form"),
logoMaxFileSize,
document.querySelector(".dark_logo")
);
//////////////////
// Asset upload //
//////////////////
setupMarkdownUpload(
document.querySelectorAll("#project_form input[type=submit]"),
document.querySelector('#file_input'),
document.querySelector('.upload_bar'),
description,
doMarkdown,
{{ .TextEditor.MaxFileSize }},
{{ .TextEditor.UploadUrl }}
);
</script>
{{ end }}