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
-
-
+
+
@@ -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:
-
+
+
@@ -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
-
+
@@ -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)",
}
| | | | | | | | | | | | | | |