Skip to content

Commit 30c9a1d

Browse files
committed
Improve comparison pages
1 parent 77b9652 commit 30c9a1d

18 files changed

+2440
-107
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@
147147
"@jest/globals": "^29.7.0",
148148
"@mdx-js/react": "^2.3.0",
149149
"@prisma/client": "^6.4.1",
150+
"@tailwindcss/aspect-ratio": "^0.4.2",
150151
"@tailwindcss/typography": "^0.5.10",
151152
"@testing-library/jest-dom": "^6.6.3",
152153
"@testing-library/react": "^14.3.1",

pnpm-lock.yaml

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

src/app/devtools/compare/page.jsx

Lines changed: 83 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,88 @@
1-
import { getTools } from '@/lib/getTools';
2-
import ComparePage from './ComparePage';
1+
'use client'
32

4-
export async function generateMetadata(props) {
5-
const searchParams = await props.searchParams;
6-
const allTools = getTools();
7-
const selectedToolNames = searchParams.tools
8-
? searchParams.tools.split(',').map(name => name.trim())
9-
: [];
10-
const selectedTools = selectedToolNames.map(name =>
11-
allTools.find(tool => tool.name === name)
12-
).filter(Boolean);
3+
import React from 'react'
4+
import { SimpleLayout } from '@/components/SimpleLayout'
5+
import ComparisonAccordionExample from '@/components/ComparisonAccordionExample'
6+
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
7+
import { Info, AlertTriangle } from 'lucide-react'
138

14-
const toolNames = selectedTools.length > 0
15-
? selectedTools.map(tool => tool.name).join(', ')
16-
: 'Developer Tools';
9+
export default function CompareDevToolsPage() {
10+
return (
11+
<SimpleLayout
12+
title="Developer Tools Comparison"
13+
intro="Compare features, capabilities, and use cases of popular AI-powered developer tools."
14+
>
15+
<div className="bg-amber-50 border-l-4 border-amber-500 p-4 mb-8 rounded-r-md">
16+
<div className="flex">
17+
<div className="flex-shrink-0">
18+
<AlertTriangle className="h-5 w-5 text-amber-500" />
19+
</div>
20+
<div className="ml-3">
21+
<h3 className="text-sm font-medium text-amber-800">
22+
Experimental Feature
23+
</h3>
24+
<div className="mt-2 text-sm text-amber-700">
25+
<p>
26+
This comparison page is still under development. The data displayed may not be complete or fully accurate.
27+
</p>
28+
</div>
29+
</div>
30+
</div>
31+
</div>
1732

18-
return {
19-
title: `Compare ${toolNames} - AI Developer Tools`,
20-
description: `Compare features and specifications of ${toolNames} for developers.`,
21-
openGraph: {
22-
title: `Compare ${toolNames} - Developer Tools Comparison`,
23-
description: `Compare features and specifications of ${toolNames} for developers.`,
24-
type: 'website',
25-
url: `https://zackproser.com/devtools/compare?tools=${selectedToolNames.join(',')}`,
26-
images: [
27-
{
28-
url: 'https://zackproser.com/api/og',
29-
width: 1200,
30-
height: 630,
31-
alt: 'Developer Tools Comparison',
32-
},
33-
],
34-
},
35-
twitter: {
36-
card: 'summary_large_image',
37-
title: `Compare ${toolNames} - Developer Tools Comparison`,
38-
description: `Compare features and specifications of ${toolNames} for developers. Find the best tools for your development needs.`,
39-
images: ['https://zackproser.com/api/og/'],
40-
},
41-
};
42-
}
33+
<div className="bg-blue-50 border border-blue-200 rounded-lg p-6 mb-8">
34+
<div className="flex items-start">
35+
<Info className="h-6 w-6 text-blue-500 mt-0.5 mr-3 flex-shrink-0" />
36+
<div>
37+
<h3 className="font-semibold text-blue-800 mb-2">About This Comparison</h3>
38+
<p className="text-blue-700">
39+
This page presents a side-by-side comparison of Codeium and Aider, two popular AI-powered coding tools.
40+
The comparison includes features, use cases, technical implementation details, and more.
41+
</p>
42+
</div>
43+
</div>
44+
</div>
4345

44-
export default async function Page(props) {
45-
const searchParams = await props.searchParams;
46-
const allTools = getTools();
47-
return <ComparePage searchParams={searchParams} allTools={allTools} />;
46+
<Tabs defaultValue="feature-matrix" className="mb-12">
47+
<TabsList className="grid w-full grid-cols-2">
48+
<TabsTrigger value="feature-matrix">Feature Matrix</TabsTrigger>
49+
<TabsTrigger value="extra-details">Additional Details</TabsTrigger>
50+
</TabsList>
51+
<TabsContent value="feature-matrix" className="pt-6">
52+
<h2 className="text-2xl font-bold mb-4">Feature Comparison</h2>
53+
<p className="text-gray-600 dark:text-gray-300 mb-6">
54+
Compare core features and capabilities between these developer tools to find the best fit for your workflow.
55+
</p>
56+
57+
{/* Feature Matrix will render here from the actual tools data */}
58+
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 border border-gray-200 dark:border-gray-700">
59+
<h3 className="text-xl font-semibold mb-4">Feature Matrix</h3>
60+
<p>Feature matrix component will be loaded dynamically based on selected tools.</p>
61+
</div>
62+
</TabsContent>
63+
64+
<TabsContent value="extra-details" className="pt-6">
65+
<h2 className="text-2xl font-bold mb-4">Additional Comparison Details</h2>
66+
<p className="text-gray-600 dark:text-gray-300 mb-6">
67+
Beyond core features, these additional details can help you make a more informed decision about which tool is right for your needs.
68+
</p>
69+
70+
<ComparisonAccordionExample />
71+
</TabsContent>
72+
</Tabs>
73+
74+
<div className="bg-gradient-to-r from-blue-500 to-purple-600 text-white rounded-xl p-8 shadow-xl">
75+
<h2 className="text-2xl font-bold mb-3">Need a Personalized Recommendation?</h2>
76+
<p className="mb-6 opacity-90">
77+
Book a consultation to get personalized advice on which AI development tools are best for your specific needs and workflow.
78+
</p>
79+
<a
80+
href="/contact"
81+
className="inline-block bg-white text-blue-600 font-semibold px-6 py-3 rounded-lg hover:bg-blue-50 transition-colors shadow-md"
82+
>
83+
Schedule a Consultation
84+
</a>
85+
</div>
86+
</SimpleLayout>
87+
)
4888
}
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
'use client'
2+
3+
import React, { useState } from 'react'
4+
import { ChevronDown, ChevronUp, Link as LinkIcon, Youtube, Globe, Check, ExternalLink } from 'lucide-react'
5+
import { Badge } from '@/components/ui/badge'
6+
import { Button } from '@/components/ui/button'
7+
import Link from 'next/link'
8+
import YoutubeEmbed from './YoutubeEmbed'
9+
10+
const ComparisonAccordion = ({ sections, tool1, tool2 }) => {
11+
const [expandedSections, setExpandedSections] = useState([sections[0]?.id])
12+
13+
const toggleSection = (sectionId) => {
14+
if (expandedSections.includes(sectionId)) {
15+
setExpandedSections(expandedSections.filter(id => id !== sectionId))
16+
} else {
17+
setExpandedSections([...expandedSections, sectionId])
18+
}
19+
}
20+
21+
const renderValue = (value, type) => {
22+
if (!value) return <span className="text-gray-400"></span>
23+
24+
switch (type) {
25+
case 'link':
26+
return (
27+
<Link
28+
href={value}
29+
className="inline-flex items-center gap-1 text-blue-600 hover:text-blue-800 transition-colors rounded-md border border-blue-200 px-3 py-1.5 text-sm font-medium"
30+
target="_blank"
31+
rel="noopener noreferrer"
32+
>
33+
<LinkIcon size={14} />
34+
{formatLink(value)}
35+
<ExternalLink size={12} className="ml-1 opacity-70" />
36+
</Link>
37+
)
38+
39+
case 'video':
40+
return <YoutubeEmbed urls={value} />
41+
42+
case 'languages':
43+
if (Array.isArray(value)) {
44+
return (
45+
<div className="flex flex-wrap gap-2">
46+
{value.map((lang, i) => (
47+
<Badge key={i} variant="outline" className="px-2 py-1">
48+
{lang}
49+
</Badge>
50+
))}
51+
</div>
52+
)
53+
}
54+
return <Badge variant="outline" className="px-2 py-1">{value}</Badge>
55+
56+
case 'localization':
57+
return (
58+
<Badge variant="outline" className="px-2 py-1 bg-blue-50 dark:bg-blue-900/20 border-blue-200 dark:border-blue-800 text-blue-800 dark:text-blue-200">
59+
<Globe className="mr-1 inline-block" size={14} />
60+
{value}
61+
</Badge>
62+
)
63+
64+
case 'count':
65+
return (
66+
<span className="text-lg font-semibold">{value || 0}</span>
67+
)
68+
69+
default:
70+
if (typeof value === 'boolean') {
71+
return value ? <Check className="text-green-600" size={20} /> : <span className="text-gray-400"></span>
72+
}
73+
return value
74+
}
75+
}
76+
77+
const formatLink = (url) => {
78+
try {
79+
const urlObj = new URL(url)
80+
const path = urlObj.pathname
81+
82+
// Format case study links
83+
if (path.includes('case-studies')) {
84+
return path.split('/').pop()
85+
}
86+
87+
// Format blog links
88+
if (urlObj.hostname.includes('blog')) {
89+
return 'Blog'
90+
}
91+
92+
return urlObj.hostname.replace('www.', '')
93+
} catch (e) {
94+
return url
95+
}
96+
}
97+
98+
return (
99+
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-md overflow-hidden">
100+
{sections.map((section) => {
101+
const isExpanded = expandedSections.includes(section.id)
102+
103+
return (
104+
<div key={section.id} className="border-b last:border-b-0 dark:border-gray-700">
105+
<div
106+
className="flex items-center justify-between p-4 cursor-pointer bg-gradient-to-r from-gray-50 to-white dark:from-gray-800 dark:to-gray-900"
107+
onClick={() => toggleSection(section.id)}
108+
>
109+
<h3 className="text-xl font-semibold text-blue-600 dark:text-blue-400 flex items-center">
110+
{section.icon && <section.icon className="mr-2" size={20} />}
111+
{section.title}
112+
</h3>
113+
<Button variant="ghost" size="sm" className="text-gray-500">
114+
{isExpanded ? <ChevronUp size={18} /> : <ChevronDown size={18} />}
115+
</Button>
116+
</div>
117+
118+
{isExpanded && (
119+
<div className="overflow-x-auto">
120+
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
121+
<thead className="bg-gray-50 dark:bg-gray-800">
122+
<tr>
123+
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider w-1/3">
124+
Feature
125+
</th>
126+
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider w-1/3">
127+
{tool1?.name || 'Platform 1'}
128+
</th>
129+
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider w-1/3">
130+
{tool2?.name || 'Platform 2'}
131+
</th>
132+
</tr>
133+
</thead>
134+
<tbody className="bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-700">
135+
{section.features.map((feature) => (
136+
<tr key={feature.id} className="hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors">
137+
<td className="px-4 py-4 text-sm font-medium">
138+
{feature.name}
139+
</td>
140+
<td className="px-4 py-4 text-sm">
141+
{renderValue(feature.value1, feature.type)}
142+
</td>
143+
<td className="px-4 py-4 text-sm">
144+
{renderValue(feature.value2, feature.type)}
145+
</td>
146+
</tr>
147+
))}
148+
</tbody>
149+
</table>
150+
</div>
151+
)}
152+
</div>
153+
)
154+
})}
155+
</div>
156+
)
157+
}
158+
159+
export default ComparisonAccordion

0 commit comments

Comments
 (0)