early admin edit blog page
This commit is contained in:
parent
65366032fd
commit
82fd17c836
7 changed files with 297 additions and 15 deletions
|
|
@ -15,12 +15,8 @@ func Handler(app *model.AppState) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
|
|
||||||
mux.Handle("/{id}", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
mux.Handle("/{id}", serveBlogPost(app))
|
||||||
blogID := r.PathValue("id")
|
mux.Handle("/", serveBlogIndex(app))
|
||||||
w.Write([]byte(blogID))
|
|
||||||
}))
|
|
||||||
|
|
||||||
mux.Handle("/", handleBlogIndex(app))
|
|
||||||
|
|
||||||
mux.ServeHTTP(w, r)
|
mux.ServeHTTP(w, r)
|
||||||
})
|
})
|
||||||
|
|
@ -38,14 +34,9 @@ type (
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func handleBlogIndex(app *model.AppState) http.Handler {
|
func serveBlogIndex(app *model.AppState) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
session, err := controller.GetSessionFromRequest(app, r)
|
session := r.Context().Value("session").(*model.Session)
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "WARN: Failed to retrieve session: %v\n", err)
|
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
posts, err := controller.GetBlogPosts(app.DB, false, -1, 0)
|
posts, err := controller.GetBlogPosts(app.DB, false, -1, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -108,3 +99,35 @@ func handleBlogIndex(app *model.AppState) http.Handler {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func serveBlogPost(app *model.AppState) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
session := r.Context().Value("session").(*model.Session)
|
||||||
|
|
||||||
|
blogID := r.PathValue("id")
|
||||||
|
|
||||||
|
post, err := controller.GetBlogPost(app.DB, blogID)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("can't find blog with ID %s\n", blogID)
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type blogPostData struct {
|
||||||
|
core.AdminPageData
|
||||||
|
Post *model.BlogPost
|
||||||
|
}
|
||||||
|
|
||||||
|
err = templates.EditBlogTemplate.Execute(w, blogPostData{
|
||||||
|
AdminPageData: core.AdminPageData{
|
||||||
|
Session: session,
|
||||||
|
},
|
||||||
|
Post: post,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "WARN: Error rendering admin edit page for blog %s: %v\n", blogID, err)
|
||||||
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
||||||
100
admin/static/edit-blog.css
Normal file
100
admin/static/edit-blog.css
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
input[type="text"] {
|
||||||
|
padding: .3em .5em;
|
||||||
|
font-size: inherit;
|
||||||
|
font-family: inherit;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
outline: none;
|
||||||
|
color: inherit;
|
||||||
|
background-color: var(--bg-1);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
#blogpost {
|
||||||
|
margin-bottom: 1em;
|
||||||
|
padding: 1.5em;
|
||||||
|
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: var(--bg-2);
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
|
||||||
|
transition: background .1s ease-out, color .1s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
#blogpost label {
|
||||||
|
margin: 1.2em 0 .2em .1em;
|
||||||
|
display: block;
|
||||||
|
font-size: .8em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
#blogpost label:first-of-type {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#blogpost h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#blogpost #title {
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 -.2em;
|
||||||
|
padding: 0 .2em;
|
||||||
|
resize: none;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
font-weight: bold;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--fg-3);
|
||||||
|
outline: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background .1s ease-out, border-color .1s ease-out;
|
||||||
|
|
||||||
|
/*position: relative; outline: none;*/
|
||||||
|
white-space: pre-wrap; overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
#blogpost #title:hover {
|
||||||
|
background-color: var(--bg-3);
|
||||||
|
border-color: var(--fg-0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#blogpost #title:active,
|
||||||
|
#blogpost #title:focus {
|
||||||
|
background-color: var(--bg-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
#blogpost textarea {
|
||||||
|
width: calc(100% - 2em);
|
||||||
|
margin: 0;
|
||||||
|
padding: 1em;
|
||||||
|
display: block;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: var(--bg-1);
|
||||||
|
color: var(--fg-3);
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
resize: vertical;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#blogpost #description {
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
#blogpost select {
|
||||||
|
padding: .5em .8em;
|
||||||
|
font-size: inherit;
|
||||||
|
border: none;
|
||||||
|
border-radius: 10em;
|
||||||
|
color: var(--fg-3);
|
||||||
|
background-color: var(--bg-1);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
#blogpost .blog-actions {
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
63
admin/static/edit-blog.js
Normal file
63
admin/static/edit-blog.js
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
const blogID = document.getElementById("blogpost").dataset.id;
|
||||||
|
const titleInput = document.getElementById("title");
|
||||||
|
const descInput = document.getElementById("description");
|
||||||
|
const mdInput = document.getElementById("markdown");
|
||||||
|
const blueskyActorInput = document.getElementById("bluesky-actor");
|
||||||
|
const blueskyPostInput = document.getElementById("bluesky-post");
|
||||||
|
const visInput = document.getElementById("visibility");
|
||||||
|
const saveBtn = document.getElementById("save");
|
||||||
|
const deleteBtn = document.getElementById("delete");
|
||||||
|
|
||||||
|
saveBtn.addEventListener("click", () => {
|
||||||
|
fetch("/api/v1/blog/" + blogID, {
|
||||||
|
method: "PUT",
|
||||||
|
body: JSON.stringify({
|
||||||
|
title: titleInput.value,
|
||||||
|
description: descInput.value,
|
||||||
|
markdown: mdInput.value,
|
||||||
|
bluesky_actor: blueskyActorInput.value,
|
||||||
|
bluesky_post: blueskyPostInput.value,
|
||||||
|
visible: visInput.value === "true",
|
||||||
|
}),
|
||||||
|
headers: { "Content-Type": "application/json" }
|
||||||
|
}).then(res => {
|
||||||
|
if (!res.ok) {
|
||||||
|
res.text().then(error => {
|
||||||
|
console.error(error);
|
||||||
|
alert("Failed to update blog post: " + error);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = location;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
deleteBtn.addEventListener("click", () => {
|
||||||
|
if (blogID != prompt(
|
||||||
|
"You are about to permanently delete " + blogID + ". " +
|
||||||
|
"This action is irreversible. " +
|
||||||
|
"Please enter \"" + blogID + "\" to continue.")) return;
|
||||||
|
fetch("/api/v1/blog/" + blogID, {
|
||||||
|
method: "DELETE",
|
||||||
|
}).then(res => {
|
||||||
|
if (!res.ok) {
|
||||||
|
res.text().then(error => {
|
||||||
|
console.error(error);
|
||||||
|
alert("Failed to delete blog post: " + error);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = "/admin";
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
[titleInput, descInput, mdInput, blueskyActorInput, blueskyPostInput, visInput].forEach(input => {
|
||||||
|
input.addEventListener("change", () => {
|
||||||
|
saveBtn.disabled = false;
|
||||||
|
});
|
||||||
|
input.addEventListener("keypress", () => {
|
||||||
|
saveBtn.disabled = false;
|
||||||
|
});
|
||||||
|
});
|
||||||
87
admin/templates/html/edit-blog.html
Normal file
87
admin/templates/html/edit-blog.html
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
{{define "head"}}
|
||||||
|
<title>Editing {{.Post.Title}} - ari melody 💫</title>
|
||||||
|
<link rel="shortcut icon" href="/img/favicon.png" type="image/x-icon">
|
||||||
|
<link rel="stylesheet" href="/admin/static/edit-blog.css">
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{define "content"}}
|
||||||
|
<main>
|
||||||
|
<h1>Editing Blog Post</h1>
|
||||||
|
|
||||||
|
<div id="blogpost" data-id="{{.Post.ID}}">
|
||||||
|
<label for="title">Title</label>
|
||||||
|
<h2 id="blog-title">
|
||||||
|
<div
|
||||||
|
id="title"
|
||||||
|
name="title"
|
||||||
|
role="textbox"
|
||||||
|
aria-multiline="true"
|
||||||
|
spellcheck="true"
|
||||||
|
aria-haspopup="listbox"
|
||||||
|
aria-invalid="false"
|
||||||
|
aria-autocomplete="list"
|
||||||
|
autocorrect="off"
|
||||||
|
contenteditable="true"
|
||||||
|
zindex="-1"
|
||||||
|
>{{.Post.Title}}</div>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<label for="description">Description</label>
|
||||||
|
<textarea
|
||||||
|
id="description"
|
||||||
|
name="description"
|
||||||
|
value="{{.Post.Description}}"
|
||||||
|
placeholder="No description provided."
|
||||||
|
rows="3"
|
||||||
|
>{{.Post.Description}}</textarea>
|
||||||
|
|
||||||
|
<label for="markdown">Markdown</label>
|
||||||
|
<textarea
|
||||||
|
id="markdown"
|
||||||
|
name="markdown"
|
||||||
|
value="{{.Post.Markdown}}"
|
||||||
|
rows="30"
|
||||||
|
>{{.Post.Markdown}}</textarea>
|
||||||
|
|
||||||
|
<label for="bluesky-actor">Bluesky Author DID</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="bluesky-actor"
|
||||||
|
id="bluesky-actor"
|
||||||
|
placeholder="did:plc:1234abcd..."
|
||||||
|
value="{{if .Post.BlueskyActorID}}{{.Post.BlueskyActorID}}{{end}}">
|
||||||
|
<label for="bluesky-post">Bluesky Post ID</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="bluesky-post"
|
||||||
|
id="bluesky-post"
|
||||||
|
placeholder="3m109a03..."
|
||||||
|
value="{{if .Post.BlueskyPostID}}{{.Post.BlueskyPostID}}{{end}}">
|
||||||
|
|
||||||
|
<label for="visibility">Visibility</label>
|
||||||
|
<select name="visibility" id="visibility">
|
||||||
|
<option value="true"{{if .Post.Visible}} selected{{end}}>Visible</option>
|
||||||
|
<option value="false"{{if not .Post.Visible}} selected{{end}}>Hidden</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<div class="blog-actions">
|
||||||
|
<a href="/blog/{{.Post.ID}}" class="button">View</a>
|
||||||
|
<button type="submit" class="save" id="save" disabled>Save</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card" id="danger">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2>Danger Zone</h2>
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
Clicking the button below will delete this blog post.
|
||||||
|
This action is <strong>irreversible</strong>.
|
||||||
|
You will be prompted to confirm this decision.
|
||||||
|
</p>
|
||||||
|
<button class="delete" id="delete">Delete Release</button>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script type="module" src="/admin/static/edit-blog.js"></script>
|
||||||
|
{{end}}
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
{{define "content"}}
|
{{define "content"}}
|
||||||
<main>
|
<main>
|
||||||
<h1>Editing {{.Release.Title}}</h1>
|
<h1>Editing Release</h1>
|
||||||
|
|
||||||
<div id="release" data-id="{{.Release.ID}}">
|
<div id="release" data-id="{{.Release.ID}}">
|
||||||
<div class="release-artwork">
|
<div class="release-artwork">
|
||||||
|
|
|
||||||
|
|
@ -200,3 +200,12 @@ var BlogsTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(
|
||||||
componentBlogPostHTML,
|
componentBlogPostHTML,
|
||||||
}, "\n"),
|
}, "\n"),
|
||||||
))
|
))
|
||||||
|
|
||||||
|
//go:embed "html/edit-blog.html"
|
||||||
|
var editBlogHTML string
|
||||||
|
var EditBlogTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(
|
||||||
|
strings.Join([]string{
|
||||||
|
editBlogHTML,
|
||||||
|
componentBlogPostHTML,
|
||||||
|
}, "\n"),
|
||||||
|
))
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue