New project form

This commit is contained in:
Asaf Gartner 2021-11-25 05:59:51 +02:00
parent 0f749d8232
commit 03c82c9d1a
14 changed files with 647 additions and 18 deletions

View File

@ -0,0 +1,97 @@
function ImageSelector(form, maxFileSize, container) {
this.form = form;
this.maxFileSize = maxFileSize;
this.fileInput = container.querySelector(".image_input");
this.removeImageInput = container.querySelector(".remove_input");
this.imageEl = container.querySelector("img");
this.resetLink = container.querySelector(".reset");
this.removeLink = container.querySelector(".remove");
this.originalImageUrl = this.imageEl.getAttribute("data-original");
this.currentImageUrl = this.originalImageUrl;
this.fileInput.value = "";
this.removeImageInput.value = "";
this.setImageUrl(this.originalImageUrl);
this.updateButtons();
this.fileInput.addEventListener("change", function(ev) {
if (this.fileInput.files.length > 0) {
this.handleNewImageFile(this.fileInput.files[0]);
}
}.bind(this));
this.resetLink.addEventListener("click", function(ev) {
this.resetImage();
}.bind(this));
if (this.removeLink) {
this.removeLink.addEventListener("click", function(ev) {
this.removeImage();
}.bind(this));
}
}
ImageSelector.prototype.handleNewImageFile = function(file) {
if (file) {
this.updateSizeLimit(file.size);
this.removeImageInput.value = "";
this.setImageUrl(URL.createObjectURL(file));
this.updateButtons();
}
};
ImageSelector.prototype.removeImage = function() {
this.updateSizeLimit(0);
this.fileInput.value = "";
this.removeImageInput.value = "true";
this.setImageUrl("");
this.updateButtons();
};
ImageSelector.prototype.resetImage = function() {
this.updateSizeLimit(0);
this.fileInput.value = "";
this.removeImageInput.value = "";
this.setImageUrl(this.originalImageUrl);
this.updateButtons();
};
ImageSelector.prototype.updateSizeLimit = function(size) {
this.fileTooBig = size > this.maxFileSize;
if (this.fileTooBig) {
this.setError("File too big. Max filesize is " + this.maxFileSize + " bytes.");
} else {
this.setError("");
}
};
ImageSelector.prototype.setError = function(error) {
this.fileInput.setCustomValidity(error);
this.fileInput.reportValidity();
}
ImageSelector.prototype.setImageUrl = function(url) {
this.currentImageUrl = url;
this.imageEl.src = url;
if (url.length > 0) {
this.imageEl.style.display = "block";
} else {
this.imageEl.style.display = "none";
}
};
ImageSelector.prototype.updateButtons = function() {
if (this.originalImageUrl.length > 0 && this.currentImageUrl != this.originalImageUrl) {
this.resetLink.style.display = "inline-block";
} else {
this.resetLink.style.display = "none";
}
if (!this.fileInput.required && this.currentImageUrl != "") {
this.removeLink.style.display = "inline-block";
} else {
this.removeLink.style.display = "none";
}
};

View File

@ -92,3 +92,15 @@ function switchTab(container, slug) {
window.location.hash = slug;
}
function switchToTabOfElement(container, el) {
const tabs = Array.from(container.querySelectorAll('.tab'));
let target = el.parentElement;
while (target) {
if (tabs.includes(target)) {
switchTab(container, target.getAttribute("data-slug"));
return;
}
target = target.parentElement;
}
}

View File

