Skip to content

Commit f367058

Browse files
Merge pull request #1 from devinschumacher/copilot/fix-e3ac37ca-40c7-4ea8-ba87-9c403bf6fc6a
Add browser extensions directory app
2 parents 41f7425 + 009289d commit f367058

File tree

14 files changed

+752
-0
lines changed

14 files changed

+752
-0
lines changed

apps/extensions/app/favicon.ico

25.3 KB
Binary file not shown.

apps/extensions/app/layout.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { AppLayout } from "@serp-tools/app-core/components/app-layout";
2+
import type { Metadata } from "next";
3+
4+
export const metadata: Metadata = {
5+
title: "Serp Tools",
6+
description: "Description of Serp Tools",
7+
};
8+
9+
export default function RootLayout({
10+
children,
11+
}: Readonly<{
12+
children: React.ReactNode;
13+
}>) {
14+
return <AppLayout>{children}</AppLayout>;
15+
}

apps/extensions/app/page.tsx

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
"use client";
2+
3+
import { useState, useEffect } from "react";
4+
import { Button } from "@serp-tools/ui/components/button";
5+
import { Badge } from "@serp-tools/ui/components/badge";
6+
import { ExtensionCard } from "@/components/ExtensionCard";
7+
import { ExtensionsSearchBar } from "@/components/ExtensionsSearchBar";
8+
import {
9+
Sparkles,
10+
ArrowRight,
11+
Shield,
12+
Lock,
13+
Eye,
14+
ShoppingCart,
15+
Coins,
16+
Moon,
17+
CheckSquare,
18+
Bookmark,
19+
Palette,
20+
Code,
21+
Video,
22+
Clipboard,
23+
Gauge,
24+
Globe,
25+
Puzzle
26+
} from "lucide-react";
27+
import extensionsData from '@serp-tools/app-core/data/extensions.json';
28+
29+
// Icon mapping for extensions
30+
const iconMap: { [key: string]: any } = {
31+
'ublock-origin': Shield,
32+
'lastpass': Lock,
33+
'grammarly': CheckSquare,
34+
'honey': ShoppingCart,
35+
'metamask': Coins,
36+
'dark-reader': Moon,
37+
'todoist': CheckSquare,
38+
'pocket': Bookmark,
39+
'colorpick-eyedropper': Palette,
40+
'wappalyzer': Code,
41+
'json-formatter': Code,
42+
'ghostery': Shield,
43+
'loom': Video,
44+
'notion-web-clipper': Clipboard,
45+
'momentum': Gauge,
46+
};
47+
48+
// Process extensions from JSON data
49+
const processedExtensions = extensionsData
50+
.filter((extension: any) => extension.isActive)
51+
.map((extension: any) => ({
52+
id: extension.id,
53+
name: extension.name,
54+
description: extension.description,
55+
category: extension.category || 'other',
56+
icon: iconMap[extension.id] || Puzzle,
57+
href: extension.chromeStoreUrl || extension.firefoxAddonUrl || extension.url,
58+
tags: extension.tags || [],
59+
isNew: extension.isNew || false,
60+
isPopular: extension.isPopular || false,
61+
rating: extension.rating,
62+
users: extension.users,
63+
}));
64+
65+
66+
export default function HomePage() {
67+
const [selectedCategory, setSelectedCategory] = useState("all");
68+
const [searchQuery, setSearchQuery] = useState("");
69+
const [extensions, setExtensions] = useState(processedExtensions);
70+
const [categories, setCategories] = useState<any[]>([]);
71+
72+
useEffect(() => {
73+
// Create categories from extensions
74+
const categoryMap = new Map();
75+
categoryMap.set('all', { id: 'all', name: 'All Extensions', count: extensions.length });
76+
77+
extensions.forEach(extension => {
78+
if (!categoryMap.has(extension.category)) {
79+
// Use proper category names
80+
let catName = extension.category.charAt(0).toUpperCase() + extension.category.slice(1);
81+
if (extension.category === 'privacy') catName = 'Privacy & Security';
82+
else if (extension.category === 'productivity') catName = 'Productivity';
83+
else if (extension.category === 'developer') catName = 'Developer Tools';
84+
else if (extension.category === 'shopping') catName = 'Shopping';
85+
else if (extension.category === 'crypto') catName = 'Crypto & Web3';
86+
else if (extension.category === 'accessibility') catName = 'Accessibility';
87+
else if (extension.category === 'other') catName = 'Other';
88+
89+
categoryMap.set(extension.category, {
90+
id: extension.category,
91+
name: catName,
92+
count: 0
93+
});
94+
}
95+
categoryMap.get(extension.category).count++;
96+
});
97+
98+
setCategories(Array.from(categoryMap.values()));
99+
}, [extensions]);
100+
101+
// Filter extensions based on category and search
102+
const filteredExtensions = extensions.filter(extension => {
103+
const matchesCategory = selectedCategory === "all" || extension.category === selectedCategory;
104+
const matchesSearch = extension.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
105+
extension.description.toLowerCase().includes(searchQuery.toLowerCase()) ||
106+
extension.tags.some((tag: string) => tag?.toLowerCase().includes(searchQuery.toLowerCase()));
107+
return matchesCategory && matchesSearch;
108+
});
109+
110+
return (
111+
<main className="min-h-screen">
112+
{/* Hero Section */}
113+
<section className="relative overflow-hidden border-b">
114+
<div className="absolute inset-0 bg-grid-black/[0.02] dark:bg-grid-white/[0.02]" />
115+
<div className="container relative py-16 md:py-24">
116+
<div className="mx-auto max-w-2xl text-center">
117+
<Badge className="mb-4" variant="secondary">
118+
<Sparkles className="mr-1 h-3 w-3" />
119+
Discover...
120+
</Badge>
121+
<h1 className="mb-4 text-4xl font-bold tracking-tight sm:text-5xl md:text-6xl">
122+
Browser extensions that supercharge your productivity
123+
</h1>
124+
</div>
125+
</div>
126+
</section>
127+
128+
{/* Main Content */}
129+
<section className="container py-12">
130+
{/* Search and Filter Bar */}
131+
<ExtensionsSearchBar
132+
searchQuery={searchQuery}
133+
setSearchQuery={setSearchQuery}
134+
categories={categories}
135+
selectedCategory={selectedCategory}
136+
setSelectedCategory={setSelectedCategory}
137+
/>
138+
139+
{/* Extensions Grid */}
140+
<div className="grid gap-5 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
141+
{filteredExtensions.map((extension) => (
142+
<ExtensionCard key={extension.id} extension={extension} />
143+
))}
144+
</div>
145+
146+
{filteredExtensions.length === 0 && (
147+
<div className="py-12 text-center">
148+
<p className="text-lg text-muted-foreground">
149+
No extensions found matching your criteria.
150+
</p>
151+
</div>
152+
)}
153+
</section>
154+
155+
{/* CTA Section */}
156+
<section className="border-t bg-muted/30">
157+
<div className="container py-16">
158+
<div className="mx-auto max-w-2xl text-center">
159+
<h2 className="mb-4 text-3xl font-bold">
160+
Missing an extension?
161+
</h2>
162+
<p className="mb-8 text-lg text-muted-foreground">
163+
We&apos;re constantly adding new extensions to our directory. Let us know what you&apos;re looking for!
164+
</p>
165+
<Button size="lg" className="group">
166+
Suggest an Extension
167+
<ArrowRight className="ml-2 h-4 w-4 transition-transform group-hover:translate-x-1" />
168+
</Button>
169+
</div>
170+
</div>
171+
</section>
172+
</main>
173+
);
174+
}

