Skip to content

Commit 90cd87a

Browse files
rr404jdv
andauthored
Premium plan presentation page update (#1043)
* new premium features overview look * personna selector * redesign on full feature list page * uniformization of metric tag * uniformization of feature cards * code quality pass --------- Co-authored-by: jdv <julien@crowdsec.net>
1 parent 4df6afe commit 90cd87a

File tree

12 files changed

+1814
-143
lines changed

12 files changed

+1814
-143
lines changed
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
import Link from "@docusaurus/Link";
2+
import React from "react";
3+
4+
export interface FeatureCardProps {
5+
id?: string;
6+
title: string;
7+
metric?: string;
8+
description: string;
9+
comparison?: {
10+
before: string;
11+
after: string;
12+
};
13+
link?: string;
14+
category?: "protection" | "scale" | "monitoring" | "intelligence";
15+
highlight?: boolean;
16+
badges?: string[];
17+
}
18+
19+
const categoryColors = {
20+
protection: {
21+
border: "border-l-4 border-l-red-500 dark:border-l-red-400",
22+
metric: "bg-red-50 dark:bg-red-900/20 text-red-700 dark:text-red-300",
23+
},
24+
scale: {
25+
border: "border-l-4 border-l-purple-500 dark:border-l-purple-400",
26+
metric: "bg-purple-50 dark:bg-purple-900/20 text-purple-700 dark:text-purple-300",
27+
},
28+
monitoring: {
29+
border: "border-l-4 border-l-teal-500 dark:border-l-teal-400",
30+
metric: "bg-teal-50 dark:bg-teal-900/20 text-teal-700 dark:text-teal-300",
31+
},
32+
intelligence: {
33+
border: "border-l-4 border-l-yellow-600 dark:border-l-yellow-500",
34+
metric: "bg-yellow-50 dark:bg-yellow-900/20 text-yellow-700 dark:text-yellow-300",
35+
},
36+
};
37+
38+
export const FeatureCard = ({
39+
id,
40+
title,
41+
metric,
42+
description,
43+
comparison,
44+
link,
45+
category = "protection",
46+
highlight = false,
47+
badges = [],
48+
}: FeatureCardProps): React.JSX.Element => {
49+
const colors = categoryColors[category];
50+
51+
// Generate ID from title if not explicitly provided
52+
const generatedId =
53+
id ||
54+
title
55+
.toLowerCase()
56+
.replace(/\s+/g, "-")
57+
.replace(/[^\w-]/g, "");
58+
59+
const cardContent = (
60+
<div
61+
id={generatedId}
62+
className={`
63+
h-full border border-solid border-border rounded-lg p-5 bg-card
64+
hover:shadow-md hover:border-primary/30 transition-all duration-200
65+
${category ? colors.border : ""}
66+
${highlight ? "bg-gradient-to-r from-primary/5 to-transparent border-primary/30" : ""}
67+
`}
68+
>
69+
<div className="flex items-start justify-between gap-3 mb-3">
70+
<div className="flex-1">
71+
<h4 className="font-semibold text-base mb-1 text-gray-900 dark:text-gray-900">
72+
{title}
73+
{badges.map((badge) => (
74+
<span
75+
key={badge}
76+
className="ml-2 text-xs bg-yellow-100 dark:bg-yellow-900/30 text-yellow-800 dark:text-yellow-300 px-2 py-0.5 rounded-full"
77+
>
78+
{badge}
79+
</span>
80+
))}
81+
</h4>
82+
</div>
83+
{metric && (
84+
<span className={`flex-shrink-0 text-xs font-medium px-3 py-1 rounded-full whitespace-nowrap ${colors.metric}`}>
85+
{metric}
86+
</span>
87+
)}
88+
</div>
89+
<p className="text-sm text-gray-600 dark:text-gray-700 mb-3 leading-relaxed">{description}</p>
90+
{comparison && (
91+
<div className="mt-3 p-3 bg-gray-50 dark:bg-gray-900/30 rounded-md text-xs">
92+
<span className="text-gray-500 dark:text-gray-600 line-through">{comparison.before}</span>
93+
{" → "}
94+
<span className="font-semibold text-primary">{comparison.after}</span>
95+
</div>
96+
)}
97+
{link && <div className="mt-3 text-sm font-medium text-primary hover:underline">Learn more →</div>}
98+
</div>
99+
);
100+
101+
if (link) {
102+
return (
103+
<Link href={link} className="hover:no-underline block">
104+
{cardContent}
105+
</Link>
106+
);
107+
}
108+
109+
return cardContent;
110+
};
111+
112+
export interface HighlightCardProps {
113+
id?: string;
114+
title: string;
115+
description: string;
116+
stats?: Array<{
117+
value: string;
118+
label: string;
119+
}>;
120+
link?: string;
121+
category?: "protection" | "scale" | "monitoring" | "intelligence";
122+
}
123+
124+
export const HighlightCard = ({ id, title, description, stats, link }: HighlightCardProps): React.JSX.Element => {
125+
// Generate ID from title if not explicitly provided
126+
const generatedId =
127+
id ||
128+
title
129+
.toLowerCase()
130+
.replace(/\s+/g, "-")
131+
.replace(/[^\w-]/g, "");
132+
133+
const content = (
134+
<div
135+
id={generatedId}
136+
className="border border-solid border-primary/30 rounded-lg p-6 bg-gradient-to-r from-primary/5 to-transparent hover:shadow-md transition-all"
137+
>
138+
<h4 className="font-semibold text-lg mb-2 text-gray-900 dark:text-gray-900">{title}</h4>
139+
<p className="text-sm text-gray-600 dark:text-gray-700 mb-4 leading-relaxed">{description}</p>
140+
{stats && stats.length > 0 && (
141+
<div className="flex gap-8 mt-4">
142+
{stats.map((stat) => (
143+
<div key={`${stat.value}-${stat.label}`} className="text-center">
144+
<div className="text-3xl font-bold text-primary">{stat.value}</div>
145+
<div className="text-xs text-gray-500 dark:text-gray-600 mt-1">{stat.label}</div>
146+
</div>
147+
))}
148+
</div>
149+
)}
150+
{link && <div className="mt-4 text-sm font-medium text-primary hover:underline">Learn more →</div>}
151+
</div>
152+
);
153+
154+
if (link) {
155+
return (
156+
<Link href={link} className="hover:no-underline block">
157+
{content}
158+
</Link>
159+
);
160+
}
161+
162+
return content;
163+
};
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export type { FeatureCardProps, HighlightCardProps } from "./feature-card";
2+
export { FeatureCard, HighlightCard } from "./feature-card";
3+
export type { PersonaOption as PersonaSelectorOption, PersonaSelectorProps } from "./persona-selector";
4+
export { PersonaSelector } from "./persona-selector";
5+
export type { PersonaOption as PersonaTabsOption, PersonaTabsHeaderProps } from "./persona-tabs";
6+
export { PersonaTabsHeader } from "./persona-tabs";
7+
export type { PersonaOption, TabsWithPersonaProps } from "./tabs-with-persona";
8+
export { TabsWithPersona } from "./tabs-with-persona";
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import React, { useState } from "react";
2+
3+
export interface PersonaOption {
4+
id: string;
5+
icon: string;
6+
title: string;
7+
description: string;
8+
tag: string;
9+
}
10+
11+
export interface PersonaSelectorProps {
12+
options: PersonaOption[];
13+
defaultSelected?: string;
14+
onChange?: (selectedId: string) => void;
15+
label?: string;
16+
}
17+
18+
export const PersonaSelector = ({
19+
options,
20+
defaultSelected,
21+
onChange,
22+
label = "Your Profile",
23+
}: PersonaSelectorProps): React.JSX.Element => {
24+
const [selected, setSelected] = useState<string>(defaultSelected || options[0]?.id || "");
25+
26+
const handleSelect = (id: string) => {
27+
setSelected(id);
28+
onChange?.(id);
29+
};
30+
31+
return (
32+
<div className="persona-selector mb-8">
33+
<p className="text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-600 mb-4">{label}</p>
34+
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
35+
{options.map((option) => (
36+
<button
37+
key={option.id}
38+
type="button"
39+
onClick={() => handleSelect(option.id)}
40+
className={`
41+
persona-card text-left p-5 rounded-xl border-2 transition-all duration-200
42+
${
43+
selected === option.id
44+
? "border-primary bg-primary text-white shadow-lg scale-[1.02]"
45+
: "border-border bg-card hover:border-gray-400 dark:hover:border-gray-500 hover:shadow-md"
46+
}
47+
`}
48+
>
49+
<div
50+
className={`
51+
text-3xl mb-3 w-9 h-9 rounded-lg flex items-center justify-center transition-all
52+
${selected === option.id ? "bg-white/20" : "bg-gray-100 dark:bg-gray-800"}
53+
`}
54+
>
55+
{option.icon}
56+
</div>
57+
<h3
58+
className={`
59+
font-semibold text-base mb-2 transition-colors
60+
${selected === option.id ? "text-white" : "text-gray-900 dark:text-gray-100"}
61+
`}
62+
>
63+
{option.title}
64+
</h3>
65+
<p
66+
className={`
67+
text-sm mb-3 leading-relaxed transition-colors
68+
${selected === option.id ? "text-white/80" : "text-gray-600 dark:text-gray-400"}
69+
`}
70+
>
71+
{option.description}
72+
</p>
73+
<span
74+
className={`
75+
inline-block text-xs font-medium px-3 py-1 rounded-full transition-all
76+
${
77+
selected === option.id
78+
? "bg-white/20 text-white/90"
79+
: "bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400"
80+
}
81+
`}
82+
>
83+
{option.tag}
84+
</span>
85+
</button>
86+
))}
87+
</div>
88+
</div>
89+
);
90+
};
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import React from "react";
2+
3+
export interface PersonaOption {
4+
value: string;
5+
icon: string;
6+
label: string;
7+
description: string;
8+
tag: string;
9+
}
10+
11+
export interface PersonaTabsHeaderProps {
12+
options: PersonaOption[];
13+
selectedValue: string;
14+
onSelect: (value: string) => void;
15+
headerLabel?: string;
16+
}
17+
18+
/**
19+
* Custom header for Docusaurus Tabs that looks like persona selector cards
20+
* Use this with Docusaurus <Tabs> component by passing a custom tabsHeader
21+
*/
22+
export const PersonaTabsHeader = ({
23+
options,
24+
selectedValue,
25+
onSelect,
26+
headerLabel = "Your Profile",
27+
}: PersonaTabsHeaderProps): React.JSX.Element => {
28+
return (
29+
<div className="persona-tabs-header mb-8">
30+
<p className="text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-600 mb-4">{headerLabel}</p>
31+
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
32+
{options.map((option) => {
33+
const isSelected = selectedValue === option.value;
34+
return (
35+
<button
36+
key={option.value}
37+
type="button"
38+
role="tab"
39+
aria-selected={isSelected}
40+
onClick={() => onSelect(option.value)}
41+
className={`
42+
persona-card text-left p-5 rounded-xl border-2 transition-all duration-200
43+
${
44+
isSelected
45+
? "border-primary bg-primary text-white shadow-lg scale-[1.02]"
46+
: "border-border bg-card hover:border-gray-400 dark:hover:border-gray-500 hover:shadow-md"
47+
}
48+
`}
49+
>
50+
<div
51+
className={`
52+
text-3xl mb-3 w-9 h-9 rounded-lg flex items-center justify-center transition-all
53+
${isSelected ? "bg-white/20" : "bg-gray-100 dark:bg-gray-800"}
54+
`}
55+
>
56+
{option.icon}
57+
</div>
58+
<h3
59+
className={`
60+
font-semibold text-base mb-2 transition-colors
61+
${isSelected ? "text-white" : "text-gray-900 dark:text-gray-100"}
62+
`}
63+
>
64+
{option.label}
65+
</h3>
66+
<p
67+
className={`
68+
text-sm mb-3 leading-relaxed transition-colors
69+
${isSelected ? "text-white/80" : "text-gray-600 dark:text-gray-400"}
70+
`}
71+
>
72+
{option.description}
73+
</p>
74+
<span
75+
className={`
76+
inline-block text-xs font-medium px-3 py-1 rounded-full transition-all
77+
${isSelected ? "bg-white/20 text-white/90" : "bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400"}
78+
`}
79+
>
80+
{option.tag}
81+
</span>
82+
</button>
83+
);
84+
})}
85+
</div>
86+
</div>
87+
);
88+
};

0 commit comments

Comments
 (0)