Skip to content

Commit ab74f91

Browse files
committed
feat: refactor organization profile components and loading states
- Removed the old organization profile page and replaced it with a new structure that includes a loading state and a dedicated client component for organization profiles. - Introduced a loading skeleton for better user experience while fetching organization data. - Enhanced metadata generation for organization pages to include Open Graph and Twitter card support. - Added a new API route for generating Open Graph images based on organization data. - Updated organization analytics and links management to improve data handling and user interactions.
1 parent 9341a8f commit ab74f91

11 files changed

Lines changed: 567 additions & 94 deletions

File tree

app/(landing)/org/[id]/page.tsx

Lines changed: 0 additions & 42 deletions
This file was deleted.
File renamed without changes.

app/(landing)/org/[slug]/page.tsx

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { Metadata } from 'next';
2+
import { getOrganizationProfile } from '@/lib/api/organization';
3+
import OrgProfileClient from './org-profile-client';
4+
5+
const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL || 'https://boundlessfi.xyz';
6+
7+
interface OrgProfilePageProps {
8+
params: Promise<{ slug: string }>;
9+
}
10+
11+
export async function generateMetadata({
12+
params,
13+
}: OrgProfilePageProps): Promise<Metadata> {
14+
try {
15+
const { slug } = await params;
16+
const org = await getOrganizationProfile(slug);
17+
18+
const title = `${org.name} | Boundless`;
19+
const description = org.description || `View ${org.name} on Boundless`;
20+
const ogImageUrl = `${SITE_URL}/api/og?slug=${encodeURIComponent(slug)}`;
21+
22+
return {
23+
title,
24+
description,
25+
openGraph: {
26+
title,
27+
description,
28+
url: `${SITE_URL}/org/${slug}`,
29+
siteName: 'Boundless',
30+
images: [
31+
{
32+
url: ogImageUrl,
33+
width: 1200,
34+
height: 630,
35+
alt: org.name,
36+
type: 'image/png',
37+
},
38+
],
39+
},
40+
twitter: {
41+
card: 'summary_large_image',
42+
title,
43+
description,
44+
images: [ogImageUrl],
45+
},
46+
};
47+
} catch {
48+
return {
49+
title: 'Organization | Boundless',
50+
description: 'View organization profile on Boundless.',
51+
openGraph: {
52+
images: [`${SITE_URL}/api/og`],
53+
},
54+
};
55+
}
56+
}
57+
58+
export default async function OrgProfilePage({ params }: OrgProfilePageProps) {
59+
const { slug } = await params;
60+
61+
return <OrgProfileClient slug={slug} />;
62+
}

