begin admin panel, create news
This commit is contained in:
parent
af0ba28c58
commit
c7c76b413c
230
assets/admin.css
Normal file
230
assets/admin.css
Normal file
@ -0,0 +1,230 @@
|
|||||||
|
@font-face {
|
||||||
|
font-family: 'Seagram';
|
||||||
|
src: url('/assets/fonts/seagram.ttf') format('truetype');
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
color: #222;
|
||||||
|
font-family: Cambria, Cochin, Georgia, Times, 'Times New Roman', serif;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seagram {
|
||||||
|
font-family: 'Seagram', Cambria, Cochin, Georgia, Times, 'Times New Roman', serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
padding: 1rem;
|
||||||
|
background-image: url("/assets/images/backgrounds/snowstorm.jpg");
|
||||||
|
}
|
||||||
|
|
||||||
|
i { font-style: italic; }
|
||||||
|
b { font-weight: bold; }
|
||||||
|
|
||||||
|
h1, h2, h3, h4, h5 {
|
||||||
|
font-family: 'Seagram', Cambria, Cochin, Georgia, Times, 'Times New Roman', serif;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
h1 { font-size: 2rem; }
|
||||||
|
h2 { font-size: 1.7rem; }
|
||||||
|
h3 { font-size: 1.4rem; }
|
||||||
|
h4 { font-size: 1.1rem; }
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div#container {
|
||||||
|
max-width: 1024px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 180px 1fr;
|
||||||
|
margin: 0px auto;
|
||||||
|
gap: 1rem;
|
||||||
|
|
||||||
|
& > footer {
|
||||||
|
grid-column: 1 / -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nav {
|
||||||
|
border-right: 2px solid black;
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
|
||||||
|
& > section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
border-bottom: 2px solid black;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
|
||||||
|
th, td {
|
||||||
|
padding: 0.5rem;
|
||||||
|
text-align: left;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
tr:nth-child(even) {
|
||||||
|
background-color: rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #663300;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #330000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.small {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.highlight {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
.light {
|
||||||
|
color: rgba(0, 0, 0, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
button.btn {
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 1rem;
|
||||||
|
appearance: none;
|
||||||
|
outline: none;
|
||||||
|
background-color: #808080;
|
||||||
|
background-image: url("/assets/images/overlay.png");
|
||||||
|
border: 1px solid #808080;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.2);
|
||||||
|
text-shadow: 0px 1px 1px rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #909090;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.btn-primary {
|
||||||
|
color: rgba(0, 0, 0, 0.75);
|
||||||
|
background-color: #F2994A;
|
||||||
|
background-image: url("/assets/images/overlay.png"), linear-gradient(to bottom, #F2C94C, #F2994A);
|
||||||
|
border-color: #F2994A;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #ffb574;
|
||||||
|
background-image: url("/assets/images/overlay.png"), linear-gradient(to bottom, #ffd965, #ffb574);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.btn-blue {
|
||||||
|
background-color: #00c6ff;
|
||||||
|
background-image: url("/assets/images/overlay.png"), linear-gradient(to bottom, #00c6ff, #0072ff);
|
||||||
|
border-color: #0072ff;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #49d6fd;
|
||||||
|
background-image: url("/assets/images/overlay.png"), linear-gradient(to bottom, #49d6fd, #2987fa);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
form.standard {
|
||||||
|
& > div.row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
|
||||||
|
label {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
span.help {
|
||||||
|
font-size: 0.8em;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > div.actions {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.text {
|
||||||
|
appearance: none;
|
||||||
|
outline: none;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
padding: 0.25rem 0.25rem;
|
||||||
|
background-color: rgba(0, 0, 0, 0.4);
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
background-color: rgba(0, 0, 0, 0.6);
|
||||||
|
border-color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea.text {
|
||||||
|
appearance: none;
|
||||||
|
outline: none;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
padding: 0.25rem 0.25rem;
|
||||||
|
background-color: rgba(0, 0, 0, 0.4);
|
||||||
|
color: white;
|
||||||
|
min-height: 100px;
|
||||||
|
resize: vertical;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
background-color: rgba(0, 0, 0, 0.6);
|
||||||
|
border-color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.w-full {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mb-1 {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
@ -1,9 +1,3 @@
|
|||||||
/*
|
|
||||||
In general, we don't want to stray too far from the original appearance.
|
|
||||||
We'll optimize the layout for modern CSS and use better styles and fonts
|
|
||||||
for legibility and design, but keep the soul of the original project.
|
|
||||||
*/
|
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Seagram';
|
font-family: 'Seagram';
|
||||||
src: url('/assets/fonts/seagram.ttf') format('truetype');
|
src: url('/assets/fonts/seagram.ttf') format('truetype');
|
||||||
@ -279,7 +273,7 @@ form.standard {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
textarea.forum-text {
|
textarea.text {
|
||||||
appearance: none;
|
appearance: none;
|
||||||
outline: none;
|
outline: none;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
@ -321,8 +315,8 @@ form.standard {
|
|||||||
}
|
}
|
||||||
|
|
||||||
div.town {
|
div.town {
|
||||||
& > section:not(:last-child) {
|
hr {
|
||||||
margin-bottom: 2rem;
|
margin: 2rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
& > section.split {
|
& > section.split {
|
||||||
|
BIN
data/dk.db
BIN
data/dk.db
Binary file not shown.
@ -6,6 +6,7 @@ import (
|
|||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"dk/internal/models/users"
|
||||||
"dk/internal/template"
|
"dk/internal/template"
|
||||||
|
|
||||||
sushi "git.sharkk.net/Sharkk/Sushi"
|
sushi "git.sharkk.net/Sharkk/Sushi"
|
||||||
@ -44,7 +45,7 @@ func RenderPage(ctx sushi.Ctx, title, tmplPath string, additionalData map[string
|
|||||||
"_totaltime": totalTime,
|
"_totaltime": totalTime,
|
||||||
"_version": "1.0.0",
|
"_version": "1.0.0",
|
||||||
"_build": "dev",
|
"_build": "dev",
|
||||||
"user": ctx.GetCurrentUser(),
|
"user": ctx.GetCurrentUser().(*users.User),
|
||||||
"_memalloc": m.Alloc / 1024 / 1024,
|
"_memalloc": m.Alloc / 1024 / 1024,
|
||||||
"_errormsg": sess.GetFlashMessage("error"),
|
"_errormsg": sess.GetFlashMessage("error"),
|
||||||
"_successmsg": sess.GetFlashMessage("success"),
|
"_successmsg": sess.GetFlashMessage("success"),
|
||||||
@ -71,3 +72,44 @@ func PageTitle(title string) string {
|
|||||||
|
|
||||||
return title + " - Dragon Knight"
|
return title + " - Dragon Knight"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RenderAdminPage renders a page using the admin layout template with common data and additional custom data
|
||||||
|
func RenderAdminPage(ctx sushi.Ctx, title, tmplPath string, additionalData map[string]any) error {
|
||||||
|
if template.Cache == nil {
|
||||||
|
return fmt.Errorf("template.Cache not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpl, err := template.Cache.Load(tmplPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to load layout template: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var m runtime.MemStats
|
||||||
|
runtime.ReadMemStats(&m)
|
||||||
|
|
||||||
|
sess := ctx.GetCurrentSession()
|
||||||
|
|
||||||
|
seconds := timing.GetRequestDuration(ctx).Seconds()
|
||||||
|
var totalTime string
|
||||||
|
if seconds < 0.001 {
|
||||||
|
totalTime = fmt.Sprintf("%.0f", seconds)
|
||||||
|
} else {
|
||||||
|
totalTime = fmt.Sprintf("%.3f", seconds)
|
||||||
|
}
|
||||||
|
|
||||||
|
data := map[string]any{
|
||||||
|
"_title": PageTitle(title),
|
||||||
|
"csrf": csrf.HiddenField(ctx),
|
||||||
|
"_totaltime": totalTime,
|
||||||
|
"_version": "1.0.0",
|
||||||
|
"_build": "dev",
|
||||||
|
"user": ctx.GetCurrentUser().(*users.User),
|
||||||
|
"_memalloc": m.Alloc / 1024 / 1024,
|
||||||
|
"_errormsg": sess.GetFlashMessage("error"),
|
||||||
|
"_successmsg": sess.GetFlashMessage("success"),
|
||||||
|
}
|
||||||
|
|
||||||
|
maps.Copy(data, additionalData)
|
||||||
|
|
||||||
|
return tmpl.WriteTo(ctx, data)
|
||||||
|
}
|
||||||
|
@ -9,12 +9,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func GenerateTownNews() string {
|
func GenerateTownNews() string {
|
||||||
title := `<h4 class="mb-025">Latest News</h4>`
|
title := `<div class="mb-1"><h4 class="mb-025">Latest News</h4>`
|
||||||
|
|
||||||
news, err := news.Recent(1)
|
news, err := news.Recent(1)
|
||||||
if err == nil && len(news) > 0 {
|
if err == nil && len(news) > 0 {
|
||||||
item := news[0]
|
item := news[0]
|
||||||
content := fmt.Sprintf(`<span class="light">%s</span>`, item.ReadableTime())
|
content := fmt.Sprintf(`<span class="light">%s</span></div>`, item.ReadableTime())
|
||||||
return title + content + fmt.Sprintf(`<div>%s</div>`, markdown.MarkdownToHTML(item.Content))
|
return title + content + fmt.Sprintf(`<div>%s</div>`, markdown.MarkdownToHTML(item.Content))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
83
internal/routes/admin.go
Normal file
83
internal/routes/admin.go
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"dk/internal/components"
|
||||||
|
"dk/internal/models/news"
|
||||||
|
"dk/internal/models/users"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
sushi "git.sharkk.net/Sharkk/Sushi"
|
||||||
|
"git.sharkk.net/Sharkk/Sushi/auth"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RegisterAdminRoutes(app *sushi.App) {
|
||||||
|
group := app.Group("/admin")
|
||||||
|
group.Use(auth.RequireAuth())
|
||||||
|
group.Use(func(ctx sushi.Ctx, next func()) {
|
||||||
|
if ctx.GetCurrentUser().(*users.User).Auth < 4 {
|
||||||
|
ctx.Redirect("/")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
next()
|
||||||
|
})
|
||||||
|
|
||||||
|
group.Get("/", adminIndex)
|
||||||
|
group.Get("/news", adminNewsForm)
|
||||||
|
group.Post("/news", adminNewsCreate)
|
||||||
|
}
|
||||||
|
|
||||||
|
func adminIndex(ctx sushi.Ctx) {
|
||||||
|
var m runtime.MemStats
|
||||||
|
runtime.ReadMemStats(&m)
|
||||||
|
|
||||||
|
components.RenderAdminPage(ctx, "", "admin/home.html", map[string]any{
|
||||||
|
"alloc_mb": bToMb(m.Alloc),
|
||||||
|
"total_alloc_mb": bToMb(m.TotalAlloc),
|
||||||
|
"sys_mb": bToMb(m.Sys),
|
||||||
|
"heap_alloc_mb": bToMb(m.HeapAlloc),
|
||||||
|
"heap_sys_mb": bToMb(m.HeapSys),
|
||||||
|
"heap_released_mb": bToMb(m.HeapReleased),
|
||||||
|
"gc_cycles": m.NumGC,
|
||||||
|
"gc_pause_total": m.PauseTotalNs / 1000000, // ms
|
||||||
|
"goroutines": runtime.NumGoroutine(),
|
||||||
|
"cpu_cores": runtime.NumCPU(),
|
||||||
|
"go_version": runtime.Version(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func adminNewsForm(ctx sushi.Ctx) {
|
||||||
|
components.RenderAdminPage(ctx, "", "admin/news.html", map[string]any{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func adminNewsCreate(ctx sushi.Ctx) {
|
||||||
|
sess := ctx.GetCurrentSession()
|
||||||
|
content := strings.TrimSpace(ctx.Form("content").String())
|
||||||
|
|
||||||
|
if content == "" {
|
||||||
|
sess.SetFlash("error", "Content cannot be empty")
|
||||||
|
ctx.Redirect("/admin/news")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user := ctx.GetCurrentUser().(*users.User)
|
||||||
|
newsPost := &news.News{
|
||||||
|
Author: user.ID,
|
||||||
|
Content: content,
|
||||||
|
Posted: time.Now().Unix(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := newsPost.Insert(); err != nil {
|
||||||
|
sess.SetFlash("error", "Failed to create news post")
|
||||||
|
ctx.Redirect("/admin/news")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sess.SetFlash("success", "News post created successfully")
|
||||||
|
ctx.Redirect("/admin")
|
||||||
|
}
|
||||||
|
|
||||||
|
func bToMb(b uint64) uint64 {
|
||||||
|
return b / 1024 / 1024
|
||||||
|
}
|
@ -60,9 +60,9 @@ func (r *replyData) PostedTime() time.Time {
|
|||||||
func RegisterForumRoutes(app *sushi.App) {
|
func RegisterForumRoutes(app *sushi.App) {
|
||||||
authed := app.Group("/forum")
|
authed := app.Group("/forum")
|
||||||
authed.Use(auth.RequireAuth())
|
authed.Use(auth.RequireAuth())
|
||||||
authed.Get("/", index)
|
authed.Get("/", forumIndex)
|
||||||
authed.Get("/new", showNew)
|
authed.Get("/new", showNew)
|
||||||
authed.Post("/new", new)
|
authed.Post("/new", newThread)
|
||||||
authed.Get("/:id", showThread)
|
authed.Get("/:id", showThread)
|
||||||
authed.Post("/:id/reply", reply)
|
authed.Post("/:id/reply", reply)
|
||||||
}
|
}
|
||||||
@ -95,21 +95,21 @@ func getPagination(ctx sushi.Ctx, perPage int) helpers.Pagination {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func index(ctx sushi.Ctx) {
|
func forumIndex(ctx sushi.Ctx) {
|
||||||
pagination := getPagination(ctx, 30)
|
pagination := getPagination(ctx, 30)
|
||||||
|
|
||||||
// Get threads with user data
|
// Get threads with user data
|
||||||
var threads []*threadData
|
var threads []*threadData
|
||||||
query := `
|
query := `
|
||||||
SELECT f.id, f.posted, f.last_post, f.author, f.parent, f.replies, f.title, f.content,
|
SELECT f.id, f.posted, f.last_post, f.author, f.parent, f.replies, f.title, f.content,
|
||||||
COALESCE(u.username, '[Deleted]') as author_username,
|
COALESCE(u.username, '[Deleted]') as author_username,
|
||||||
COALESCE(u.level, 1) as author_level,
|
COALESCE(u.level, 1) as author_level,
|
||||||
COALESCE(c.name, 'Unknown') as author_class
|
COALESCE(c.name, 'Unknown') as author_class
|
||||||
FROM forum f
|
FROM forum f
|
||||||
LEFT JOIN users u ON f.author = u.id
|
LEFT JOIN users u ON f.author = u.id
|
||||||
LEFT JOIN classes c ON u.class_id = c.id
|
LEFT JOIN classes c ON u.class_id = c.id
|
||||||
WHERE f.parent = 0
|
WHERE f.parent = 0
|
||||||
ORDER BY f.last_post DESC, f.id DESC
|
ORDER BY f.last_post DESC, f.id DESC
|
||||||
LIMIT %d OFFSET %d`
|
LIMIT %d OFFSET %d`
|
||||||
|
|
||||||
err := database.Select(&threads, query, pagination.PerPage, pagination.Offset())
|
err := database.Select(&threads, query, pagination.PerPage, pagination.Offset())
|
||||||
@ -129,10 +129,10 @@ func index(ctx sushi.Ctx) {
|
|||||||
var lastReply LastReply
|
var lastReply LastReply
|
||||||
err := database.Get(&lastReply, `
|
err := database.Get(&lastReply, `
|
||||||
SELECT COALESCE(u.username, '[Deleted]') as username
|
SELECT COALESCE(u.username, '[Deleted]') as username
|
||||||
FROM forum f
|
FROM forum f
|
||||||
LEFT JOIN users u ON f.author = u.id
|
LEFT JOIN users u ON f.author = u.id
|
||||||
WHERE f.parent = %d
|
WHERE f.parent = %d
|
||||||
ORDER BY f.posted DESC
|
ORDER BY f.posted DESC
|
||||||
LIMIT 1`, threads[i].ID)
|
LIMIT 1`, threads[i].ID)
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@ -163,7 +163,7 @@ func showNew(ctx sushi.Ctx) {
|
|||||||
components.RenderPage(ctx, "New Forum Thread", "forum/new.html", map[string]any{})
|
components.RenderPage(ctx, "New Forum Thread", "forum/new.html", map[string]any{})
|
||||||
}
|
}
|
||||||
|
|
||||||
func new(ctx sushi.Ctx) {
|
func newThread(ctx sushi.Ctx) {
|
||||||
sess := ctx.GetCurrentSession()
|
sess := ctx.GetCurrentSession()
|
||||||
|
|
||||||
title := strings.TrimSpace(ctx.Form("title").String())
|
title := strings.TrimSpace(ctx.Form("title").String())
|
||||||
@ -212,7 +212,7 @@ func showThread(ctx sushi.Ctx) {
|
|||||||
var threadData threadData
|
var threadData threadData
|
||||||
err := database.Get(&threadData, `
|
err := database.Get(&threadData, `
|
||||||
SELECT f.id, f.posted, f.last_post, f.author, f.parent, f.replies, f.title, f.content,
|
SELECT f.id, f.posted, f.last_post, f.author, f.parent, f.replies, f.title, f.content,
|
||||||
COALESCE(u.username, '[Deleted]') as author_username,
|
COALESCE(u.username, '[Deleted]') as author_username,
|
||||||
COALESCE(u.level, 1) as author_level,
|
COALESCE(u.level, 1) as author_level,
|
||||||
COALESCE(c.name, 'Unknown') as author_class
|
COALESCE(c.name, 'Unknown') as author_class
|
||||||
FROM forum f
|
FROM forum f
|
||||||
@ -232,14 +232,14 @@ func showThread(ctx sushi.Ctx) {
|
|||||||
var replies []*replyData
|
var replies []*replyData
|
||||||
err = database.Select(&replies, `
|
err = database.Select(&replies, `
|
||||||
SELECT f.id, f.posted, f.author, f.title, f.content,
|
SELECT f.id, f.posted, f.author, f.title, f.content,
|
||||||
COALESCE(u.username, '[Deleted]') as author_username,
|
COALESCE(u.username, '[Deleted]') as author_username,
|
||||||
COALESCE(u.level, 1) as author_level,
|
COALESCE(u.level, 1) as author_level,
|
||||||
COALESCE(c.name, 'Unknown') as author_class
|
COALESCE(c.name, 'Unknown') as author_class
|
||||||
FROM forum f
|
FROM forum f
|
||||||
LEFT JOIN users u ON f.author = u.id
|
LEFT JOIN users u ON f.author = u.id
|
||||||
LEFT JOIN classes c ON u.class_id = c.id
|
LEFT JOIN classes c ON u.class_id = c.id
|
||||||
WHERE f.parent = %d
|
WHERE f.parent = %d
|
||||||
ORDER BY f.posted ASC
|
ORDER BY f.posted ASC
|
||||||
LIMIT %d OFFSET %d`, id, pagination.PerPage, pagination.Offset())
|
LIMIT %d OFFSET %d`, id, pagination.PerPage, pagination.Offset())
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
1
main.go
1
main.go
@ -188,6 +188,7 @@ func start(port string) error {
|
|||||||
routes.RegisterFightRoutes(app)
|
routes.RegisterFightRoutes(app)
|
||||||
routes.RegisterForumRoutes(app)
|
routes.RegisterForumRoutes(app)
|
||||||
routes.RegisterHelpRoutes(app)
|
routes.RegisterHelpRoutes(app)
|
||||||
|
routes.RegisterAdminRoutes(app)
|
||||||
|
|
||||||
app.Get("/assets/*path", sushi.Static(cwd))
|
app.Get("/assets/*path", sushi.Static(cwd))
|
||||||
|
|
||||||
|
24
templates/admin/home.html
Normal file
24
templates/admin/home.html
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
{include "admin/layout.html"}
|
||||||
|
|
||||||
|
{block "content"}
|
||||||
|
<h1>Admininistration</h1>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Welcome to the admin panel! Utilize the tools to the left, or <a href="/">head back</a>. Below
|
||||||
|
are the server stats as of loading this page.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr><td>Memory Allocated</td> <td>{alloc_mb} MB</td></tr>
|
||||||
|
<tr><td>Total Allocated</td> <td>{total_alloc_mb} MB</td></tr>
|
||||||
|
<tr><td>System Memory</td> <td>{sys_mb} MB</td></tr>
|
||||||
|
<tr><td>Heap Allocated</td> <td>{heap_alloc_mb} MB</td></tr>
|
||||||
|
<tr><td>Heap System</td> <td>{heap_sys_mb} MB</td></tr>
|
||||||
|
<tr><td>Heap Released</td> <td>{heap_released_mb} MB</td></tr>
|
||||||
|
<tr><td>GC Cycles</td> <td>{gc_cycles}</td></tr>
|
||||||
|
<tr><td>GC Pause Total</td> <td>{gc_pause_total} ms</td></tr>
|
||||||
|
<tr><td>Goroutines</td> <td>{goroutines}</td></tr>
|
||||||
|
<tr><td>CPU Cores</td> <td>{cpu_cores}</td></tr>
|
||||||
|
<tr><td>Go Version</td> <td>{go_version}</td></tr>
|
||||||
|
</table>
|
||||||
|
{/block}
|
43
templates/admin/layout.html
Normal file
43
templates/admin/layout.html
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{_title}</title>
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="/assets/reset.css">
|
||||||
|
<link rel="stylesheet" href="/assets/admin.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="container">
|
||||||
|
<nav>
|
||||||
|
<section>
|
||||||
|
<a href="/admin">Admin Home</a>
|
||||||
|
<a href="/">Game Home</a>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<a href="/admin/control">Main Settings</a>
|
||||||
|
<a href="/admin/news">Add News Post</a>
|
||||||
|
<a href="/admin/users">Edit Users</a>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<a href="/admin/items">Edit Items</a>
|
||||||
|
<a href="/admin/towns">Edit Towns</a>
|
||||||
|
<a href="/admin/monsters">Edit Monsters</a>
|
||||||
|
<a href="/admin/spells">Edit Spells</a>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<div>{_totaltime} Seconds, {_memalloc} MiB</div>
|
||||||
|
<div>Version {_version} {_build}</div>
|
||||||
|
</footer>
|
||||||
|
</nav>
|
||||||
|
<main>
|
||||||
|
{include "flashes.html"}
|
||||||
|
{yield "content"}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
20
templates/admin/news.html
Normal file
20
templates/admin/news.html
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{include "admin/layout.html"}
|
||||||
|
|
||||||
|
{block "content"}
|
||||||
|
<h1>Add News Post</h1>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Write a new news post that will be displayed in every town! You can use basic markdown to style the post.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<form class="standard" method="post">
|
||||||
|
{csrf}
|
||||||
|
<div>
|
||||||
|
<textarea name="content" id="content" class="text w-full mb-1" required placholder="Write the news here!"></textarea>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a href="/admin"><button type="button" class="btn">Cancel</button></a>
|
||||||
|
<button type="submit" class="btn btn-primary">Create Post</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{/block}
|
@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
<div class="mb-1">
|
<div class="mb-1">
|
||||||
<div class="mb-025"><input type="text" class="text w-full" placeholder="Thread title" name="title"></div>
|
<div class="mb-025"><input type="text" class="text w-full" placeholder="Thread title" name="title"></div>
|
||||||
<textarea class="forum-text w-full" placeholder="Type your message here!" name="content"></textarea>
|
<textarea class="text w-full" placeholder="Type your message here!" name="content"></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
@ -51,9 +51,9 @@
|
|||||||
{if hasPrev}
|
{if hasPrev}
|
||||||
<a href="/forum/{thread.ID}?page={currentPage - 1}"><button class="btn">Previous</button></a>
|
<a href="/forum/{thread.ID}?page={currentPage - 1}"><button class="btn">Previous</button></a>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<span>Page {currentPage} of {totalPages}</span>
|
<span>Page {currentPage} of {totalPages}</span>
|
||||||
|
|
||||||
{if hasNext}
|
{if hasNext}
|
||||||
<a href="/forum/{thread.ID}?page={currentPage + 1}"><button class="btn">Next</button></a>
|
<a href="/forum/{thread.ID}?page={currentPage + 1}"><button class="btn">Next</button></a>
|
||||||
{/if}
|
{/if}
|
||||||
@ -66,7 +66,7 @@
|
|||||||
{csrf}
|
{csrf}
|
||||||
|
|
||||||
<div class="mb-1">
|
<div class="mb-1">
|
||||||
<textarea class="forum-text w-full" placeholder="Type your reply here!" name="content" rows="10"></textarea>
|
<textarea class="text w-full" placeholder="Type your reply here!" name="content"></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@ -74,4 +74,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
{/block}
|
{/block}
|
||||||
|
@ -50,6 +50,9 @@
|
|||||||
<a href="/change-password">Change Password</a>
|
<a href="/change-password">Change Password</a>
|
||||||
<a class="logout-link">Log Out</a>
|
<a class="logout-link">Log Out</a>
|
||||||
<a href="/help">Help</a>
|
<a href="/help">Help</a>
|
||||||
|
{if user.Auth >= 4}
|
||||||
|
<a href="/admin">Admin</a>
|
||||||
|
{/if}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<form id="logout-form" class="hidden" action="/logout" method="post">
|
<form id="logout-form" class="hidden" action="/logout" method="post">
|
||||||
|
@ -11,10 +11,14 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
<section class="news">
|
<section class="news">
|
||||||
{newscontent}
|
{newscontent}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
<section class="split">
|
<section class="split">
|
||||||
<section class="whos-online">
|
<section class="whos-online">
|
||||||
{whosonline}
|
{whosonline}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user