mirror of
https://github.com/MeshTender/MeshTender.git
synced 2026-09-16 16:42:37 +00:00
Organization region field
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
-- +goose Up
|
||||
-- Freeform region/location for an organization (e.g. "Buffalo, NY"), shown on
|
||||
-- the org page and public directory.
|
||||
ALTER TABLE organizations ADD COLUMN region TEXT NOT NULL DEFAULT '';
|
||||
|
||||
-- +goose Down
|
||||
ALTER TABLE organizations DROP COLUMN region;
|
||||
@@ -14,6 +14,7 @@ type Org struct {
|
||||
ID int64
|
||||
Name string
|
||||
Description string
|
||||
Region string
|
||||
CreatedBy *int64
|
||||
CreatedAt time.Time
|
||||
}
|
||||
@@ -61,8 +62,8 @@ func (s *Store) CreateOrg(ctx context.Context, name string, creatorID int64) (*O
|
||||
var o Org
|
||||
if err := tx.QueryRow(ctx,
|
||||
`INSERT INTO organizations (name, created_by) VALUES ($1, $2)
|
||||
RETURNING id, name, description, created_by, created_at`,
|
||||
name, creatorID).Scan(&o.ID, &o.Name, &o.Description, &o.CreatedBy, &o.CreatedAt); err != nil {
|
||||
RETURNING id, name, description, region, created_by, created_at`,
|
||||
name, creatorID).Scan(&o.ID, &o.Name, &o.Description, &o.Region, &o.CreatedBy, &o.CreatedAt); err != nil {
|
||||
return nil, fmt.Errorf("insert org: %w", err)
|
||||
}
|
||||
if _, err := tx.Exec(ctx,
|
||||
@@ -100,8 +101,8 @@ func (s *Store) CreateOrg(ctx context.Context, name string, creatorID int64) (*O
|
||||
func (s *Store) GetOrg(ctx context.Context, id int64) (*Org, error) {
|
||||
var o Org
|
||||
err := s.pool.QueryRow(ctx,
|
||||
`SELECT id, name, description, created_by, created_at FROM organizations WHERE id = $1`, id).
|
||||
Scan(&o.ID, &o.Name, &o.Description, &o.CreatedBy, &o.CreatedAt)
|
||||
`SELECT id, name, description, region, created_by, created_at FROM organizations WHERE id = $1`, id).
|
||||
Scan(&o.ID, &o.Name, &o.Description, &o.Region, &o.CreatedBy, &o.CreatedAt)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
@@ -111,10 +112,11 @@ func (s *Store) GetOrg(ctx context.Context, id int64) (*Org, error) {
|
||||
return &o, nil
|
||||
}
|
||||
|
||||
// UpdateOrg updates an org's name and description.
|
||||
func (s *Store) UpdateOrg(ctx context.Context, orgID int64, name, description string) error {
|
||||
// UpdateOrg updates an org's name, description, and region.
|
||||
func (s *Store) UpdateOrg(ctx context.Context, orgID int64, name, description, region string) error {
|
||||
tag, err := s.pool.Exec(ctx,
|
||||
`UPDATE organizations SET name = $2, description = $3 WHERE id = $1`, orgID, name, description)
|
||||
`UPDATE organizations SET name = $2, description = $3, region = $4 WHERE id = $1`,
|
||||
orgID, name, description, region)
|
||||
if err != nil {
|
||||
return fmt.Errorf("update org: %w", err)
|
||||
}
|
||||
|
||||
@@ -130,7 +130,6 @@ func (s *Server) pageOrg(w http.ResponseWriter, r *http.Request) {
|
||||
"MemberCount": len(members),
|
||||
"RepeaterCount": len(repeaters),
|
||||
"AdminCount": adminCount,
|
||||
"MappedCount": mapped,
|
||||
"Self": uid,
|
||||
"Error": r.URL.Query().Get("error"),
|
||||
}
|
||||
@@ -198,6 +197,7 @@ func (s *Server) handleEditOrg(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
name := strings.TrimSpace(r.FormValue("name"))
|
||||
desc := strings.TrimSpace(r.FormValue("description"))
|
||||
region := strings.TrimSpace(r.FormValue("region"))
|
||||
if name == "" || len(name) > 80 {
|
||||
orgErr(w, r, id, "Enter an organization name.")
|
||||
return
|
||||
@@ -205,7 +205,10 @@ func (s *Server) handleEditOrg(w http.ResponseWriter, r *http.Request) {
|
||||
if len(desc) > 2000 {
|
||||
desc = desc[:2000]
|
||||
}
|
||||
if err := s.store.UpdateOrg(r.Context(), id, name, desc); err != nil {
|
||||
if len(region) > 120 {
|
||||
region = region[:120]
|
||||
}
|
||||
if err := s.store.UpdateOrg(r.Context(), id, name, desc, region); err != nil {
|
||||
orgErr(w, r, id, "Could not save changes.")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<a href="/" class="navbar-brand navbar-brand-autodark d-flex align-items-center text-reset text-decoration-none">
|
||||
{{template "icon-tower" "me-2"}}<span class="fw-bold fs-3">MeshTender</span>
|
||||
{{template "icon-tower" "me-1"}}<span class="fw-bold fs-3">MeshTender</span>
|
||||
</a>
|
||||
<div class="navbar-nav flex-row order-md-last align-items-center gap-2">
|
||||
{{if .UserName}}
|
||||
@@ -121,7 +121,7 @@
|
||||
<div class="container container-tight py-4">
|
||||
<div class="text-center mb-4">
|
||||
<a href="/" class="navbar-brand navbar-brand-autodark d-inline-flex align-items-center text-reset text-decoration-none">
|
||||
{{template "icon-tower" "me-2"}}<span class="fw-bold fs-2">MeshTender</span>
|
||||
{{template "icon-tower" "me-1"}}<span class="fw-bold fs-2">MeshTender</span>
|
||||
</a>
|
||||
</div>
|
||||
{{template "content" .}}
|
||||
@@ -147,7 +147,7 @@
|
||||
<header class="navbar navbar-expand-md navbar-transparent py-3">
|
||||
<div class="container">
|
||||
<a href="/" class="navbar-brand navbar-brand-autodark d-flex align-items-center text-reset text-decoration-none">
|
||||
{{template "icon-tower" "me-2"}}<span class="fw-bold fs-3">MeshTender</span>
|
||||
{{template "icon-tower" "me-1"}}<span class="fw-bold fs-3">MeshTender</span>
|
||||
</a>
|
||||
<div class="navbar-nav flex-row order-md-last align-items-center gap-2">
|
||||
<a href="/login" class="btn btn-ghost-secondary">Sign in</a>
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
{{define "icon-arrow-left"}}<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon{{if .}} {{.}}{{end}}"><path d="M5 12l14 0"/><path d="M5 12l6 6"/><path d="M5 12l6 -6"/></svg>{{end}}
|
||||
{{define "icon-check"}}<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon{{if .}} {{.}}{{end}}"><path d="M5 12l5 5l10 -10"/></svg>{{end}}
|
||||
{{define "icon-alert"}}<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon{{if .}} {{.}}{{end}}"><path d="M12 9v4"/><path d="M12 17h.01"/><path d="M10.24 3.957l-8.422 14.06a1.989 1.989 0 0 0 1.7 2.983h16.845a1.989 1.989 0 0 0 1.7 -2.983l-8.423 -14.06a1.989 1.989 0 0 0 -3.4 0z"/></svg>{{end}}
|
||||
{{define "icon-map-pin"}}<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon{{if .}} {{.}}{{end}}"><path d="M9 11a3 3 0 1 0 6 0a3 3 0 0 0 -6 0"/><path d="M17.657 16.657l-4.243 4.243a2 2 0 0 1 -2.827 0l-4.244 -4.243a8 8 0 1 1 11.314 0z"/></svg>{{end}}
|
||||
{{define "icon-key"}}<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon{{if .}} {{.}}{{end}}"><path d="M16.555 3.843l3.602 3.602a2.877 2.877 0 0 1 0 4.069l-2.643 2.643a2.877 2.877 0 0 1 -4.069 0l-.301 -.301l-6.558 6.558a2 2 0 0 1 -1.239 .578l-.175 .008h-1.476a1 1 0 0 1 -.993 -.883l-.007 -.117v-1.476a2 2 0 0 1 .467 -1.284l.119 -.13l.414 -.414h2v-2h2v-2l2.144 -2.144l-.301 -.301a2.877 2.877 0 0 1 0 -4.069l2.643 -2.643a2.877 2.877 0 0 1 4.069 0z"/><path d="M15 9h.01"/></svg>{{end}}
|
||||
{{define "icon-eye"}}<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon{{if .}} {{.}}{{end}}"><path d="M10 12a2 2 0 1 0 4 0a2 2 0 0 0 -4 0"/><path d="M21 12c-2.4 4 -5.4 6 -9 6c-3.6 0 -6.6 -2 -9 -6c2.4 -4 5.4 -6 9 -6c3.6 0 6.6 2 9 6"/></svg>{{end}}
|
||||
{{define "icon-eye-off"}}<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon{{if .}} {{.}}{{end}}"><path d="M10.585 10.587a2 2 0 0 0 2.829 2.828"/><path d="M16.681 16.673a8.717 8.717 0 0 1 -4.681 1.327c-3.6 0 -6.6 -2 -9 -6c1.272 -2.12 2.712 -3.678 4.32 -4.674m2.86 -1.146a9.055 9.055 0 0 1 1.82 -.18c3.6 0 6.6 2 9 6c-.666 1.11 -1.379 2.067 -2.138 2.87"/><path d="M3 3l18 18"/></svg>{{end}}
|
||||
|
||||
@@ -31,16 +31,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-lg-3">
|
||||
<div class="card card-sm">
|
||||
<div class="card-body">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-auto"><span class="bg-azure text-white avatar">{{template "icon-antenna" ""}}</span></div>
|
||||
<div class="col"><div class="h1 mb-0">{{.RepeaterCount}}</div><div class="text-secondary">Repeater{{if ne .RepeaterCount 1}}s{{end}}</div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-lg-3">
|
||||
<div class="card card-sm">
|
||||
<div class="card-body">
|
||||
@@ -55,8 +45,21 @@
|
||||
<div class="card card-sm">
|
||||
<div class="card-body">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-auto"><span class="bg-purple text-white avatar">{{template "icon-world" ""}}</span></div>
|
||||
<div class="col"><div class="h1 mb-0">{{.MappedCount}}</div><div class="text-secondary">On the map</div></div>
|
||||
<div class="col-auto"><span class="bg-azure text-white avatar">{{template "icon-antenna" ""}}</span></div>
|
||||
<div class="col"><div class="h1 mb-0">{{.RepeaterCount}}</div><div class="text-secondary">Repeater{{if ne .RepeaterCount 1}}s{{end}}</div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-lg-3">
|
||||
<div class="card card-sm">
|
||||
<div class="card-body">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-auto"><span class="bg-purple text-white avatar">{{template "icon-map-pin" ""}}</span></div>
|
||||
<div class="col text-truncate">
|
||||
<div class="h3 mb-0 text-truncate">{{if .Org.Region}}{{.Org.Region}}{{else}}<span class="text-secondary fw-normal">Not set</span>{{end}}</div>
|
||||
<div class="text-secondary">Region</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -147,6 +150,10 @@
|
||||
<label class="form-label">Name</label>
|
||||
<input type="text" class="form-control" name="name" value="{{.Org.Name}}" maxlength="80" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Region <span class="text-secondary">(optional)</span></label>
|
||||
<input type="text" class="form-control" name="region" value="{{.Org.Region}}" maxlength="120" placeholder="e.g. Buffalo, NY">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Description</label>
|
||||
<textarea class="form-control" name="description" rows="3" maxlength="2000" placeholder="What this organization is about, where it operates, etc.">{{.Org.Description}}</textarea>
|
||||
|
||||
@@ -2,48 +2,109 @@
|
||||
{{define "header"}}
|
||||
<div class="row g-2 align-items-center">
|
||||
<div class="col">
|
||||
<div class="page-pretitle">Organization</div>
|
||||
<div class="page-pretitle">Organization{{if .IsMember}} · public preview{{end}}</div>
|
||||
<h2 class="page-title">{{.Org.Name}}</h2>
|
||||
</div>
|
||||
{{if .IsMember}}
|
||||
<div class="col-auto ms-auto d-print-none">
|
||||
<a class="btn" href="/orgs/{{.Org.ID}}">{{template "icon-arrow-left" "me-1"}}Back to member view</a>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
{{define "content"}}
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
{{if .IsMember}}<p class="text-secondary"><a href="/orgs/{{.Org.ID}}">{{template "icon-arrow-left" "me-1"}}Back to member view</a> — you're previewing the public page.</p>{{end}}
|
||||
{{if .Org.Description}}<p>{{.Org.Description}}</p>{{else}}<p class="text-secondary">This organization hasn't added a description yet.</p>{{end}}
|
||||
<div class="text-secondary small">{{.MemberCount}} member{{if ne .MemberCount 1}}s{{end}} · {{.RepeaterCount}} repeater{{if ne .RepeaterCount 1}}s{{end}}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title">Admins</h3>
|
||||
{{if .Admins}}
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
{{range .Admins}}<span class="badge bg-success-lt">{{.}}</span>{{end}}
|
||||
<!-- Summary stats -->
|
||||
<div class="row row-cards">
|
||||
<div class="col-sm-6 col-lg-3">
|
||||
<div class="card card-sm">
|
||||
<div class="card-body">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-auto"><span class="bg-primary text-white avatar">{{template "icon-users" ""}}</span></div>
|
||||
<div class="col"><div class="h1 mb-0">{{.MemberCount}}</div><div class="text-secondary">Member{{if ne .MemberCount 1}}s{{end}}</div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-lg-3">
|
||||
<div class="card card-sm">
|
||||
<div class="card-body">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-auto"><span class="bg-green text-white avatar">{{template "icon-user" ""}}</span></div>
|
||||
<div class="col"><div class="h1 mb-0">{{len .Admins}}</div><div class="text-secondary">Admin{{if ne (len .Admins) 1}}s{{end}}</div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-lg-3">
|
||||
<div class="card card-sm">
|
||||
<div class="card-body">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-auto"><span class="bg-azure text-white avatar">{{template "icon-antenna" ""}}</span></div>
|
||||
<div class="col"><div class="h1 mb-0">{{.RepeaterCount}}</div><div class="text-secondary">Repeater{{if ne .RepeaterCount 1}}s{{end}}</div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-lg-3">
|
||||
<div class="card card-sm">
|
||||
<div class="card-body">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-auto"><span class="bg-purple text-white avatar">{{template "icon-map-pin" ""}}</span></div>
|
||||
<div class="col text-truncate">
|
||||
<div class="h3 mb-0 text-truncate">{{if .Org.Region}}{{.Org.Region}}{{else}}<span class="text-secondary fw-normal">Not set</span>{{end}}</div>
|
||||
<div class="text-secondary">Region</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}<p class="text-secondary mb-0">No admins listed.</p>{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{if .HasMap}}
|
||||
<div class="card mt-3">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title">Public repeater map</h3>
|
||||
<p class="text-secondary">Repeaters whose owners chose to show them publicly.</p>
|
||||
<link rel="stylesheet" href="/static/leaflet.css">
|
||||
<div id="map"></div>
|
||||
<script src="/static/leaflet.js"></script>
|
||||
<script src="/static/meshmap.js"></script>
|
||||
<script>
|
||||
meshMap('map', [
|
||||
{{range .Repeaters}}{{if .HasLocation}}{name: {{.Name}}, lat: {{.Lat}}, lon: {{.Lon}}},{{end}}{{end}}
|
||||
]);
|
||||
</script>
|
||||
<div class="row row-cards mt-1">
|
||||
<!-- Left column -->
|
||||
<div class="col-lg-8">
|
||||
<div class="card">
|
||||
<div class="card-header"><h3 class="card-title">Public repeater map</h3></div>
|
||||
{{if .HasMap}}
|
||||
<div class="card-body p-2">
|
||||
<link rel="stylesheet" href="/static/leaflet.css">
|
||||
<div id="map" style="height:340px"></div>
|
||||
<script src="/static/leaflet.js"></script>
|
||||
<script src="/static/meshmap.js"></script>
|
||||
<script>
|
||||
meshMap('map', [
|
||||
{{range .Repeaters}}{{if .HasLocation}}{name: {{.Name}}, lat: {{.Lat}}, lon: {{.Lon}}},{{end}}{{end}}
|
||||
]);
|
||||
</script>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="card-body"><p class="text-secondary mb-0">No repeaters are shown publicly. Members can opt a repeater into the public map when editing it.</p></div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right column -->
|
||||
<div class="col-lg-4">
|
||||
<div class="card">
|
||||
<div class="card-header"><h3 class="card-title">About</h3></div>
|
||||
<div class="card-body">
|
||||
{{if .Org.Region}}<div class="d-flex align-items-center text-secondary mb-2">{{template "icon-map-pin" "me-1"}}{{.Org.Region}}</div>{{end}}
|
||||
{{if .Org.Description}}<p class="mb-0">{{.Org.Description}}</p>{{else}}<p class="text-secondary mb-0">This organization hasn't added a description yet.</p>{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-header"><h3 class="card-title">Admins</h3></div>
|
||||
<div class="card-body">
|
||||
{{if .Admins}}
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
{{range .Admins}}<span class="badge bg-success-lt">{{.}}</span>{{end}}
|
||||
</div>
|
||||
{{else}}<p class="text-secondary mb-0">No admins listed.</p>{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<a class="btn btn-link px-0 mt-3" href="/orgs">{{template "icon-arrow-left" "me-1"}}All organizations</a>
|
||||
{{end}}
|
||||
|
||||
Reference in New Issue
Block a user