partial implementation of community profile page - next, straighten out left menus

This commit is contained in:
2025-10-15 23:20:43 -06:00
parent 0a8f67c676
commit 681b30272d
12 changed files with 915 additions and 8 deletions
+136
View File
@@ -0,0 +1,136 @@
/*
* 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/.
*/
// Package main contains the high-level Amsterdam logic.
package main
import (
"net/http"
"strings"
"git.erbosoft.com/amy/amsterdam/database"
"git.erbosoft.com/amy/amsterdam/ui"
"github.com/biter777/countries"
"golang.org/x/text/language/display"
)
func ShowCommunity(ctxt ui.AmContext) (string, any, error) {
me := ctxt.CurrentUser()
prefs, err := me.Prefs()
if err != nil {
return ui.ErrorPage(ctxt, err)
}
globals, err := database.AmGlobals()
if err != nil {
return ui.ErrorPage(ctxt, err)
}
globalFlags, err := globals.Flags()
if err != nil {
return ui.ErrorPage(ctxt, err)
}
comm, err := database.AmGetCommunityFromParam(ctxt.URLParam("cid"))
if err != nil {
ctxt.SetRC(http.StatusNotFound)
return ui.ErrorPage(ctxt, err)
}
member, _, level, err := comm.Membership(me)
if err != nil {
return ui.ErrorPage(ctxt, err)
}
effectiveLevel := me.BaseLevel
if member && level > effectiveLevel {
effectiveLevel = level
}
ci, err := comm.ContactInfo()
if err != nil {
return ui.ErrorPage(ctxt, err)
}
host, err := comm.Host()
if err != nil {
return ui.ErrorPage(ctxt, err)
}
var cats []*database.Category
if !globalFlags.Get(database.GlobalFlagNoCategories) {
cats, err = database.AmGetCategoryHierarchy(comm.CategoryId)
if err != nil {
return ui.ErrorPage(ctxt, err)
}
}
var pvtAddr bool
if database.AmTestPermission("Global.SeeHiddenContactInfo", effectiveLevel) {
pvtAddr = false
} else {
pvtAddr = ci.PrivateAddr
}
ctxt.VarMap().Set("commName", comm.Name)
// TODO: set photo URL
tz := prefs.Location()
loc := prefs.Localizer()
ctxt.VarMap().Set("dateCreated", loc.Strftime("%x %X", comm.CreateDate.In(tz)))
if comm.LastAccess != nil {
ctxt.VarMap().Set("dateLastAccess", loc.Strftime("%x %X", (*comm.LastAccess).In(tz)))
}
if comm.LastUpdate != nil {
ctxt.VarMap().Set("dateLastUpdate", loc.Strftime("%x %X", (*comm.LastUpdate).In(tz)))
}
if !member && effectiveLevel >= comm.JoinLevel {
ctxt.VarMap().Set("canJoin", true)
}
if member && !me.IsAnon {
ctxt.VarMap().Set("canInvite", true)
}
ctxt.VarMap().Set("public", comm.Public())
if !globalFlags.Get(database.GlobalFlagNoCategories) {
ctxt.VarMap().Set("categories", cats)
}
if comm.Synopsis != nil && *comm.Synopsis != "" {
ctxt.VarMap().Set("description", *comm.Synopsis)
}
ctxt.VarMap().Set("hostName", host.Username)
if ci.Company != nil && *ci.Company != "" {
ctxt.VarMap().Set("company", *ci.Company)
}
if !pvtAddr && ci.Addr1 != nil && *ci.Addr1 != "" {
ctxt.VarMap().Set("addr1", *ci.Addr1)
}
if !pvtAddr && ci.Addr2 != nil && *ci.Addr2 != "" {
ctxt.VarMap().Set("addr2", *ci.Addr2)
}
var b strings.Builder
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 && *ci.Country != "" {
country := countries.ByName(*ci.Country)
ctxt.VarMap().Set("country", country.String())
}
tag, err := comm.LanguageTag()
if err == nil && tag != nil {
ctxt.VarMap().Set("language", display.Languages(*prefs.LanguageTag()).Name(tag))
}
if comm.Rules != nil && *comm.Rules != "" {
ctxt.VarMap().Set("rules", *comm.Rules)
}
if ci.URL != nil && *ci.URL != "" {
ctxt.VarMap().Set("homePage", *ci.URL)
}
ctxt.VarMap().Set("amsterdam_pageTitle", "Community Profile: "+comm.Name)
return "framed_template", "comprofile.jet", nil
}
+115
View File
@@ -0,0 +1,115 @@
/*
* 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/.
*/
// The database package contains database management and storage logic.
package database
import (
"errors"
"sync"
)
// Category is the structure defining a category.
type Category struct {
CatId int32 `db:"catid"`
Parent int32 `db:"parent"`
SymLink int32 `db:"symlink"`
HideDirectory int32 `db:"hide_dir"`
HideSearch int32 `db:"hide_search"`
Name string `db:"name"`
}
// allCategories is the list of all categories loaded from the database.
var allCategories []Category
// categoryIdMap maps IDs to categories.
var categoryIdMap map[int32]*Category = make(map[int32]*Category)
// categoryMutex syncs the loading of the categories.
var categoryMutex sync.Mutex
// loadCategories loads the categories list from the database.
func loadCategories() error {
categoryMutex.Lock()
defer categoryMutex.Unlock()
if allCategories == nil {
rs, err := amdb.Query("SELECT COUNT(*) FROM refcategory")
if err != nil {
return err
}
if !rs.Next() {
return errors.New("internal error loading categories")
}
var ncats int32
rs.Scan(&ncats)
allCategories = make([]Category, 0, ncats)
err = amdb.Select(&allCategories, "SELECT * FROM refcategory ORDER BY parent, name")
if err != nil {
return err
}
for i, c := range allCategories {
categoryIdMap[c.CatId] = &(allCategories[i])
}
}
return nil
}
/* AmGetCategory returns the category for the given name.
* Parameters:
* catid - The ID of the category to get.
* Returns:
* Pointer to the appropriate Category, or nil.
* Standard Go error status.
*/
func AmGetCategory(catid int32) (*Category, error) {
err := loadCategories()
if err != nil {
return nil, err
}
c := categoryIdMap[catid]
d := 5
for c.SymLink != -1 {
d--
if d == 0 {
return nil, errors.New("symlink resolution error")
}
c = categoryIdMap[c.SymLink]
}
return c, nil
}
/* AmGetCategoryHierarchy returns the category hierarchy for the given ID.
* Parameters:
* catid - The ID of the category to get.
* Returns:
* Array of pointers to the categories in hierarchical order, or nil.
* Standard Go error status.
*/
func AmGetCategoryHierarchy(catid int32) ([]*Category, error) {
err := loadCategories()
if err != nil {
return nil, err
}
// walk all the way to the "root" (parent = -1)
p := catid
ia := make([]*Category, 0, 3)
for p != -1 {
c := categoryIdMap[p]
for c.SymLink != -1 {
c = categoryIdMap[c.SymLink]
}
ia = append(ia, c)
p = c.Parent
}
// reverse the array for return
rc := make([]*Category, 0, len(ia))
for i := range ia {
rc = append(rc, ia[len(ia)-(i+1)])
}
return rc, nil
}
+151 -1
View File
@@ -12,10 +12,13 @@ package database
import (
"fmt"
"slices"
"strconv"
"sync"
"time"
"git.erbosoft.com/amy/amsterdam/util"
lru "github.com/hashicorp/golang-lru"
"golang.org/x/text/language"
)
// Community struct contains the high level data for a community.
@@ -52,6 +55,27 @@ var communityCache *lru.TwoQueueCache = nil
// getCommunityMutex is a mutex on AmGetCommunity.
var getCommunityMutex sync.Mutex
// memberCacheData caches membership information for communities.
type memberCacheData struct {
isMember bool
locked bool
level uint16
}
// memberCache contains the memberCacheData entries.
var memberCache *lru.Cache = nil
// memberMutex syncs access to the memberCache.
var memberMutex sync.Mutex
// stuffMembership stuffs a membership record into the cache.
func stuffMembership(cid int32, uid int32, member bool, locked bool, level uint16) {
key := fmt.Sprintf("%d:%d", cid, uid)
memberMutex.Lock()
memberCache.Add(key, &memberCacheData{isMember: member, locked: locked, level: level})
memberMutex.Unlock()
}
// init initializes the community cache.
func init() {
var err error
@@ -59,6 +83,98 @@ func init() {
if err != nil {
panic(err)
}
memberCache, err = lru.New(250)
if err != nil {
panic(err)
}
}
// Public returns true if the community is public.
func (c *Community) Public() bool {
return c.JoinKey == nil || *c.JoinKey == ""
}
// ContactInfo returns the contact info structure for the community.
func (c *Community) ContactInfo() (*ContactInfo, error) {
if c.ContactId < 0 {
return nil, nil
}
return AmGetContactInfo(c.ContactId)
}
// Host returns the reference to the host of the community.
func (c *Community) Host() (*User, error) {
if c.HostUid == nil {
return nil, nil
}
return AmGetUser(*c.HostUid)
}
func (c *Community) LanguageTag() (*language.Tag, error) {
if c.Language == nil {
return nil, nil
}
t, err := language.Parse(*c.Language)
if err != nil {
return nil, err
}
return &t, nil
}
/* Membership returns the details of the specified user's membership in the community.
* Parameters:
* u - The user to check the membership of.
* Returns:
* true if the user is a member, false if not.
* true if the user's membership is "locked" (cannot unjoin), false if not.
* User's access level in the community, or 0 if the user is not a member.
* Standard Go error status.
*/
func (c *Community) Membership(u *User) (bool, bool, uint16, error) {
key := fmt.Sprintf("%d:%d", c.Id, u.Uid)
memberMutex.Lock()
defer memberMutex.Unlock()
mbr, ok := memberCache.Get(key)
if ok {
m := mbr.(*memberCacheData)
return m.isMember, m.locked, m.level, nil
}
if AmTestPermission("Community.NoJoinRequired", u.BaseLevel) {
// "no join required" - they are effectively a member, but don't cache that
return true, false, u.BaseLevel, nil
}
rs, err := amdb.Query("SELECT locked, granted_lvl FROM commmember WHERE commid = ? AND uid = ?", c.Id, u.Uid)
if err == nil {
if rs.Next() {
var locked bool
var level uint16
rs.Scan(&locked, &level)
memberCache.Add(key, &memberCacheData{isMember: true, locked: locked, level: level})
return true, locked, level, nil
}
memberCache.Add(key, &memberCacheData{isMember: false, locked: false, level: uint16(0)})
}
return false, false, uint16(0), err
}
/* TestPermission is shorthand that tests if a user has a permission with respect to the community.
* Parameters:
* user - The user to be checked.
* perm - The permission to be tested.
* Returns:
* true if the user has the permission, false if not.
* Standard Go error status.
*/
func (c *Community) TestPermission(user *User, perm string) (bool, error) {
member, _, level, err := c.Membership(user)
if err != nil {
return false, err
}
effectiveLevel := user.BaseLevel
if member && level > effectiveLevel {
effectiveLevel = level
}
return AmTestPermission(perm, effectiveLevel), nil
}
/* AmGetCommunity returns a reference to the specified community.
@@ -79,7 +195,9 @@ func AmGetCommunity(id int32) (*Community, error) {
if err != nil {
return nil, err
}
if len(dbdata) > 1 {
if len(dbdata) == 0 {
return nil, fmt.Errorf("community with ID %d not found", id)
} else if len(dbdata) > 1 {
return nil, fmt.Errorf("AmGetCommunity(%d): too many responses(%d)", id, len(dbdata))
}
rc = &(dbdata[0])
@@ -88,6 +206,37 @@ func AmGetCommunity(id int32) (*Community, error) {
return rc.(*Community), err
}
/* AmGetCommunityFromParam returns a reference to the specified community based on the parameter.
* If the parameter is numeric, it's interpreted as a community ID. Otherwise, it's interpreted
* as a community alias.
* Parameters:
* id - The ID of the community.
* Returns:
* Pointer to Community containing community data, or nil
* Standard Go error status
*/
func AmGetCommunityFromParam(param string) (*Community, error) {
if util.IsNumeric(param) {
v, _ := strconv.Atoi(param)
c, err := AmGetCommunity(int32(v))
if err == nil {
return c, nil
}
// else fall through to trying as alias
}
rs, err := amdb.Query("SELECT commid FROM communities WHERE alias = ?", param)
if err == nil {
if rs.Next() {
var cid int32
rs.Scan(&cid)
return AmGetCommunity(cid)
} else {
return nil, fmt.Errorf("community with alias \"%s\" not found", param)
}
}
return nil, err
}
/* AmGetCommunitiesForUser returns a list of communities the user is a member of.
* Parameters:
* uid - The ID of the user.
@@ -164,6 +313,7 @@ func AmAutoJoinCommunities(user *User) error {
if err != nil {
break
}
stuffMembership(cid, user.Uid, true, lock, grantLevel)
}
}
}
+160
View File
@@ -0,0 +1,160 @@
/*
* 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/.
*/
// The database package contains database management and storage logic.
package database
import (
"errors"
"sync"
"git.erbosoft.com/amy/amsterdam/util"
)
// Globals contains the global data.
type Globals struct {
Mutex sync.Mutex
PostsPerPage int32 `db:"posts_per_page"`
OldPostsAtTop int32 `db:"old_posts_at_top"`
MaxSearchPage int32 `db:"max_search_page"`
MaxCommunityMemberPage int32 `db:"max_comm_mbr_page"`
MaxConferenceMemberPage int32 `db:"max_conf_mbr_page"`
FrontPagePosts int32 `db:"fp_posts"`
NumAuditPage int32 `db:"num_audit_page"`
CommunityCreateLevel int32 `db:"comm_create_lvl"`
flags *util.OptionSet
}
// GlobalProperties contains global property entries.
type GlobalProperties struct {
Index int32 `db:"ndx"`
Data string `db:"data"`
}
// Global property indexes defined.
const (
GlobalPropFlags = int32(0)
)
// Global flag indexes defined.
const (
GlobalFlagPicturesInPosts = uint(0)
GlobalFlagNoCategories = uint(1)
)
// theGlobals contains the singleton instance of Globals.
var theGlobals *Globals = nil
// globalsMutex controls access to theGlobals.
var globalsMutex sync.Mutex
// globalProps is the global properties store.
var globalProps map[int32]string = make(map[int32]string)
// globalPropMutex controls access to globalProps.
var globalPropMutex sync.Mutex
// Flags returns the global flags.
func (g *Globals) Flags() (*util.OptionSet, error) {
g.Mutex.Lock()
defer g.Mutex.Unlock()
if g.flags == nil {
s, err := AmGetGlobalProperty(GlobalPropFlags)
if err != nil {
return nil, err
}
g.flags = util.OptionSetFromString(s)
}
return g.flags, nil
}
// SaveFlags saves off the global flags.
func (g *Globals) SaveFlags(f *util.OptionSet) error {
s := f.AsString()
g.Mutex.Lock()
defer g.Mutex.Unlock()
err := AmSetGlobalProperty(GlobalPropFlags, s)
if err == nil {
g.flags = f
}
return err
}
// AmGlobals returns trhe pointer to the singleton Globals instance.
func AmGlobals() (*Globals, error) {
globalsMutex.Lock()
defer globalsMutex.Unlock()
if theGlobals == nil {
var dbdata []Globals
err := amdb.Select(&dbdata, "SELECT * FROM globals")
if err != nil {
return nil, err
}
if len(dbdata) > 1 {
return nil, errors.New("should only be one globals record")
}
theGlobals = &(dbdata[0])
}
return theGlobals, nil
}
/* AmGetGlobalProperty returns the value of a global property.
* Parameters:
* index - The index of the property to retrieve.
* Returns:
* Value of the property, or empty string.
* Standard Go error status.
*/
func AmGetGlobalProperty(index int32) (string, error) {
globalPropMutex.Lock()
defer globalPropMutex.Unlock()
rc, ok := globalProps[index]
if !ok {
rs, err := amdb.Query("SELECT data FROM propglobal WHERE ndx = ?", index)
if err != nil {
return "", err
}
if rs.Next() {
rs.Scan(&rc)
globalProps[index] = rc
return rc, nil
}
rc = ""
}
return rc, nil
}
/* AmSetGlobalProperty sets the value of a global property.
* Parameters:
* index - The index of the property to set.
* value - The value of the property to set.
* Returns:
* Standard Go error status.
*/
func AmSetGlobalProperty(index int32, value string) error {
globalPropMutex.Lock()
defer globalPropMutex.Unlock()
_, updateMode := globalProps[index]
if !updateMode {
rs, err := amdb.Query("SELECT data FROM propglobal WHERE ndx = ?", index)
if err != nil {
return err
}
updateMode = rs.Next()
}
var err error = nil
if updateMode {
_, err = amdb.Exec("UPDATE propglobal SET data = ? WHERE ndx = ?", value, index)
} else {
_, err = amdb.Exec("INSERT INTO propglobal (ndx, data) VALUES (?, ?)", index, value)
}
if err == nil {
globalProps[index] = value
}
return err
}
+47
View File
@@ -0,0 +1,47 @@
#
# 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/.
#
domains:
- domain: "community"
services:
- id: "Profile"
index: 0
default: true
locked: true
requirePermission: ""
requireRole: ""
linkSequence: 4900
link: "/comm/[CID]/profile"
title: "Profile"
- id: "Admin"
index: 1
default: true
locked: true
requirePermission: "Community.Read"
requireRole: "Community.AnyAdmin"
linkSequence: 5000
link: "/TODO/comm/[CID]/admin"
title: "Administration"
- id: "Conference"
index: 3
default: true
locked: false
requirePermission: "Community.Read"
requireRole: ""
linkSequence: 500
link: "/TODO/comm/[CID]/conf"
title: "Conferences"
- id: "Members"
index: 4
default: true
locked: true
requirePermission: "Community.Read"
requireRole: "Community.Member"
linkSequence: 4800
link: "/TODO/comm/[CID]/members"
title: "Members"
+116
View File
@@ -0,0 +1,116 @@
/*
* 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/.
*/
// The database package contains database management and storage logic.
package database
import (
_ "embed"
"slices"
"sync"
lru "github.com/hashicorp/golang-lru"
"gopkg.in/yaml.v3"
)
// ServiceDef holds the definition for an individual service.
type ServiceDef struct {
Id string `yaml:"id"`
Index int16 `yaml:"index"`
Default bool `yaml:"default"`
Locked bool `yaml:"locked"`
RequirePermission string `yaml:"requirePermission"`
RequireRole string `yaml:"requireRole"`
LinkSequence int `yaml:"linkSequence"`
Link int `yaml:"link"`
Title string `yaml:"title"`
}
// ServiceDomain holds each individual configured service domain.
type ServiceDomain struct {
DomainName string `yaml:"domain"`
Services []ServiceDef `yaml:"services"`
byId map[string]*ServiceDef
byIndex map[int16]*ServiceDef
seqOrder []*ServiceDef
}
// ServiceConfiguration holds the service configuration.
type ServiceConfiguration struct {
Domains []ServiceDomain `yaml:"domains"`
byName map[string]*ServiceDomain
}
//go:embed servicedefs.yaml
var initServiceData []byte
// The service configuration loaded from YAML.
var serviceRoot ServiceConfiguration
// The services cache for communities.
var servicesCache *lru.TwoQueueCache
// Mutex on the services cache.
var servicesCacheMutex sync.Mutex
// init loads the service configuration and builds all the internal indexes.
func init() {
var err error
if err = yaml.Unmarshal(initServiceData, &serviceRoot); err != nil {
panic(err) // can't happen
}
serviceRoot.byName = make(map[string]*ServiceDomain)
for i, dom := range serviceRoot.Domains {
serviceRoot.Domains[i].byId = make(map[string]*ServiceDef)
serviceRoot.Domains[i].byIndex = make(map[int16]*ServiceDef)
sqo := make([]*ServiceDef, 0, len(serviceRoot.Domains[i].Services))
for j, svc := range serviceRoot.Domains[i].Services {
serviceRoot.Domains[i].byId[svc.Id] = &(serviceRoot.Domains[i].Services[j])
serviceRoot.Domains[i].byIndex[svc.Index] = &(serviceRoot.Domains[i].Services[j])
sqo = append(sqo, &(serviceRoot.Domains[i].Services[j]))
}
slices.SortFunc(sqo, func(a, b *ServiceDef) int {
return a.LinkSequence - b.LinkSequence
})
serviceRoot.Domains[i].seqOrder = sqo
serviceRoot.byName[dom.DomainName] = &(serviceRoot.Domains[i])
}
servicesCache, err = lru.New2Q(50)
if err != nil {
panic(err)
}
}
/* AmGetCommunityServices returns all the community service definitions for a community.
* Parameters:
* cid - Community ID to get services for.
* Returns:
* Array of ServiceDef pointers for the community's services.
* Standard Go error status.
*/
func AmGetCommunityServices(cid int32) ([]*ServiceDef, error) {
servicesCacheMutex.Lock()
defer servicesCacheMutex.Unlock()
rc, ok := servicesCache.Get(cid)
if !ok {
rs, err := amdb.Query("SELECT ftr_code FROM commftrs WHERE commid = ?", cid)
if err != nil {
return nil, err
}
dom := serviceRoot.byName["community"]
a := make([]*ServiceDef, 0, len(dom.Services))
for rs.Next() {
var ndx int16
rs.Scan(&ndx)
a = append(a, dom.byIndex[ndx])
}
servicesCache.Add(cid, a)
rc = a
}
return rc.([]*ServiceDef), nil
}
+1
View File
@@ -59,6 +59,7 @@ func setupEcho() *echo.Echo {
e.GET("/user/:uname", ui.AmWrap(ShowProfile))
e.POST("/quick_email", ui.AmWrap(QuickEMail))
e.GET("/sysadmin", ui.AmWrap(SysAdminMenu))
e.GET("/comm/:cid/profile", ui.AmWrap(ShowCommunity))
return e
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

+141
View File
@@ -0,0 +1,141 @@
{*
* 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">Community Profile:</h1>
<span class="text-blue-800 text-2xl font-bold ml-2">{{ commName }}</span>
<hr class="border-2 border-gray-400 w-4/5 mt-2 mb-6">
</div>
<!-- Community Profile Card -->
<div class="bg-gray-50 p-6 rounded-lg mb-8 max-w-4xl">
<div class="flex gap-6">
<!-- Left Column: Image and Metadata -->
<div class="flex-shrink-0 w-32">
<div class="border-2 border-gray-300 rounded mb-4">
<img src="/img/static/default-community.jpg"
alt="{{ commName}} community logo" class="w-full h-auto">
</div>
<div class="text-xs text-gray-700 space-y-2 mb-4">
<div><strong>Community created:</strong><br>{{ dateCreated }}</div>
{{ if isset(dateLastAccess) }}
<div><strong>Last accessed:</strong><br>{{ dateLastAccess }}</div>
{{ end }}
{{ if isset(dateLastUpdate) }}
<div><strong>Profile last updated:</strong><br>{{ dateLastUpdate }}</div>
{{ end }}
</div>
{{ if isset(canJoin) }}
<div class="text-center">
<a href="/TODO/comm/join"
class="inline-block bg-blue-600 hover:bg-blue-700 text-white px-4 py-1 rounded text-xs font-medium transition-colors">
Join Now
</a>
</div>
{{ end }}
{{ if isset(canInvite) }}
<div class="text-center">
<a href="/TODO/comm/invite"
class="inline-block bg-blue-600 hover:bg-blue-700 text-white px-4 py-1 rounded text-xs font-medium transition-colors">
Invite
</a>
</div>
{{ end }}
</div>
<!-- Right Column: Community Information -->
<div class="flex-1 text-sm text-black">
<div class="space-y-4">
<!-- Community Type and Category -->
<div>
{{ if public }}
<div class="text-lg font-bold underline mb-2">Public Community</div>
{{ else }}
<div class="text-lg font-bold underline mb-2">Private Community</div>
{{ end }}
{{ if isset(categories) }}
<div>
<strong>Category:</strong>
{{ range i := categories }}
{{ if i > 0 }}: {{ end }}
<a href="/TODO/find/communities-for-category"
class="text-blue-700 hover:text-blue-900">{{ .Name }}</a>
{{ end }}
</div>
{{ end }}
</div>
{{ if isset(description) }}
<!-- Description -->
<div class="italic text-gray-700">{{ description }}</div>
{{ end }}
<!-- Host -->
<div>
<strong>Host:</strong>
<a href="/users/{{ hostName }}" class="text-blue-700 hover:text-blue-900">{{ hostName }}</a>
</div>
<!-- Location -->
<div class="pt-2 border-t border-gray-300">
<strong>Location:</strong>
<div class="mt-1">
{{ 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>
</div>
{{ if isset(language) || isset(rules) || isset(homePage) }}
<!-- Additional Information -->
<div class="pt-2 border-t border-gray-300 space-y-2">
{{ if isset(language) }}
<div><strong>Primary Language:</strong> {{ language }}</div>
{{ end }}
{{ if isset(rules) }}
<div><strong>Standards of Conduct:</strong> {{ rules }}</div>
{{ end }}
{{ if isset(homePage) }}
<div>
<strong>Homepage:</strong>
<a href="{{ homepage }}" target="_blank"
class="text-blue-700 hover:text-blue-900">{{ homepage }}</a>
</div>
{{ end }}
</div>
{{ end }}
</div>
</div>
</div>
</div>
<!-- Community Information Note -->
<div class="max-w-4xl p-4 bg-blue-50 border-l-4 border-blue-400">
<p class="text-sm text-gray-700">
<strong>About This Community:</strong>
{{ if public }}
This is a public community open to all users of the {{ GlobalConfig.Site.Title }} site.
You can join discussions in the conferences, view member lists, and participate in community activities.
{{ else }}
This is a private community, open by invitation only. You can only participate if you have been invited
by one of the community hosts.
{{ end }}
</p>
</div>
</div>
+6
View File
@@ -26,5 +26,11 @@
<div class="text-small">You are not a member of any communities.</div>
{{ end }}
</div>
<div class="mt-3 text-center">
<span class="text-black text-xs font-bold">
[ <a href="/TODO/manage_comms" class="text-blue-700 hover:text-blue-900">Manage</a> |
<a href="/TODO/create_comm" class="text-blue-700 hover:text-blue-900">Create New</a> ]
</span>
</div>
</div>
</div>
+18 -6
View File
@@ -339,6 +339,18 @@ func ShowProfile(ctxt ui.AmContext) (string, any, error) {
if err != nil {
return ui.ErrorPage(ctxt, err)
}
var pvtAddr, pvtPhone, pvtFax, pvtEmail bool
if database.AmTestPermission("Global.SeeHiddenContactInfo", me.BaseLevel) {
pvtAddr = false
pvtPhone = false
pvtFax = false
pvtEmail = false
} else {
pvtAddr = ci.PrivateAddr
pvtPhone = ci.PrivatePhone
pvtFax = ci.PrivateFax
pvtEmail = ci.PrivateEmail
}
// Fill all the page variables for display.
ctxt.VarMap().Set("uid", user.Uid)
@@ -369,7 +381,7 @@ func ShowProfile(ctxt ui.AmContext) (string, any, error) {
if user.Description != nil {
ctxt.VarMap().Set("description", *user.Description)
}
if !ci.PrivateEmail && ci.Email != nil {
if !pvtEmail && ci.Email != nil {
ctxt.VarMap().Set("email", *ci.Email)
}
if ci.URL != nil && *ci.URL != "" {
@@ -378,10 +390,10 @@ func ShowProfile(ctxt ui.AmContext) (string, any, error) {
if ci.Company != nil {
ctxt.VarMap().Set("company", *ci.Company)
}
if !ci.PrivateAddr && ci.Addr1 != nil {
if !pvtAddr && ci.Addr1 != nil {
ctxt.VarMap().Set("addr1", *ci.Addr1)
}
if !ci.PrivateAddr && ci.Addr2 != nil {
if !pvtAddr && ci.Addr2 != nil {
ctxt.VarMap().Set("addr2", *ci.Addr2)
}
b.Reset()
@@ -402,13 +414,13 @@ func ShowProfile(ctxt ui.AmContext) (string, any, error) {
country := countries.ByName(*ci.Country)
ctxt.VarMap().Set("country", country.String())
}
if !ci.PrivatePhone && ci.Phone != nil {
if !pvtPhone && ci.Phone != nil {
ctxt.VarMap().Set("phone", *ci.Phone)
}
if !ci.PrivateFax && ci.Fax != nil {
if !pvtFax && ci.Fax != nil {
ctxt.VarMap().Set("fax", *ci.Fax)
}
if !ci.PrivatePhone && ci.Mobile != nil {
if !pvtPhone && ci.Mobile != nil {
ctxt.VarMap().Set("mobile", *ci.Mobile)
}
ctxt.VarMap().Set("amsterdam_pageTitle", fmt.Sprintf("User Profile - %s", user.Username))
+24 -1
View File
@@ -10,7 +10,20 @@
// Package util contains utility definitions.
package util
import "unicode"
import (
"regexp"
"unicode"
)
var numeric *regexp.Regexp
func init() {
re, err := regexp.Compile("^[0-9]+$")
if err != nil {
panic(err)
}
numeric = re
}
/* CapitalizeString changes the first character of the string to a capital.
* Parameters:
@@ -26,3 +39,13 @@ func CapitalizeString(s string) string {
}
return ""
}
/* IsNumeric returns true if the string is numeric (all digits).
* Parameters:
* s - String to be tested.
* Returns:
* true if string is numeric, false if not.
*/
func IsNumeric(s string) bool {
return numeric.MatchString(s)
}