Compare commits
No commits in common. "00185e67744671910d3978f20aee23d656335ee3" and "ef06bf160ab08565e03ab3bc380543a3243eb26a" have entirely different histories.
00185e6774
...
ef06bf160a
|
|
@ -6,5 +6,3 @@ files/
|
||||||
|
|
||||||
# nano
|
# nano
|
||||||
.swp
|
.swp
|
||||||
|
|
||||||
*.todo
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
327
static/app.js
327
static/app.js
|
|
@ -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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
188
static/style.css
188
static/style.css
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -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" .}}
|
||||||
|
|
|
||||||
|
|
@ -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" .}}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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" .}}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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}}
|
||||||
|
|
|
||||||
|
|
@ -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" .}}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue