From 61f92e183f4ad1435bc02e8917ac58797d5c3f52 Mon Sep 17 00:00:00 2001 From: juchechu Date: Tue, 18 Aug 2026 09:03:18 +0100 Subject: [PATCH] professional banner redesign + gold email accents MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - BroadcastBanner: remove icons, top color bar; solid dark bg, clean card - GoogleLinkBanner: matching clean style, no icons - All 3 email templates: purple (#7F56D9/#53389E) → gold (#D4AF37/#B8941F) - Best Practices email: remove all SVG icon badges, clean feature cards - Tests: 13/13 email tests updated for gold assertions --- frontend/components/BroadcastBanner.tsx | 123 +++++++---------------- frontend/components/GoogleLinkBanner.tsx | 39 +++---- internal/email/email.go | 113 ++++++++------------- internal/email/email_test.go | 28 ++---- internal/email/problem_reminder_test.go | 4 +- 5 files changed, 102 insertions(+), 205 deletions(-) diff --git a/frontend/components/BroadcastBanner.tsx b/frontend/components/BroadcastBanner.tsx index 0d83de1..264d4ec 100644 --- a/frontend/components/BroadcastBanner.tsx +++ b/frontend/components/BroadcastBanner.tsx @@ -1,7 +1,7 @@ "use client"; import { useCallback, useEffect, useState } from "react"; -import { AlertTriangle, Info, Sparkles, RefreshCw, Wrench, Megaphone, X, ArrowRight } from "lucide-react"; +import { X, ArrowRight } from "lucide-react"; import { fetchActiveBroadcasts, dismissBroadcast } from "@/lib/api"; import { useWebSocket } from "@/lib/event"; import { Broadcast } from "@/lib/types"; @@ -10,88 +10,46 @@ import { cn } from "@/lib/utils"; const TYPE_STYLES: Record< string, { - icon: React.ElementType; - accentBar: string; - bg: string; border: string; - iconBg: string; - iconColor: string; - titleColor: string; textColor: string; button: string; dismissHover: string; } > = { info: { - icon: Info, - accentBar: "bg-blue-500", - bg: "bg-blue-500/[0.04]", - border: "border-blue-500/15", - iconBg: "bg-blue-500/10", - iconColor: "text-blue-400", - titleColor: "text-blue-100", - textColor: "text-blue-200/60", - button: "bg-blue-500/15 text-blue-300 hover:bg-blue-500/25 border-blue-500/20", + 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", }, warning: { - icon: AlertTriangle, - accentBar: "bg-amber-500", - bg: "bg-amber-500/[0.04]", - border: "border-amber-500/15", - iconBg: "bg-amber-500/10", - iconColor: "text-amber-400", - titleColor: "text-amber-100", - textColor: "text-amber-200/60", - button: "bg-amber-500/15 text-amber-300 hover:bg-amber-500/25 border-amber-500/20", + 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", }, update: { - icon: RefreshCw, - accentBar: "bg-emerald-500", - bg: "bg-emerald-500/[0.04]", - border: "border-emerald-500/15", - iconBg: "bg-emerald-500/10", - iconColor: "text-emerald-400", - titleColor: "text-emerald-100", - textColor: "text-emerald-200/60", - button: "bg-emerald-500/15 text-emerald-300 hover:bg-emerald-500/25 border-emerald-500/20", + 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", }, new_feature: { - icon: Sparkles, - accentBar: "bg-purple-500", - bg: "bg-purple-500/[0.04]", - border: "border-purple-500/15", - iconBg: "bg-purple-500/10", - iconColor: "text-purple-400", - titleColor: "text-purple-100", - textColor: "text-purple-200/60", - button: "bg-purple-500/15 text-purple-300 hover:bg-purple-500/25 border-purple-500/20", + 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", }, maintenance: { - icon: Wrench, - accentBar: "bg-red-500", - bg: "bg-red-500/[0.04]", - border: "border-red-500/15", - iconBg: "bg-red-500/10", - iconColor: "text-red-400", - titleColor: "text-red-100", - textColor: "text-red-200/60", - button: "bg-red-500/15 text-red-300 hover:bg-red-500/25 border-red-500/20", + 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", }, announcement: { - icon: Megaphone, - accentBar: "bg-sky-500", - bg: "bg-sky-500/[0.04]", - border: "border-sky-500/15", - iconBg: "bg-sky-500/10", - iconColor: "text-sky-400", - titleColor: "text-sky-100", - textColor: "text-sky-200/60", - button: "bg-sky-500/15 text-sky-300 hover:bg-sky-500/25 border-sky-500/20", + 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", }, }; @@ -159,30 +117,22 @@ export default function BroadcastBanner() {
{visible.map((broadcast) => { const style = TYPE_STYLES[broadcast.type] || TYPE_STYLES.info; - const Icon = style.icon; const hasMessage = broadcast.message && broadcast.message.trim().length > 0; return (
-
-
-
-
- -
- +
-

+

{broadcast.title}

{hasMessage && ( @@ -190,24 +140,12 @@ export default function BroadcastBanner() { {broadcast.message}

)} - {broadcast.action_label && broadcast.action_url && ( - - {broadcast.action_label} - - - )}
+ + {broadcast.action_label && broadcast.action_url && ( + + {broadcast.action_label} + + + )}
); diff --git a/frontend/components/GoogleLinkBanner.tsx b/frontend/components/GoogleLinkBanner.tsx index 6e0c424..c021979 100644 --- a/frontend/components/GoogleLinkBanner.tsx +++ b/frontend/components/GoogleLinkBanner.tsx @@ -1,8 +1,7 @@ "use client"; import { useEffect, useState } from "react"; -import { AlertTriangle } from "@untitledui/icons"; -import { Chrome, X } from "lucide-react"; +import { X } from "lucide-react"; import { cn } from "@/lib/utils"; import { fetchUser } from "@/lib/api"; import { User } from "@/lib/types"; @@ -32,45 +31,39 @@ export default function GoogleLinkBanner() { return (
-
-
-
-
- -
- +
-

+

Secure your account

-

+

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

- - - Link Google -
+ + + Link Google +
); diff --git a/internal/email/email.go b/internal/email/email.go index ad295e7..b2748ae 100644 --- a/internal/email/email.go +++ b/internal/email/email.go @@ -26,10 +26,10 @@ const ( MutedText = "#4B5563" // muted paragraph text ButtonGold = "#D4AF37" // CTA background ButtonGoldDark = "#B8941F" // CTA bottom border (3D effect) - PurplePrimary = "#53389E" // brand purple (numbers, titles) - PurpleMid = "#7F56D9" // brand purple (accents, badges) - PurpleLight = "#F3E8FF" // light purple (icon badge bg) - PurpleDark = "#9E77ED" // brand purple (lighter accent) + PurplePrimary = "#53389E" // brand purple (numbers, titles) — legacy, prefer gold + PurpleMid = "#7F56D9" // brand purple (accents, badges) — legacy, prefer gold + PurpleLight = "#F3E8FF" // light purple tint + PurpleDark = "#9E77ED" // brand purple (lighter accent) — legacy, prefer gold ) // LockIconDataURI is an inline SVG padlock (Lucide-style stroke) encoded as a @@ -309,7 +309,7 @@ func bestPracticesBody() string { - +
@@ -335,11 +335,9 @@ 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. @@ -351,26 +349,26 @@ See how top developers solve real problems. Browse community solutions, get AI-p

- +
@@ -381,91 +379,62 @@ See how top developers solve real problems. Browse community solutions, get AI-p - - - {{if .TopSolutions}}
-
{{.SolutionCount}}
+
{{.SolutionCount}}
Solutions
-
{{.DeveloperCount}}
+
{{.DeveloperCount}}
Developers
-
{{.TotalLikes}}
+
{{.TotalLikes}}
Likes
-
{{.GoCount}}
+
{{.GoCount}}
Go
-
{{.PythonCount}}
+
{{.PythonCount}}
Python
- +
-
- - - -
-
-
-
Community Solutions
+
Community Solutions

Compare how others approached the same problem. Sort by most liked, fastest runtime, or newest submissions. Every solution includes the full source code and developer stats.

-
- +
-
- - - -
-
-
-
AI-Powered Code Analysis
+
AI-Powered Code Analysis

Click any solution to get instant AI analysis: quality scores, efficiency and readability ratings, time and space complexity breakdown, key techniques, strengths, and areas for improvement. Ask follow-up questions about the approach.

-
- +
-
- - - -
-
-
-
How to Get Featured
+
How to Get Featured

Solve any problem to submit your solution. Other developers can like your code, and the top-rated solutions appear at the top of Best Practices. The more problems you solve, the more your solutions get discovered.

-
-
Top Rated Solutions
+
Top Rated Solutions
{{range $i, $s := .TopSolutions}} - - @@ -525,7 +494,7 @@ func problemReminderBody() string { -
+
{{.ProblemTitle}}
-
by {{.UserName}} · {{.Language}}
+
by {{.UserName}} · {{.Language}}
-
-{{.Likes}} -
+♥ {{.Likes}}
@@ -495,21 +464,21 @@ See how top developers solve real problems. Browse community solutions, get AI-p
-
+
Button not working?
- +
+ -
{{.PlatformName}}
+
{{.PlatformName}}
{{.Tagline}}
Sent by Jerry Koko from Koder
- +
© {{.Year}} {{.PlatformName}}. All rights reserved.
+
@@ -564,10 +533,10 @@ Sharpen your skills with this short exercise: - +
- @@ -653,7 +622,7 @@ const passwordResetBody = `{{define "content"}} -
-
{{.ProblemTitle}}
+
{{.ProblemTitle}}
{{.ProblemExcerptHTML}}
Open Problem @@ -582,21 +551,21 @@ Sharpen your skills with this short exercise: -
+
Button not working?
- +
+ -
{{.PlatformName}}
+
{{.PlatformName}}
{{.Tagline}}
Sent by Jerry Koko from Koder
- +
© {{.Year}} {{.PlatformName}}. All rights reserved.
+ @@ -749,12 +718,12 @@ This secure link expires in {{.Expire - diff --git a/internal/email/email_test.go b/internal/email/email_test.go index a2720e5..dc4c234 100644 --- a/internal/email/email_test.go +++ b/internal/email/email_test.go @@ -49,9 +49,8 @@ func TestRenderPasswordReset_ContainsBrandAndStructure(t *testing.T) { "Sent by Jerry Koko from Koder", "© ", "Koder", - "border-left:4px solid #7F56D9", - "border-top:2px solid #7F56D9", - "color:#53389E", + "border-left:4px solid #D4AF37", + "border-top:2px solid #D4AF37", } for _, want := range required { if !strings.Contains(out, want) { @@ -210,11 +209,9 @@ func TestRenderBestPractices_ContainsBrandAndStructure(t *testing.T) { "mailto:support@koder.sbs", "Koder turns every problem into an instant feedback loop.", "© ", - // Purple accent checks - "border-left:4px solid #7F56D9", - "border-top:3px solid #7F56D9", - "border-top:2px solid #7F56D9", - "color:#53389E", + // 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", } @@ -224,20 +221,10 @@ func TestRenderBestPractices_ContainsBrandAndStructure(t *testing.T) { } } - // No emoji glyphs or heart entities anywhere in the output. + // No emoji glyphs in the output. if strings.ContainsAny(out, "😀🔒🤖🚀✨🔥") { t.Errorf("rendered email contains emoji characters") } - if strings.Contains(out, "♥") { - t.Errorf("rendered email contains raw ♥ entity (should use SVG img)") - } - - // Inline SVG icons must be present. - for _, icon := range []string{SmallHeartIconDataURI, TrophyIconDataURI, HeartIconDataURI, SparklesIconDataURI, StarIconDataURI} { - if !strings.Contains(out, icon) { - t.Errorf("rendered email missing inline SVG icon data URI (truncated: %s...)", icon[:60]) - } - } // Stats bar should have separate Go and Python cells, not "Go / Python". if strings.Contains(out, "Go / Python") { @@ -359,8 +346,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 #7F56D9", - "color:#53389E", + "border-left:4px solid #D4AF37", } { 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 ff25554..f0dcc69 100644 --- a/internal/email/problem_reminder_test.go +++ b/internal/email/problem_reminder_test.go @@ -37,9 +37,7 @@ func TestRenderProblemReminder_ContainsExpectedFields(t *testing.T) { "Open Problem", "https://koder.sbs/logo.png", "support@koder.sbs", - "border-left:4px solid #7F56D9", - "border-top:2px solid #7F56D9", - "color:#53389E", + "border-left:4px solid #D4AF37", "border-bottom:2px solid #B8941F", "box-shadow:0 4px 14px rgba(212,175,55,0.35)", }
@@ -739,9 +708,9 @@ This secure link expires in {{.Expire
-
+
Button not working?
- +
+ -
{{.PlatformName}}
+
{{.PlatformName}}
{{.Tagline}}
Sent by Jerry Koko from Koder
- +
© {{.Year}} {{.PlatformName}}. All rights reserved.