added user profile display and quick E-mail functions
This commit is contained in:
@@ -22,7 +22,10 @@ import (
|
||||
|
||||
"git.erbosoft.com/amy/amsterdam/util"
|
||||
lru "github.com/hashicorp/golang-lru"
|
||||
"github.com/klauspost/lctime"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/text/language"
|
||||
"golang.org/x/text/message"
|
||||
)
|
||||
|
||||
// UserPrefs represents the user's preferences in a table (one row per user).
|
||||
@@ -60,6 +63,30 @@ func (p *UserPrefs) Save(u *User) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Localizer returns a localizer for this locale.
|
||||
func (p *UserPrefs) Localizer() lctime.Localizer {
|
||||
lc, err := lctime.NewLocalizer(p.LocaleID)
|
||||
if err != nil {
|
||||
log.Fatalf("BOGUS LANGUAGE TAG %s in user prefs for uid %d", p.LocaleID, p.Uid)
|
||||
}
|
||||
return lc
|
||||
}
|
||||
|
||||
// LanguageTag returns the user's language tag.
|
||||
func (p *UserPrefs) LanguageTag() *language.Tag {
|
||||
lt, err := language.Parse(p.ReadLocale())
|
||||
if err != nil {
|
||||
log.Fatalf("BOGUS LANGUAGE TAG %s in user prefs for uid %d", p.LocaleID, p.Uid)
|
||||
return nil
|
||||
}
|
||||
return <
|
||||
}
|
||||
|
||||
// MessagePrinter returns a message printer for the user's selected locale.
|
||||
func (p *UserPrefs) MessagePrinter() *message.Printer {
|
||||
return message.NewPrinter(*p.LanguageTag())
|
||||
}
|
||||
|
||||
// User represents a user in the Amsterdam database.
|
||||
type User struct {
|
||||
Mutex sync.RWMutex
|
||||
|
||||
@@ -12,6 +12,7 @@ require (
|
||||
github.com/gorilla/sessions v1.4.0
|
||||
github.com/hashicorp/golang-lru v1.0.2
|
||||
github.com/jmoiron/sqlx v1.4.0
|
||||
github.com/klauspost/lctime v0.1.0
|
||||
github.com/labstack/echo-contrib v0.17.4
|
||||
github.com/labstack/echo/v4 v4.13.4
|
||||
github.com/labstack/gommon v0.4.2
|
||||
|
||||
@@ -32,6 +32,8 @@ github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iP
|
||||
github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
||||
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
|
||||
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
|
||||
github.com/klauspost/lctime v0.1.0 h1:nINsuFc860M9cyYhT6vfg6U1USh7kiVBj/s/2b04U70=
|
||||
github.com/klauspost/lctime v0.1.0/go.mod h1:OwdMhr8tbQvusAsnilqkkgDQqivWlqyg0w5cfXkLiDk=
|
||||
github.com/labstack/echo-contrib v0.17.4 h1:g5mfsrJfJTKv+F5uNKCyrjLK7js+ZW6HTjg4FnDxxgk=
|
||||
github.com/labstack/echo-contrib v0.17.4/go.mod h1:9O7ZPAHUeMGTOAfg80YqQduHzt0CzLak36PZRldYrZ0=
|
||||
github.com/labstack/echo/v4 v4.13.4 h1:oTZZW+T3s9gAu5L8vmzihV7/lkXGZuITzTQkTEhcXEA=
|
||||
|
||||
@@ -56,6 +56,8 @@ func setupEcho() *echo.Echo {
|
||||
e.POST("/profile", ui.AmWrap(EditProfile))
|
||||
e.GET("/profile_photo", ui.AmWrap(ProfilePhotoForm))
|
||||
e.POST("/profile_photo", ui.AmWrap(ProfilePhoto))
|
||||
e.GET("/user/:uname", ui.AmWrap(ShowProfile))
|
||||
e.POST("/quick_email", ui.AmWrap(QuickEMail))
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
@@ -121,7 +121,7 @@ func buildUsersOnline(uid int32, out *RenderedSidebox, in *database.Sidebox) err
|
||||
}
|
||||
for i, n := range users {
|
||||
out.Items[b+i].Text = n
|
||||
lk := fmt.Sprintf("/TODO/user/%s", n)
|
||||
lk := fmt.Sprintf("/user/%s", n)
|
||||
out.Items[b+i].Link = &lk
|
||||
out.Items[b+i].Flags = make(map[string]bool)
|
||||
out.Items[b+i].Flags["bold"] = true
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
{*
|
||||
* Amsterdam Web Communities System
|
||||
* Copyright (c) 2025 Erbosoft Metaverse Design Solutions, All Rights Reserved
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*}
|
||||
<div class="p-4">
|
||||
<!-- Page Title -->
|
||||
<div class="mb-6">
|
||||
<h1 class="text-blue-800 text-4xl font-bold inline">User Profile:</h1>
|
||||
<span class="text-blue-800 text-2xl font-bold ml-2">{{ username }}</span>
|
||||
<hr class="border-2 border-gray-400 w-4/5 mt-2 mb-6">
|
||||
</div>
|
||||
|
||||
<!-- Profile Information Card -->
|
||||
<div class="bg-gray-50 p-6 rounded-lg mb-8 max-w-4xl">
|
||||
<div class="flex gap-6">
|
||||
<!-- Left Column: Photo and Metadata -->
|
||||
<div class="flex-shrink-0 w-40">
|
||||
<div class="border-2 border-gray-300 rounded mb-4">
|
||||
<img src="{{ photoURL }}" alt="{{ username }}'s photo" class="w-full h-auto">
|
||||
</div>
|
||||
<div class="text-xs text-gray-700 space-y-2">
|
||||
<div>
|
||||
<strong>Account created:</strong><br>{{ dateCreated }}
|
||||
</div>
|
||||
{{ if isset(dateLastLogin) }}
|
||||
<div>
|
||||
<strong>Last login:</strong><br>{{ dateLastLogin }}
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ if isset(dateLastUpdate) }}
|
||||
<div>
|
||||
<strong>Profile last updated:</strong><br>{{ dateLastUpdate }}
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Column: Contact Information -->
|
||||
<div class="flex-1 text-sm text-black">
|
||||
<div class="space-y-2">
|
||||
<div class="text-lg font-bold">{{ fullname }}</div>
|
||||
{{ if isset(description) }}<div>{{ description }}</div>{{ end }}
|
||||
{{ if isset(email) }}<div><strong>E-mail:</strong> {{ email }}</div>{{ end }}
|
||||
{{ if isset(url) }}
|
||||
<div>
|
||||
<strong>URL: </strong>
|
||||
<a href="{{ url }}" target="_blank" class="text-blue-700 hover:text-blue-900">{{ url }}</a>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
<div class="pt-2 border-t border-gray-300">
|
||||
{{ if isset(company) }}<div>{{ company }}</div>{{ end }}
|
||||
{{ if isset(addr1) }}<div>{{ addr1 }}</div>{{ end }}
|
||||
{{ if isset(addr2) }}<div>{{ addr2 }}</div>{{ end }}
|
||||
<div>{{ addrLast }}</div>
|
||||
{{ if isset(country) }}<div>{{ country }}</div>{{ end }}
|
||||
</div>
|
||||
|
||||
{{ if isset(phone) || isset(fax) || isset(mobile) }}
|
||||
<div class="pt-2 border-t border-gray-300">
|
||||
{{ if isset(phone) }}<div><strong>Phone:</strong> {{ phone }}</div>{{ end }}
|
||||
{{ if isset(fax) }}<div><strong>Fax:</strong> {{ fax }}</div>{{ end }}
|
||||
{{ if isset(mobile) }}<div><strong>Mobile:</strong> {{ mobile }}</div>{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ if !.CurrentUser().IsAnon }}
|
||||
<!-- Quick Email Section -->
|
||||
<div class="max-w-4xl">
|
||||
<hr class="border-gray-400 mb-4">
|
||||
<h2 class="text-lg font-bold text-black mb-4">Send Quick E-Mail To {{ username }}:</h2>
|
||||
|
||||
<form method="POST" action="/quick_email">
|
||||
<input type="hidden" name="to_uid" value="{{ uid }}">
|
||||
|
||||
<div class="bg-gray-50 p-6 rounded-lg space-y-4">
|
||||
<!-- Subject Field -->
|
||||
<div>
|
||||
<label for="subj" class="block text-black text-sm font-medium mb-2">
|
||||
Subject:
|
||||
</label>
|
||||
<input type="text"
|
||||
id="subj"
|
||||
name="subj"
|
||||
size="65"
|
||||
maxlength="255"
|
||||
value=""
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
</div>
|
||||
|
||||
<!-- Message Body -->
|
||||
<div>
|
||||
<label for="pb" class="block text-black text-sm font-medium mb-2">
|
||||
Message:
|
||||
</label>
|
||||
<textarea id="pb"
|
||||
name="pb"
|
||||
wrap="hard"
|
||||
rows="7"
|
||||
cols="80"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded font-mono text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Send Button -->
|
||||
<div>
|
||||
<button type="submit"
|
||||
name="send"
|
||||
class="bg-blue-600 hover:bg-blue-700 text-white px-8 py-2 rounded font-medium transition-colors">
|
||||
Send E-Mail
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
+145
-1
@@ -12,13 +12,16 @@ package main
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.erbosoft.com/amy/amsterdam/database"
|
||||
"git.erbosoft.com/amy/amsterdam/email"
|
||||
"git.erbosoft.com/amy/amsterdam/ui"
|
||||
"git.erbosoft.com/amy/amsterdam/util"
|
||||
"github.com/biter777/countries"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
@@ -226,7 +229,7 @@ func ProfilePhotoForm(ctxt ui.AmContext) (string, any, error) {
|
||||
return ui.ErrorPage(ctxt, err)
|
||||
}
|
||||
|
||||
/* ProfilePhoto handles processing the uploaded user photo..
|
||||
/* ProfilePhoto handles processing the uploaded user photo.
|
||||
* Parameters:
|
||||
* ctxt - The AmContext for the request.
|
||||
* Returns:
|
||||
@@ -310,3 +313,144 @@ func ProfilePhoto(ctxt ui.AmContext) (string, any, error) {
|
||||
}
|
||||
return ui.ErrorPage(ctxt, errors.New("invalid button detected in photo upload"))
|
||||
}
|
||||
|
||||
/* ShowProfile displays a user's profile.
|
||||
* Parameters:
|
||||
* ctxt - The AmContext for the request.
|
||||
* Returns:
|
||||
* Command string dictating what to be rendered.
|
||||
* Data as a parameter for the command string.
|
||||
* Standard Go error status.
|
||||
*/
|
||||
func ShowProfile(ctxt ui.AmContext) (string, any, error) {
|
||||
me := ctxt.CurrentUser()
|
||||
prefs, err := me.Prefs()
|
||||
if err != nil {
|
||||
return ui.ErrorPage(ctxt, err)
|
||||
}
|
||||
|
||||
// Gather the info on the current user.
|
||||
user, err := database.AmGetUserByName(ctxt.URLParam("uname"))
|
||||
if err != nil {
|
||||
ctxt.SetRC(http.StatusNotFound)
|
||||
return ui.ErrorPage(ctxt, err)
|
||||
}
|
||||
ci, err := user.ContactInfo()
|
||||
if err != nil {
|
||||
return ui.ErrorPage(ctxt, err)
|
||||
}
|
||||
|
||||
// Fill all the page variables for display.
|
||||
ctxt.VarMap().Set("uid", user.Uid)
|
||||
ctxt.VarMap().Set("username", user.Username)
|
||||
ctxt.VarMap().Set("photoURL", userPhotoURL(ci))
|
||||
loc := prefs.Localizer()
|
||||
ctxt.VarMap().Set("dateCreated", loc.Strftime("%x %X", user.Created))
|
||||
if user.LastAccess != nil {
|
||||
ctxt.VarMap().Set("dateLastLogin", loc.Strftime("%x %X", *user.LastAccess))
|
||||
}
|
||||
if ci.LastUpdate != nil {
|
||||
ctxt.VarMap().Set("dateLastUpdate", loc.Strftime("%x %X", *ci.LastUpdate))
|
||||
}
|
||||
var b strings.Builder
|
||||
if ci.Prefix != nil && *ci.Prefix != "" {
|
||||
b.WriteString(*ci.Prefix + " ")
|
||||
}
|
||||
b.WriteString(ci.GivenName)
|
||||
if ci.MiddleInit != nil && *ci.MiddleInit != "" && *ci.MiddleInit != " " {
|
||||
b.WriteString(" " + *ci.MiddleInit + ".")
|
||||
}
|
||||
b.WriteString(" " + ci.FamilyName)
|
||||
if ci.Suffix != nil && *ci.Suffix != "" {
|
||||
b.WriteString(" " + *ci.Suffix)
|
||||
}
|
||||
ctxt.VarMap().Set("fullname", b.String())
|
||||
if user.Description != nil {
|
||||
ctxt.VarMap().Set("description", *user.Description)
|
||||
}
|
||||
if !ci.PrivateEmail && ci.Email != nil {
|
||||
ctxt.VarMap().Set("email", *ci.Email)
|
||||
}
|
||||
if ci.URL != nil {
|
||||
ctxt.VarMap().Set("url", *ci.URL)
|
||||
}
|
||||
if ci.Company != nil {
|
||||
ctxt.VarMap().Set("company", *ci.Company)
|
||||
}
|
||||
if !ci.PrivateAddr && ci.Addr1 != nil {
|
||||
ctxt.VarMap().Set("addr1", *ci.Addr1)
|
||||
}
|
||||
if !ci.PrivateAddr && ci.Addr2 != nil {
|
||||
ctxt.VarMap().Set("addr2", *ci.Addr2)
|
||||
}
|
||||
b.Reset()
|
||||
if ci.Locality != nil {
|
||||
b.WriteString(*ci.Locality)
|
||||
if ci.Region != nil {
|
||||
b.WriteString(", ")
|
||||
}
|
||||
}
|
||||
if ci.Region != nil {
|
||||
b.WriteString(*ci.Region)
|
||||
}
|
||||
if ci.PostalCode != nil {
|
||||
b.WriteString(" " + *ci.PostalCode)
|
||||
}
|
||||
ctxt.VarMap().Set("addrLast", b.String())
|
||||
if ci.Country != nil {
|
||||
country := countries.ByName(*ci.Country)
|
||||
ctxt.VarMap().Set("country", country.String())
|
||||
}
|
||||
if !ci.PrivatePhone && ci.Phone != nil {
|
||||
ctxt.VarMap().Set("phone", *ci.Phone)
|
||||
}
|
||||
if !ci.PrivateFax && ci.Fax != nil {
|
||||
ctxt.VarMap().Set("fax", *ci.Fax)
|
||||
}
|
||||
if !ci.PrivatePhone && ci.Mobile != nil {
|
||||
ctxt.VarMap().Set("mobile", *ci.Mobile)
|
||||
}
|
||||
ctxt.VarMap().Set("amsterdam_pageTitle", fmt.Sprintf("User Profile - %s", user.Username))
|
||||
return "framed_template", "profile.jet", nil
|
||||
}
|
||||
|
||||
/* QuickEMail sends quick E-mail to a user.
|
||||
* Parameters:
|
||||
* ctxt - The AmContext for the request.
|
||||
* Returns:
|
||||
* Command string dictating what to be rendered.
|
||||
* Data as a parameter for the command string.
|
||||
* Standard Go error status.
|
||||
*/
|
||||
func QuickEMail(ctxt ui.AmContext) (string, any, error) {
|
||||
me := ctxt.CurrentUser()
|
||||
if me.IsAnon {
|
||||
return ui.ErrorPage(ctxt, errors.New("you are not logged in"))
|
||||
}
|
||||
myCI, err := me.ContactInfo()
|
||||
if err != nil {
|
||||
return ui.ErrorPage(ctxt, err)
|
||||
}
|
||||
toUid, err := ctxt.FormFieldInt("to_uid")
|
||||
if err != nil {
|
||||
return ui.ErrorPage(ctxt, err)
|
||||
}
|
||||
user, err := database.AmGetUser(int32(toUid))
|
||||
if err != nil {
|
||||
return ui.ErrorPage(ctxt, err)
|
||||
}
|
||||
if user.IsAnon {
|
||||
return ui.ErrorPage(ctxt, errors.New("cannot send quick E-mail to anonymous user"))
|
||||
}
|
||||
ci, err := user.ContactInfo()
|
||||
if err != nil {
|
||||
return ui.ErrorPage(ctxt, err)
|
||||
}
|
||||
msg := email.AmNewEmailMessage(me.Uid, ctxt.RemoteIP())
|
||||
msg.AddTo(*ci.Email, user.Username)
|
||||
msg.AddHeader("X-Originally-From", fmt.Sprintf("%s <%s>", me.Username, *myCI.Email))
|
||||
msg.SetSubject(ctxt.FormField("subj"))
|
||||
msg.SetText(ctxt.FormField("pb"))
|
||||
msg.Send()
|
||||
return "redirect", "/user/" + user.Username, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user