Skip to content

Commit f008779

Browse files
abhivaikarclaude
andcommitted
Add view toggle and improve contributors attribution
1. Removed Cloudflare Turnstile from Credits section 2. Added co-author attribution in commits: - Contributors who provide GitHub username in submission form - Will now appear in GitHub contributors graph after PR merge - Uses Git's Co-authored-by trailer format 3. Added Grid/List view toggle for Companies section: - Grid view: Current card-based layout (default) - List view: Compact table-like layout with: * Company name and industry * Resource count * Resource types (first 3 shown) * Topics (first 5 shown) * Table headers on desktop - Toggle buttons with icons in header - Responsive design for both views 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 863c153 commit f008779

File tree

4 files changed

+160
-30
lines changed

4 files changed

+160
-30
lines changed

app/page.tsx

Lines changed: 65 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { useState, useMemo } from 'react';
44
import { getDatabase } from '@/lib/database';
55
import { Company } from '@/types/database';
66
import CompanyCard from '@/components/CompanyCard';
7+
import CompanyList from '@/components/CompanyList';
78
import CompanyModal from '@/components/CompanyModal';
89
import FilterBar, { FilterState } from '@/components/FilterBar';
910
import ThemeToggle from '@/components/ThemeToggle';
@@ -24,6 +25,7 @@ export default function Home() {
2425
});
2526

2627
const [selectedCompany, setSelectedCompany] = useState<Company | null>(null);
28+
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
2729

