add news component, style fixes, markdown to html parser
This commit is contained in:
parent
7a9f5b732f
commit
85af81a818
@ -4,23 +4,33 @@
|
||||
for legibility and design, but keep the soul of the original project.
|
||||
*/
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:root {
|
||||
color: #222;
|
||||
font-family: Cambria, Cochin, Georgia, Times, 'Times New Roman', serif;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
* {
|
||||
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 { font-size: 2rem; font-weight: bold; }
|
||||
h2 { font-size: 1.7rem; font-weight: bold; }
|
||||
h3 { font-size: 1.4rem; font-weight: bold; }
|
||||
h4 { font-size: 1.1rem; font-weight: bold; }
|
||||
|
||||
div#container {
|
||||
width: 90vw;
|
||||
display: flex;
|
||||
@ -46,6 +56,10 @@ section#game {
|
||||
margin: 1rem 0;
|
||||
border-top: 2px solid #000;
|
||||
|
||||
& > aside {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
& > aside > section:not(:last-child) {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
@ -58,18 +72,16 @@ section#game {
|
||||
& > aside#left {
|
||||
grid-column: 1;
|
||||
border-right: 2px solid #000;
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
& > main {
|
||||
grid-column: 2;
|
||||
padding: 3px;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
& > aside#right {
|
||||
grid-column: 3;
|
||||
border-left: 2px solid #000;
|
||||
padding: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
@ -100,7 +112,6 @@ div.title {
|
||||
background-color: #eeeeee;
|
||||
font-weight: bold;
|
||||
padding: 5px;
|
||||
margin: 3px;
|
||||
}
|
||||
|
||||
footer {
|
||||
@ -202,6 +213,24 @@ form.standard {
|
||||
& > 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mb-1 {
|
||||
@ -210,4 +239,28 @@ form.standard {
|
||||
|
||||
.mb-05 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
div.town {
|
||||
& > div:not(:last-child) {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
& > div.split {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
|
||||
& > div {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
button.img-button {
|
||||
appearance: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
}
|
48
assets/reset.css
Normal file
48
assets/reset.css
Normal file
@ -0,0 +1,48 @@
|
||||
html, body, div, span, applet, object, iframe,
|
||||
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
|
||||
a, abbr, acronym, address, big, cite, code,
|
||||
del, dfn, em, img, ins, kbd, q, s, samp,
|
||||
small, strike, strong, sub, sup, tt, var,
|
||||
b, u, i, center,
|
||||
dl, dt, dd, ol, ul, li,
|
||||
fieldset, form, label, legend,
|
||||
table, caption, tbody, tfoot, thead, tr, th, td,
|
||||
article, aside, canvas, details, embed,
|
||||
figure, figcaption, footer, header, hgroup,
|
||||
menu, nav, output, ruby, section, summary,
|
||||
time, mark, audio, video {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
font-size: 100%;
|
||||
font: inherit;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
article, aside, details, figcaption, figure,
|
||||
footer, header, hgroup, menu, nav, section {
|
||||
display: block;
|
||||
}
|
||||
|
||||
body {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
ol, ul {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
blockquote, q {
|
||||
quotes: none;
|
||||
}
|
||||
|
||||
blockquote:before, blockquote:after,
|
||||
q:before, q:after {
|
||||
content: '';
|
||||
content: none;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
}
|
117
internal/helpers/markdown/md.go
Normal file
117
internal/helpers/markdown/md.go
Normal file
@ -0,0 +1,117 @@
|
||||
package markdown
|
||||
|
||||
import (
|
||||
"html"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// MarkdownToHTML converts a basic subset of markdown to HTML, for
|
||||
// use in the news posts, forums, or user blurbs.
|
||||
func MarkdownToHTML(s string) string {
|
||||
// Escape HTML entities first to sanitize user input
|
||||
s = html.EscapeString(s)
|
||||
|
||||
// Handle headers first (line-based)
|
||||
lines := strings.Split(s, "\n")
|
||||
for i, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if strings.HasPrefix(line, "### ") {
|
||||
lines[i] = "<h3>" + line[4:] + "</h3>"
|
||||
} else if strings.HasPrefix(line, "## ") {
|
||||
lines[i] = "<h2>" + line[3:] + "</h2>"
|
||||
} else if strings.HasPrefix(line, "# ") {
|
||||
lines[i] = "<h1>" + line[2:] + "</h1>"
|
||||
}
|
||||
}
|
||||
s = strings.Join(lines, "\n")
|
||||
|
||||
// Handle code spans before other formatting
|
||||
s = replaceCodeSpans(s)
|
||||
|
||||
// Handle links [text](url)
|
||||
s = replaceLinks(s)
|
||||
|
||||
// Handle bold and italic
|
||||
s = replaceBoldItalic(s)
|
||||
|
||||
// Handle line breaks last
|
||||
s = strings.ReplaceAll(s, "\n", "<br>")
|
||||
|
||||
return `<div class="md-lib">` + s + "</div>"
|
||||
}
|
||||
|
||||
func replaceCodeSpans(s string) string {
|
||||
for {
|
||||
start := strings.Index(s, "`")
|
||||
if start == -1 {
|
||||
break
|
||||
}
|
||||
end := strings.Index(s[start+1:], "`")
|
||||
if end == -1 {
|
||||
break
|
||||
}
|
||||
end += start + 1
|
||||
code := s[start+1 : end]
|
||||
s = s[:start] + "<code>" + code + "</code>" + s[end+1:]
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func replaceLinks(s string) string {
|
||||
for {
|
||||
linkStart := strings.Index(s, "[")
|
||||
if linkStart == -1 {
|
||||
break
|
||||
}
|
||||
linkEnd := strings.Index(s[linkStart:], "](")
|
||||
if linkEnd == -1 {
|
||||
break
|
||||
}
|
||||
linkEnd += linkStart
|
||||
urlStart := linkEnd + 2
|
||||
urlEnd := strings.Index(s[urlStart:], ")")
|
||||
if urlEnd == -1 {
|
||||
break
|
||||
}
|
||||
urlEnd += urlStart
|
||||
|
||||
text := s[linkStart+1 : linkEnd]
|
||||
url := s[urlStart:urlEnd]
|
||||
s = s[:linkStart] + `<a href="` + url + `">` + text + "</a>" + s[urlEnd+1:]
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func replaceBoldItalic(s string) string {
|
||||
// Handle bold first
|
||||
for strings.Contains(s, "**") {
|
||||
first := strings.Index(s, "**")
|
||||
if first == -1 {
|
||||
break
|
||||
}
|
||||
second := strings.Index(s[first+2:], "**")
|
||||
if second == -1 {
|
||||
break
|
||||
}
|
||||
second += first + 2
|
||||
text := s[first+2 : second]
|
||||
s = s[:first] + "<b>" + text + "</b>" + s[second+2:]
|
||||
}
|
||||
|
||||
// Handle italic
|
||||
for strings.Contains(s, "*") {
|
||||
first := strings.Index(s, "*")
|
||||
if first == -1 {
|
||||
break
|
||||
}
|
||||
second := strings.Index(s[first+1:], "*")
|
||||
if second == -1 {
|
||||
break
|
||||
}
|
||||
second += first + 1
|
||||
text := s[first+1 : second]
|
||||
s = s[:first] + "<i>" + text + "</i>" + s[second+1:]
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
@ -21,9 +21,9 @@ type News struct {
|
||||
// New creates a new News with sensible defaults
|
||||
func New() *News {
|
||||
return &News{
|
||||
Author: 0, // No author by default
|
||||
Posted: time.Now().Unix(), // Current time
|
||||
Content: "", // Empty content
|
||||
Author: 0, // No author by default
|
||||
Posted: time.Now().Unix(), // Current time
|
||||
Content: "", // Empty content
|
||||
}
|
||||
}
|
||||
|
||||
@ -229,6 +229,11 @@ func (n *News) Age() time.Duration {
|
||||
return time.Since(n.PostedTime())
|
||||
}
|
||||
|
||||
// ReadableTime converts a time.Time to a human-readable date string
|
||||
func (n *News) ReadableTime() string {
|
||||
return n.PostedTime().Format("Jan 2, 2006 3:04 PM")
|
||||
}
|
||||
|
||||
// IsAuthor returns true if the given user ID is the author of this news post
|
||||
func (n *News) IsAuthor(userID int) bool {
|
||||
return n.Author == userID
|
||||
@ -317,13 +322,14 @@ func (n *News) ToMap() map[string]any {
|
||||
"Author": n.Author,
|
||||
"Posted": n.Posted,
|
||||
"Content": n.Content,
|
||||
|
||||
|
||||
// Computed values
|
||||
"PostedTime": n.PostedTime(),
|
||||
"IsRecent": n.IsRecent(),
|
||||
"Age": n.Age(),
|
||||
"WordCount": n.WordCount(),
|
||||
"Length": n.Length(),
|
||||
"IsEmpty": n.IsEmpty(),
|
||||
"PostedTime": n.PostedTime(),
|
||||
"IsRecent": n.IsRecent(),
|
||||
"Age": n.Age(),
|
||||
"ReadableTime": n.ReadableTime(),
|
||||
"WordCount": n.WordCount(),
|
||||
"Length": n.Length(),
|
||||
"IsEmpty": n.IsEmpty(),
|
||||
}
|
||||
}
|
||||
|
@ -18,6 +18,8 @@ func RegisterTownRoutes(r *router.Router) {
|
||||
func showTown(ctx router.Ctx, _ []string) {
|
||||
town := ctx.UserValue("town").(*towns.Town)
|
||||
components.RenderPageTemplate(ctx, town.Name, "town/town.html", map[string]any{
|
||||
"town": town,
|
||||
"town": town,
|
||||
"newscontent": components.GenerateTownNews(),
|
||||
"whosonline": components.GenerateTownWhosOnline(),
|
||||
})
|
||||
}
|
||||
|
26
internal/template/components/town.go
Normal file
26
internal/template/components/town.go
Normal file
@ -0,0 +1,26 @@
|
||||
package components
|
||||
|
||||
import (
|
||||
"dk/internal/helpers/markdown"
|
||||
"dk/internal/news"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func GenerateTownNews() string {
|
||||
title := `<div class="title">Latest News</div>`
|
||||
|
||||
news, err := news.Recent(1)
|
||||
if err == nil && len(news) > 0 {
|
||||
item := news[0]
|
||||
content := fmt.Sprintf(`<span class="light">%s</span>`, item.ReadableTime())
|
||||
return title + content + fmt.Sprintf(`<div>%s</div>`, markdown.MarkdownToHTML(item.Content))
|
||||
}
|
||||
|
||||
return title + "<div>No news!</div>"
|
||||
}
|
||||
|
||||
func GenerateTownWhosOnline() string {
|
||||
title := `<div class="title">Who's Online</div>`
|
||||
|
||||
return title + "<div>No one!</div>"
|
||||
}
|
@ -7,12 +7,12 @@
|
||||
|
||||
<div class="row">
|
||||
<label for="id">Email/Username</label>
|
||||
<input id="id" type="text" name="id" value="{id}" required>
|
||||
<input class="text" id="id" type="text" name="id" value="{id}" required>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<label for="password">Password</label>
|
||||
<input id="password" type="password" name="password" required>
|
||||
<input class="text" id="password" type="password" name="password" required>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
|
@ -10,7 +10,7 @@
|
||||
<label for="username">Username</label>
|
||||
<span class="help">Must be 30 alphanumeric characters or less.</span>
|
||||
</div>
|
||||
<input type="text" id="username" name="username" maxlength="30" value="{username}" required>
|
||||
<input class="text" type="text" id="username" name="username" maxlength="30" value="{username}" required>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
@ -18,14 +18,14 @@
|
||||
<label for="password">Password</label>
|
||||
<span class="help">Passwords must be at least 6 characters.</span>
|
||||
</div>
|
||||
<input type="password" id="password" name="password" required class="mb-05">
|
||||
<input class="text" type="password" id="password" name="password" required class="mb-05">
|
||||
<label for="confirm_password">Verify Password</label>
|
||||
<input type="password" id="confirm_password" name="confirm_password" required>
|
||||
<input class="text" type="password" id="confirm_password" name="confirm_password" required>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<label for="email">Email</label>
|
||||
<input type="email" id="email" name="email" maxlength="100" value="{email}" required>
|
||||
<input class="text" type="email" id="email" name="email" maxlength="100" value="{email}" required>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
|
@ -5,6 +5,7 @@
|
||||
<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/dk.css">
|
||||
|
||||
<script>
|
||||
|
@ -10,15 +10,17 @@
|
||||
</div>
|
||||
|
||||
<div class="news">
|
||||
{news}
|
||||
{newscontent}
|
||||
</div>
|
||||
|
||||
<div class="whos-online">
|
||||
{whosonline}
|
||||
</div>
|
||||
<div class="split">
|
||||
<div class="whos-online">
|
||||
{whosonline}
|
||||
</div>
|
||||
|
||||
<div class="babblebox">
|
||||
<div class="title">Babblebox</div>
|
||||
@TODO
|
||||
<div class="babblebox">
|
||||
<div class="title">Babblebox</div>
|
||||
@TODO
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
Loading…
x
Reference in New Issue
Block a user