Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,9 +193,32 @@ Located in `/Users/dima/code/pcom/cmd/web/client/scss/_dark-mode.scss`:

**Do NOT define ad-hoc colors directly on elements** - always use Bootstrap's CSS variables to ensure proper inheritance and theming.

## Date/Time Rendering

Use `renderHumanTime` template helper to display timestamps with relative time and full timestamp on hover:

```html
{{ renderHumanTime .CreatedAt $.User.DBUser }}
```

**Output**: `<span title="Mon, 15 Jan 2024 12:30">5 minutes ago</span>`

- First argument: `time.Time` value to display
- Second argument: `*core.User` for timezone localization (can be nil for UTC)
- Implementation: `pkg/util/date`

## Development

### Verification
Use `make check` to verify code compiles and tests pass without producing build artifacts:
```bash
make check
```

## File Locations
- HTML Templates: `/Users/dima/code/pcom/cmd/web/client/html/`
- JavaScript: `/Users/dima/code/pcom/cmd/web/client/js/`
- Main JS entry: `/Users/dima/code/pcom/cmd/web/client/js/index.js`
- Markdown package: `/Users/dima/code/pcom/pkg/markdown/`
- Dark mode styles: `/Users/dima/code/pcom/cmd/web/client/scss/_dark-mode.scss`
- Date utilities: `/Users/dima/code/pcom/pkg/util/date/`
6 changes: 5 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: shell tunnel lint test build
.PHONY: shell tunnel lint test build check

shell:
flyctl postgres connect -a pcomdb
Expand All @@ -20,3 +20,7 @@ test:

build:
go build -v ./...

