Skip to content

Commit 41fbac2

Browse files
feat:ambassadors Page (#1108)
* feat:ambassadors Page * href error was due to null changed to undefined * Updated illustrations and texts * update ambassadors data with symlink * update symlink to relative * Update symlink to new format * dark theme fonts colors fix * fix images and styles * Update community * format style * fix css * fix css * fix css * fix style banner * change border * trying fix to the banner * buttons alignment * more tests * div width * more fixes --------- Co-authored-by: Benjamin Granados <[email protected]>
1 parent 43aef96 commit 41fbac2

File tree

17 files changed

+455
-2
lines changed

17 files changed

+455
-2
lines changed

data/ambassador_lists.json

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
{
2+
"contents": [
3+
{
4+
"title": "Written content",
5+
"details": "Write guides, step-by-step tutorials, community documentation, JSON Schema blog posts, and beyond.",
6+
"icon": "/img/ambassadors/illustrations/content.png"
7+
},
8+
{
9+
"title": "Podcasts and Videos",
10+
"details": "Produce educational videos, podcasts or livestreams speaking about JSON Schema.",
11+
"icon": "/img/ambassadors/illustrations/video.png"
12+
},
13+
{
14+
"title": "Give talks",
15+
"details": "Speak at meetups and conferences; we’ll help with slides, abstract submissions, and travel budget.",
16+
"icon": "/img/ambassadors/illustrations/speaker.png"
17+
},
18+
{
19+
"title": "JSON Contributions",
20+
"details": "Collaborate with the JSON community via diverse contributions and improvements.",
21+
"icon": "/img/ambassadors/illustrations/contributing.png"
22+
},
23+
{
24+
"title": "Ecosystem Impact",
25+
"details": "Promote JSON Schema adoption in other ecosystems.",
26+
"icon": "/img/ambassadors/illustrations/ecosystem.png"
27+
},
28+
{
29+
"title": "Gather Case Studies",
30+
"details": "Collaborate in the production of case-studies.",
31+
"icon": "/img/ambassadors/illustrations/case-studies.png"
32+
}
33+
],
34+
"tokens": [
35+
{
36+
"emoji": "🗺️",
37+
"title": "Travel",
38+
"details": "Ambassadors are provided free entry to JSON conferences."
39+
},
40+
{
41+
"emoji": "🌟",
42+
"title": "Recognition",
43+
"details": "Ambassadors receive community-wide recognition."
44+
},
45+
{
46+
"emoji": "🎁",
47+
"title": "Special Swags",
48+
"details": "Community members recognize you by gifting you exclusive JSON Ambassador swag."
49+
},
50+
{
51+
"emoji": "🧰",
52+
"title": "Workshop Swags",
53+
"details": "Ambassadors are gifted swag from JSON conferences and workshops."
54+
}
55+
]
56+
}

data/ambassadors.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../_includes/community/programs/ambassadors/ambassadors.json
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import Link from 'next/link';
2+
import React from 'react';
3+
4+
const AmbassadorBanner: React.FC = () => {
5+
return (
6+
<div className='flex justify-center mx-4 md:mx-10 my-8 w-full'>
7+
<div className='group relative h-auto md:w-5/6 lg:w-1/2 xl:w-1/2 rounded-lg border border-gray-200 bg-white p-6 px-12 shadow-3xl dark:shadow-2xl dark:shadow-slate-900 transition-colors ease-in-out hover:bg-slate-100 dark:bg-slate-800 hover:dark:bg-slate-900/30'>
8+
<h3 className='text-2xl font-bold mb-2 text-gray-800 dark:text-slate-100 text-center'>
9+
Become a JSON Schema Ambassador
10+
</h3>
11+
<p className='text-gray-600 dark:text-slate-100 mb-6 text-center'>
12+
The JSON Schema Ambassador program is now open for applications! If
13+
you're selected, you'll join JSON Schema's mission of helping
14+
community members all over the world build the future of JSON Schema.
15+
</p>
16+
<div className='w-full lg:w-full grid grid-cols-2 sm:grid-cols-2 gap-6 my-[10px] mx-auto mt-8 '>
17+
<Link
18+
href='https://github.com/json-schema-org/community/tree/main/programs/ambassadors#become-an-json-schema-ambassador'
19+
className='inline-block px-6 py-3 bg-blue-600 text-white font-semibold text-center rounded hover:bg-blue-700 transition duration-300'
20+
>
21+
Become Ambassador
22+
</Link>
23+
<Link
24+
href='https://github.com/json-schema-org/community/tree/main/programs/ambassadors'
25+
className='inline-block bg-gray-300 dark:bg-gray-700 text-gray-700 text-center dark:text-white px-6 py-3 rounded-md shadow hover:bg-gray-400 dark:hover:bg-gray-600 transition'
26+
>
27+
Learn More
28+
</Link>
29+
</div>
30+
</div>
31+
</div>
32+
);
33+
};
34+
35+
export default AmbassadorBanner;
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
import React, { useState } from 'react';
2+
import Image from 'next/image';
3+
// import { Github, Twitter, Linkedin } from 'lucide-react';
4+
5+
// Define the types for the `ambassador` prop
6+
interface Contribution {
7+
title: string;
8+
date?: { month: string; year: number };
9+
link: string;
10+
type: string;
11+
}
12+
13+
interface Ambassador {
14+
img?: string;
15+
name?: string;
16+
title?: string;
17+
bio?: string;
18+
company?: string;
19+
country?: string;
20+
github?: string;
21+
twitter?: string;
22+
mastodon?: string;
23+
linkedin?: string;
24+
contributions?: Contribution[];
25+
}
26+
27+
interface AmbassadorCardProps {
28+
ambassador: Ambassador;
29+
}
30+
31+
type SocialPlatform = 'github' | 'twitter' | 'mastodon' | 'linkedin';
32+
33+
// Utility function to generate full URLs for social media
34+
const getSocialMediaUrl = (
35+
platform: SocialPlatform,
36+
username: string | undefined,
37+
) => {
38+
const baseUrls: Record<SocialPlatform, string> = {
39+
github: 'https://github.com/',
40+
twitter: 'https://twitter.com/',
41+
mastodon: 'https://mastodon.social/',
42+
linkedin: 'https://www.linkedin.com/in/',
43+
};
44+
return username ? baseUrls[platform] + username : undefined;
45+
};
46+
47+
// Social media icon component with proper typing
48+
const SocialIcon: React.FC<{ platform: SocialPlatform }> = ({ platform }) => {
49+
const icons: Record<SocialPlatform, JSX.Element> = {
50+
github: (
51+
<svg
52+
className='w-7 h-7 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100'
53+
viewBox='0 0 24 24'
54+
fill='currentColor'
55+
xmlns='http://www.w3.org/2000/svg'
56+
>
57+
<path d='M12 0C5.373 0 0 5.373 0 12c0 5.304 3.438 9.8 8.207 11.387.6.113.82-.26.82-.577 0-.287-.01-1.049-.016-2.06-3.338.726-4.043-1.609-4.043-1.609-.546-1.387-1.333-1.756-1.333-1.756-1.09-.746.084-.73.084-.73 1.205.084 1.84 1.238 1.84 1.238 1.07 1.834 2.81 1.303 3.492.997.108-.774.42-1.303.763-1.603-2.665-.304-5.467-1.332-5.467-5.93 0-1.31.469-2.381 1.236-3.22-.124-.303-.536-1.523.117-3.176 0 0 1.008-.322 3.302 1.23A11.503 11.503 0 0 1 12 6.802c1.018.004 2.046.137 3.003.403 2.293-1.552 3.3-1.23 3.3-1.23.654 1.653.242 2.873.118 3.176.77.839 1.236 1.91 1.236 3.22 0 4.61-2.804 5.624-5.474 5.921.43.37.816 1.102.816 2.221 0 1.606-.014 2.901-.014 3.293 0 .32.216.694.824.576C20.566 21.796 24 17.3 24 12 24 5.373 18.627 0 12 0z' />
58+
</svg>
59+
),
60+
twitter: (
61+
<svg
62+
className='w-7 h-7 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100'
63+
viewBox='0 0 24 24'
64+
fill='currentColor'
65+
xmlns='http://www.w3.org/2000/svg'
66+
>
67+
<path d='M23.954 4.569c-.885.389-1.83.654-2.825.775a4.932 4.932 0 0 0 2.163-2.723 9.85 9.85 0 0 1-3.127 1.195 4.916 4.916 0 0 0-8.374 4.482A13.936 13.936 0 0 1 1.64 3.161 4.916 4.916 0 0 0 3.195 9.86a4.897 4.897 0 0 1-2.229-.616v.061a4.919 4.919 0 0 0 3.946 4.827 4.897 4.897 0 0 1-2.224.085 4.923 4.923 0 0 0 4.604 3.42A9.869 9.869 0 0 1 .977 19.569a13.94 13.94 0 0 0 7.548 2.211c9.056 0 14.012-7.497 14.012-13.986 0-.213-.005-.425-.015-.636A9.936 9.936 0 0 0 24 4.59a9.94 9.94 0 0 1-2.046.561z' />
68+
</svg>
69+
),
70+
linkedin: (
71+
<svg
72+
className='w-7 h-7 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100'
73+
viewBox='0 0 24 24'
74+
fill='currentColor'
75+
xmlns='http://www.w3.org/2000/svg'
76+
>
77+
<path d='M22.225 0H1.771C.792 0 0 .775 0 1.729V22.27c0 .955.793 1.729 1.771 1.729h20.451c.978 0 1.778-.775 1.778-1.729V1.729C24 .774 23.203 0 22.225 0zM7.113 20.452H3.56V8.997h3.552v11.455zm-1.78-13.044a2.073 2.073 0 1 1 0-4.145 2.073 2.073 0 0 1 0 4.145zm15.34 13.044h-3.552v-5.697c0-1.357-.027-3.1-1.892-3.1-1.892 0-2.182 1.48-2.182 3.003v5.794H10.84V8.997h3.412v1.568h.049c.474-.896 1.637-1.84 3.37-1.84 3.604 0 4.268 2.371 4.268 5.451v6.276h-.002z' />
78+
</svg>
79+
),
80+
mastodon: (
81+
<svg
82+
className='w-7 h-7 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100'
83+
viewBox='0 0 24 24'
84+
fill='currentColor'
85+
xmlns='http://www.w3.org/2000/svg'
86+
>
87+
<path d='M23.268 5.313c-.35-2.578-2.617-4.61-5.304-5.004C17.51.242 15.792 0 11.813 0h-.03c-3.98 0-4.835.242-5.288.309C3.882.692 1.496 2.518.917 5.127.64 6.412.61 7.837.661 9.143c.074 1.874.088 3.745.26 5.611.118 1.24.325 2.47.62 3.68.55 2.237 2.777 4.098 4.96 4.857 2.336.792 4.849.923 7.256.38.265-.061.527-.132.786-.213.585-.184 1.27-.39 1.774-.753a.057.057 0 0 0 .023-.043v-1.809a.052.052 0 0 0-.02-.041.053.053 0 0 0-.046-.01 20.282 20.282 0 0 1-4.709.545c-2.73 0-3.463-1.284-3.674-1.818a5.593 5.593 0 0 1-.319-1.433.053.053 0 0 1 .066-.054c1.517.363 3.072.546 4.632.546.376 0 .75 0 1.125-.01 1.57-.044 3.224-.124 4.768-.422.038-.008.077-.015.11-.024 2.435-.464 4.753-1.92 4.989-5.604.008-.145.03-1.52.03-1.67.002-.512.167-3.63-.024-5.545zm-3.748 9.195h-2.561V8.29c0-1.309-.55-1.976-1.67-1.976-1.23 0-1.846.79-1.846 2.35v3.403h-2.546V8.663c0-1.56-.617-2.35-1.848-2.35-1.112 0-1.668.668-1.67 1.977v6.218H4.822V8.102c0-1.31.337-2.35 1.011-3.12.696-.77 1.608-1.164 2.74-1.164 1.311 0 2.302.5 2.962 1.498l.638 1.06.638-1.06c.66-.999 1.65-1.498 2.96-1.498 1.13 0 2.043.395 2.74 1.164.675.77 1.012 1.81 1.012 3.12z' />
88+
</svg>
89+
),
90+
};
91+
return icons[platform];
92+
};
93+
94+
const AmbassadorCard: React.FC<AmbassadorCardProps> = ({ ambassador }) => {
95+
const [showContributions, setShowContributions] = useState(false);
96+
const [imgSrc, setImgSrc] = useState(
97+
ambassador.img || '/api/placeholder/400/320',
98+
);
99+
100+
const {
101+
name = 'Ambassador',
102+
title,
103+
bio,
104+
company,
105+
country,
106+
contributions = [],
107+
} = ambassador;
108+
109+
// Available social platforms with proper typing
110+
const socialPlatforms: SocialPlatform[] = [
111+
'github',
112+
'twitter',
113+
'mastodon',
114+
'linkedin',
115+
];
116+
117+
return (
118+
<div className='relative max-w-sm md:max-w-md lg:max-w-lg mx-auto bg-white dark:bg-gray-800 shadow-lg rounded-lg overflow-hidden my-4 transition-all duration-300 h-fit'>
119+
{/* Black Borders */}
120+
<div className='absolute top-0 right-0 w-1 h-20 bg-black dark:bg-gray-400'></div>
121+
<div className='absolute bottom-100 right-0 w-20 h-1 bg-black dark:bg-gray-400'></div>
122+
<div className='absolute bottom-0 left-0 w-1 h-20 bg-black dark:bg-gray-400'></div>
123+
<div className='absolute bottom-0 left-0 w-20 h-1 bg-black dark:bg-gray-400'></div>
124+
125+
<Image
126+
className='w-full object-cover p-5 rounded-3xl'
127+
src={imgSrc}
128+
alt={`${name} profile`}
129+
width={400}
130+
height={320}
131+
onError={() => setImgSrc(`/img/ambassadors/${name}.jpg`)}
132+
/>
133+
134+
<div className='p-6'>
135+
<h3 className='text-xl font-semibold mb-2 text-gray-900 dark:text-white'>
136+
{name}
137+
</h3>
138+
{title && (
139+
<p className='text-gray-500 dark:text-slate-100 mb-1'>{title}</p>
140+
)}
141+
{bio && (
142+
<p className='text-gray-700 dark:text-slate-100 text-sm mb-4'>
143+
{bio}
144+
</p>
145+
)}
146+
{(company || country) && (
147+
<p className='text-gray-500 dark:text-slate-100 mb-4'>
148+
{company}
149+
{company && country && ', '}
150+
{country}
151+
</p>
152+
)}
153+
154+
{/* Social Media Links */}
155+
<div className='flex justify-center mb-4'>
156+
{socialPlatforms.map((platform) => {
157+
const username = ambassador[platform];
158+
return username ? (
159+
<a
160+
key={platform}
161+
href={getSocialMediaUrl(platform, username)}
162+
target='_blank'
163+
rel='noopener noreferrer'
164+
className='transition-colors duration-200 mr-4'
165+
aria-label={`${name}'s ${platform} profile`}
166+
>
167+
<SocialIcon platform={platform} />
168+
</a>
169+
) : null;
170+
})}
171+
</div>
172+
173+
{/* Button to Show/Hide Contributions */}
174+
{contributions.length > 0 && (
175+
<button
176+
onClick={() => setShowContributions(!showContributions)}
177+
className={`w-full bg-blue-600 dark:bg-blue-500 hover:bg-blue-700 dark:hover:bg-blue-400 text-white dark:text-slate-100 font-semibold py-2 px-4 rounded transition-all duration-300 transform ${
178+
showContributions ? 'rotate' : ''
179+
}`}
180+
>
181+
{showContributions ? 'Hide Details' : 'Show Full Details'}
182+
</button>
183+
)}
184+
185+
{/* Contributions Section (Toggled) */}
186+
{showContributions && contributions.length > 0 && (
187+
<div className='mt-4'>
188+
<h4 className='text-lg font-semibold mb-2 text-gray-900 dark:text-white'>
189+
Contributions
190+
</h4>
191+
<ul className='text-gray-600 dark:text-slate-100 text-sm'>
192+
{contributions.map((contribution, index) => (
193+
<li key={index} className='mb-2'>
194+
<strong>{contribution.title}</strong>
195+
{contribution.date &&
196+
` (${contribution.date.month} ${contribution.date.year})`}{' '}
197+
-
198+
<a
199+
href={contribution.link}
200+
className='text-blue-600 dark:text-blue-400 ml-1 hover:underline'
201+
target='_blank'
202+
rel='noopener noreferrer'
203+
>
204+
{contribution.type}
205+
</a>
206+
</li>
207+
))}
208+
</ul>
209+
</div>
210+
)}
211+
</div>
212+
</div>
213+
);
214+
};
215+
216+
export default AmbassadorCard;
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import React from 'react';
2+
3+
interface AmbassadorLink {
4+
title: string;
5+
icon: string;
6+
details: string;
7+
}
8+
9+
interface AmbassadorListProps {
10+
ambassadorList: {
11+
contents: AmbassadorLink[];
12+
};
13+
}
14+
15+
const AmbassadorList: React.FC<AmbassadorListProps> = ({ ambassadorList }) => {
16+
return (
17+
<ul className='mt-10 grid grid-cols-1 gap-8 px-5 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-3'>
18+
{ambassadorList.contents.map((link) => (
19+
<li
20+
key={link.title}
21+
className='flex flex-col items-center text-center p-5 bg-white dark:bg-gray-800 rounded-lg shadow-lg transform transition hover:scale-105'
22+
data-testid='Ambassadors-list'
23+
>
24+
<img
25+
src={link.icon}
26+
alt={link.title}
27+
className='w-[150px] h-auto object-contain mb-5'
28+
/>
29+
<h2 className='text-lg md:text-xl font-semibold text-gray-900 dark:text-white mb-3'>
30+
{link.title}
31+
</h2>
32+
<p className='text-sm md:text-base text-gray-700 dark:text-slate-100 leading-relaxed'>
33+
{link.details}
34+
</p>
35+
</li>
36+
))}
37+
</ul>
38+
);
39+
};
40+
41+
export default AmbassadorList;

0 commit comments

Comments
 (0)