app/api/og/route.tsx

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
import { ImageResponse } from 'next/og';
2+
3+
export const runtime = 'edge';
4+
5+
const API_BASE =
6+
process.env.NEXT_PUBLIC_API_URL || 'https://staging-api.boundlessfi.xyz';
7+
const API_URL = API_BASE.replace(/\/$/, '').replace(/\/api$/i, '') + '/api';
8+
9+
interface OrganizationProfile {
10+
id: string;
11+
name: string;
12+
logoUrl: string;
13+
description: string;
14+
stats: {
15+
projectsCount: number;
16+
totalHackathons: number;
17+
totalBounties: number;
18+
totalGrants: number;
19+
};
20+
}
21+
22+
function truncate(text: string, maxLen: number): string {
23+
if (!text || text.length <= maxLen) return text || '';
24+
return text.slice(0, maxLen - 3).trim() + '...';
25+
}
26+
27+
export async function GET(request: Request) {
28+
const { searchParams } = new URL(request.url);
29+
const slug = searchParams.get('slug');
30+
31+
if (!slug) {
32+
return new ImageResponse(
33+
<div
34+
style={{
35+
width: '100%',
36+
height: '100%',
37+
display: 'flex',
38+
flexDirection: 'column',
39+
alignItems: 'center',
40+
justifyContent: 'center',
41+
background: '#09090b',
42+
color: '#a1a1aa',
43+
fontSize: 24,
44+
}}
45+
>
46+
<span>Organization | Boundless</span>
47+
</div>,
48+
{ width: 1200, height: 630 }
49+
);
50+
}
51+
52+
let name = 'Organization';
53+
let description = 'View on Boundless';
54+
let logoUrl: string | null = null;
55+
56+
try {
57+
const res = await fetch(
58+
`${API_URL}/organizations/profile/${encodeURIComponent(slug)}`
59+
);
60+
if (res.ok) {
61+
const json = await res.json();
62+
const data = json?.data as OrganizationProfile | undefined;
63+
if (data) {
64+
name = data.name || name;
65+
description = truncate(data.description || description, 140);
66+
logoUrl = data.logoUrl || null;
67+
}
68+
}
69+
} catch {
70+
// use defaults
71+
}
72+
73+
return new ImageResponse(
74+
<div
75+
style={{
76+
width: '100%',
77+
height: '100%',
78+
display: 'flex',
79+
flexDirection: 'column',
80+
background: '#09090b',
81+
padding: 60,
82+
fontFamily: 'system-ui, sans-serif',
83+
}}
84+
>
85+
<div
86+
style={{
87+
position: 'absolute',
88+
top: 0,
89+
left: 0,
90+
right: 0,
91+
height: 4,
92+
background: 'linear-gradient(90deg, #a7f950 0%, #a7f95080 100%)',
93+
}}
94+
/>
95+
<div
96+
style={{
97+
display: 'flex',
98+
flexDirection: 'row',
99+
alignItems: 'center',
100+
gap: 40,
101+
flex: 1,
102+
}}
103+
>
104+
{logoUrl ? (
105+
<img
106+
src={logoUrl}
107+
alt=''
108+
width={160}
109+
height={160}
110+
style={{
111+
borderRadius: 20,
112+
objectFit: 'cover',
113+
border: '2px solid #27272a',
114+
}}
115+
/>
116+
) : (
117+
<div
118+
style={{
119+
width: 160,
120+
height: 160,
121+
borderRadius: 20,
122+
background: '#27272a',
123+
display: 'flex',
124+
alignItems: 'center',
125+
justifyContent: 'center',
126+
color: '#71717a',
127+
fontSize: 48,
128+
}}
129+
>
130+
{name.charAt(0).toUpperCase()}
131+
</div>
132+
)}
133+
<div
134+
style={{
135+
display: 'flex',
136+
flexDirection: 'column',
137+
gap: 16,
138+
flex: 1,
139+
}}
140+
>
141+
<h1
142+
style={{
143+
margin: 0,
144+
fontSize: 56,
145+
fontWeight: 700,
146+
color: '#fff',
147+
lineHeight: 1.2,
148+
}}
149+
>
150+
{name}
151+
</h1>
152+
<p
153+
style={{
154+
margin: 0,
155+
fontSize: 28,
156+
color: '#a1a1aa',
157+
lineHeight: 1.4,
158+
}}
159+
>
160+
{description}
161+
</p>
162+
<div
163+
style={{
164+
display: 'flex',
165+
alignItems: 'center',
166+
gap: 8,
167+
marginTop: 8,
168+
}}
169+
>
170+
<span
171+
style={{
172+
fontSize: 22,
173+
color: '#a7f950',
174+
fontWeight: 600,
175+
}}
176+
>
177+
Boundless
178+
</span>
179+
</div>
180+
</div>
181+
</div>
182+
</div>,
183+
{
184+
width: 1200,
185+
height: 630,
186+
}
187+
);
188+
}

components/organization/OrganizationAnalytics.tsx

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client';
22

3-
import React, { useMemo } from 'react';
3+
import React, { useMemo, useState, useEffect } from 'react';
44
import Link from 'next/link';
55
import {
66
Users,
@@ -13,6 +13,7 @@ import {
1313
TrendingDown,
1414
AlertCircle,
1515
} from 'lucide-react';
16+
import { ShareLinkPopover } from '@/components/ui/share-link-popover';
1617
import {
1718
ChartConfig,
1819
ChartContainer,
@@ -37,6 +38,14 @@ const OrganizationAnalytics = () => {
3738
const { analytics, isLoading: isLoadingAnalytics } =
3839
useOrganizationAnalytics();
3940

41+
const [profileUrl, setProfileUrl] = useState('');
42+
43+
useEffect(() => {
44+
if (activeOrg?.slug && typeof window !== 'undefined') {
45+
setProfileUrl(`${window.location.origin}/org/${activeOrg.slug}`);
46+
}
47+
}, [activeOrg?.slug]);
48+
4049
const chartData = useMemo(() => {
4150
if (!analytics?.timeSeries?.hackathons) return [];
4251
return analytics.timeSeries.hackathons.map(item => ({
@@ -121,11 +130,21 @@ const OrganizationAnalytics = () => {
121130
<div className='min-h-screen bg-black'>
122131
<div className='p-10'>
123132
{/* Header */}
124-
<div className='mb-8'>
125-
<h1 className='mb-1 text-2xl font-medium text-white'>Analytics</h1>
126-
<p className='text-sm text-zinc-500'>
127-
Track your organization's performance
128-
</p>
133+
<div className='mb-8 flex flex-wrap items-start justify-between gap-4'>
134+
<div>
135+
<h1 className='mb-1 text-2xl font-medium text-white'>Analytics</h1>
136+
<p className='text-sm text-zinc-500'>
137+
Track your organization's performance
138+
</p>
139+
</div>
140+
{activeOrg?.slug && profileUrl && (
141+
<ShareLinkPopover
142+
url={profileUrl}
143+
title={`${activeOrg.name} on Boundless`}
144+
align='end'
145+
side='bottom'
146+
/>
147+
)}
129148
</div>
130149

131150
{/* Stats Grid */}

0 commit comments

Comments
 (0)