?
@@ -16,10 +16,10 @@ version: 0.1.0
@@ -31,7 +31,7 @@ version: 0.1.0
☕ added a new brew
-
+
@@ -39,7 +39,7 @@ version: 0.1.0
-
+
⭐ 8/10
@@ -47,6 +47,12 @@ version: 0.1.0
"Notes with "quotes" and <html>tags</html> and 'single quotes'"
+
\ No newline at end of file
diff --git a/internal/components/about.templ b/internal/components/about.templ
new file mode 100644
index 0000000..f0e8e57
--- /dev/null
+++ b/internal/components/about.templ
@@ -0,0 +1,110 @@
+package components
+
+templ AboutContent(isAuthenticated bool) {
+
+
+
+
+
+
+
+
About Arabica
+
ALPHA
+
+
+
+ Alpha Software: Arabica is currently in early development. Features may change, and data structures could be modified in future updates.
+
+
+
+
+ Your Coffee Journey, Your Data
+
+ Arabica is a coffee brew tracking application built on the AT Protocol, a decentralized social networking protocol.
+ Unlike traditional apps where your data is locked in a company's database, Arabica stores your brew logs,
+ coffee beans, and equipment information in your own Personal Data Server (PDS) .
+
+
+
+ What Makes Arabica Different?
+
+ You own your data - All your brew logs live in your PDS, not our servers
+ Portable identity - Switch PDS providers anytime without losing your data
+ Privacy by design - You control who sees your brews
+ Open protocol - Built on the AT Protocol, the same technology powering Bluesky
+
+
+
+ Features
+
+
+
Track Your Brews
+
Log every detail: beans, grind size, water temp, brew time, and tasting notes
+
+
+
Manage Equipment
+
Keep track of your grinders, brewers, beans, and roasters
+
+
+
Community Feed
+
Share your best brews with the community (coming soon: likes and comments!)
+
+
+
Export Your Data
+
Export all your brews anytime in JSON format
+
+
+
+
+ The AT Protocol Advantage
+
+ The AT Protocol is a decentralized social networking protocol that gives users true ownership of their data
+ and identity. When you use Arabica:
+
+
+ Your brew data is stored as ATProto records in collections on your PDS
+ You authenticate via OAuth with your PDS, not with us
+ References between records (like linking a brew to a bean) use AT-URIs
+ Your identity is portable - change PDS providers without losing your account
+
+
+
+ Getting Started
+
+ To use Arabica, you'll need an account on a PDS that supports the AT Protocol.
+ The easiest way is to create a Bluesky account at bsky.app .
+
+
+ Once you have an account, simply log in with your handle (e.g., yourname.bsky.social) and start tracking your brews!
+
+
+
+ Open Source
+
+ Arabica is open source software. You can view the code, contribute, or even run your own instance.
+ Visit our GitHub repository to learn more.
+
+
+
+
+ if !isAuthenticated {
+
+ Get Started
+
+ } else {
+
+ Log Your Next Brew
+
+ }
+
+
+}
+
+// About is the full About page with layout
+templ About(data *LayoutData) {
+ @Layout(data, AboutContent(data.IsAuthenticated))
+}
diff --git a/internal/components/about_templ.go b/internal/components/about_templ.go
new file mode 100644
index 0000000..e87b211
--- /dev/null
+++ b/internal/components/about_templ.go
@@ -0,0 +1,85 @@
+// Code generated by templ - DO NOT EDIT.
+
+// templ: version: v0.3.960
+package components
+
+//lint:file-ignore SA4006 This context is only used if a nested component is present.
+
+import "github.com/a-h/templ"
+import templruntime "github.com/a-h/templ/runtime"
+
+func AboutContent(isAuthenticated bool) templ.Component {
+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
+ return templ_7745c5c3_CtxErr
+ }
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var1 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var1 == nil {
+ templ_7745c5c3_Var1 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "
Alpha Software: Arabica is currently in early development. Features may change, and data structures could be modified in future updates.
Your Coffee Journey, Your Data Arabica is a coffee brew tracking application built on the AT Protocol, a decentralized social networking protocol. Unlike traditional apps where your data is locked in a company's database, Arabica stores your brew logs, coffee beans, and equipment information in your own Personal Data Server (PDS) .
What Makes Arabica Different? You own your data - All your brew logs live in your PDS, not our serversPortable identity - Switch PDS providers anytime without losing your dataPrivacy by design - You control who sees your brewsOpen protocol - Built on the AT Protocol, the same technology powering BlueskyFeatures Track Your Brews Log every detail: beans, grind size, water temp, brew time, and tasting notes
Manage Equipment Keep track of your grinders, brewers, beans, and roasters
Community Feed Share your best brews with the community (coming soon: likes and comments!)
Export Your Data Export all your brews anytime in JSON format
The AT Protocol Advantage The AT Protocol is a decentralized social networking protocol that gives users true ownership of their data and identity. When you use Arabica:
Your brew data is stored as ATProto records in collections on your PDS You authenticate via OAuth with your PDS, not with us References between records (like linking a brew to a bean) use AT-URIs Your identity is portable - change PDS providers without losing your account Getting Started To use Arabica, you'll need an account on a PDS that supports the AT Protocol. The easiest way is to create a Bluesky account at bsky.app .
Once you have an account, simply log in with your handle (e.g., yourname.bsky.social) and start tracking your brews!
Open Source Arabica is open source software. You can view the code, contribute, or even run your own instance. Visit our GitHub repository to learn more.
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if !isAuthenticated {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "
Get Started ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ } else {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "
Log Your Next Brew ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+}
+
+// About is the full About page with layout
+func About(data *LayoutData) templ.Component {
+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
+ return templ_7745c5c3_CtxErr
+ }
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var2 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var2 == nil {
+ templ_7745c5c3_Var2 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ templ_7745c5c3_Err = Layout(data, AboutContent(data.IsAuthenticated)).Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+}
+
+var _ = templruntime.GeneratedTemplate
diff --git a/internal/components/about_test.go b/internal/components/about_test.go
new file mode 100644
index 0000000..9a7332d
--- /dev/null
+++ b/internal/components/about_test.go
@@ -0,0 +1,81 @@
+package components
+
+import (
+ "arabica/internal/bff"
+ "bytes"
+ "context"
+ "strings"
+ "testing"
+)
+
+func TestAboutComponent(t *testing.T) {
+ // Create test data
+ data := &LayoutData{
+ Title: "About",
+ IsAuthenticated: false,
+ UserDID: "",
+ UserProfile: nil,
+ }
+
+ // Render the component
+ var buf bytes.Buffer
+ err := About(data).Render(context.Background(), &buf)
+ if err != nil {
+ t.Fatalf("Failed to render About component: %v", err)
+ }
+
+ html := buf.String()
+
+ // Verify key content is present
+ tests := []struct {
+ name string
+ content string
+ }{
+ {"title", "About Arabica"},
+ {"alpha badge", "ALPHA"},
+ {"main heading", "Your Coffee Journey, Your Data"},
+ {"AT Protocol mention", "AT Protocol"},
+ {"footer", "Your brew, your data"},
+ {"get started button", "Get Started"},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if !strings.Contains(html, tt.content) {
+ t.Errorf("Expected HTML to contain %q, but it was not found", tt.content)
+ }
+ })
+ }
+}
+
+func TestAboutComponentAuthenticated(t *testing.T) {
+ // Create test data with authenticated user
+ data := &LayoutData{
+ Title: "About",
+ IsAuthenticated: true,
+ UserDID: "did:plc:test123",
+ UserProfile: &bff.UserProfile{
+ Handle: "testuser.bsky.social",
+ DisplayName: "Test User",
+ Avatar: "",
+ },
+ }
+
+ // Render the component
+ var buf bytes.Buffer
+ err := About(data).Render(context.Background(), &buf)
+ if err != nil {
+ t.Fatalf("Failed to render About component: %v", err)
+ }
+
+ html := buf.String()
+
+ // When authenticated, should show "Log Your Next Brew" instead of "Get Started"
+ if !strings.Contains(html, "Log Your Next Brew") {
+ t.Error("Expected authenticated view to show 'Log Your Next Brew' button")
+ }
+
+ if strings.Contains(html, "Get Started") {
+ t.Error("Expected authenticated view NOT to show 'Get Started' button")
+ }
+}
diff --git a/internal/components/brew_form.templ b/internal/components/brew_form.templ
new file mode 100644
index 0000000..85511d0
--- /dev/null
+++ b/internal/components/brew_form.templ
@@ -0,0 +1,125 @@
+package components
+
+import "arabica/internal/models"
+
+type BrewFormProps struct {
+ // Brew being edited; nil if creating new
+ Brew *models.Brew
+
+ // Collections for selects
+ Beans []models.Bean
+ Grinders []models.Grinder
+ Brewers []models.Brewer
+
+ // Derived JSON or values for client Alpine initialization
+ PoursJSON string // optional, only if editing
+
+ // Back button
+ BackURL string
+}
+
+templ BrewFormPage(layout *LayoutData, props BrewFormProps) {
+ @Layout(layout, BrewFormCard(props))
+}
+
+
+templ BrewFormCard(props BrewFormProps) {
+
+
+ BrewHeader(props)
+ BrewForm(props)
+
+
+}
+
+templ BrewHeader(props BrewFormProps) {
+
+ BackButton("/brews")
+ if props.Brew != nil {
+
Edit Brew
+ } else {
+ New Brew
+ }
+
+}
+
+templ BrewForm(props BrewFormProps) {
+
+}
+
+templ EntitySelect(
+ label string,
+ name string,
+ options []Entity,
+ selected string,
+ onNew string,
+) {
+
+
{label}
+
+
+ Select…
+ for _, opt := range options {
+
+ {opt.Label()}
+
+ }
+
+
+ + New
+
+
+
+}
+
+templ PoursEditor(props BrewFormProps) {
+
+
+ Pours
+ + Add Pour
+
+
+
+
+
+
+}
+
+templ PourRow(index string) {
+
+
+
+ ✕
+
+}
+
+templ RatingInput(props BrewFormProps) {
+
+}
diff --git a/internal/components/footer.templ b/internal/components/footer.templ
new file mode 100644
index 0000000..f856bb0
--- /dev/null
+++ b/internal/components/footer.templ
@@ -0,0 +1,19 @@
+package components
+
+templ Footer() {
+
+}
diff --git a/internal/components/footer_templ.go b/internal/components/footer_templ.go
new file mode 100644
index 0000000..26b91c7
--- /dev/null
+++ b/internal/components/footer_templ.go
@@ -0,0 +1,40 @@
+// Code generated by templ - DO NOT EDIT.
+
+// templ: version: v0.3.960
+package components
+
+//lint:file-ignore SA4006 This context is only used if a nested component is present.
+
+import "github.com/a-h/templ"
+import templruntime "github.com/a-h/templ/runtime"
+
+func Footer() templ.Component {
+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
+ return templ_7745c5c3_CtxErr
+ }
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var1 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var1 == nil {
+ templ_7745c5c3_Var1 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+}
+
+var _ = templruntime.GeneratedTemplate
diff --git a/internal/components/header.templ b/internal/components/header.templ
new file mode 100644
index 0000000..2b439c2
--- /dev/null
+++ b/internal/components/header.templ
@@ -0,0 +1,109 @@
+package components
+
+import "arabica/internal/bff"
+
+templ Header(isAuthenticated bool, userProfile *bff.UserProfile) {
+
+
+
+
+
+ ☕ Arabica
+ ALPHA
+
+
+
+ if isAuthenticated {
+
+
+
+ if userProfile != nil && userProfile.Avatar != "" {
+ @safeAvatarHTML(userProfile.Avatar, userProfile.DisplayName)
+ } else {
+
+
+ if userProfile != nil && userProfile.DisplayName != "" {
+ { userProfile.DisplayName[:1] }
+ } else {
+ ?
+ }
+
+
+ }
+
+
+
+
+
+
+
+ }
+
+
+
+
+}
+
+// Helper function to get profile identifier (handle or DID)
+func getProfileIdentifier(userProfile *bff.UserProfile) string {
+ if userProfile != nil && userProfile.Handle != "" {
+ return userProfile.Handle
+ }
+ if userProfile != nil {
+ // In the original template, it uses UserDID, but we don't have it here
+ // We'll need to pass it separately or handle it differently
+ return ""
+ }
+ return ""
+}
+
+// safeAvatarHTML renders the avatar image if the URL is safe
+templ safeAvatarHTML(avatarURL string, displayName string) {
+ // We need to call the SafeAvatarURL helper function from bff
+ // For now, we'll do a simple check
+ if avatarURL != "" && (len(avatarURL) > 8 && avatarURL[:8] == "https://") {
+
+ } else {
+
+
+ if displayName != "" {
+ { displayName[:1] }
+ } else {
+ ?
+ }
+
+
+ }
+}
diff --git a/internal/components/header_templ.go b/internal/components/header_templ.go
new file mode 100644
index 0000000..141d0c2
--- /dev/null
+++ b/internal/components/header_templ.go
@@ -0,0 +1,233 @@
+// Code generated by templ - DO NOT EDIT.
+
+// templ: version: v0.3.960
+package components
+
+//lint:file-ignore SA4006 This context is only used if a nested component is present.
+
+import "github.com/a-h/templ"
+import templruntime "github.com/a-h/templ/runtime"
+
+import (
+ "arabica/internal/bff"
+)
+
+func Header(isAuthenticated bool, userProfile *bff.UserProfile) templ.Component {
+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
+ return templ_7745c5c3_CtxErr
+ }
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var1 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var1 == nil {
+ templ_7745c5c3_Var1 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "
☕ Arabica ALPHA ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if isAuthenticated {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if userProfile != nil && userProfile.Avatar != "" {
+ templ_7745c5c3_Err = safeAvatarHTML(userProfile.Avatar, userProfile.DisplayName).Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ } else {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if userProfile != nil && userProfile.DisplayName != "" {
+ var templ_7745c5c3_Var2 string
+ templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(userProfile.DisplayName[:1])
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/components/header.templ`, Line: 28, Col: 41}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ } else {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "?")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, " ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if userProfile != nil && userProfile.Handle != "" {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if userProfile.DisplayName != "" {
+ var templ_7745c5c3_Var3 string
+ templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(userProfile.DisplayName)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/components/header.templ`, Line: 45, Col: 37}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ } else {
+ var templ_7745c5c3_Var4 string
+ templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(userProfile.Handle)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/components/header.templ`, Line: 47, Col: 32}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var5 string
+ templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs("@" + userProfile.Handle)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/components/header.templ`, Line: 50, Col: 79}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "
View Profile My Brews Manage Records Settings (coming soon)
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+}
+
+// Helper function to get profile identifier (handle or DID)
+func getProfileIdentifier(userProfile *bff.UserProfile) string {
+ if userProfile != nil && userProfile.Handle != "" {
+ return userProfile.Handle
+ }
+ if userProfile != nil {
+ // In the original template, it uses UserDID, but we don't have it here
+ // We'll need to pass it separately or handle it differently
+ return ""
+ }
+ return ""
+}
+
+// safeAvatarHTML renders the avatar image if the URL is safe
+func safeAvatarHTML(avatarURL string, displayName string) templ.Component {
+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
+ return templ_7745c5c3_CtxErr
+ }
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var7 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var7 == nil {
+ templ_7745c5c3_Var7 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ if avatarURL != "" && (len(avatarURL) > 8 && avatarURL[:8] == "https://") {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ } else {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if displayName != "" {
+ var templ_7745c5c3_Var9 string
+ templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(displayName[:1])
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/components/header.templ`, Line: 104, Col: 22}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ } else {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "?")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ return nil
+ })
+}
+
+var _ = templruntime.GeneratedTemplate
diff --git a/internal/components/layout.templ b/internal/components/layout.templ
new file mode 100644
index 0000000..7d08cc2
--- /dev/null
+++ b/internal/components/layout.templ
@@ -0,0 +1,52 @@
+package components
+
+import "arabica/internal/bff"
+
+// LayoutData contains all the data needed for the layout
+type LayoutData struct {
+ Title string
+ IsAuthenticated bool
+ UserDID string
+ UserProfile *bff.UserProfile
+}
+
+templ Layout(data *LayoutData, content templ.Component) {
+
+
+
+
+
+
+
+
+
+
+
{ data.Title } - Arabica
+
+
+
+
+
+
+
+
+ if data.IsAuthenticated {
+
+ }
+
+
+
+
+ @Header(data.IsAuthenticated, data.UserProfile)
+
+ @content
+
+ @Footer()
+
+
+}
diff --git a/internal/components/layout_templ.go b/internal/components/layout_templ.go
new file mode 100644
index 0000000..7b9ca6e
--- /dev/null
+++ b/internal/components/layout_templ.go
@@ -0,0 +1,122 @@
+// Code generated by templ - DO NOT EDIT.
+
+// templ: version: v0.3.960
+package components
+
+//lint:file-ignore SA4006 This context is only used if a nested component is present.
+
+import "github.com/a-h/templ"
+import templruntime "github.com/a-h/templ/runtime"
+
+import (
+ "arabica/internal/bff"
+)
+
+// LayoutData contains all the data needed for the layout
+type LayoutData struct {
+ Title string
+ IsAuthenticated bool
+ UserDID string
+ UserProfile *bff.UserProfile
+}
+
+func Layout(data *LayoutData, content templ.Component) templ.Component {
+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
+ return templ_7745c5c3_CtxErr
+ }
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var1 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var1 == nil {
+ templ_7745c5c3_Var1 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var2 string
+ templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(data.Title)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/components/layout.templ`, Line: 26, Col: 22}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, " - Arabica ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if data.IsAuthenticated {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = Header(data.IsAuthenticated, data.UserProfile).Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = content.Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, " ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = Footer().Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+}
+
+var _ = templruntime.GeneratedTemplate
diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go
index 7f9c169..f8700c1 100644
--- a/internal/handlers/handlers.go
+++ b/internal/handlers/handlers.go
@@ -10,6 +10,7 @@ import (
"arabica/internal/atproto"
"arabica/internal/bff"
+ "arabica/internal/components"
"arabica/internal/database"
"arabica/internal/feed"
"arabica/internal/models"
@@ -1277,14 +1278,15 @@ func (h *Handler) HandleAbout(w http.ResponseWriter, r *http.Request) {
userProfile = h.getUserProfile(r.Context(), didStr)
}
- data := &bff.PageData{
+ data := &components.LayoutData{
Title: "About",
IsAuthenticated: isAuthenticated,
UserDID: didStr,
UserProfile: userProfile,
}
- if err := bff.RenderTemplate(w, r, "about.tmpl", data); err != nil {
+ // Use templ component
+ if err := components.About(data).Render(r.Context(), w); err != nil {
http.Error(w, "Failed to render page", http.StatusInternalServerError)
log.Error().Err(err).Msg("Failed to render about page")
}
diff --git a/templates/404.tmpl b/templates/404.tmpl
index f509142..d600ef1 100644
--- a/templates/404.tmpl
+++ b/templates/404.tmpl
@@ -1,12 +1,12 @@
{{define "content"}}
-
-
404
-
Page Not Found
-
The page you're looking for doesn't exist or has been moved.
-
- Back to Home
-
-
+
+
404
+
Page Not Found
+
The page you're looking for doesn't exist or has been moved.
+
+ Back to Home
+
+
{{end}}
diff --git a/templates/brew_form.tmpl b/templates/brew_form.tmpl
index 176a111..6dbd648 100644
--- a/templates/brew_form.tmpl
+++ b/templates/brew_form.tmpl
@@ -1,278 +1,281 @@
{{define "content"}}
-
+
+
+
-
-
-
-
-
-
-
-
-
-
- {{if .Brew}}Edit Brew{{else}}New Brew{{end}}
-
+
+
+
+
+
+
+
+
+
+
+
+ {{if .Brew}}Edit Brew{{else}}New Brew{{end}}
+
+
+
+
{{end}}
diff --git a/templates/brew_list.tmpl b/templates/brew_list.tmpl
index ad0a7fc..a0393b8 100644
--- a/templates/brew_list.tmpl
+++ b/templates/brew_list.tmpl
@@ -1,58 +1,58 @@
{{define "content"}}
-
+
-
-
-
-
-
-
- Date
- Bean
- Roaster
- Method
- Rating
- Actions
-
-
-
- {{range iterate 5}}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{end}}
-
-
-
+
+
+
+
+
+
+ {{range iterate 5}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{end}}
+
+
+
{{end}}
diff --git a/templates/brew_view.tmpl b/templates/brew_view.tmpl
index 43ce1db..7ba5f33 100644
--- a/templates/brew_view.tmpl
+++ b/templates/brew_view.tmpl
@@ -1,6 +1,6 @@
{{define "content"}}
-
+
@@ -10,7 +10,7 @@
{{if .IsOwnProfile}}
+ class="inline-flex items-center btn-secondary">
Edit
-
+
diff --git a/templates/manage.tmpl b/templates/manage.tmpl
index 23585b6..a2f3377 100644
--- a/templates/manage.tmpl
+++ b/templates/manage.tmpl
@@ -1,5 +1,6 @@
{{define "content"}}
-
+
+
diff --git a/templates/partials/bean_form_modal.tmpl b/templates/partials/bean_form_modal.tmpl
index a70c009..959cd8b 100644
--- a/templates/partials/bean_form_modal.tmpl
+++ b/templates/partials/bean_form_modal.tmpl
@@ -1,20 +1,18 @@
{{define "bean_form_modal"}}
-
-
-
+
+
diff --git a/templates/partials/brew_list_content.tmpl b/templates/partials/brew_list_content.tmpl
index d45ac6d..ad953e2 100644
--- a/templates/partials/brew_list_content.tmpl
+++ b/templates/partials/brew_list_content.tmpl
@@ -1,145 +1,145 @@
{{define "brew_list_content"}}
{{if not .Brews}}
-
- {{if $.IsOwnProfile}}
-
No brews yet! Start tracking your coffee journey.
-
- Add Your First Brew
-
- {{else}}
-
No brews yet.
- {{end}}
+
+ {{if $.IsOwnProfile}}
+
No brews yet! Start tracking your coffee journey.
+
+ Add Your First Brew
+
+ {{else}}
+
No brews yet.
+ {{end}}
{{else}}
-
-
-
-
- Date
- Bean
- Brewer
- Variables
- Notes
- Rating
- Actions
-
-
-
- {{range .Brews}}
-
-
-
- {{.CreatedAt.Format "Jan 2"}}
- {{.CreatedAt.Format "2006"}}
-
-
-
-
- {{if .Bean}}
-
- {{if .Bean.Name}}{{.Bean.Name}}{{else}}{{.Bean.Origin}}{{end}}
-
- {{if and .Bean.Roaster .Bean.Roaster.Name}}
-
- {{.Bean.Roaster.Name}}
-
- {{end}}
-
- {{if .Bean.Origin}}📍 {{.Bean.Origin}} {{end}}
- {{if .Bean.RoastLevel}}🔥 {{.Bean.RoastLevel}} {{end}}
- {{if hasValue .CoffeeAmount}}⚖️ {{.CoffeeAmount}}g {{end}}
-
- {{else}}
- -
- {{end}}
-
-
-
-
- {{if .BrewerObj}}
- {{.BrewerObj.Name}}
- {{else if .Method}}
- {{.Method}}
- {{else}}
- -
- {{end}}
-
-
-
-
-
- {{if .GrinderObj}}
-
Grinder: {{.GrinderObj.Name}}{{if .GrindSize}} ({{.GrindSize}}){{end}}
- {{else if .GrindSize}}
-
Grind: {{.GrindSize}}
- {{end}}
-
- {{if hasTemp .Temperature}}
-
Temp: {{formatTemp .Temperature}}
- {{end}}
-
- {{if .Pours}}
-
Pours:
- {{range .Pours}}
-
• {{.WaterAmount}}g @ {{formatTime .TimeSeconds}}
- {{end}}
- {{else if hasValue .WaterAmount}}
-
Water: {{.WaterAmount}}g
- {{end}}
-
- {{if hasValue .TimeSeconds}}
-
Time: {{formatTime .TimeSeconds}}
- {{end}}
-
-
-
-
-
- {{if .TastingNotes}}
- {{.TastingNotes}}
- {{else}}
- -
- {{end}}
-
-
-
-
- {{if hasValue .Rating}}
-
- ⭐ {{formatRating .Rating}}
-
- {{else}}
- -
- {{end}}
-
-
-
-
- {{if $.IsOwnProfile}}
- View
- {{else if $.ProfileHandle}}
- View
- {{else}}
- View
- {{end}}
- {{if $.IsOwnProfile}}
- Edit
-
- Delete
-
- {{end}}
-
-
+
+
+
+
+ {{range .Brews}}
+
+
+
+ {{.CreatedAt.Format "Jan 2"}}
+ {{.CreatedAt.Format "2006"}}
+
+
+
+
+ {{if .Bean}}
+
+ {{if .Bean.Name}}{{.Bean.Name}}{{else}}{{.Bean.Origin}}{{end}}
+
+ {{if and .Bean.Roaster .Bean.Roaster.Name}}
+
+ {{.Bean.Roaster.Name}}
+
+ {{end}}
+
+ {{if .Bean.Origin}}📍 {{.Bean.Origin}} {{end}}
+ {{if .Bean.RoastLevel}}🔥 {{.Bean.RoastLevel}} {{end}}
+ {{if hasValue .CoffeeAmount}}⚖️ {{.CoffeeAmount}}g {{end}}
+
+ {{else}}
+ -
+ {{end}}
+
+
+
+
+ {{if .BrewerObj}}
+ {{.BrewerObj.Name}}
+ {{else if .Method}}
+ {{.Method}}
+ {{else}}
+ -
+ {{end}}
+
+
+
+
+
+ {{if .GrinderObj}}
+
Grinder: {{.GrinderObj.Name}}{{if .GrindSize}} ({{.GrindSize}}){{end}}
+ {{else if .GrindSize}}
+
Grind: {{.GrindSize}}
{{end}}
-
-
+
+ {{if hasTemp .Temperature}}
+
Temp: {{formatTemp .Temperature}}
+ {{end}}
+
+ {{if .Pours}}
+
Pours:
+ {{range .Pours}}
+
• {{.WaterAmount}}g @ {{formatTime .TimeSeconds}}
+ {{end}}
+ {{else if hasValue .WaterAmount}}
+
Water: {{.WaterAmount}}g
+ {{end}}
+
+ {{if hasValue .TimeSeconds}}
+
Time: {{formatTime .TimeSeconds}}
+ {{end}}
+
+
+
+
+
+ {{if .TastingNotes}}
+ {{.TastingNotes}}
+ {{else}}
+ -
+ {{end}}
+
+
+
+
+ {{if hasValue .Rating}}
+
+ ⭐ {{formatRating .Rating}}
+
+ {{else}}
+ -
+ {{end}}
+
+
+
+
+ {{if $.IsOwnProfile}}
+ View
+ {{else if $.ProfileHandle}}
+ View
+ {{else}}
+ View
+ {{end}}
+ {{if $.IsOwnProfile}}
+ Edit
+
+ Delete
+
+ {{end}}
+
+
+ {{end}}
+
+
{{end}}
{{end}}
diff --git a/templates/partials/brewer_form_modal.tmpl b/templates/partials/brewer_form_modal.tmpl
index 2c7c4aa..ef1142f 100644
--- a/templates/partials/brewer_form_modal.tmpl
+++ b/templates/partials/brewer_form_modal.tmpl
@@ -1,22 +1,17 @@
{{define "brewer_form_modal"}}
-
-
-
-
-
-
-
-
- Save
- Cancel
-
-
+
{{end}}
diff --git a/templates/partials/feed.tmpl b/templates/partials/feed.tmpl
index 4a1e299..5bebc84 100644
--- a/templates/partials/feed.tmpl
+++ b/templates/partials/feed.tmpl
@@ -1,217 +1,217 @@
{{define "feed"}}
- {{if .FeedItems}}
- {{range .FeedItems}}
-
-
-
-
- {{if .Author.Avatar}}
- {{$safeAvatar := safeAvatarURL .Author.Avatar}}
- {{if $safeAvatar}}
-
- {{else}}
-
- ?
-
- {{end}}
- {{else}}
-
- ?
-
- {{end}}
-
-
+ {{if .FeedItems}}
+ {{range .FeedItems}}
+
+
+
+
+
+
+ {{.Action}}
+
-
-
- {{.Action}}
+
+ {{if eq .RecordType "brew"}}
+
+
+
+
+
+ {{if .Brew.Bean}}
+
+ {{if .Brew.Bean.Name}}{{.Brew.Bean.Name}}{{else}}{{.Brew.Bean.Origin}}{{end}}
+
+ {{if and .Brew.Bean.Roaster .Brew.Bean.Roaster.Name}}
+
+ 🏭 {{.Brew.Bean.Roaster.Name}}
+
+ {{end}}
+
+ {{if .Brew.Bean.Origin}}📍 {{.Brew.Bean.Origin}} {{end}}
+ {{if .Brew.Bean.RoastLevel}}🔥 {{.Brew.Bean.RoastLevel}} {{end}}
+ {{if .Brew.Bean.Process}}🌱 {{.Brew.Bean.Process}} {{end}}
+ {{if hasValue .Brew.CoffeeAmount}}⚖️ {{.Brew.CoffeeAmount}}g {{end}}
+
+ {{end}}
+ {{if hasValue .Brew.Rating}}
+
+ ⭐ {{.Brew.Rating}}/10
+
+ {{end}}
+
-
- {{if eq .RecordType "brew"}}
-
-
-
-
-
- {{if .Brew.Bean}}
-
- {{if .Brew.Bean.Name}}{{.Brew.Bean.Name}}{{else}}{{.Brew.Bean.Origin}}{{end}}
-
- {{if and .Brew.Bean.Roaster .Brew.Bean.Roaster.Name}}
-
- 🏭 {{.Brew.Bean.Roaster.Name}}
-
- {{end}}
-
- {{if .Brew.Bean.Origin}}📍 {{.Brew.Bean.Origin}} {{end}}
- {{if .Brew.Bean.RoastLevel}}🔥 {{.Brew.Bean.RoastLevel}} {{end}}
- {{if .Brew.Bean.Process}}🌱 {{.Brew.Bean.Process}} {{end}}
- {{if hasValue .Brew.CoffeeAmount}}⚖️ {{.Brew.CoffeeAmount}}g {{end}}
-
- {{end}}
-
- {{if hasValue .Brew.Rating}}
-
- ⭐ {{.Brew.Rating}}/10
-
- {{end}}
-
-
-
- {{if or .Brew.BrewerObj .Brew.Method}}
-
- Brewer:
-
- {{if .Brew.BrewerObj}}{{.Brew.BrewerObj.Name}}{{else if .Brew.Method}}{{.Brew.Method}}{{end}}
-
-
- {{end}}
-
-
-
- {{if .Brew.GrinderObj}}
-
- Grinder: {{.Brew.GrinderObj.Name}}{{if .Brew.GrindSize}} ({{.Brew.GrindSize}}){{end}}
-
- {{else if .Brew.GrindSize}}
-
- Grind: {{.Brew.GrindSize}}
-
- {{end}}
- {{if .Brew.Pours}}
-
-
Pours:
- {{range .Brew.Pours}}
-
• {{.WaterAmount}}g @ {{formatTime .TimeSeconds}}
- {{end}}
-
- {{else if hasValue .Brew.WaterAmount}}
-
- Water: {{.Brew.WaterAmount}}g
-
- {{end}}
- {{if hasTemp .Brew.Temperature}}
-
- Temp: {{formatTemp .Brew.Temperature}}
-
- {{end}}
- {{if hasValue .Brew.TimeSeconds}}
-
- Time: {{formatTime .Brew.TimeSeconds}}
-
- {{end}}
-
+
+ {{if or .Brew.BrewerObj .Brew.Method}}
+
+ Brewer:
+
+ {{if .Brew.BrewerObj}}{{.Brew.BrewerObj.Name}}{{else if .Brew.Method}}{{.Brew.Method}}{{end}}
+
+
+ {{end}}
- {{if .Brew.TastingNotes}}
-
- "{{.Brew.TastingNotes}}"
-
- {{end}}
-
-
-
+
+
+ {{if .Brew.GrinderObj}}
+
+ Grinder: {{.Brew.GrinderObj.Name}}{{if .Brew.GrindSize}} ({{.Brew.GrindSize}}){{end}}
- {{else if eq .RecordType "bean"}}
-
-
-
-
- {{if .Bean.Name}}{{.Bean.Name}}{{else}}{{.Bean.Origin}}{{end}}
-
- {{if and .Bean.Roaster .Bean.Roaster.Name}}
- from {{.Bean.Roaster.Name}}
- {{end}}
-
-
- {{if .Bean.Origin}}
-
Origin: {{.Bean.Origin}}
- {{end}}
- {{if .Bean.RoastLevel}}
-
Roast: {{.Bean.RoastLevel}}
- {{end}}
- {{if .Bean.Process}}
-
Process: {{.Bean.Process}}
- {{end}}
- {{if .Bean.Description}}
-
"{{.Bean.Description}}"
- {{end}}
-
+ {{else if .Brew.GrindSize}}
+
+ Grind: {{.Brew.GrindSize}}
- {{else if eq .RecordType "roaster"}}
-
-
-
- {{.Roaster.Name}}
-
-
- {{if .Roaster.Location}}
-
Location: {{.Roaster.Location}}
- {{end}}
- {{if .Roaster.Website}}
- {{$safeWebsite := safeWebsiteURL .Roaster.Website}}
- {{if $safeWebsite}}
-
- {{end}}
- {{end}}
-
+ {{end}}
+ {{if .Brew.Pours}}
+
+
Pours:
+ {{range .Brew.Pours}}
+
• {{.WaterAmount}}g @ {{formatTime .TimeSeconds}}
+ {{end}}
+
+ {{else if hasValue .Brew.WaterAmount}}
+
+ Water: {{.Brew.WaterAmount}}g
- {{else if eq .RecordType "grinder"}}
-
-
-
- {{.Grinder.Name}}
-
-
- {{if .Grinder.GrinderType}}
-
Type: {{.Grinder.GrinderType}}
- {{end}}
- {{if .Grinder.BurrType}}
-
Burr: {{.Grinder.BurrType}}
- {{end}}
- {{if .Grinder.Notes}}
-
"{{.Grinder.Notes}}"
- {{end}}
-
+ {{end}}
+ {{if hasTemp .Brew.Temperature}}
+
+ Temp: {{formatTemp .Brew.Temperature}}
- {{else if eq .RecordType "brewer"}}
-
-
-
- {{.Brewer.Name}}
-
- {{if .Brewer.Description}}
-
"{{.Brewer.Description}}"
- {{end}}
+ {{end}}
+ {{if hasValue .Brew.TimeSeconds}}
+
+ Time: {{formatTime .Brew.TimeSeconds}}
{{end}}
+
+
+ {{if .Brew.TastingNotes}}
+
+ "{{.Brew.TastingNotes}}"
+
+ {{end}}
+
+
+
- {{end}}
-
-
-
-
-
- {{else}}
-
-
No activity in the feed yet.
-
Be the first to add something!
+ {{else if eq .RecordType "bean"}}
+
+
+
+
+ {{if .Bean.Name}}{{.Bean.Name}}{{else}}{{.Bean.Origin}}{{end}}
+
+ {{if and .Bean.Roaster .Bean.Roaster.Name}}
+ from {{.Bean.Roaster.Name}}
+ {{end}}
+
+
+ {{if .Bean.Origin}}
+
Origin: {{.Bean.Origin}}
+ {{end}}
+ {{if .Bean.RoastLevel}}
+
Roast: {{.Bean.RoastLevel}}
+ {{end}}
+ {{if .Bean.Process}}
+
Process: {{.Bean.Process}}
+ {{end}}
+ {{if .Bean.Description}}
+
"{{.Bean.Description}}"
+ {{end}}
+
+
+ {{else if eq .RecordType "roaster"}}
+
+
+
+ {{.Roaster.Name}}
+
+
+ {{if .Roaster.Location}}
+
Location: {{.Roaster.Location}}
+ {{end}}
+ {{if .Roaster.Website}}
+ {{$safeWebsite := safeWebsiteURL .Roaster.Website}}
+ {{if $safeWebsite}}
+
+ {{end}}
+ {{end}}
+
+
+ {{else if eq .RecordType "grinder"}}
+
+
+
+ {{.Grinder.Name}}
+
+
+ {{if .Grinder.GrinderType}}
+
Type: {{.Grinder.GrinderType}}
+ {{end}}
+ {{if .Grinder.BurrType}}
+
Burr: {{.Grinder.BurrType}}
+ {{end}}
+ {{if .Grinder.Notes}}
+
"{{.Grinder.Notes}}"
+ {{end}}
+
+
+ {{else if eq .RecordType "brewer"}}
+
+
+
+ {{.Brewer.Name}}
+
+ {{if .Brewer.Description}}
+
"{{.Brewer.Description}}"
+ {{end}}
{{end}}
+
+ {{end}}
+
+
+
+
+
+ {{else}}
+
+
No activity in the feed yet.
+
Be the first to add something!
+
+ {{end}}
{{end}}
diff --git a/templates/partials/footer.tmpl b/templates/partials/footer.tmpl
index 7d61476..6aa39a5 100644
--- a/templates/partials/footer.tmpl
+++ b/templates/partials/footer.tmpl
@@ -12,11 +12,6 @@
AT Protocol
-
{{end}}
diff --git a/templates/partials/grinder_form_modal.tmpl b/templates/partials/grinder_form_modal.tmpl
index 6716568..f9d98cd 100644
--- a/templates/partials/grinder_form_modal.tmpl
+++ b/templates/partials/grinder_form_modal.tmpl
@@ -1,29 +1,25 @@
{{define "grinder_form_modal"}}
-
-
-
+
+
diff --git a/templates/partials/manage_content.tmpl b/templates/partials/manage_content.tmpl
index 73ffe00..6ad6d47 100644
--- a/templates/partials/manage_content.tmpl
+++ b/templates/partials/manage_content.tmpl
@@ -5,7 +5,7 @@
Coffee Beans
+ class="btn-primary shadow-md hover:shadow-lg">
+ Add Bean
@@ -15,22 +15,22 @@
No beans yet. Add your first bean to get started!
{{else}}
-
-
-
+
+
+
-
+
{{range .Beans}}
-
+
{{.Name}}
{{.Origin}}
@@ -45,9 +45,9 @@
{{.Description}}
Edit
+ class="btn-link">Edit
Delete
+ class="btn-danger">Delete
{{end}}
@@ -63,7 +63,7 @@
Roasters
+ class="btn-primary shadow-md hover:shadow-lg">
+ Add Roaster
@@ -73,19 +73,19 @@
No roasters yet. Add your first roaster!
{{else}}
-
-
-
+
+
+
-
+
{{range .Roasters}}
-
+
{{.Name}}
{{.Location}}
@@ -97,9 +97,9 @@
Edit
+ class="btn-link">Edit
Delete
+ class="btn-danger">Delete
{{end}}
@@ -115,7 +115,7 @@
Grinders
+ class="btn-primary shadow-md hover:shadow-lg">
+ Add Grinder
@@ -125,29 +125,29 @@
No grinders yet. Add your first grinder!
{{else}}
-
-
-
+
+
+
-
+
{{range .Grinders}}
-
+
{{.Name}}
{{.GrinderType}}
{{.BurrType}}
{{.Notes}}
Edit
+ class="btn-link">Edit
Delete
+ class="btn-danger">Delete
{{end}}
@@ -162,7 +162,7 @@
Brewers
+ class="btn-primary shadow-md hover:shadow-lg">
+ Add Brewer
@@ -172,17 +172,17 @@
No brewers yet. Add your first brewer!
{{else}}
-
-
-
+
+
+
-
+
{{range .Brewers}}
{{.Description}}
Edit
+ class="btn-link">Edit
Delete
+ class="btn-danger">Delete
{{end}}
diff --git a/templates/partials/new_roaster_form.tmpl b/templates/partials/new_roaster_form.tmpl
index b323d25..39d814c 100644
--- a/templates/partials/new_roaster_form.tmpl
+++ b/templates/partials/new_roaster_form.tmpl
@@ -1,20 +1,15 @@
{{define "new_roaster_form"}}
-
-
-
+
+
diff --git a/templates/partials/profile_content.tmpl b/templates/partials/profile_content.tmpl
index 72e28ec..e9cda68 100644
--- a/templates/partials/profile_content.tmpl
+++ b/templates/partials/profile_content.tmpl
@@ -19,21 +19,21 @@
{{if .Beans}}
☕ Coffee Beans
-
-
-
+
+
+
-
+
{{range .Beans}}
-
+
{{if .Name}}{{.Name}}{{else}}{{.Origin}}{{end}}
@@ -63,7 +63,7 @@
{{if $.IsOwnProfile}}
-
+
+
Add New Bean
@@ -76,18 +76,18 @@
{{if .Roasters}}
🏭 Favorite Roasters
-
-
-
+
+
+
-
+
{{range .Roasters}}
-
+
{{.Name}}
{{if .Location}}{{.Location}}{{else}}- {{end}}
@@ -111,7 +111,7 @@
{{if $.IsOwnProfile}}
-
+
+
Add New Roaster
@@ -133,19 +133,19 @@
{{if .Grinders}}
⚙️ Grinders
-
-
-
+
+
+
-
+
{{range .Grinders}}
-
+
{{.Name}}
{{if .GrinderType}}{{.GrinderType}}{{else}}- {{end}}
@@ -163,7 +163,7 @@
{{if $.IsOwnProfile}}
-
+
+
Add New Grinder
@@ -176,18 +176,18 @@
{{if .Brewers}}
☕ Brewers
-
-
-
+
+
+
-
+
{{range .Brewers}}
-
+
{{.Name}}
{{if .BrewerType}}{{.BrewerType}}{{else}}- {{end}}
@@ -202,7 +202,7 @@
{{if $.IsOwnProfile}}
-
+