From 8a50f3a40462e83b68c9a100c0e82b8d847dafc8 Mon Sep 17 00:00:00 2001 From: juchechu Date: Tue, 18 Aug 2026 09:47:09 +0100 Subject: [PATCH] plain emails + Supabase-style full-width banners - Emails: remove all gold left/top borders from headers, cards, fallback links, footer; remove stats bar from Best Practices template; bold 'Jerry Koko' in footer; add padding after fallback link box - BroadcastBanner: full-width thin strip (Supabase/HeaderBanner style), type-colored bg tint + bottom border, inline icon + title + message - GoogleLinkBanner: matching full-width amber strip - Tests: 13/13 email tests updated, all pass --- frontend/components/BroadcastBanner.tsx | 177 ++++++++++++++--------- frontend/components/GoogleLinkBanner.tsx | 51 +++---- internal/email/email.go | 72 +++------ internal/email/email_test.go | 21 +-- internal/email/problem_reminder_test.go | 2 +- 5 files changed, 155 insertions(+), 168 deletions(-) diff --git a/frontend/components/BroadcastBanner.tsx b/frontend/components/BroadcastBanner.tsx index 264d4ec..64c30bd 100644 --- a/frontend/components/BroadcastBanner.tsx +++ b/frontend/components/BroadcastBanner.tsx @@ -1,56 +1,86 @@ "use client"; import { useCallback, useEffect, useState } from "react"; -import { X, ArrowRight } from "lucide-react"; +import { + Info, + AlertTriangle, + Sparkles, + Star, + ShieldAlert, + Bell, + X, + ArrowRight, +} from "lucide-react"; import { fetchActiveBroadcasts, dismissBroadcast } from "@/lib/api"; import { useWebSocket } from "@/lib/event"; import { Broadcast } from "@/lib/types"; import { cn } from "@/lib/utils"; -const TYPE_STYLES: Record< +const TYPE_CONFIG: Record< string, { + icon: React.ElementType; + bg: string; border: string; - textColor: string; - button: string; - dismissHover: string; + text: string; + muted: string; + link: string; + linkHover: string; } > = { info: { - border: "border-l-blue-500", - textColor: "text-blue-100/70", - button: "bg-blue-500/15 text-blue-300 hover:bg-blue-500/25", - dismissHover: "hover:bg-blue-500/10 hover:text-blue-300", + icon: Info, + bg: "bg-blue-500/10", + border: "border-b border-blue-500/20", + text: "text-blue-200", + muted: "text-blue-300/60", + link: "text-blue-300", + linkHover: "hover:text-blue-200", }, warning: { - border: "border-l-amber-500", - textColor: "text-amber-100/70", - button: "bg-amber-500/15 text-amber-300 hover:bg-amber-500/25", - dismissHover: "hover:bg-amber-500/10 hover:text-amber-300", + icon: AlertTriangle, + bg: "bg-amber-500/10", + border: "border-b border-amber-500/20", + text: "text-amber-200", + muted: "text-amber-300/60", + link: "text-amber-300", + linkHover: "hover:text-amber-200", }, update: { - border: "border-l-emerald-500", - textColor: "text-emerald-100/70", - button: "bg-emerald-500/15 text-emerald-300 hover:bg-emerald-500/25", - dismissHover: "hover:bg-emerald-500/10 hover:text-emerald-300", + icon: Star, + bg: "bg-emerald-500/10", + border: "border-b border-emerald-500/20", + text: "text-emerald-200", + muted: "text-emerald-300/60", + link: "text-emerald-300", + linkHover: "hover:text-emerald-200", }, new_feature: { - border: "border-l-purple-500", - textColor: "text-purple-100/70", - button: "bg-purple-500/15 text-purple-300 hover:bg-purple-500/25", - dismissHover: "hover:bg-purple-500/10 hover:text-purple-300", + icon: Sparkles, + bg: "bg-violet-500/10", + border: "border-b border-violet-500/20", + text: "text-violet-200", + muted: "text-violet-300/60", + link: "text-violet-300", + linkHover: "hover:text-violet-200", }, maintenance: { - border: "border-l-red-500", - textColor: "text-red-100/70", - button: "bg-red-500/15 text-red-300 hover:bg-red-500/25", - dismissHover: "hover:bg-red-500/10 hover:text-red-300", + icon: ShieldAlert, + bg: "bg-red-500/10", + border: "border-b border-red-500/20", + text: "text-red-200", + muted: "text-red-300/60", + link: "text-red-300", + linkHover: "hover:text-red-200", }, announcement: { - border: "border-l-sky-500", - textColor: "text-sky-100/70", - button: "bg-sky-500/15 text-sky-300 hover:bg-sky-500/25", - dismissHover: "hover:bg-sky-500/10 hover:text-sky-300", + icon: Bell, + bg: "bg-sky-500/10", + border: "border-b border-sky-500/20", + text: "text-sky-200", + muted: "text-sky-300/60", + link: "text-sky-300", + linkHover: "hover:text-sky-200", }, }; @@ -98,11 +128,20 @@ export default function BroadcastBanner() { }; }, [fetchBanners]); - useWebSocket({ - 'broadcast.created': useCallback(() => { fetchBanners(); }, [fetchBanners]), - 'broadcast.updated': useCallback(() => { fetchBanners(); }, [fetchBanners]), - 'broadcast.deleted': useCallback(() => { fetchBanners(); }, [fetchBanners]), - }, [fetchBanners]); + useWebSocket( + { + "broadcast.created": useCallback(() => { + fetchBanners(); + }, [fetchBanners]), + "broadcast.updated": useCallback(() => { + fetchBanners(); + }, [fetchBanners]), + "broadcast.deleted": useCallback(() => { + fetchBanners(); + }, [fetchBanners]), + }, + [fetchBanners], + ); const handleDismiss = async (id: string) => { await dismissBroadcast(id); @@ -114,58 +153,60 @@ export default function BroadcastBanner() { if (visible.length === 0) return null; return ( -
+
{visible.map((broadcast) => { - const style = TYPE_STYLES[broadcast.type] || TYPE_STYLES.info; - const hasMessage = broadcast.message && broadcast.message.trim().length > 0; + const config = TYPE_CONFIG[broadcast.type] || TYPE_CONFIG.info; + const Icon = config.icon; + const hasMessage = + broadcast.message && broadcast.message.trim().length > 0; return (
-
-
-
-

- {broadcast.title} -

- {hasMessage && ( -

- {broadcast.message} -

- )} -
- - -
+
+ + +

+ {broadcast.title} + {hasMessage && ( + + {broadcast.message} + + )} +

{broadcast.action_label && broadcast.action_url && ( {broadcast.action_label} - + )} + +
); diff --git a/frontend/components/GoogleLinkBanner.tsx b/frontend/components/GoogleLinkBanner.tsx index c021979..aa5a495 100644 --- a/frontend/components/GoogleLinkBanner.tsx +++ b/frontend/components/GoogleLinkBanner.tsx @@ -1,7 +1,7 @@ "use client"; import { useEffect, useState } from "react"; -import { X } from "lucide-react"; +import { Link2, X } from "lucide-react"; import { cn } from "@/lib/utils"; import { fetchUser } from "@/lib/api"; import { User } from "@/lib/types"; @@ -9,7 +9,8 @@ import { User } from "@/lib/types"; export default function GoogleLinkBanner() { const [visible, setVisible] = useState(() => { if (typeof window === "undefined") return false; - if (localStorage.getItem("google-banner-dismissed") === "true") return false; + if (localStorage.getItem("google-banner-dismissed") === "true") + return false; return true; }); const [user, setUser] = useState(null); @@ -29,41 +30,31 @@ export default function GoogleLinkBanner() { if (!visible || !user || user.google_linked) return null; return ( -
-
-
-
-

- Secure your account -

-

- Link your Google account for seamless sign-in and automatic profile syncing across all your devices. -

-
+
+
+ - -
+

+ Secure your account + + Link Google for seamless sign-in and automatic profile syncing. + +

Link Google + +
); diff --git a/internal/email/email.go b/internal/email/email.go index b2748ae..9bee86c 100644 --- a/internal/email/email.go +++ b/internal/email/email.go @@ -309,7 +309,7 @@ func bestPracticesBody() string { - + - - - - - -
@@ -337,8 +337,6 @@ func bestPracticesBody() string {

Discover Best Practices

-
-

See how top developers solve real problems. Browse community solutions, get AI-powered code analysis, and learn from the best.

@@ -346,40 +344,10 @@ See how top developers solve real problems. Browse community solutions, get AI-p
- - - - - - - - -
-
{{.SolutionCount}}
-
Solutions
-
-
{{.DeveloperCount}}
-
Developers
-
-
{{.TotalLikes}}
-
Likes
-
-
{{.GoCount}}
-
Go
-
-
{{.PythonCount}}
-
Python
-
-
- +
+
Community Solutions
@@ -393,7 +361,7 @@ See how top developers solve real problems. Browse community solutions, get AI-p
- +
AI-Powered Code Analysis
@@ -407,7 +375,7 @@ See how top developers solve real problems. Browse community solutions, get AI-p
- +
How to Get Featured
@@ -422,7 +390,7 @@ See how top developers solve real problems. Browse community solutions, get AI-p {{if .TopSolutions}}
-
Top Rated Solutions
+
Top Rated Solutions
{{range $i, $s := .TopSolutions}} @@ -463,8 +431,8 @@ See how top developers solve real problems. Browse community solutions, get AI-p - - -
-
+
+
Button not working?
@@ -473,11 +441,11 @@ See how top developers solve real problems. Browse community solutions, get AI-p
+
{{.PlatformName}}
{{.Tagline}}
-
Sent by Jerry Koko from Koder
+
Sent by Jerry Koko from Koder
© {{.Year}} {{.PlatformName}}. All rights reserved.
@@ -494,7 +462,7 @@ func problemReminderBody() string {
+
@@ -533,7 +501,7 @@ Sharpen your skills with this short exercise: - +
-
{{.ProblemTitle}}
@@ -550,8 +518,8 @@ Sharpen your skills with this short exercise: -
+
+
Button not working?
@@ -560,11 +528,11 @@ Sharpen your skills with this short exercise: +
{{.PlatformName}}
{{.Tagline}}
-
Sent by Jerry Koko from Koder
+
Sent by Jerry Koko from Koder
© {{.Year}} {{.PlatformName}}. All rights reserved.
@@ -622,7 +590,7 @@ const passwordResetBody = `{{define "content"}}
+ -
@@ -708,7 +676,7 @@ This secure link expires in {{.Expire
-
+
Button not working?
@@ -718,11 +686,11 @@ This secure link expires in {{.Expire
+
{{.PlatformName}}
{{.Tagline}}
-
Sent by Jerry Koko from Koder
+
Sent by Jerry Koko from Koder
© {{.Year}} {{.PlatformName}}. All rights reserved.
diff --git a/internal/email/email_test.go b/internal/email/email_test.go index dc4c234..7909d95 100644 --- a/internal/email/email_test.go +++ b/internal/email/email_test.go @@ -46,11 +46,10 @@ func TestRenderPasswordReset_ContainsBrandAndStructure(t *testing.T) { "support@koder.sbs", "mailto:support@koder.sbs", "Koder turns every problem into an instant feedback loop.", - "Sent by Jerry Koko from Koder", + "Sent by Jerry Koko from Koder", "© ", "Koder", - "border-left:4px solid #D4AF37", - "border-top:2px solid #D4AF37", + "border-top:1px solid #E5E7EB", } for _, want := range required { if !strings.Contains(out, want) { @@ -189,11 +188,6 @@ func TestRenderBestPractices_ContainsBrandAndStructure(t *testing.T) { "See top-rated community solutions", "Discover Best Practices", "See how top developers solve real problems", - "42", // solution count - "18", // developer count - "156", // total likes - "28", // Go count - "14", // Python count "Community Solutions", "AI-Powered Code Analysis", "How to Get Featured", @@ -208,10 +202,8 @@ func TestRenderBestPractices_ContainsBrandAndStructure(t *testing.T) { "support@koder.sbs", "mailto:support@koder.sbs", "Koder turns every problem into an instant feedback loop.", + "Sent by Jerry Koko from Koder", "© ", - // Gold accent checks - "border-left:4px solid #D4AF37", - "border-left:3px solid #D4AF37", "box-shadow:0 4px 14px rgba(212,175,55,0.35)", "border-bottom:2px solid #B8941F", } @@ -225,11 +217,6 @@ func TestRenderBestPractices_ContainsBrandAndStructure(t *testing.T) { if strings.ContainsAny(out, "😀🔒🤖🚀✨🔥") { t.Errorf("rendered email contains emoji characters") } - - // Stats bar should have separate Go and Python cells, not "Go / Python". - if strings.Contains(out, "Go / Python") { - t.Errorf("rendered email still has combined 'Go / Python' stat (should be separate cells)") - } } func TestRenderBestPractices_FeatureCardsPresent(t *testing.T) { @@ -346,7 +333,7 @@ func TestRenderProblemReminderString_ContainsLightThemeStyling(t *testing.T) { "support@koder.sbs", "mailto:support@koder.sbs", "Koder turns every problem into an instant feedback loop.", - "border-left:4px solid #D4AF37", + "Sent by Jerry Koko from Koder", } { if !strings.Contains(out, want) { t.Errorf("rendered reminder missing %q", want) diff --git a/internal/email/problem_reminder_test.go b/internal/email/problem_reminder_test.go index f0dcc69..69ae510 100644 --- a/internal/email/problem_reminder_test.go +++ b/internal/email/problem_reminder_test.go @@ -37,7 +37,7 @@ func TestRenderProblemReminder_ContainsExpectedFields(t *testing.T) { "Open Problem", "https://koder.sbs/logo.png", "support@koder.sbs", - "border-left:4px solid #D4AF37", + "Sent by Jerry Koko from Koder", "border-bottom:2px solid #B8941F", "box-shadow:0 4px 14px rgba(212,175,55,0.35)", }