Skip to content

Commit a77b86b

Browse files
committed
refactor(brand-logos): Add color support to BrandLogo component
This commit refactors the BrandLogo component to accept a `color` prop, allowing for dynamic logo coloring. It also introduces a `BRAND_COLOR` map for specific brand hex values. Changes include: - Modified `BrandLogo.tsx` to accept a `color` prop and use brand-specific colors. - Updated `IconTile.tsx` to pass the new `logoColor` prop to `BrandLogo`. - Added `getProductLogoColor` function in `productIconStyles.ts` to determine logo color based on product attributes and gradient. - Extended `tailwind.config.js` to include the `lib` directory for scanning.
1 parent 2697355 commit a77b86b

5 files changed

Lines changed: 71 additions & 49 deletions

File tree

frontend/components/BrandLogo.tsx

Lines changed: 40 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -12,70 +12,61 @@ interface Props {
1212
brand: BrandKey | string;
1313
size?: number;
1414
className?: string;
15+
/** Defaults to white — use brand hex on light product tiles */
16+
color?: string;
1517
}
1618

19+
const BRAND_COLOR: Record<string, string> = {
20+
ChatGPT: "#10A37F",
21+
Gemini: "#4285F4",
22+
Cursor: "#1E293B",
23+
Spotify: "#1DB954",
24+
Telegram: "#229ED9",
25+
Discord: "#5865F2",
26+
YouTube: "#FF0000",
27+
SoundCloud: "#FF5500",
28+
};
29+
1730
// ─── ChatGPT / OpenAI "bloom" logo ───────────────────────────────────────────
1831
// Accurate OpenAI hexagonal-knot logomark, scaled to 24 × 24 viewBox.
19-
function ChatGPTLogo({ size }: { size: number }) {
32+
function ChatGPTLogo({ size, color }: { size: number; color: string }) {
2033
return (
2134
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" aria-hidden>
2235
<path
23-
fill="#fff"
24-
d="M20.89 9.54a5.83 5.83 0 0 0-.5-4.79 5.9 5.9 0 0 0-6.35-2.83
25-
A5.83 5.83 0 0 0 9.65 0a5.9 5.9 0 0 0-5.63 4.09
26-
A5.83 5.83 0 0 0 .12 6.92a5.9 5.9 0 0 0 .73 6.93
27-
A5.83 5.83 0 0 0 1.35 18.64a5.9 5.9 0 0 0 6.35 2.83
28-
A5.83 5.83 0 0 0 12.08 24a5.9 5.9 0 0 0 5.64-4.09
29-
A5.83 5.83 0 0 0 21.62 17.08a5.9 5.9 0 0 0-.73-6.93zm-8.96 12.55
30-
a4.37 4.37 0 0 1-2.82-1.01l.14-.08 4.67-2.7a.78.78 0 0 0 .39-.67V10.9
31-
l1.97 1.14a.07.07 0 0 1 .04.06v5.45a4.41 4.41 0 0 1-4.39 4.54zm-9.44-4.07
32-
a4.37 4.37 0 0 1-.52-2.97l.14.08 4.67 2.7a.78.78 0 0 0 .78 0l5.7-3.29v2.28
33-
a.07.07 0 0 1-.03.06l-4.72 2.73a4.41 4.41 0 0 1-6.02-1.59zm-1.23-10.2
34-
a4.37 4.37 0 0 1 2.28-1.92v5.51a.78.78 0 0 0 .39.67l5.7 3.29-1.97 1.14
35-
a.07.07 0 0 1-.07 0L3.8 13.8a4.41 4.41 0 0 1-.54-5.98zm16.22 3.8
36-
-5.7-3.3 1.97-1.14a.07.07 0 0 1 .07 0l4.73 2.73a4.41 4.41 0 0 1-.68 7.96
37-
v-5.51a.78.78 0 0 0-.39-.74zm1.96-2.98-.14-.08-4.67-2.7
38-
a.78.78 0 0 0-.78 0L8.11 9.15V6.87a.07.07 0 0 1 .03-.06l4.72-2.72
39-
a4.41 4.41 0 0 1 6.58 4.55zm-12.34 4.06L5.13 11.56
40-
a.07.07 0 0 1-.04-.06V6.05a4.41 4.41 0 0 1 7.25-3.39l-.14.08
41-
L7.53 5.44a.78.78 0 0 0-.39.67v6.59zm1.07-2.3 2.54-1.47 2.54 1.46
42-
v2.93l-2.54 1.47-2.54-1.47z"
36+
fill={color}
37+
d="M20.89 9.54a5.83 5.83 0 0 0-.5-4.79 5.9 5.9 0 0 0-6.35-2.83A5.83 5.83 0 0 0 9.65 0a5.9 5.9 0 0 0-5.63 4.09A5.83 5.83 0 0 0 .12 6.92a5.9 5.9 0 0 0 .73 6.93A5.83 5.83 0 0 0 1.35 18.64a5.9 5.9 0 0 0 6.35 2.83A5.83 5.83 0 0 0 12.08 24a5.9 5.9 0 0 0 5.64-4.09A5.83 5.83 0 0 0 21.62 17.08a5.9 5.9 0 0 0-.73-6.93zm-8.96 12.55a4.37 4.37 0 0 1-2.82-1.01l.14-.08 4.67-2.7a.78.78 0 0 0 .39-.67V10.9l1.97 1.14a.07.07 0 0 1 .04.06v5.45a4.41 4.41 0 0 1-4.39 4.54zm-9.44-4.07a4.37 4.37 0 0 1-.52-2.97l.14.08 4.67 2.7a.78.78 0 0 0 .78 0l5.7-3.29v2.28a.07.07 0 0 1-.03.06l-4.72 2.73a4.41 4.41 0 0 1-6.02-1.59zm-1.23-10.2a4.37 4.37 0 0 1 2.28-1.92v5.51a.78.78 0 0 0 .39.67l5.7 3.29-1.97 1.14a.07.07 0 0 1-.07 0L3.8 13.8a4.41 4.41 0 0 1-.54-5.98zm16.22 3.8-5.7-3.3 1.97-1.14a.07.07 0 0 1 .07 0l4.73 2.73a4.41 4.41 0 0 1-.68 7.96v-5.51a.78.78 0 0 0-.39-.74zm1.96-2.98-.14-.08-4.67-2.7a.78.78 0 0 0-.78 0L8.11 9.15V6.87a.07.07 0 0 1 .03-.06l4.72-2.72a4.41 4.41 0 0 1 6.58 4.55zm-12.34 4.06L5.13 11.56a.07.07 0 0 1-.04-.06V6.05a4.41 4.41 0 0 1 7.25-3.39l-.14.08L7.53 5.44a.78.78 0 0 0-.39.67v6.59zm1.07-2.3 2.54-1.47 2.54 1.46v2.93l-2.54 1.47-2.54-1.47z"
4338
/>
4439
</svg>
4540
);
4641
}
4742

4843
// ─── Gemini — smooth 4-pointed sparkle star ───────────────────────────────────
4944
// Matches Google Gemini's characteristic 4-point glint shape.
50-
function GeminiLogo({ size }: { size: number }) {
45+
function GeminiLogo({ size, color }: { size: number; color: string }) {
5146
return (
5247
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" aria-hidden>
5348
{/* Main 4-pointed star via two thin diamond shapes */}
5449
<path
55-
fill="#fff"
56-
d="M12 2
57-
C12 2 13.18 8.18 22 12
58-
C13.18 15.82 12 22 12 22
59-
C12 22 10.82 15.82 2 12
60-
C10.82 8.18 12 2Z"
50+
fill={color}
51+
d="M12 2C12 2 13.18 8.18 22 12C13.18 15.82 12 22 12 22C12 22 10.82 15.82 2 12C10.82 8.18 12 2Z"
6152
/>
6253
</svg>
6354
);
6455
}
6556

6657
// ─── Cursor — the Cursor IDE bold X logo ─────────────────────────────────────
6758
// Two thick rounded rectangles crossing diagonally at 45°.
68-
function CursorLogo({ size }: { size: number }) {
59+
function CursorLogo({ size, color }: { size: number; color: string }) {
6960
return (
7061
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" aria-hidden>
7162
<rect
7263
x="4" y="10.6" width="16" height="2.8" rx="1.4"
73-
fill="#fff"
64+
fill={color}
7465
transform="rotate(45 12 12)"
7566
/>
7667
<rect
7768
x="4" y="10.6" width="16" height="2.8" rx="1.4"
78-
fill="#fff"
69+
fill={color}
7970
transform="rotate(-45 12 12)"
8071
/>
8172
</svg>
@@ -84,70 +75,70 @@ function CursorLogo({ size }: { size: number }) {
8475

8576
// ─── Spotify — three concentric arcs ─────────────────────────────────────────
8677
// Classic Spotify "sound wave" bars, accurately centered.
87-
function SpotifyLogo({ size }: { size: number }) {
78+
function SpotifyLogo({ size, color }: { size: number; color: string }) {
8879
return (
8980
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" aria-hidden>
9081
{/* Top arc — widest */}
9182
<path
9283
d="M5.5 10.2c4.2-1.3 8.8-.9 12.6 1.5"
93-
stroke="#fff" strokeWidth="1.8" strokeLinecap="round"
84+
stroke={color} strokeWidth="1.8" strokeLinecap="round"
9485
/>
9586
{/* Middle arc */}
9687
<path
9788
d="M6.5 13.1c3.3-1 6.9-.7 10 1.1"
98-
stroke="#fff" strokeWidth="1.6" strokeLinecap="round"
89+
stroke={color} strokeWidth="1.6" strokeLinecap="round"
9990
/>
10091
{/* Bottom arc — narrowest */}
10192
<path
10293
d="M7.5 15.9c2.4-.7 5-.5 7.2.8"
103-
stroke="#fff" strokeWidth="1.4" strokeLinecap="round"
94+
stroke={color} strokeWidth="1.4" strokeLinecap="round"
10495
/>
10596
</svg>
10697
);
10798
}
10899

109100
// ─── Telegram — paper-plane icon ─────────────────────────────────────────────
110-
function TelegramLogo({ size }: { size: number }) {
101+
function TelegramLogo({ size, color }: { size: number; color: string }) {
111102
return (
112103
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" aria-hidden>
113104
<path
114105
d="M20.67 3.56 3.23 10.31c-1.15.45-1.14 1.07-.21 1.35l4.45 1.39 10.28-6.49c.49-.3.93-.14.57.19l-8.33 7.53-.32 4.7c.47 0 .67-.2.94-.45l2.26-2.2 4.52 3.33c.83.46 1.42.22 1.63-.77l2.95-13.93c.29-1.19-.46-1.73-1.3-1.4z"
115-
fill="#fff"
106+
fill={color}
116107
/>
117108
</svg>
118109
);
119110
}
120111

121112
// ─── Discord — Wumpus / controller face ──────────────────────────────────────
122-
function DiscordLogo({ size }: { size: number }) {
113+
function DiscordLogo({ size, color }: { size: number; color: string }) {
123114
return (
124115
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" aria-hidden>
125116
<path
126117
d="M18.93 5.34A16.4 16.4 0 0 0 15.12 4c-.17.3-.37.72-.5 1.04a15.17 15.17 0 0 0-5.25 0C9.24 4.72 9.03 4.3 8.87 4A16.4 16.4 0 0 0 5.06 5.35C2.27 9.49 1.5 13.52 1.89 17.48c1.77 1.3 3.49 2.1 5.18 2.61.42-.56.79-1.16 1.1-1.8a10.7 10.7 0 0 1-1.73-.84l.43-.34a11.7 11.7 0 0 0 10.26 0l.43.34c-.55.33-1.13.62-1.74.84.32.64.69 1.24 1.1 1.8 1.7-.51 3.42-1.31 5.19-2.61.46-4.6-.79-8.59-3.18-12.14zM8.68 15.04c-1.13 0-2.06-1.04-2.06-2.31s.9-2.32 2.06-2.32c1.15 0 2.08 1.04 2.06 2.32 0 1.27-.91 2.31-2.06 2.31zm6.64 0c-1.13 0-2.06-1.04-2.06-2.31s.9-2.32 2.06-2.32c1.15 0 2.08 1.04 2.06 2.32 0 1.27-.9 2.31-2.06 2.31z"
127-
fill="#fff"
118+
fill={color}
128119
/>
129120
</svg>
130121
);
131122
}
132123

133124
// ─── YouTube — rounded rect + play triangle ───────────────────────────────────
134-
function YouTubeLogo({ size }: { size: number }) {
125+
function YouTubeLogo({ size, color }: { size: number; color: string }) {
135126
return (
136127
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" aria-hidden>
137128
{/* Outer rounded rectangle */}
138-
<rect x="2" y="6" width="20" height="13" rx="4" fill="#fff" fillOpacity="0.25" />
129+
<rect x="2" y="6" width="20" height="13" rx="4" fill={color} fillOpacity="0.25" />
139130
{/* Inner play triangle */}
140-
<path d="M9.5 8.5v7l6-3.5-6-3.5z" fill="#fff" />
131+
<path d="M9.5 8.5v7l6-3.5-6-3.5z" fill={color} />
141132
</svg>
142133
);
143134
}
144135

145136
// ─── SoundCloud — layered waveform bars ──────────────────────────────────────
146-
function SoundCloudLogo({ size }: { size: number }) {
137+
function SoundCloudLogo({ size, color }: { size: number; color: string }) {
147138
return (
148139
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" aria-hidden>
149140
{/* Waveform — 9 bars of varying height, centered */}
150-
<g fill="#fff" rx="1">
141+
<g fill={color} rx="1">
151142
<rect x="3" y="13" width="1.6" height="4.5" rx="0.8" />
152143
<rect x="5.2" y="11" width="1.6" height="6.5" rx="0.8" />
153144
<rect x="7.4" y="9" width="1.6" height="8.5" rx="0.8" />
@@ -162,7 +153,7 @@ function SoundCloudLogo({ size }: { size: number }) {
162153
}
163154

164155
// ─── Registry ─────────────────────────────────────────────────────────────────
165-
const LOGOS: Record<string, (props: { size: number }) => JSX.Element> = {
156+
const LOGOS: Record<string, (props: { size: number; color: string }) => JSX.Element> = {
166157
ChatGPT: ChatGPTLogo,
167158
Gemini: GeminiLogo,
168159
Cursor: CursorLogo,
@@ -173,7 +164,9 @@ const LOGOS: Record<string, (props: { size: number }) => JSX.Element> = {
173164
SoundCloud: SoundCloudLogo,
174165
};
175166

176-
export default function BrandLogo({ brand, size = 24, className = "" }: Props) {
167+
export { BRAND_COLOR };
168+
169+
export default function BrandLogo({ brand, size = 24, className = "", color = "#fff" }: Props) {
177170
const Logo = LOGOS[brand];
178171
if (!Logo) {
179172
return (
@@ -184,7 +177,7 @@ export default function BrandLogo({ brand, size = 24, className = "" }: Props) {
184177
}
185178
return (
186179
<span className={`inline-flex items-center justify-center ${className}`}>
187-
<Logo size={size} />
180+
<Logo size={size} color={color} />
188181
</span>
189182
);
190183
}

frontend/components/IconTile.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ interface Props {
77
gradient: string;
88
size?: TileSize;
99
comingSoon?: boolean;
10+
logoColor?: string;
1011
className?: string;
1112
}
1213

@@ -20,6 +21,7 @@ export default function IconTile({
2021
gradient,
2122
size = "md",
2223
comingSoon = false,
24+
logoColor,
2325
className = "",
2426
}: Props) {
2527
const { tile, logo } = TILE_CLASS[size];
@@ -36,7 +38,7 @@ export default function IconTile({
3638
<span className="absolute inset-0 bg-black/20 rounded-2xl" aria-hidden />
3739
</>
3840
)}
39-
<BrandLogo brand={brand} size={logo} className="relative z-[1]" />
41+
<BrandLogo brand={brand} size={logo} color={logoColor} className="relative z-[1]" />
4042
</div>
4143
);
4244
}

frontend/components/ProductCard.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Product } from "@/lib/api";
22
import IconTile from "@/components/IconTile";
3-
import { getProductGradient } from "@/lib/productIconStyles";
3+
import { getProductGradient, getProductLogoColor } from "@/lib/productIconStyles";
44

55
interface Props {
66
product: Product;
@@ -17,6 +17,7 @@ export default function ProductCard({ product, onBuy }: Props) {
1717
<IconTile
1818
brand={product.category}
1919
gradient={getProductGradient(product)}
20+
logoColor={getProductLogoColor(product)}
2021
size="lg"
2122
/>
2223

frontend/lib/productIconStyles.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Product } from "@/lib/api";
2+
import { BRAND_COLOR } from "@/components/BrandLogo";
23
import { CATEGORY_GRADIENT_CLASS, getCategoryGradientClass } from "@/lib/categoryStyles";
34

45
/**
@@ -68,3 +69,27 @@ export function getProductGradient(product: Product): string {
6869
getCategoryGradientClass(product.category)
6970
);
7071
}
72+
73+
const LIGHT_PRODUCT_KEYS = new Set([
74+
"ChatGPT:personal_email",
75+
"ChatGPT:ready_email:37",
76+
]);
77+
78+
const LIGHT_GRADIENT_PATTERN =
79+
/to-white|#F0F9FF|#E0F2FE|#BFDBFE|#BAE6FD/i;
80+
81+
/** Pale product tiles need brand-colored logos instead of white */
82+
export function getProductLogoColor(product: Product): string {
83+
const fullKey = `${product.category}:${product.activation_type}:${product.duration_days}`;
84+
const simpleKey = `${product.category}:${product.activation_type}`;
85+
86+
if (LIGHT_PRODUCT_KEYS.has(fullKey) || LIGHT_PRODUCT_KEYS.has(simpleKey)) {
87+
return BRAND_COLOR[product.category] ?? "#fff";
88+
}
89+
90+
if (LIGHT_GRADIENT_PATTERN.test(getProductGradient(product))) {
91+
return BRAND_COLOR[product.category] ?? "#fff";
92+
}
93+
94+
return "#fff";
95+
}

frontend/tailwind.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module.exports = {
33
content: [
44
"./pages/**/*.{js,ts,jsx,tsx}",
55
"./components/**/*.{js,ts,jsx,tsx}",
6+
"./lib/**/*.{js,ts,jsx,tsx}",
67
],
78
theme: {
89
extend: {

0 commit comments

Comments
 (0)