Skip to content

Commit dba39d8

Browse files
committed
feat: implement plan, model, and tool browsing pages with card components
- Add ModelCard, PlanCard, PlanDetail, and ToolCard components - Create dynamic routes for individual model/plan/tool detail pages - Build index pagesfor models, plans, and tools with collections - Update homepage with hero section and structured data - Add content config schema updates for new fields
1 parent 5c97ab9 commit dba39d8

12 files changed

Lines changed: 1359 additions & 13 deletions

File tree

src/components/ModelCard.astro

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
---
2+
interface Props {
3+
name: string;
4+
slug: string;
5+
provider: string;
6+
vibe_coding_score: number;
7+
context_window: number;
8+
description: string;
9+
}
10+
11+
const { name, slug, provider, vibe_coding_score, context_window, description } = Astro.props;
12+
13+
const formatContextWindow = (tokens: number): string => {
14+
if (tokens >= 1000000) return `${(tokens / 1000000).toFixed(0)}M`;
15+
if (tokens >= 1000) return `${(tokens / 1000).toFixed(0)}K`;
16+
return tokens.toString();
17+
};
18+
19+
const scoreColor = vibe_coding_score >= 8.5 ? 'text-success' : vibe_coding_score >= 7.0 ? 'text-warning' : 'text-error';
20+
const scoreRingColor = vibe_coding_score >= 8.5 ? 'stroke-success' : vibe_coding_score >= 7.0 ? 'stroke-warning' : 'stroke-error';
21+
---
22+
23+
<a href={`/models/${slug}/`} class="model-card card bg-base-100 border border-base-300 shadow-sm hover:shadow-xl transition-all duration-300 hover:-translate-y-1 group" id={`model-card-${slug}`}>
24+
<div class="card-body p-5">
25+
<!-- Header -->
26+
<div class="flex items-start justify-between gap-3">
27+
<div class="flex-1 min-w-0">
28+
<h3 class="card-title text-base font-bold group-hover:text-primary transition-colors duration-200 leading-tight">{name}</h3>
29+
<p class="text-xs text-base-content/50 mt-0.5">{provider}</p>
30+
</div>
31+
<!-- Vibe Score Circle -->
32+
<div class="relative flex items-center justify-center shrink-0" title={`Vibe coding score: ${vibe_coding_score}/10`}>
33+
<svg class="w-12 h-12 -rotate-90" viewBox="0 0 36 36">
34+
<path class="stroke-base-200" stroke-width="3" fill="none" d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831" />
35+
<path class={scoreRingColor} stroke-width="3" fill="none" stroke-linecap="round" stroke-dasharray={`${(vibe_coding_score / 10) * 100}, 100`} d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831" />
36+
</svg>
37+
<span class={`absolute text-xs font-bold ${scoreColor}`}>{vibe_coding_score}</span>
38+
</div>
39+
</div>
40+
41+
<!-- Description -->
42+
<p class="text-sm text-base-content/60 mt-2 line-clamp-2 leading-relaxed">{description}</p>
43+
44+
<!-- Context Window -->
45+
<div class="flex items-center justify-between mt-3 pt-3 border-t border-base-200">
46+
<div class="flex items-center gap-1.5">
47+
<svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5 text-base-content/40" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
48+
<path stroke-linecap="round" stroke-linejoin="round" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4" />
49+
</svg>
50+
<span class="text-xs text-base-content/60">{formatContextWindow(context_window)} tokens</span>
51+
</div>
52+
<span class="text-xs font-medium text-primary group-hover:translate-x-1 transition-transform duration-200 flex items-center gap-1">
53+
View details
54+
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5">
55+
<path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7" />
56+
</svg>
57+
</span>
58+
</div>
59+
</div>
60+
</a>

src/components/PlanCard.astro

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
---
2+
interface Props {
3+
name: string;
4+
slug: string;
5+
provider: string;
6+
badge: 'FREE' | 'PROMO' | 'PAID';
7+
price_monthly: number;
8+
promotional_price?: number | null;
9+
models: string[];
10+
community_score?: number;
11+
description: string;
12+
}
13+
14+
const { name, slug, provider, badge, price_monthly, promotional_price, models, community_score, description } = Astro.props;
15+
16+
const badgeColor = {
17+
FREE: 'badge-success',
18+
PROMO: 'badge-warning',
19+
PAID: 'badge-info',
20+
}[badge];
21+
22+
const priceDisplay = price_monthly === 0 ? 'Free' : `$${price_monthly}/mo`;
23+
const hasPromo = promotional_price !== null && promotional_price !== undefined;
24+
---
25+
26+
<a href={`/plans/${slug}/`} class="plan-card card bg-base-100 border border-base-300 shadow-sm hover:shadow-xl transition-all duration-300 hover:-translate-y-1 group" id={`plan-card-${slug}`}>
27+
<div class="card-body p-5">
28+
<!-- Header -->
29+
<div class="flex items-start justify-between gap-2 mb-1">
30+
<div class="flex-1 min-w-0">
31+
<h3 class="card-title text-base font-bold group-hover:text-primary transition-colors duration-200 leading-tight">{name}</h3>
32+
<p class="text-xs text-base-content/50 mt-0.5">{provider}</p>
33+
</div>
34+
<span class={`badge ${badgeColor} badge-sm font-semibold shrink-0`}>{badge}</span>
35+
</div>
36+
37+
<!-- Price -->
38+
<div class="mt-2">
39+
{hasPromo ? (
40+
<div class="flex items-baseline gap-2">
41+
<span class="text-2xl font-extrabold text-primary">${promotional_price}/mo</span>
42+
<span class="text-sm text-base-content/40 line-through">${price_monthly}/mo</span>
43+
</div>
44+
) : (
45+
<span class={`text-2xl font-extrabold ${price_monthly === 0 ? 'text-success' : 'text-base-content'}`}>{priceDisplay}</span>
46+
)}
47+
</div>
48+
49+
<!-- Description -->
50+
<p class="text-sm text-base-content/60 mt-2 line-clamp-2 leading-relaxed">{description}</p>
51+
52+
<!-- Models -->
53+
{models.length > 0 && (
54+
<div class="mt-3">
55+
<div class="flex flex-wrap gap-1">
56+
{models.slice(0, 3).map((model) => (
57+
<span class="badge badge-ghost badge-xs text-[10px] font-medium">{model}</span>
58+
))}
59+
{models.length > 3 && (
60+
<span class="badge badge-ghost badge-xs text-[10px]">+{models.length - 3} more</span>
61+
)}
62+
</div>
63+
</div>
64+
)}
65+
66+
<!-- Footer -->
67+
<div class="flex items-center justify-between mt-3 pt-3 border-t border-base-200">
68+
{community_score !== undefined ? (
69+
<div class="flex items-center gap-1">
70+
<svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5 text-warning" viewBox="0 0 20 20" fill="currentColor">
71+
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
72+
</svg>
73+
<span class="text-xs font-semibold text-base-content/70">{community_score}</span>
74+
</div>
75+
) : (
76+
<div></div>
77+
)}
78+
<span class="text-xs font-medium text-primary group-hover:translate-x-1 transition-transform duration-200 flex items-center gap-1">
79+
View details
80+
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5">
81+
<path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7" />
82+
</svg>
83+
</span>
84+
</div>
85+
</div>
86+
</a>

0 commit comments

Comments
 (0)