@ -99,6 +99,61 @@ func init() {
}
adminCommand.AddCommand(activateUserCommand)
userStatusCommand := &cobra.Command{
Use: "userstatus [username] [status]",
Short: "Set a user's status manually",
Run: func(cmd *cobra.Command, args []string) {
if len(args) < 2 {
fmt.Printf("You must provide a username and status.\n\n")
fmt.Printf("Statuses:\n")
fmt.Printf("1. inactive:\n")
fmt.Printf("2. confirmed:\n")
fmt.Printf("3. approved:\n")
fmt.Printf("4. banned:\n")
cmd.Usage()
os.Exit(1)
}
username := args[0]
statusStr := args[1]
status := models.UserStatusInactive
switch statusStr {
case "inactive":
status = models.UserStatusInactive
case "confirmed":
status = models.UserStatusConfirmed
case "approved":
status = models.UserStatusApproved
case "banned":
status = models.UserStatusBanned
default:
fmt.Printf("You must provide a valid status\n\n")
fmt.Printf("Statuses:\n")
fmt.Printf("1. inactive:\n")
fmt.Printf("2. confirmed:\n")
fmt.Printf("3. approved:\n")
fmt.Printf("4. banned:\n")
cmd.Usage()
os.Exit(1)
}
ctx := context.Background()
conn := db.NewConnPool(1, 1)
defer conn.Close()
res, err := conn.Exec(ctx, "UPDATE auth_user SET status = $1 WHERE LOWER(username) = LOWER($2);", status, username)
if err != nil {
panic(err)
}
if res.RowsAffected() == 0 {
fmt.Printf("User not found.\n\n")
}
fmt.Printf("%s is now %s\n\n", username, statusStr)
},
}
adminCommand.AddCommand(userStatusCommand)
sendTestMailCommand := &cobra.Command{
Use: "sendtestmail [type] [toAddress] [toName]",
Short: "Sends a test mail",

View File

@ -302,6 +302,10 @@ func TestEditorPreviewsJS(t *testing.T) {
AssertRegexMatch(t, BuildEditorPreviewsJS(), RegexEditorPreviewsJS, nil)
}
func TestAPICheckUsername(t *testing.T) {
AssertRegexmatch(t, BuildAPICheckUsername(), RegexAPICheckUsername, nil)
}
func TestPublic(t *testing.T) {
AssertRegexMatch(t, BuildPublic("test", false), RegexPublic, nil)
AssertRegexMatch(t, BuildPublic("/test", true), RegexPublic, nil)

View File

@ -284,12 +284,12 @@ func BuildProjectIndex(page int) string {
}
}
var RegexProjectNew = regexp.MustCompile("^/projects/new$")
var RegexProjectNew = regexp.MustCompile("^/p/new$")
func BuildProjectNew() string {
defer CatchPanic()
return Url("/projects/new", nil)
return Url("/p/new", nil)
}
var RegexPersonalProject = regexp.MustCompile("^/p/(?P<projectid>[0-9]+)(/(?P<projectslug>[a-zA-Z0-9-]+))?")
@ -670,6 +670,16 @@ func BuildDiscordShowcaseBacklog() string {
return Url("/discord_showcase_backlog", nil)
}
/*
* API
*/
var RegexAPICheckUsername = regexp.MustCompile("^/api/check_username$")
func BuildAPICheckUsername() string {
return Url("/api/check_username", nil)
}
/*
* User assets
*/

View File

@ -0,0 +1,11 @@
{{/* NOTE(asaf): Make sure to include js/image_selector.js */}}
<input {{ if .Required }}required{{ end }} class="image_input" type="file" accept="image/*" name="{{ .Name }}" />
<input class="remove_input" type="hidden" name="remove_{{ .Name }}" />
<span class="error dn notice notice-failure mv2 mw6"></span>
<div class="image_container">
<img class="mw6" data-original="{{ .Src }}" src="{{ .Src }}" />
<div >
<a href="javascript:;" class="reset">Reset</a>
<a href="javascript:;" class="remove">Remove</a>
</div>
</div>

View File

@ -0,0 +1,287 @@
{{ template "base.html" . }}
{{ define "extrahead" }}
<script src="{{ static "js/tabs.js" }}"></script>
<script src="{{ static "js/image_selector.js" }}"></script>
<script src="{{ static "js/templates.js" }}"></script>
{{ end }}
{{ define "content" }}
<div class="content-block">
{{ if .Editing }}
<h1>Edit {{ .ProjectSettings.Name }}</h1>
{{ else }}
<h1>Create a new 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 }}>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">
{{/* TODO(asaf): Should this be admin only??*/}}
<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">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 ne .ID $.User.ID }}
<a class="remove_owner" href="javascript:;">X</a>
{{ end }}
</div>
{{ end }}
</div>
</div>
</div>
{{ if .User.IsStaff }}
<div class="edit-form-row">
<div class="pt-input-ns">Admin settings</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>
</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>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.</div>
</div>
</div>
{{ end }}
<div class="edit-form-row">
<div></div>
<div>
{{ if .Editing }}
<input type="submit" value="Save" />
{{ else }}
<a class="button" 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 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">Full description:</div>
<div>
<textarea class="w-100 h6 minh-6 mono lh-copy" name="description">
{{- .ProjectSettings.Description -}}
</textarea>
{{/* TODO(asaf): Replace with the full editor */}}
</div>
</div>
<div class="edit-form-row">
<div></div>
<div>
{{ if .Editing }}
<input type="submit" value="Save" />
{{ else }}
<a class="button" 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>
</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);
}
//////////////////////
// Owner management //
//////////////////////
const OWNER_QUERY_STATE_IDLE = 0;
const OWNER_QUERY_STATE_QUERYING = 1;
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 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.open("POST", ownerCheckUrl);
xhr.responseType = "json";
xhr.addEventListener("load", function(ev) {
let result = xhr.response;
if (result.found) {
addOwner(result.canonical);
addOwnerInput.value = "";
} else {
ownersError.textContent = "Username not found";
}
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;
}
function addOwner(username) {
let ownerEl = ownerTemplate();
ownerEl.input.value = username;
ownerEl.name.textContent = username;
ownerList.appendChild(ownerEl.root);
}
ownerList.addEventListener("click", function(ev) {
if (ev.target.classList.contains("remove_owner")) {
ev.target.parentElement.remove();
}
});
/////////////////////
// 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")
);
</script>
{{ end }}

