Skip to content

Commit f39e9a9

Browse files
committed
Add members logos
1 parent 9c95072 commit f39e9a9

File tree

6 files changed

+246
-22
lines changed

6 files changed

+246
-22
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,7 @@ pnpm-debug.log*
2222

2323
# jetbrains setting folder
2424
.idea/
25+
26+
27+
public/assets/logos/
28+
src/data/

package-lock.json

Lines changed: 24 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
"type": "module",
44
"version": "0.0.1",
55
"scripts": {
6-
"dev": "astro dev",
7-
"build": "astro build",
6+
"dev": "node src/lib/fetch-logos.mjs && astro dev",
7+
"build": "node src/lib/fetch-logos.mjs && astro build",
88
"preview": "astro preview",
99
"astro": "astro"
1010
},
@@ -13,6 +13,7 @@
1313
"@astrojs/react": "^4.3.1",
1414
"@hookform/resolvers": "^5.2.1",
1515
"@lottiefiles/dotlottie-web": "^0.52.2",
16+
"@notionhq/client": "^5.1.0",
1617
"@radix-ui/react-accordion": "^1.2.12",
1718
"@radix-ui/react-alert-dialog": "^1.1.15",
1819
"@radix-ui/react-aspect-ratio": "^1.1.7",
@@ -70,6 +71,7 @@
7071
},
7172
"devDependencies": {
7273
"@astrojs/sitemap": "^3.6.0",
74+
"dotenv": "^17.2.3",
7375
"prettier-plugin-astro": "^0.14.1",
7476
"tw-animate-css": "^1.3.8"
7577
}

src/components/logo.astro

Lines changed: 49 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,16 @@
1-
<section class="pb-12 md:pb-16 lg:pb-20">
1+
---
2+
interface Logo {
3+
name: string;
4+
logo: string;
5+
website: string;
6+
}
7+
8+
const logos: Logo[] = (await import("../data/logos.json")).default;
9+
10+
const marqueeDuration = logos.length * 3;
11+
---
12+
13+
<section class="pb-12 md:pb-16 lg:pb-20" style={`--marquee-duration: ${marqueeDuration}s`}>
214
<div class="relative container overflow-hidden">
315
<h1 class="mb-6 text-center text-base font-medium md:mb-8">
416
Ratified by our member organizations
@@ -12,25 +24,43 @@
1224
class="absolute top-0 right-0 bottom-0 z-10 w-20 bg-gradient-to-l from-accent-lightest-2 to-transparent"
1325
>
1426
</div>
15-
<div class="flex shrink-0 animate-marquee items-center">
16-
<img class="mx-6 max-h-12 shrink-0 md:max-h-14" src="/assets/logo1.svg" />
17-
<img class="mx-6 max-h-12 shrink-0 md:max-h-14" src="/assets/logo2.svg" />
18-
<img class="mx-6 max-h-12 shrink-0 md:max-h-14" src="/assets/logo3.svg" />
19-
<img class="mx-6 max-h-12 shrink-0 md:max-h-14" src="/assets/logo4.svg" />
20-
<img class="mx-6 max-h-12 shrink-0 md:max-h-14" src="/assets/logo5.svg" />
21-
<img class="mx-6 max-h-12 shrink-0 md:max-h-14" src="/assets/logo6.svg" />
22-
<img class="mx-6 max-h-12 shrink-0 md:max-h-14" src="/assets/logo7.svg" />
23-
<img class="mx-6 max-h-12 shrink-0 md:max-h-14" src="/assets/logo8.svg" />
27+
<div class="flex shrink-0 animate-marquee items-center gap-12">
28+
{
29+
logos.map((logo) => (
30+
<a
31+
href={`//${logo.website.replace(/^https?:\/\//, '').replace(/^www\./, '')}`}
32+
target="_blank"
33+
rel="noopener noreferrer"
34+
class="mx-6 flex h-12 w-32 shrink-0 items-center justify-center md:h-14 md:w-40"
35+
>
36+
<img
37+
class="max-h-full max-w-full object-contain"
38+
src={logo.logo}
39+
alt={logo.name}
40+
loading="lazy"
41+
/>
42+
</a>
43+
))
44+
}
2445
</div>
25-
<div class="flex shrink-0 animate-marquee items-center">
26-
<img class="mx-6 max-h-12 shrink-0 md:max-h-14" src="/assets/logo1.svg" />
27-
<img class="mx-6 max-h-12 shrink-0 md:max-h-14" src="/assets/logo2.svg" />
28-
<img class="mx-6 max-h-12 shrink-0 md:max-h-14" src="/assets/logo3.svg" />
29-
<img class="mx-6 max-h-12 shrink-0 md:max-h-14" src="/assets/logo4.svg" />
30-
<img class="mx-6 max-h-12 shrink-0 md:max-h-14" src="/assets/logo5.svg" />
31-
<img class="mx-6 max-h-12 shrink-0 md:max-h-14" src="/assets/logo6.svg" />
32-
<img class="mx-6 max-h-12 shrink-0 md:max-h-14" src="/assets/logo7.svg" />
33-
<img class="mx-6 max-h-12 shrink-0 md:max-h-14" src="/assets/logo8.svg" />
46+
<div class="flex shrink-0 animate-marquee items-center gap-12">
47+
{
48+
logos.map((logo) => (
49+
<a
50+
href={`//${logo.website.replace(/^https?:\/\//, '').replace(/^www\./, '')}`}
51+
target="_blank"
52+
rel="noopener noreferrer"
53+
class="mx-6 flex h-12 w-32 shrink-0 items-center justify-center md:h-14 md:w-40"
54+
>
55+
<img
56+
class="max-h-full max-w-full object-contain"
57+
src={logo.logo}
58+
alt={logo.name}
59+
loading="lazy"
60+
/>
61+
</a>
62+
))
63+
}
3464
</div>
3565
</div>
3666
</div>

