Compare commits

..

No commits in common. "00185e67744671910d3978f20aee23d656335ee3" and "ef06bf160ab08565e03ab3bc380543a3243eb26a" have entirely different histories.

17 changed files with 77 additions and 639 deletions

2
.gitignore vendored
View File

@ -6,5 +6,3 @@ files/
# nano # nano
.swp .swp
*.todo

View File

@ -1,94 +1,66 @@
package handlers package handlers
import ( import (
"github.com/gorilla/sessions" "log"
"log" "net/http"
"net/http" "threadr/models"
"threadr/models" "github.com/gorilla/sessions"
) )
func SignupHandler(app *App) http.HandlerFunc { func SignupHandler(app *App) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
session := r.Context().Value("session").(*sessions.Session) session := r.Context().Value("session").(*sessions.Session)
cookie, _ := r.Cookie("threadr_cookie_banner") cookie, _ := r.Cookie("threadr_cookie_banner")
if r.Method == http.MethodPost { if r.Method == http.MethodPost {
username := r.FormValue("username") username := r.FormValue("username")
password := r.FormValue("password") password := r.FormValue("password")
passwordConfirm := r.FormValue("password_confirm") err := models.CreateUser(app.DB, username, password)
if err != nil {
// Server-side validation for password confirmation log.Printf("Error creating user: %v", err)
if password != passwordConfirm { data := struct {
log.Printf("Password confirmation mismatch for user: %s", username) PageData
data := struct { Error string
PageData }{
Error string PageData: PageData{
}{ Title: "ThreadR - Sign Up",
PageData: PageData{ Navbar: "signup",
Title: "ThreadR - Sign Up", LoggedIn: false,
Navbar: "signup", ShowCookieBanner: cookie == nil || cookie.Value != "accepted",
LoggedIn: false, BasePath: app.Config.ThreadrDir,
ShowCookieBanner: cookie == nil || cookie.Value != "accepted", StaticPath: app.Config.ThreadrDir + "/static",
BasePath: app.Config.ThreadrDir, CurrentURL: r.URL.Path,
StaticPath: app.Config.ThreadrDir + "/static", },
CurrentURL: r.URL.Path, Error: "An error occurred during sign up. Please try again.",
}, }
Error: "Passwords do not match. Please try again.", if err := app.Tmpl.ExecuteTemplate(w, "signup", data); err != nil {
} log.Printf("Error executing template in SignupHandler: %v", err)
if err := app.Tmpl.ExecuteTemplate(w, "signup", data); err != nil { http.Error(w, "Internal Server Error", http.StatusInternalServerError)
log.Printf("Error executing template in SignupHandler: %v", err) return
http.Error(w, "Internal Server Error", http.StatusInternalServerError) }
return return
} }
return http.Redirect(w, r, app.Config.ThreadrDir+"/login/", http.StatusFound)
} return
}
err := models.CreateUser(app.DB, username, password) data := struct {
if err != nil { PageData
log.Printf("Error creating user: %v", err) Error string
data := struct { }{
PageData PageData: PageData{
Error string Title: "ThreadR - Sign Up",
}{ Navbar: "signup",
PageData: PageData{ LoggedIn: session.Values["user_id"] != nil,
Title: "ThreadR - Sign Up", ShowCookieBanner: cookie == nil || cookie.Value != "accepted",
Navbar: "signup", BasePath: app.Config.ThreadrDir,
LoggedIn: false, StaticPath: app.Config.ThreadrDir + "/static",
ShowCookieBanner: cookie == nil || cookie.Value != "accepted", CurrentURL: r.URL.Path,
BasePath: app.Config.ThreadrDir, },
StaticPath: app.Config.ThreadrDir + "/static", Error: "",
CurrentURL: r.URL.Path, }
}, if err := app.Tmpl.ExecuteTemplate(w, "signup", data); err != nil {
Error: "An error occurred during sign up. Please try again.", log.Printf("Error executing template in SignupHandler: %v", err)
} http.Error(w, "Internal Server Error", http.StatusInternalServerError)
if err := app.Tmpl.ExecuteTemplate(w, "signup", data); err != nil { return
log.Printf("Error executing template in SignupHandler: %v", err) }
http.Error(w, "Internal Server Error", http.StatusInternalServerError) }
return }
}
return
}
http.Redirect(w, r, app.Config.ThreadrDir+"/login/", http.StatusFound)
return
}
data := struct {
PageData
Error string
}{
PageData: PageData{
Title: "ThreadR - Sign Up",
Navbar: "signup",
LoggedIn: session.Values["user_id"] != nil,
ShowCookieBanner: cookie == nil || cookie.Value != "accepted",
BasePath: app.Config.ThreadrDir,
StaticPath: app.Config.ThreadrDir + "/static",
CurrentURL: r.URL.Path,
},
Error: "",
}
if err := app.Tmpl.ExecuteTemplate(w, "signup", data); err != nil {
log.Printf("Error executing template in SignupHandler: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
}
}