View File

@ -36,31 +36,33 @@
</div>
</div>
<div class="flex-grow-1 overflow-hidden">
{{ with .ProfileUserProjects }}
{{ if or .OwnProfile .ProfileUserProjects }}
<div class="content-block ph3 ph0-ns">
<h2>Projects</h2>
{{ range . }}
<h2>{{ if .OwnProfile }}My {{ end }}Projects</h2>
{{ range .ProfileUserProjects }}
<div class="mv3">
{{ template "project_card.html" projectcarddata . "" }}
</div>
{{ end }}
{{ if .OwnProfile }}
<a href="{{ .NewProjectUrl }}">+ New Project</a>
{{ end }}
</div>
{{ end }}
{{ if eq 1 0 }}
<div class="content-block ph3 ph0-ns">
<h2>Add Snippets</h2>
<div class="note">
Show us what you're working on.<br />
You can upload videos, images, and audio clips.<br />
Your snippets may appear on the <a href="{{ .ShowcaseUrl }}">showcase page</a>.
</div>
</div>
{{ end }}
{{ if gt (len .TimelineItems) 0 }}
<div class="content-block timeline-container ph3 ph0-ns">
<div class="mv3 content-block timeline-container ph3 ph0-ns">
<h2>Recent Activity</h2>
<div class="timeline-filters mb2">
{{/*
{{ if gt .NumForums 0 }}
<div class="dib filter forums mr2"><input data-type="forums" class="v-mid mr1" type="checkbox" id="timeline-checkbox-forums" checked /><label class="v-mid" for="timeline-checkbox-forums">Forums (<span class="count">{{ .NumForums }}</span>)</label></div>
{{ end }}
{{ if gt .NumBlogs 0 }}
<div class="dib filter blogs mr2"><input data-type="blogs" class="v-mid mr1" type="checkbox" id="timeline-checkbox-blogs" checked /><label class="v-mid" for="timeline-checkbox-blogs">Blogs (<span class="count">{{ .NumBlogs }}</span>)</label></div>
{{ end }}
{{ if gt .NumSnippets 0 }}
<div class="dib filter snippets mr2"><input data-type="snippets" class="v-mid mr1" type="checkbox" id="timeline-checkbox-snippets" checked /><label class="v-mid" for="timeline-checkbox-snippets">Snippets (<span class="count">{{ .NumSnippets }}</span>)</label></div>
{{ end }}
*/}}
</div>
<div class="timeline">
{{ range .TimelineItems }}