check:
go build -o /dev/null ./...
go test ./...
2 changes: 1 addition & 1 deletion cmd/web/client/html/controls.html
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ <h5 class="card-header">Drafts</h5>
{{ range . }}
<tr>
<td><a href="{{ link "edit_post" .PostID }}">{{ with .Subject }}{{ . }}{{ else }}No subject{{ end }}</a></td>
<td>{{ renderTimestamp .LastUpdatedAt $.User.DBUser }}</td>
<td>{{ renderHumanTime .LastUpdatedAt $.User.DBUser }}</td>
<td>
<button type="button"
class="btn btn-sm btn-danger"
Expand Down
6 changes: 3 additions & 3 deletions cmd/web/client/html/feed.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ <h1 class="us-user-header user-header">{{ if .User.IsLoggedIn }}<a href="{{ link
<div class="card">
<div class="card-header">
<h5 class="card-title fs-6">New Comment: <a href="{{ link "post" .Post.ID }}">{{ .Post.PostSubject }}</a> </h5>
<small> <a href="{{ link "user" .Author.Username }}">{{ .Author.Username }}</a> left a <a hx-boost="false" href="{{ link "comment" .PostID .ID }}">comment</a> {{ if eq $.User.DBUser.ID .Post.UserID }} to your post {{ else }} to the post you've commented on {{ end }} at {{ renderTimestamp .CreatedAt $.User.DBUser }} </small>
<small> <a href="{{ link "user" .Author.Username }}">{{ .Author.Username }}</a> left a <a hx-boost="false" href="{{ link "comment" .PostID .ID }}">comment</a> {{ if eq $.User.DBUser.ID .Post.UserID }} to your post {{ else }} to the post you've commented on {{ end }} {{ renderHumanTime .CreatedAt $.User.DBUser }} </small>
</div>
<div class="card-body">
<div class="mt-3 post-in-feed">{{ markdown_feed .Body }}</div>
Expand All @@ -35,7 +35,7 @@ <h5 class="card-title fs-6">New Comment: <a href="{{ link "post" .Post.ID }}">{{
<div class="card">
<div class="card-header">
<h5 class="card-title fs-6"><a href="{{ link "post" .ID }}">{{ .PostSubject }}</a> </h5>
<small><a href="{{ link "user" .Author.Username }}">{{ .Author.Username }}</a> {{ if gt (len .Via) 0 }} &#8594; {{ range $index, $element := .Via }}{{ if $index }}, {{ end }}<a href="{{ link "user" $element.Username }}">{{ .Username }}</a>{{ end }}{{ end }} <span class="us-post-date post-date">posted at {{ renderTimestamp .PublishedAt.Time $.User.DBUser }}</span></small>
<small><a href="{{ link "user" .Author.Username }}">{{ .Author.Username }}</a> {{ if gt (len .Via) 0 }} &#8594; {{ range $index, $element := .Via }}{{ if $index }}, {{ end }}<a href="{{ link "user" $element.Username }}">{{ .Username }}</a>{{ end }}{{ end }} <span class="us-post-date post-date">posted {{ renderHumanTime .PublishedAt.Time $.User.DBUser }}</span></small>
</div>

<div class="card-body">
Expand Down Expand Up @@ -77,7 +77,7 @@ <h5 class="card-title fs-6"><a href="{{ link "post" .ID }}">{{ .PostSubject }}</
<div class="d-flex w-100 justify-content-between">
<div>
<h5 class="card-title fs-6"><a href="{{ .URL }}" target="_blank" rel="noopener noreferrer">{{ with .Title }}{{ . }}{{ else }}No Title{{ end }}</a></h5>
<small><i class="bi bi-rss"></i> <a href="{{ .FeedURL }}">{{ with .FeedTitle }}{{ . }}{{ else }}no name yet{{ end }}</a> <span class="us-post-date post-date">posted at {{ renderTimestamp .PublishedAt $.User.DBUser }}</span></small>
<small><i class="bi bi-rss"></i> <a href="{{ .FeedURL }}">{{ with .FeedTitle }}{{ . }}{{ else }}no name yet{{ end }}</a> <span class="us-post-date post-date">posted {{ renderHumanTime .PublishedAt $.User.DBUser }}</span></small>
</div>
<div>
<button type="button"
Expand Down
4 changes: 2 additions & 2 deletions cmd/web/client/html/form--post.html
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{{ if .DraftSaved }}
<div id="last_draft_save">
<input type="hidden" name="post_id" value="{{ .PostID }}" />
<p class="fw-lighter">Last Updated at {{ renderTimestamp .LastUpdatedAt .User }}</p>
<p class="fw-lighter">Last Updated {{ renderHumanTime .LastUpdatedAt .User }}</p>
</div>
{{ else }}
<div id="form-container" class="container mt-lg-4 mt-2 flex-grow-1 d-flex flex-column">
Expand All @@ -27,7 +27,7 @@ <h1>{{ if .PostID }}Edit Post{{ if not .IsPublished }} <small class="text-muted"
<div id="last_draft_save">
{{ if .PostID }}
<input type="hidden" name="post_id" value="{{ .PostID }}" />
<small class="fw-lighter">Last Updated at {{ renderTimestamp .LastUpdatedAt .User }}</small>
<small class="fw-lighter">Last Updated {{ renderHumanTime .LastUpdatedAt .User }}</small>
{{ else }}
{{ with .Prompt }}
<input type="hidden" name="prompt_id" value="{{ .Prompt.ID }}" />
Expand Down
2 changes: 1 addition & 1 deletion cmd/web/client/html/partial--feed-prompts.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<div class="mt-3">
<div class="card">
<h5 class="card-header fs-6">
<a href="{{ link "user" .Author.Username }}">{{ .Author.Username }}</a> prompted you to write a post at {{ renderTimestamp .Prompt.CreatedAt $.User.DBUser }}
<a href="{{ link "user" .Author.Username }}">{{ .Author.Username }}</a> prompted you to write a post {{ renderHumanTime .Prompt.CreatedAt $.User.DBUser }}
</h5>
<div class="card-body">
<div class="d-flex">
Expand Down
6 changes: 3 additions & 3 deletions cmd/web/client/html/partial--settings_feeds.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ <h5 class="card-header">Feeds</h5>
<a href="{{ .URL }}" target="_blank" rel="noopener noreferrer" class="ms-1 text-muted small" title="RSS Feed"><i class="bi bi-rss"></i></a>
</div>
<div class="d-flex flex-wrap small gap-2">
<span><span class="text-muted">Fetched:</span> {{ with .LastFetchedAt }}<span title="{{ renderTimestamp . $.User.DBUser }}">{{ relativeTime . }}</span>{{ else }}Never{{ end }}</span>
<span><span class="text-muted">Post:</span> {{ with .LastImportedAt }}<span title="{{ renderTimestamp . $.User.DBUser }}">{{ relativeTime . }}</span>{{ else }}None{{ end }}</span>
<span><span class="text-muted">Next:</span> {{ with .NextFetchAt }}<span title="{{ renderTimestamp . $.User.DBUser }}">{{ relativeTime . }}</span>{{ else }}Now{{ end }}</span>
<span><span class="text-muted">Fetched:</span> {{ with .LastFetchedAt }}{{ renderHumanTime . $.User.DBUser }}{{ else }}Never{{ end }}</span>
<span><span class="text-muted">Post:</span> {{ with .LastImportedAt }}{{ renderHumanTime . $.User.DBUser }}{{ else }}None{{ end }}</span>
<span><span class="text-muted">Next:</span> {{ with .NextFetchAt }}{{ renderHumanTime . $.User.DBUser }}{{ else }}Now{{ end }}</span>
</div>
{{ with .LastError }}
<details class="small">
Expand Down
2 changes: 1 addition & 1 deletion cmd/web/client/html/single_post.html
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ <h1 class="us-post-header"><a href="{{ link "user" .Author.Username }}">{{ .Auth
<div class="card">
<h5 class="card-header fs-6">
[<a hx-boost="false" href="{{ link "comment" .PostID .ID }}">#</a>]
<a href="{{ link "user" .Author.Username }}">{{ .Author.Username }}</a> responded at {{ renderTimestamp .CreatedAt $.User.DBUser }}
<a href="{{ link "user" .Author.Username }}">{{ .Author.Username }}</a> responded {{ renderHumanTime .CreatedAt $.User.DBUser }}
</h5>
<div class="card-body">
<div class="mt-3 post-user-home">{{ markdown_comment .Body }}</div>
Expand Down
2 changes: 1 addition & 1 deletion cmd/web/client/html/user_home.html
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ <h1 class="us-user-header user-header"><a href="{{ link "user" .Author.Username
{{ range .Posts }}
<div class="mt-3 us-feed-post">
<div class="card">
<h5 class="card-header"><a href="{{ link "post" .ID }}">{{ .PostSubject }}</a> <span class="us-post-date post-date">posted at {{ renderTimestamp .PublishedAt.Time $.User.DBUser }}</span></h5>
<h5 class="card-header"><a href="{{ link "post" .ID }}">{{ .PostSubject }}</a> <span class="us-post-date post-date">posted {{ renderHumanTime .PublishedAt.Time $.User.DBUser }}</span></h5>
<div class="card-body">
<div class="mt-3 post-user-home">{{ markdown_feed .Body .ID }}</div>

Expand Down
14 changes: 3 additions & 11 deletions cmd/web/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,11 @@ import (
"github.com/can3p/pcom/pkg/types"
"github.com/can3p/pcom/pkg/userops"
"github.com/can3p/pcom/pkg/util"
"github.com/can3p/pcom/pkg/util/date"
"github.com/can3p/pcom/pkg/util/ginhelpers"
"github.com/can3p/pcom/pkg/util/ginhelpers/csp"
"github.com/can3p/pcom/pkg/util/ginhelpers/csrf"
"github.com/can3p/pcom/pkg/web"
"github.com/dustin/go-humanize"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
"github.com/jmoiron/sqlx"
Expand Down Expand Up @@ -890,16 +890,8 @@ func funcmap(staticAsset staticAssetFunc) template.FuncMap {

"abslink": links.AbsLink,

"renderTimestamp": func(t time.Time, user *core.User) string {
if user != nil {
t = util.LocalizeTime(user, t)
}

return t.Format("Mon, 02 Jan 2006 15:04")
},

"relativeTime": func(t time.Time) string {
return humanize.Time(t)
"renderHumanTime": func(t time.Time, user *core.User) template.HTML {
return date.RenderTimeHTML(t, user, time.Now())
},

"toMap": func(args ...interface{}) map[string]interface{} {
Expand Down
42 changes: 42 additions & 0 deletions pkg/util/date/date.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package date

import (
"fmt"
"html/template"
"time"

"github.com/can3p/pcom/pkg/model/core"
"github.com/dustin/go-humanize"
)

const TimestampFormat = "Mon, 02 Jan 2006 15:04"

func LocalizeTime(user *core.User, t time.Time) time.Time {
if user == nil {
return t
}

l, err := time.LoadLocation(user.Timezone)
if err != nil {
return t
}

return t.In(l)
}

func FormatTimestamp(t time.Time, user *core.User) string {
if user != nil {
t = LocalizeTime(user, t)
}
return t.Format(TimestampFormat)
}

func RelativeTime(t time.Time, now time.Time) string {
return humanize.RelTime(t, now, "ago", "from now")
}

func RenderTimeHTML(t time.Time, user *core.User, now time.Time) template.HTML {
timestamp := FormatTimestamp(t, user)
relative := RelativeTime(t, now)
return template.HTML(fmt.Sprintf(`<span title="%s">%s</span>`, template.HTMLEscapeString(timestamp), template.HTMLEscapeString(relative)))
}
112 changes: 112 additions & 0 deletions pkg/util/date/date_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package date

import (
"html/template"
"testing"
"time"

"github.com/can3p/pcom/pkg/model/core"
"github.com/stretchr/testify/require"
)

func TestLocalizeTime(t *testing.T) {
baseTime := time.Date(2024, 1, 15, 12, 0, 0, 0, time.UTC)

tests := []struct {
name string
user *core.User
input time.Time
expected time.Time
}{
{
name: "nil user returns original time",
user: nil,
input: baseTime,
expected: baseTime,
},
{
name: "user with UTC timezone",
user: &core.User{Timezone: "UTC"},
input: baseTime,
expected: baseTime,
},
{
name: "user with America/New_York timezone",
user: &core.User{Timezone: "America/New_York"},
input: baseTime,
expected: baseTime.In(mustLoadLocation("America/New_York")),
},
{
name: "user with invalid timezone returns original time",
user: &core.User{Timezone: "Invalid/Timezone"},
input: baseTime,
expected: baseTime,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := LocalizeTime(tt.user, tt.input)
require.Equal(t, tt.expected, result)
})
}
}

func TestFormatTimestamp(t *testing.T) {
baseTime := time.Date(2024, 1, 15, 12, 30, 0, 0, time.UTC)

tests := []struct {
name string
user *core.User
input time.Time
expected string
}{
{
name: "nil user formats in UTC",
user: nil,
input: baseTime,
expected: "Mon, 15 Jan 2024 12:30",
},
{
name: "user with timezone formats in local time",
user: &core.User{Timezone: "America/New_York"},
input: baseTime,
expected: "Mon, 15 Jan 2024 07:30",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := FormatTimestamp(tt.input, tt.user)
require.Equal(t, tt.expected, result)
})
}
}

func TestRelativeTime(t *testing.T) {
now := time.Date(2024, 1, 15, 12, 30, 0, 0, time.UTC)

result := RelativeTime(now.Add(-5*time.Minute), now)
require.Equal(t, "5 minutes ago", result)

result = RelativeTime(now.Add(-2*time.Hour), now)
require.Equal(t, "2 hours ago", result)
}

func TestRenderTimeHTML(t *testing.T) {
now := time.Date(2024, 1, 15, 12, 30, 0, 0, time.UTC)
baseTime := now.Add(-5 * time.Minute)

result := RenderTimeHTML(baseTime, nil, now)

expected := template.HTML(`<span title="Mon, 15 Jan 2024 12:25">5 minutes ago</span>`)
require.Equal(t, expected, result)
}

func mustLoadLocation(name string) *time.Location {
loc, err := time.LoadLocation(name)
if err != nil {
panic(err)
}
return loc
}
Loading