src/lib/fetch-logos.mjs

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import { Client } from "@notionhq/client";
2+
import fs from "fs";
3+
import path from "path";
4+
import { fileURLToPath } from "url";
5+
import { config } from "dotenv";
6+
7+
config();
8+
9+
const __filename = fileURLToPath(import.meta.url);
10+
const __dirname = path.dirname(__filename);
11+
12+
const logosDir = path.join(process.cwd(), "public", "assets", "logos");
13+
const dataDir = path.join(process.cwd(), "src", "data");
14+
15+
if (!fs.existsSync(logosDir)) {
16+
fs.mkdirSync(logosDir, { recursive: true });
17+
}
18+
19+
if (!fs.existsSync(dataDir)) {
20+
fs.mkdirSync(dataDir, { recursive: true });
21+
}
22+
23+
async function downloadLogo(url, filename) {
24+
try {
25+
const response = await fetch(url);
26+
if (!response.ok) throw new Error(`Failed to fetch: ${response.statusText}`);
27+
28+
const arrayBuffer = await response.arrayBuffer();
29+
const buffer = Buffer.from(arrayBuffer);
30+
31+
const ext = path.extname(new URL(url).pathname) || '.png';
32+
const fullFilename = `${filename}${ext}`;
33+
const filepath = path.join(logosDir, fullFilename);
34+
35+
fs.writeFileSync(filepath, buffer);
36+
return `/assets/logos/${fullFilename}`;
37+
} catch (error) {
38+
console.error(`[Logo] Failed to download ${filename}:`, error);
39+
return url;
40+
}
41+
}
42+
43+
async function fetchLogos() {
44+
const databaseId = process.env.NOTION_MEMBERS_DATABASE_ID;
45+
const apiKey = process.env.NOTION_TOKEN;
46+
let logos = [];
47+
48+
if (!databaseId || !apiKey) {
49+
console.warn(
50+
"[Notion] Missing NOTION_MEMBERS_DATABASE_ID or NOTION_TOKEN env var"
51+
);
52+
53+
fs.writeFileSync(
54+
path.join(dataDir, "logos.json"),
55+
JSON.stringify(logos, null, 2)
56+
);
57+
return;
58+
}
59+
60+
const notion = new Client({
61+
auth: apiKey,
62+
});
63+
64+
try {
65+
const dataSteering = await notion.dataSources.query({
66+
data_source_id: databaseId,
67+
filter: {
68+
and: [
69+
{
70+
property: "Status",
71+
select: {
72+
equals: "Active",
73+
},
74+
},
75+
{
76+
property: "Membership Level ",
77+
select: {
78+
equals: "Steering",
79+
},
80+
},
81+
],
82+
},
83+
sorts: [
84+
{
85+
property: "Member Name ",
86+
direction: "ascending",
87+
},
88+
],
89+
});
90+
91+
const steeringLogos = await Promise.all(
92+
dataSteering.results.map(async (result) => {
93+
const name = result.properties["Member Name "]?.title[0]?.plain_text || "";
94+
const logoUrl = result.properties.Logo.files[0]?.file?.url || "";
95+
const filename = name.toLowerCase().replace(/\s+/g, '-');
96+
97+
const localLogoPath = logoUrl ? await downloadLogo(logoUrl, filename) : "";
98+
99+
return {
100+
name,
101+
logo: localLogoPath,
102+
website: result.properties.Website?.url || "",
103+
};
104+
})
105+
);
106+
107+
logos = [...steeringLogos];
108+
109+
const dataGeneral = await notion.dataSources.query({
110+
data_source_id: databaseId,
111+
filter: {
112+
and: [
113+
{
114+
property: "Status",
115+
select: {
116+
equals: "Active",
117+
},
118+
},
119+
{
120+
property: "Membership Level ",
121+
select: {
122+
equals: "General",
123+
},
124+
},
125+
],
126+
},
127+
sorts: [
128+
{
129+
property: "Member Name ",
130+
direction: "ascending",
131+
},
132+
],
133+
});
134+
135+
const generalLogos = await Promise.all(
136+
dataGeneral.results.map(async (result) => {
137+
const name = result.properties["Member Name "]?.title[0]?.plain_text || "";
138+
const logoUrl = result.properties.Logo.files[0]?.file?.url || "";
139+
const filename = name.toLowerCase().replace(/\s+/g, '-');
140+
141+
const localLogoPath = logoUrl ? await downloadLogo(logoUrl, filename) : "";
142+
143+
return {
144+
name,
145+
logo: localLogoPath,
146+
website: result.properties.Website?.url || "",
147+
};
148+
})
149+
);
150+
151+
logos = [...logos, ...generalLogos];
152+
153+
console.log(`[Notion] Successfully fetched ${logos.length} logos`);
154+
} catch (error) {
155+
console.error("[Notion] Query failed", error);
156+
}
157+
158+
fs.writeFileSync(
159+
path.join(dataDir, "logos.json"),
160+
JSON.stringify(logos, null, 2)
161+
);
162+
}
163+
164+
fetchLogos();

src/styles/global.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
}
4949
}
5050

51-
--animate-marquee: marquee 20s linear infinite;
51+
--animate-marquee: marquee var(--marquee-duration, 60s) linear infinite;
5252
@keyframes marquee {
5353
from {
5454
transform: translateX(0);

0 commit comments

Comments
 (0)