View File

@ -1,327 +0,0 @@
// ThreadR UI Enhancement JavaScript
// Show notification toast
function showNotification(message, type = 'info', duration = 3000) {
const notification = document.createElement('div');
notification.className = `notification ${type}`;
notification.textContent = message;
document.body.appendChild(notification);
setTimeout(() => {
notification.classList.add('hiding');
setTimeout(() => {
document.body.removeChild(notification);
}, 300);
}, duration);
}
// Add loading state to form submission
function handleFormSubmit(form, button) {
if (button) {
button.disabled = true;
button.classList.add('loading');
}
// Find all submit buttons in the form and disable them
const submitButtons = form.querySelectorAll('input[type="submit"], button[type="submit"]');
submitButtons.forEach(btn => {
btn.disabled = true;
btn.classList.add('loading');
});
}
// Remove loading state
function removeLoadingState(form) {
const submitButtons = form.querySelectorAll('input[type="submit"], button[type="submit"]');
submitButtons.forEach(btn => {
btn.disabled = false;
btn.classList.remove('loading');
});
}
// Enable Enter-to-submit for single-line inputs
function enableEnterToSubmit(input, form) {
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
form.requestSubmit();
}
});
}
// Auto-resize textarea as user types
function autoResizeTextarea(textarea) {
textarea.style.height = 'auto';
textarea.style.height = textarea.scrollHeight + 'px';
}
// Add character counter to textarea
function addCharacterCounter(textarea, maxLength) {
const counter = document.createElement('div');
counter.className = 'char-counter';
textarea.parentNode.insertBefore(counter, textarea.nextSibling);
function updateCounter() {
const length = textarea.value.length;
counter.textContent = `${length}${maxLength ? '/' + maxLength : ''} characters`;
if (maxLength && length > maxLength * 0.9) {
counter.classList.add('warning');
} else {
counter.classList.remove('warning');
}
}
textarea.addEventListener('input', updateCounter);
updateCounter();
}
// Client-side validation helpers
function validateUsername(username) {
if (username.length < 3) {
return 'Username must be at least 3 characters';
}
if (username.length > 30) {
return 'Username must be less than 30 characters';
}
if (!/^[a-zA-Z0-9_-]+$/.test(username)) {
return 'Username can only contain letters, numbers, underscores, and hyphens';
}
return null;
}
function validatePassword(password) {
if (password.length < 8) {
return 'Password must be at least 8 characters';
}
if (password.length > 128) {
return 'Password is too long';
}
return null;
}
function validateRequired(value, fieldName) {
if (!value || value.trim() === '') {
return `${fieldName} is required`;
}
return null;
}
// Show field error
function showFieldError(input, message) {
input.classList.add('error');
// Remove existing error message
const existingError = input.parentNode.querySelector('.field-error');
if (existingError) {
existingError.remove();
}
if (message) {
const errorDiv = document.createElement('div');
errorDiv.className = 'field-error';
errorDiv.textContent = message;
input.parentNode.insertBefore(errorDiv, input.nextSibling);
}
}
// Clear field error
function clearFieldError(input) {
input.classList.remove('error');
const errorDiv = input.parentNode.querySelector('.field-error');
if (errorDiv) {
errorDiv.remove();
}
}
// Relative time formatting
function formatRelativeTime(date) {
const now = new Date();
const diff = now - date;
const seconds = Math.floor(diff / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
const months = Math.floor(days / 30);
const years = Math.floor(days / 365);
if (seconds < 60) return 'just now';
if (minutes < 60) return `${minutes} minute${minutes !== 1 ? 's' : ''} ago`;
if (hours < 24) return `${hours} hour${hours !== 1 ? 's' : ''} ago`;
if (days < 30) return `${days} day${days !== 1 ? 's' : ''} ago`;
if (months < 12) return `${months} month${months !== 1 ? 's' : ''} ago`;
return `${years} year${years !== 1 ? 's' : ''} ago`;
}
// Convert timestamps to relative time
function initRelativeTimestamps() {
document.querySelectorAll('[data-timestamp]').forEach(element => {
const timestamp = element.getAttribute('data-timestamp');
const date = new Date(timestamp);
const originalText = element.textContent;
element.textContent = formatRelativeTime(date);
element.title = originalText;
element.style.cursor = 'help';
});
}
// Initialize on DOM ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initRelativeTimestamps);
} else {
initRelativeTimestamps();
}
// Add form submission handlers
document.addEventListener('DOMContentLoaded', () => {
// Handle all form submissions
document.querySelectorAll('form').forEach(form => {
form.addEventListener('submit', (e) => {
const submitButton = form.querySelector('input[type="submit"], button[type="submit"]');
handleFormSubmit(form, submitButton);
});
});
// Auto-resize textareas
document.querySelectorAll('textarea').forEach(textarea => {
textarea.addEventListener('input', () => autoResizeTextarea(textarea));
// Add character counter for content fields
if (textarea.id === 'content' || textarea.name === 'content') {
addCharacterCounter(textarea, 10000);
}
if (textarea.id === 'bio' || textarea.name === 'bio') {
addCharacterCounter(textarea, 500);
}
});
// Enable Enter-to-submit for single-line forms (login, etc.)
const loginForm = document.querySelector('form[action*="login"]');
if (loginForm) {
const passwordInput = loginForm.querySelector('input[type="password"]');
if (passwordInput) {
enableEnterToSubmit(passwordInput, loginForm);
}
}
// Add validation to login form
const loginUsername = document.querySelector('input[name="username"]');
const loginPassword = document.querySelector('input[name="password"]');
if (loginUsername && loginPassword) {
loginUsername.addEventListener('blur', () => {
const error = validateRequired(loginUsername.value, 'Username');
if (error) {
showFieldError(loginUsername, error);
} else {
clearFieldError(loginUsername);
}
});
loginPassword.addEventListener('blur', () => {
const error = validateRequired(loginPassword.value, 'Password');
if (error) {
showFieldError(loginPassword, error);
} else {
clearFieldError(loginPassword);
}
});
}
// Add validation to signup form
const signupForm = document.querySelector('form[action*="signup"]');
if (signupForm) {
const usernameInput = signupForm.querySelector('input[name="username"]');
const passwordInput = signupForm.querySelector('input[name="password"]');
const confirmInput = signupForm.querySelector('input[name="password_confirm"]');
if (usernameInput) {
usernameInput.addEventListener('blur', () => {
const error = validateUsername(usernameInput.value);
if (error) {
showFieldError(usernameInput, error);
} else {
clearFieldError(usernameInput);
}
});
}
if (passwordInput) {
passwordInput.addEventListener('blur', () => {
const error = validatePassword(passwordInput.value);
if (error) {
showFieldError(passwordInput, error);
} else {
clearFieldError(passwordInput);
}
});
}
if (confirmInput && passwordInput) {
confirmInput.addEventListener('blur', () => {
if (confirmInput.value !== passwordInput.value) {
showFieldError(confirmInput, 'Passwords do not match');
} else {
clearFieldError(confirmInput);
}
});
}
signupForm.addEventListener('submit', (e) => {
let hasError = false;
if (usernameInput) {
const error = validateUsername(usernameInput.value);
if (error) {
showFieldError(usernameInput, error);
hasError = true;
}
}
if (passwordInput) {
const error = validatePassword(passwordInput.value);
if (error) {
showFieldError(passwordInput, error);
hasError = true;
}
}
if (confirmInput && passwordInput && confirmInput.value !== passwordInput.value) {
showFieldError(confirmInput, 'Passwords do not match');
hasError = true;
}
if (hasError) {
e.preventDefault();
removeLoadingState(signupForm);
showNotification('Please fix the errors before submitting', 'error');
}
});
}
// Add validation to thread/post forms
document.querySelectorAll('input[name="title"]').forEach(input => {
input.addEventListener('blur', () => {
const error = validateRequired(input.value, 'Title');
if (error) {
showFieldError(input, error);
} else if (input.value.length > 255) {
showFieldError(input, 'Title is too long (max 255 characters)');
} else {
clearFieldError(input);
}
});
});
document.querySelectorAll('textarea[name="content"]').forEach(textarea => {
textarea.addEventListener('blur', () => {
const error = validateRequired(textarea.value, 'Content');
if (error) {
showFieldError(textarea, error);
} else {
clearFieldError(textarea);
}
});
});
});