apps/extensions/components.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"$schema": "https://ui.shadcn.com/schema.json",
3+
"style": "new-york",
4+
"rsc": true,
5+
"tsx": true,
6+
"tailwind": {
7+
"config": "",
8+
"css": "../../packages/ui/src/styles/globals.css",
9+
"baseColor": "neutral",
10+
"cssVariables": true
11+
},
12+
"iconLibrary": "lucide",
13+
"aliases": {
14+
"components": "@/components",
15+
"hooks": "@/hooks",
16+
"lib": "@/lib",
17+
"utils": "@serp-tools/ui/lib/utils",
18+
"ui": "@serp-tools/ui/components"
19+
}
20+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
"use client";
2+
3+
import { useState, useRef } from "react";
4+
import { Card, CardDescription, CardHeader, CardTitle } from "@serp-tools/ui/components/card";
5+
import { LucideIcon } from "lucide-react";
6+
7+
interface ExtensionCardProps {
8+
extension: {
9+
id: string;
10+
name: string;
11+
description: string;
12+
href: string;
13+
icon: LucideIcon;
14+
rating?: number;
15+
users?: string;
16+
isPopular?: boolean;
17+
isNew?: boolean;
18+
};
19+
}
20+
21+
const colors = [
22+
"rgb(239, 68, 68)", // red-500
23+
"rgb(245, 158, 11)", // amber-500
24+
"rgb(34, 197, 94)", // green-500
25+
"rgb(59, 130, 246)", // blue-500
26+
"rgb(168, 85, 247)", // purple-500
27+
"rgb(236, 72, 153)", // pink-500
28+
"rgb(20, 184, 166)", // teal-500
29+
"rgb(251, 146, 60)", // orange-500
30+
"rgb(99, 102, 241)", // indigo-500
31+
"rgb(244, 63, 94)", // rose-500
32+
"rgb(14, 165, 233)", // sky-500
33+
"rgb(163, 230, 53)", // lime-400
34+
];
35+
36+
export function ExtensionCard({ extension }: ExtensionCardProps) {
37+
const [borderColor, setBorderColor] = useState<string>("");
38+
const colorIndexRef = useRef(0);
39+
const Icon = extension.icon;
40+
41+
const handleMouseEnter = () => {
42+
// Cycle through colors sequentially instead of random
43+
colorIndexRef.current = (colorIndexRef.current + 1) % colors.length;
44+
const color = colors[colorIndexRef.current];
45+
if (color) {
46+
setBorderColor(color);
47+
}
48+
};
49+
50+
const handleMouseLeave = () => {
51+
setBorderColor("");
52+
};
53+
54+
return (
55+
<a href={extension.href} target="_blank" rel="noopener noreferrer">
56+
<Card
57+
className="group h-full transition-all hover:shadow-lg hover:-translate-y-0.5 cursor-pointer border-2"
58+
style={{
59+
borderColor: borderColor || undefined,
60+
transition: "all 0.3s ease, border-color 0.2s ease",
61+
}}
62+
onMouseEnter={handleMouseEnter}
63+
onMouseLeave={handleMouseLeave}
64+
>
65+
<CardHeader>
66+
<div className="flex items-start gap-3 mb-2">
67+
<Icon
68+
className="h-6 w-6 mt-0.5 transition-colors duration-300"
69+
style={{ color: borderColor || undefined }}
70+
/>
71+
<CardTitle className="text-lg group-hover:text-primary transition-colors">
72+
{extension.name}
73+
</CardTitle>
74+
</div>
75+
<CardDescription className="line-clamp-2">
76+
{extension.description}
77+
</CardDescription>
78+
</CardHeader>
79+
</Card>
80+
</a>
81+
);
82+
}

0 commit comments

Comments
 (0)