View File

@ -110,6 +110,9 @@ var HMNTemplateFuncs = template.FuncMap{
"csrftoken": func(s Session) template.HTML {
return template.HTML(fmt.Sprintf(`<input type="hidden" name="%s" value="%s">`, auth.CSRFFieldName, s.CSRFToken))
},
"csrftokenjs": func(s Session) template.HTML {
return template.HTML(fmt.Sprintf(`{ "field": "%s", "token": "%s" }`, auth.CSRFFieldName, s.CSRFToken))
},
"darken": func(amount float64, color noire.Color) noire.Color {
return color.Shade(amount)
},
@ -199,6 +202,14 @@ var HMNTemplateFuncs = template.FuncMap{
}
},
"imageselectordata": func(name string, src string, required bool) ImageSelectorData {
return ImageSelectorData{
Name: name,
Src: src,
Required: required,
}
},
"mediaimage": func() TimelineItemMediaType { return TimelineItemMediaTypeImage },
"mediavideo": func() TimelineItemMediaType { return TimelineItemMediaTypeVideo },
"mediaaudio": func() TimelineItemMediaType { return TimelineItemMediaTypeAudio },

View File

@ -133,6 +133,22 @@ type Project struct {
DateApproved time.Time
}
type ProjectSettings struct {
Name string
Slug string
Hidden bool
Featured bool
Personal bool
Lifecycle string
Blurb string
Description string
Owners []User
LightLogo string
DarkLogo string
}
type User struct {
ID int
Username string
@ -304,6 +320,12 @@ type ProjectCardData struct {
Classes string
}
type ImageSelectorData struct {
Name string
Src string
Required bool
}
type Breadcrumb struct {
Name, Url string
Current bool

54
src/website/api.go Normal file
View File

@ -0,0 +1,54 @@
package website
import (
"errors"
"fmt"
"net/http"
"git.handmade.network/hmn/hmn/src/db"
"git.handmade.network/hmn/hmn/src/models"
"git.handmade.network/hmn/hmn/src/oops"
)
func APICheckUsername(c *RequestContext) ResponseData {
c.Req.ParseForm()
usernameArgs, hasUsername := c.Req.Form["username"]
found := false
canonicalUsername := ""
if hasUsername {
requestedUsername := usernameArgs[0]
found = true
c.Perf.StartBlock("SQL", "Fetch user")
userResult, err := db.QueryOne(c.Context(), c.Conn, models.User{},
`
SELECT $columns
FROM
auth_user
WHERE
LOWER(auth_user.username) = LOWER($1)
AND status = ANY ($2)
`,
requestedUsername,
[]models.UserStatus{models.UserStatusConfirmed, models.UserStatusApproved},
)
c.Perf.EndBlock()
if err != nil {
if errors.Is(err, db.NotFound) {
found = false
} else {
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch user: %s", requestedUsername))
}
} else {
canonicalUsername = userResult.(*models.User).Username
}
}
var res ResponseData
res.Header().Set("Content-Type", "application/json")
if found {
res.Write([]byte(fmt.Sprintf(`{ "found": true, "canonical": "%s" }`, canonicalUsername)))
} else {
res.Write([]byte(`{ "found": false }`))
}
return res
}

View File

@ -362,3 +362,53 @@ func ProjectHomepage(c *RequestContext) ResponseData {
}
return res
}
var ProjectLogoMaxFileSize = 2 * 1024 * 1024
type ProjectEditData struct {
templates.BaseData
Editing bool
ProjectSettings templates.ProjectSettings
APICheckUsernameUrl string
LogoMaxFileSize int
}
func ProjectNew(c *RequestContext) ResponseData {
var project templates.ProjectSettings
project.Owners = append(project.Owners, templates.UserToTemplate(c.CurrentUser, c.Theme))
project.Personal = true
var res ResponseData
res.MustWriteTemplate("project_edit.html", ProjectEditData{
BaseData: getBaseDataAutocrumb(c, "New Project"),
Editing: false,
ProjectSettings: project,
APICheckUsernameUrl: hmnurl.BuildAPICheckUsername(),
LogoMaxFileSize: ProjectLogoMaxFileSize,
}, c.Perf)
return res
}
func ProjectNewSubmit(c *RequestContext) ResponseData {
return FourOhFour(c)
}
func ProjectEdit(c *RequestContext) ResponseData {
// Find project
// Convert to template
var project templates.ProjectSettings
var res ResponseData
res.MustWriteTemplate("project_edit.html", ProjectEditData{
BaseData: getBaseDataAutocrumb(c, "Edit Project"),
Editing: true,
ProjectSettings: project,
}, c.Perf)
return res
}
func ProjectEditSubmit(c *RequestContext) ResponseData {
return FourOhFour(c)
}

View File

@ -196,6 +196,9 @@ func NewWebsiteRoutes(longRequestContext context.Context, conn *pgxpool.Pool) ht
hmnOnly.GET(hmnurl.RegexSnippet, Snippet)
hmnOnly.GET(hmnurl.RegexProjectIndex, ProjectIndex)
hmnOnly.GET(hmnurl.RegexProjectNew, authMiddleware(ProjectNew))
hmnOnly.POST(hmnurl.RegexProjectNew, authMiddleware(csrfMiddleware(ProjectNewSubmit)))
hmnOnly.GET(hmnurl.RegexDiscordOAuthCallback, authMiddleware(DiscordOAuthCallback))
hmnOnly.POST(hmnurl.RegexDiscordUnlink, authMiddleware(csrfMiddleware(DiscordUnlink)))
hmnOnly.POST(hmnurl.RegexDiscordShowcaseBacklog, authMiddleware(csrfMiddleware(DiscordShowcaseBacklog)))
@ -214,6 +217,8 @@ func NewWebsiteRoutes(longRequestContext context.Context, conn *pgxpool.Pool) ht
hmnOnly.GET(hmnurl.RegexPodcastEpisode, PodcastEpisode)
hmnOnly.GET(hmnurl.RegexPodcastRSS, PodcastRSS)
hmnOnly.POST(hmnurl.RegexAPICheckUsername, csrfMiddleware(APICheckUsername))
hmnOnly.GET(hmnurl.RegexLibraryAny, LibraryNotPortedYet)
attachProjectRoutes := func(rb *RouteBuilder) {
@ -225,6 +230,9 @@ func NewWebsiteRoutes(longRequestContext context.Context, conn *pgxpool.Pool) ht
}
})
rb.GET(hmnurl.RegexProjectEdit, authMiddleware(ProjectEdit))
rb.POST(hmnurl.RegexProjectEdit, authMiddleware(csrfMiddleware(ProjectEditSubmit)))
// Middleware used for forum action routes - anything related to actually creating or editing forum content
needsForums := func(h Handler) Handler {
return func(c *RequestContext) ResponseData {

View File

@ -26,6 +26,9 @@ type UserProfileTemplateData struct {
ProfileUserLinks []templates.Link
ProfileUserProjects []templates.Project
TimelineItems []templates.TimelineItem
OwnProfile bool
ShowcaseUrl string
NewProjectUrl string
}
func UserProfile(c *RequestContext) ResponseData {
@ -194,6 +197,9 @@ func UserProfile(c *RequestContext) ResponseData {
ProfileUserLinks: profileUserLinks,
ProfileUserProjects: templateProjects,
TimelineItems: timelineItems,
OwnProfile: c.CurrentUser.ID == profileUser.ID,
ShowcaseUrl: hmnurl.BuildShowcase(),
NewProjectUrl: hmnurl.BuildProjectNew(),
}, c.Perf)
return res
}