2830
// Get all unique company names for the filter
2931
const companyNames = useMemo(
@@ -131,22 +133,72 @@ export default function Home() {
131133
/>
132134
</div>
133135

134-
{/* Companies Grid */}
136+
{/* Companies Section */}
135137
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pb-8">
136-
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-6">
137-
Companies ({filteredCompanies.length})
138-
</h2>
138+
<div className="flex justify-between items-center mb-6">
139+
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">
140+
Companies ({filteredCompanies.length})
141+
</h2>
142+
143+
{/* View Toggle */}
144+
<div className="flex items-center gap-2 bg-white dark:bg-gray-800 rounded-lg p-1 shadow">
145+
<button
146+
onClick={() => setViewMode('grid')}
147+
className={`px-3 py-2 rounded-md transition-colors ${
148+
viewMode === 'grid'
149+
? 'bg-blue-600 text-white'
150+
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700'
151+
}`}
152+
aria-label="Grid view"
153+
>
154+
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
155+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z" />
156+
</svg>
157+
</button>
158+
<button
159+
onClick={() => setViewMode('list')}
160+
className={`px-3 py-2 rounded-md transition-colors ${
161+
viewMode === 'list'
162+
? 'bg-blue-600 text-white'
163+
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700'
164+
}`}
165+
aria-label="List view"
166+
>
167+
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
168+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
169+
</svg>
170+
</button>
171+
</div>
172+
</div>
139173

140174
{filteredCompanies.length > 0 ? (
141-
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
142-
{filteredCompanies.map((company) => (
143-
<CompanyCard
144-
key={company.id}
145-
company={company}
146-
onClick={() => setSelectedCompany(company)}
147-
/>
148-
))}
149-
</div>
175+
viewMode === 'grid' ? (
176+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
177+
{filteredCompanies.map((company) => (
178+
<CompanyCard
179+
key={company.id}
180+
company={company}
181+
onClick={() => setSelectedCompany(company)}
182+
/>
183+
))}
184+
</div>
185+
) : (
186+
<div className="bg-white dark:bg-gray-800 rounded-lg shadow overflow-hidden">
187+
<div className="hidden md:grid grid-cols-12 gap-4 px-6 py-3 bg-gray-50 dark:bg-gray-900 border-b border-gray-200 dark:border-gray-700 text-sm font-medium text-gray-700 dark:text-gray-300">
188+
<div className="col-span-3">Company</div>
189+
<div className="col-span-2 text-center">Resources</div>
190+
<div className="col-span-3">Types</div>
191+
<div className="col-span-4">Topics</div>
192+
</div>
193+
{filteredCompanies.map((company) => (
194+
<CompanyList
195+
key={company.id}
196+
company={company}
197+
onClick={() => setSelectedCompany(company)}
198+
/>
199+
))}
200+
</div>
201+
)
150202
) : (
151203
<div className="text-center py-12">
152204
<svg

components/CompanyList.tsx

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { Company } from '@/types/database';
2+
3+
interface CompanyListProps {
4+
company: Company;
5+
onClick: () => void;
6+
}
7+
8+
export default function CompanyList({ company, onClick }: CompanyListProps) {
9+
const totalResources = company.resources.length;
10+
const resourceTypes = Array.from(
11+
new Set(company.resources.map((r) => r.type))
12+
).slice(0, 3);
13+
14+
const allTopics = Array.from(
15+
new Set(company.resources.flatMap((r) => r.topics))
16+
).slice(0, 5);
17+
18+
return (
19+
<button
20+
onClick={onClick}
21+
className="w-full text-left bg-white dark:bg-gray-800 hover:shadow-lg transition-shadow border-b border-gray-200 dark:border-gray-700 last:border-b-0"
22+
>
23+
<div className="px-6 py-4 grid grid-cols-12 gap-4 items-center">
24+
{/* Company Name - 3 cols */}
25+
<div className="col-span-12 md:col-span-3">
26+
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
27+
{company.name}
28+
</h3>
29+
<p className="text-sm text-gray-500 dark:text-gray-400 capitalize">
30+
{company.industry.replace(/-/g, ' ')}
31+
</p>
32+
</div>
33+
34+
{/* Resources Count - 2 cols */}
35+
<div className="col-span-6 md:col-span-2 text-center">
36+
<div className="text-2xl font-bold text-blue-600 dark:text-blue-400">
37+
{totalResources}
38+
</div>
39+
<div className="text-xs text-gray-500 dark:text-gray-400">
40+
Resources
41+
</div>
42+
</div>
43+
44+
{/* Resource Types - 3 cols */}
45+
<div className="col-span-6 md:col-span-3">
46+
<div className="flex flex-wrap gap-1">
47+
{resourceTypes.map((type) => (
48+
<span
49+
key={type}
50+
className="inline-block px-2 py-1 text-xs rounded-full bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200"
51+
>
52+
{type}
53+
</span>
54+
))}
55+
{resourceTypes.length < Array.from(new Set(company.resources.map((r) => r.type))).length && (
56+
<span className="inline-block px-2 py-1 text-xs text-gray-500 dark:text-gray-400">
57+
+{Array.from(new Set(company.resources.map((r) => r.type))).length - resourceTypes.length}
58+
</span>
59+
)}
60+
</div>
61+
</div>
62+
63+
{/* Topics - 4 cols */}
64+
<div className="col-span-12 md:col-span-4">
65+
<div className="flex flex-wrap gap-1">
66+
{allTopics.map((topic) => (
67+
<span
68+
key={topic}
69+
className="inline-block px-2 py-1 text-xs rounded-full bg-purple-100 dark:bg-purple-900 text-purple-800 dark:text-purple-200"
70+
>
71+
{topic}
72+
</span>
73+
))}
74+
{allTopics.length < Array.from(new Set(company.resources.flatMap((r) => r.topics))).length && (
75+
<span className="inline-block px-2 py-1 text-xs text-gray-500 dark:text-gray-400">
76+
+{Array.from(new Set(company.resources.flatMap((r) => r.topics))).length - allTopics.length}
77+
</span>
78+
)}
79+
</div>
80+
</div>
81+
</div>
82+
</button>
83+
);
84+
}

components/Footer.tsx

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -125,20 +125,6 @@ export default function Footer() {
125125
</a>
126126
</span>
127127
</li>
128-
<li className="flex items-start">
129-
<span className="mr-2"></span>
130-
<span>
131-
Security by{' '}
132-
<a
133-
href="https://www.cloudflare.com/products/turnstile/"
134-
target="_blank"
135-
rel="noopener noreferrer"
136-
className="text-blue-600 dark:text-blue-400 hover:underline"
137-
>
138-
Cloudflare Turnstile
139-
</a>
140-
</span>
141-
</li>
142128
<li className="flex items-start">
143129
<span className="mr-2"></span>
144130
<span>

netlify/functions/submit-resource.mjs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -368,13 +368,21 @@ export const handler = async (event) => {
368368
const filePath = `data/companies/${companySlug}.json`;
369369
const fileContent = JSON.stringify(companyData, null, 2);
370370

371+
// Build commit message with co-author if GitHub username provided
372+
let commitMessage = existingData
373+
? `Add resource to ${formData.companyName}`
374+
: `Add new company: ${formData.companyName}`;
375+
376+
// Add co-author trailer if GitHub username is provided
377+
if (formData.githubUsername) {
378+
commitMessage += `\n\nCo-authored-by: ${formData.githubUsername} <${formData.githubUsername}@users.noreply.github.com>`;
379+
}
380+
371381
await octokit.repos.createOrUpdateFileContents({
372382
owner: REPO_OWNER,
373383
repo: REPO_NAME,
374384
path: filePath,
375-
message: existingData
376-
? `Add resource to ${formData.companyName}`
377-
: `Add new company: ${formData.companyName}`,
385+
message: commitMessage,
378386
content: Buffer.from(fileContent).toString('base64'),
379387
branch: branchName,
380388
...(sha && { sha }), // Include sha if updating existing file

0 commit comments

Comments
 (0)