View File

@ -174,8 +174,6 @@ h1, h2, h3, h4, h5, h6 {
p, a, li { p, a, li {
font-family: monospace; font-family: monospace;
color: #001858; color: #001858;
word-wrap: break-word;
overflow-wrap: break-word;
} }
/* Styles for board lists */ /* Styles for board lists */
@ -192,8 +190,6 @@ li.board-item {
border: 1px solid #001858; border: 1px solid #001858;
border-radius: 8px; border-radius: 8px;
transition: transform 0.2s ease, box-shadow 0.2s ease; transition: transform 0.2s ease, box-shadow 0.2s ease;
word-wrap: break-word;
overflow-wrap: break-word;
} }
li.board-item:hover { li.board-item:hover {
@ -217,8 +213,6 @@ p.board-desc {
margin: 0.5em 0 0 0; margin: 0.5em 0 0 0;
color: #001858; color: #001858;
font-size: 1em; font-size: 1em;
word-wrap: break-word;
overflow-wrap: break-word;
} }
/* Styles for thread lists */ /* Styles for thread lists */
@ -235,8 +229,6 @@ li.thread-item {
border: 1px solid #001858; border: 1px solid #001858;
border-radius: 8px; border-radius: 8px;
transition: transform 0.2s ease, box-shadow 0.2s ease; transition: transform 0.2s ease, box-shadow 0.2s ease;
word-wrap: break-word;
overflow-wrap: break-word;
} }
li.thread-item:hover { li.thread-item:hover {
@ -276,8 +268,6 @@ p.thread-info {
margin-bottom: 1.5em; margin-bottom: 1.5em;
padding: 1.2em 1.5em; padding: 1.2em 1.5em;
transition: transform 0.2s ease, box-shadow 0.2s ease; transition: transform 0.2s ease, box-shadow 0.2s ease;
word-wrap: break-word;
overflow-wrap: break-word;
} }
.post-item:hover { .post-item:hover {
@ -310,9 +300,6 @@ p.thread-info {
padding: 0.8em; padding: 0.8em;
line-height: 1.5; line-height: 1.5;
font-size: 1em; font-size: 1em;
word-wrap: break-word;
overflow-wrap: break-word;
word-break: break-word;
} }
.post-actions { .post-actions {
@ -435,175 +422,6 @@ p.thread-info {
} }
} }
/* Loading spinner */
.spinner {
display: inline-block;
width: 16px;
height: 16px;
border: 3px solid #fef6e4;
border-top: 3px solid #001858;
border-radius: 50%;
animation: spin 0.8s linear infinite;
margin-left: 8px;
vertical-align: middle;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Loading state for buttons */
button:disabled, input[type="submit"]:disabled {
opacity: 0.6;
cursor: not-allowed;
}
button.loading, input[type="submit"].loading {
position: relative;
color: transparent;
}
button.loading::after, input[type="submit"].loading::after {
content: "";
position: absolute;
width: 16px;
height: 16px;
top: 50%;
left: 50%;
margin-left: -8px;
margin-top: -8px;
border: 3px solid #fef6e4;
border-top: 3px solid transparent;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
/* Success/error message notifications */
.notification {
position: fixed;
top: 80px;
right: 20px;
padding: 14px 20px;
border-radius: 5px;
box-shadow: 0px 4px 12px rgba(0,0,0,0.3);
z-index: 1001;
animation: slideIn 0.3s ease-out;
max-width: 400px;
font-family: monospace;
}
.notification.success {
background-color: #8bd3dd;
color: #001858;
border: 1px solid #001858;
}
.notification.error {
background-color: #f582ae;
color: #fef6e4;
border: 1px solid #001858;
}
.notification.info {
background-color: #f3d2c1;
color: #001858;
border: 1px solid #001858;
}
@keyframes slideIn {
from {
transform: translateX(400px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes slideOut {
from {
transform: translateX(0);
opacity: 1;
}
to {
transform: translateX(400px);
opacity: 0;
}
}
.notification.hiding {
animation: slideOut 0.3s ease-out forwards;
}
/* Form validation styles */
input.error, textarea.error, select.error {
border-color: #f582ae;
border-width: 2px;
}
.field-error {
color: #f582ae;
font-size: 0.9em;
margin-top: 4px;
margin-bottom: 8px;
display: block;
}
/* Character counter */
.char-counter {
text-align: right;
font-size: 0.85em;
color: #001858;
opacity: 0.7;
margin-top: 4px;
}
.char-counter.warning {
color: #f582ae;
font-weight: bold;
}
@media (prefers-color-scheme: dark) {
.spinner {
border-color: #444;
border-top-color: #fef6e4;
}
button.loading::after, input[type="submit"].loading::after {
border-color: #001858;
border-top-color: transparent;
}
.notification.success {
background-color: #8bd3dd;
color: #001858;
}
.notification.error {
background-color: #f582ae;
color: #001858;
}
.notification.info {
background-color: #555;
color: #fef6e4;
}
input.error, textarea.error, select.error {
border-color: #f582ae;
}
.field-error {
color: #f582ae;
}
.char-counter {
color: #fef6e4;
}
}
@media (max-width: 600px) { @media (max-width: 600px) {
ul.topnav li { ul.topnav li {
float: none; float: none;
@ -623,10 +441,4 @@ input.error, textarea.error, select.error {
.thread-posts { .thread-posts {
width: 95%; width: 95%;
} }
.notification {
right: 10px;
left: 10px;
max-width: none;
}
} }

View File

@ -4,7 +4,6 @@
<head> <head>
<title>{{.Title}}</title> <title>{{.Title}}</title>
<link rel="stylesheet" href="{{.StaticPath}}/style.css"> <link rel="stylesheet" href="{{.StaticPath}}/style.css">
<script src="{{.StaticPath}}/app.js" defer></script>
</head> </head>
<body> <body>
{{template "navbar" .}} {{template "navbar" .}}

View File

@ -4,7 +4,6 @@
<head> <head>
<title>{{.Title}}</title> <title>{{.Title}}</title>
<link rel="stylesheet" href="{{.StaticPath}}/style.css"> <link rel="stylesheet" href="{{.StaticPath}}/style.css">
<script src="{{.StaticPath}}/app.js" defer></script>
</head> </head>
<body> <body>
{{template "navbar" .}} {{template "navbar" .}}

View File

@ -4,7 +4,6 @@
<head> <head>
<title>{{.Title}}</title> <title>{{.Title}}</title>
<link rel="stylesheet" href="{{.StaticPath}}/style.css"> <link rel="stylesheet" href="{{.StaticPath}}/style.css">
<script src="{{.StaticPath}}/app.js" defer></script>
</head> </head>
<body> <body>
{{template "navbar" .}} {{template "navbar" .}}
@ -20,7 +19,7 @@
{{range .Threads}} {{range .Threads}}
<li class="thread-item"> <li class="thread-item">
<a href="{{$.BasePath}}/thread/?id={{.ID}}">{{.Title}}</a> <a href="{{$.BasePath}}/thread/?id={{.ID}}">{{.Title}}</a>
<p class="thread-info" data-timestamp="{{.UpdatedAt.Format "2006-01-02T15:04:05Z07:00"}}">Updated on {{.UpdatedAt.Format "02/01/2006 - 15:04"}}</p> <p class="thread-info">Updated on {{.UpdatedAt.Format "02/01/2006 - 15:04"}}</p>
</li> </li>
{{end}} {{end}}
</ul> </ul>
@ -33,7 +32,7 @@
<h3>Create New Thread</h3> <h3>Create New Thread</h3>
<form method="post" action="{{.BasePath}}/board/?id={{.Board.ID}}&action=create_thread"> <form method="post" action="{{.BasePath}}/board/?id={{.Board.ID}}&action=create_thread">
<label for="title">Thread Title:</label> <label for="title">Thread Title:</label>
<input type="text" id="title" name="title" required maxlength="255"><br> <input type="text" id="title" name="title" required><br>
<input type="submit" value="Create Thread"> <input type="submit" value="Create Thread">
</form> </form>
</section> </section>

View File

@ -4,7 +4,6 @@
<head> <head>
<title>{{.Title}}</title> <title>{{.Title}}</title>
<link rel="stylesheet" href="{{.StaticPath}}/style.css"> <link rel="stylesheet" href="{{.StaticPath}}/style.css">
<script src="{{.StaticPath}}/app.js" defer></script>
</head> </head>
<body> <body>
{{template "navbar" .}} {{template "navbar" .}}
@ -57,7 +56,7 @@
<h3>Create New Public Board</h3> <h3>Create New Public Board</h3>
<form method="post" action="{{.BasePath}}/boards/"> <form method="post" action="{{.BasePath}}/boards/">
<label for="name">Board Name:</label> <label for="name">Board Name:</label>
<input type="text" id="name" name="name" required maxlength="255"><br> <input type="text" id="name" name="name" required><br>
<label for="description">Description:</label> <label for="description">Description:</label>
<textarea id="description" name="description"></textarea><br> <textarea id="description" name="description"></textarea><br>
<label for="type">Board Type:</label> <label for="type">Board Type:</label>

View File

@ -73,9 +73,6 @@
border-radius: 5px; border-radius: 5px;
line-height: 1.3; line-height: 1.3;
font-size: 0.9em; font-size: 0.9em;
word-wrap: break-word;
overflow-wrap: break-word;
word-break: break-word;
} }
.chat-message-reply { .chat-message-reply {
background-color: rgba(0,0,0,0.1); background-color: rgba(0,0,0,0.1);

View File

@ -4,7 +4,6 @@
<head> <head>
<title>{{.Title}}</title> <title>{{.Title}}</title>
<link rel="stylesheet" href="{{.StaticPath}}/style.css"> <link rel="stylesheet" href="{{.StaticPath}}/style.css">
<script src="{{.StaticPath}}/app.js" defer></script>
</head> </head>
<body> <body>
{{template "navbar" .}} {{template "navbar" .}}

View File

@ -4,7 +4,6 @@
<head> <head>
<title>{{.Title}}</title> <title>{{.Title}}</title>
<link rel="stylesheet" href="{{.StaticPath}}/style.css"> <link rel="stylesheet" href="{{.StaticPath}}/style.css">
<script src="{{.StaticPath}}/app.js" defer></script>
</head> </head>
<body> <body>
{{template "navbar" .}} {{template "navbar" .}}
@ -14,13 +13,13 @@
</header> </header>
<section> <section>
{{if .Error}} {{if .Error}}
<p class="field-error" style="text-align: center; font-size: 1em;">{{.Error}}</p> <p style="color: red;">{{.Error}}</p>
{{end}} {{end}}
<form method="post" action="{{.BasePath}}/login/"> <form method="post" action="{{.BasePath}}/login/">
<label for="username">Username:</label> <label for="username">Username:</label>
<input type="text" id="username" name="username" required autocomplete="username"><br> <input type="text" id="username" name="username" required><br>
<label for="password">Password:</label> <label for="password">Password:</label>
<input type="password" id="password" name="password" required autocomplete="current-password"><br> <input type="password" id="password" name="password" required><br>
<input type="submit" value="Login"> <input type="submit" value="Login">
</form> </form>
</section> </section>

View File

@ -4,7 +4,6 @@
<head> <head>
<title>{{.Title}}</title> <title>{{.Title}}</title>
<link rel="stylesheet" href="{{.StaticPath}}/style.css"> <link rel="stylesheet" href="{{.StaticPath}}/style.css">
<script src="{{.StaticPath}}/app.js" defer></script>
</head> </head>
<body> <body>
{{template "navbar" .}} {{template "navbar" .}}
@ -16,7 +15,7 @@
{{if .News}} {{if .News}}
<ul> <ul>
{{range .News}} {{range .News}}
<li><strong>{{.Title}}</strong> - <span data-timestamp="{{.CreatedAt.Format "2006-01-02T15:04:05Z07:00"}}">Posted on {{.CreatedAt.Format "02/01/2006 - 15:04"}}</span> <li><strong>{{.Title}}</strong> - Posted on {{.CreatedAt.Format "02/01/2006 - 15:04"}}
<p>{{.Content}}</p> <p>{{.Content}}</p>
{{if $.IsAdmin}} {{if $.IsAdmin}}
<form method="post" action="{{$.BasePath}}/news/?action=delete&id={{.ID}}" style="display:inline;"> <form method="post" action="{{$.BasePath}}/news/?action=delete&id={{.ID}}" style="display:inline;">
@ -35,7 +34,7 @@
<h3>Post New Announcement</h3> <h3>Post New Announcement</h3>
<form method="post" action="{{.BasePath}}/news/"> <form method="post" action="{{.BasePath}}/news/">
<label for="title">Title:</label> <label for="title">Title:</label>
<input type="text" id="title" name="title" required maxlength="255"><br> <input type="text" id="title" name="title" required><br>
<label for="content">Content:</label> <label for="content">Content:</label>
<textarea id="content" name="content" required></textarea><br> <textarea id="content" name="content" required></textarea><br>
<input type="submit" value="Post News"> <input type="submit" value="Post News">

View File

@ -4,7 +4,6 @@
<head> <head>
<title>{{.Title}}</title> <title>{{.Title}}</title>
<link rel="stylesheet" href="{{.StaticPath}}/style.css"> <link rel="stylesheet" href="{{.StaticPath}}/style.css">
<script src="{{.StaticPath}}/app.js" defer></script>
</head> </head>
<body> <body>
{{template "navbar" .}} {{template "navbar" .}}
@ -19,8 +18,8 @@
<img src="{{.BasePath}}/file?id={{.User.PfpFileID.Int64}}" alt="Profile Picture"> <img src="{{.BasePath}}/file?id={{.User.PfpFileID.Int64}}" alt="Profile Picture">
{{end}} {{end}}
<p>Bio: {{.User.Bio}}</p> <p>Bio: {{.User.Bio}}</p>
<p data-timestamp="{{.User.CreatedAt.Format "2006-01-02T15:04:05Z07:00"}}">Joined: {{.User.CreatedAt}}</p> <p>Joined: {{.User.CreatedAt}}</p>
<p data-timestamp="{{.User.UpdatedAt.Format "2006-01-02T15:04:05Z07:00"}}">Last Updated: {{.User.UpdatedAt}}</p> <p>Last Updated: {{.User.UpdatedAt}}</p>
<p>Verified: {{.User.Verified}}</p> <p>Verified: {{.User.Verified}}</p>
<a href="{{.BasePath}}/profile/edit/">Edit Profile</a> <a href="{{.BasePath}}/profile/edit/">Edit Profile</a>
</section> </section>

View File

@ -4,7 +4,6 @@
<head> <head>
<title>{{.Title}}</title> <title>{{.Title}}</title>
<link rel="stylesheet" href="{{.StaticPath}}/style.css"> <link rel="stylesheet" href="{{.StaticPath}}/style.css">
<script src="{{.StaticPath}}/app.js" defer></script>
</head> </head>
<body> <body>
{{template "navbar" .}} {{template "navbar" .}}
@ -15,11 +14,11 @@
<section> <section>
<form method="post" action="{{.BasePath}}/profile/edit/" enctype="multipart/form-data"> <form method="post" action="{{.BasePath}}/profile/edit/" enctype="multipart/form-data">
<label for="display_name">Display Name:</label> <label for="display_name">Display Name:</label>
<input type="text" id="display_name" name="display_name" value="{{.User.DisplayName}}" maxlength="255"><br> <input type="text" id="display_name" name="display_name" value="{{.User.DisplayName}}"><br>
<label for="pfp">Profile Picture:</label> <label for="pfp">Profile Picture:</label>
<input type="file" id="pfp" name="pfp" accept="image/*"><br> <input type="file" id="pfp" name="pfp"><br>
<label for="bio">Bio:</label> <label for="bio">Bio:</label>
<textarea id="bio" name="bio" maxlength="500">{{.User.Bio}}</textarea><br> <textarea id="bio" name="bio">{{.User.Bio}}</textarea><br>
<input type="submit" value="Save"> <input type="submit" value="Save">
</form> </form>
</section> </section>

View File

@ -4,7 +4,6 @@
<head> <head>
<title>{{.Title}}</title> <title>{{.Title}}</title>
<link rel="stylesheet" href="{{.StaticPath}}/style.css"> <link rel="stylesheet" href="{{.StaticPath}}/style.css">
<script src="{{.StaticPath}}/app.js" defer></script>
</head> </head>
<body> <body>
{{template "navbar" .}} {{template "navbar" .}}
@ -14,15 +13,13 @@
</header> </header>
<section> <section>
{{if .Error}} {{if .Error}}
<p class="field-error" style="text-align: center; font-size: 1em;">{{.Error}}</p> <p style="color: red;">{{.Error}}</p>
{{end}} {{end}}
<form method="post" action="{{.BasePath}}/signup/"> <form method="post" action="{{.BasePath}}/signup/">
<label for="username">Username:</label> <label for="username">Username:</label>
<input type="text" id="username" name="username" required autocomplete="username" minlength="3" maxlength="30"><br> <input type="text" id="username" name="username" required><br>
<label for="password">Password:</label> <label for="password">Password:</label>
<input type="password" id="password" name="password" required autocomplete="new-password" minlength="8" maxlength="128"><br> <input type="password" id="password" name="password" required><br>
<label for="password_confirm">Confirm Password:</label>
<input type="password" id="password_confirm" name="password_confirm" required autocomplete="new-password" minlength="8" maxlength="128"><br>
<input type="submit" value="Sign Up"> <input type="submit" value="Sign Up">
</form> </form>
</section> </section>

View File

@ -4,7 +4,6 @@
<head> <head>
<title>{{.Title}}</title> <title>{{.Title}}</title>
<link rel="stylesheet" href="{{.StaticPath}}/style.css"> <link rel="stylesheet" href="{{.StaticPath}}/style.css">
<script src="{{.StaticPath}}/app.js" defer></script>
</head> </head>
<body> <body>
{{template "navbar" .}} {{template "navbar" .}}
@ -17,7 +16,7 @@
<article id="{{.ID}}" class="post-item" style="margin-left: {{if gt .ReplyTo 0}}20px{{else}}0px{{end}};"> <article id="{{.ID}}" class="post-item" style="margin-left: {{if gt .ReplyTo 0}}20px{{else}}0px{{end}};">
<header> <header>
<h3>{{if .Title}}{{.Title}}{{else}}Post #{{.ID}}{{end}}</h3> <h3>{{if .Title}}{{.Title}}{{else}}Post #{{.ID}}{{end}}</h3>
<p data-timestamp="{{.PostTime.Format "2006-01-02T15:04:05Z07:00"}}">Posted on {{.PostTime.Format "02/01/2006 - 15:04"}}</p> <p>Posted on {{.PostTime.Format "02/01/2006 - 15:04"}}</p>
{{if gt .ReplyTo 0}} {{if gt .ReplyTo 0}}
<p>Reply to post <a href="#{{.ReplyTo}}">{{.ReplyTo}}</a></p> <p>Reply to post <a href="#{{.ReplyTo}}">{{.ReplyTo}}</a></p>
{{end}} {{end}}

View File

@ -4,7 +4,6 @@
<head> <head>
<title>{{.Title}}</title> <title>{{.Title}}</title>
<link rel="stylesheet" href="{{.StaticPath}}/style.css"> <link rel="stylesheet" href="{{.StaticPath}}/style.css">
<script src="{{.StaticPath}}/app.js" defer></script>
</head> </head>
<body> <body>
{{template "navbar" .}} {{template "navbar" .}}