diff --git a/assets/dk.css b/assets/dk.css index 00e9478..f5b4c2f 100644 --- a/assets/dk.css +++ b/assets/dk.css @@ -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; } \ No newline at end of file diff --git a/assets/reset.css b/assets/reset.css new file mode 100644 index 0000000..20637d5 --- /dev/null +++ b/assets/reset.css @@ -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; +} \ No newline at end of file diff --git a/internal/helpers/markdown/md.go b/internal/helpers/markdown/md.go new file mode 100644 index 0000000..22c54a6 --- /dev/null +++ b/internal/helpers/markdown/md.go @@ -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] = "

" + line[4:] + "

" + } else if strings.HasPrefix(line, "## ") { + lines[i] = "

" + line[3:] + "

" + } else if strings.HasPrefix(line, "# ") { + lines[i] = "

" + line[2:] + "

" + } + } + 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", "
") + + return `
` + s + "
" +} + +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 + "" + 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] + `` + text + "" + 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] + "" + text + "" + 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] + "" + text + "" + s[second+1:] + } + + return s +} diff --git a/internal/news/news.go b/internal/news/news.go index 7949be8..a06e9e8 100644 --- a/internal/news/news.go +++ b/internal/news/news.go @@ -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(), } } diff --git a/internal/routes/town.go b/internal/routes/town.go index ad61ee1..47bcc57 100644 --- a/internal/routes/town.go +++ b/internal/routes/town.go @@ -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(), }) } diff --git a/internal/template/components/town.go b/internal/template/components/town.go new file mode 100644 index 0000000..24ca39e --- /dev/null +++ b/internal/template/components/town.go @@ -0,0 +1,26 @@ +package components + +import ( + "dk/internal/helpers/markdown" + "dk/internal/news" + "fmt" +) + +func GenerateTownNews() string { + title := `
Latest News
` + + news, err := news.Recent(1) + if err == nil && len(news) > 0 { + item := news[0] + content := fmt.Sprintf(`%s`, item.ReadableTime()) + return title + content + fmt.Sprintf(`
%s
`, markdown.MarkdownToHTML(item.Content)) + } + + return title + "
No news!
" +} + +func GenerateTownWhosOnline() string { + title := `
Who's Online
` + + return title + "
No one!
" +} diff --git a/templates/auth/login.html b/templates/auth/login.html index 4915284..445cfbd 100644 --- a/templates/auth/login.html +++ b/templates/auth/login.html @@ -7,12 +7,12 @@
- +
- +
diff --git a/templates/auth/register.html b/templates/auth/register.html index 8d5b9fa..bca6f00 100644 --- a/templates/auth/register.html +++ b/templates/auth/register.html @@ -10,7 +10,7 @@ Must be 30 alphanumeric characters or less.
- +
@@ -18,14 +18,14 @@ Passwords must be at least 6 characters.
- + - +
- +
diff --git a/templates/layout.html b/templates/layout.html index b4d0873..cf37f94 100644 --- a/templates/layout.html +++ b/templates/layout.html @@ -5,6 +5,7 @@ {title} +