Skip to content

Commit 4d0d400

Browse files
committed
Add new page to show best dev tools on PH, and change john picture
1 parent f858468 commit 4d0d400

File tree

10 files changed

+462
-12
lines changed

10 files changed

+462
-12
lines changed

app/api/ph-dev-tools/route.ts

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import axios from 'axios';
2+
import { NextResponse } from 'next/server';
3+
import request from 'request';
4+
5+
export async function GET() {
6+
const today = new Date();
7+
const oneWeekAgo = new Date(today);
8+
oneWeekAgo.setDate(today.getDate() - 7);
9+
10+
const PH_ACCESS_TOKEN = process.env.PH_ACCESS_TOKEN;
11+
12+
const config = {
13+
headers: {
14+
Authorization: `Bearer ${PH_ACCESS_TOKEN}`,
15+
'Content-Type': 'application/json',
16+
Accept: 'application/json',
17+
},
18+
};
19+
20+
const body = {
21+
query: `query { posts(order: VOTES, topic: "developer-tools", postedAfter: "${new Date(oneWeekAgo).toISOString()}") {
22+
edges{
23+
cursor
24+
node{
25+
id
26+
name
27+
description
28+
url
29+
slug
30+
tagline
31+
votesCount
32+
website
33+
productLinks {
34+
url
35+
}
36+
thumbnail {
37+
url
38+
}
39+
}
40+
}
41+
}
42+
}`,
43+
};
44+
45+
const {
46+
data: {
47+
data: {
48+
posts: { edges },
49+
},
50+
},
51+
} = await axios.post('https://api.producthunt.com/v2/api/graphql', body, config);
52+
return NextResponse.json({ posts: edges.slice(0, 10) });
53+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import axios from 'axios';
2+
import ToolName from '@/components/ui/ToolCard/Tool.Name';
3+
import Title from '@/components/ui/ToolCard/Tool.Title';
4+
import ToolFooter from '@/components/ui/ToolCard/Tool.Footer';
5+
import Image from 'next/image';
6+
import ProductHuntCard from '@/components/ui/ProductHuntCard';
7+
import request from 'request';
8+
9+
function extractHostAndPath(url: string): { host: string; path: string } {
10+
const regex = /^(?:https?:\/\/)?([^\/?#]+)(\/[^?#]*)?/;
11+
const matches = url.match(regex);
12+
13+
if (matches && matches.length >= 2) {
14+
const host = matches[1];
15+
const path = matches[2] || '/'; // Default to '/' if path is not provided
16+
return { host, path };
17+
} else {
18+
throw new Error('Invalid URL format');
19+
}
20+
}
21+
22+
function extractLink(url: string) {
23+
// Regular expression to match URLs with "www." or without any protocol
24+
const regex = /^(?:https?:\/\/)?(?:www\.)?(.*)/;
25+
26+
// Extract the domain from the URL using regex
27+
const matches = url.match(regex);
28+
29+
// If matches found, construct the link with "https://" prefix
30+
if (matches && matches.length > 1) {
31+
const domain = matches[1];
32+
return `https://${domain}`;
33+
}
34+
35+
// If no matches found, return the original URL
36+
return url;
37+
}
38+
39+
type Product = {
40+
node: {
41+
id: string;
42+
name: string;
43+
description: string;
44+
slug: string;
45+
tagline: string;
46+
votesCount: number;
47+
thumbnail: {
48+
url: string;
49+
};
50+
productLinks: {
51+
url: string;
52+
};
53+
website: string;
54+
};
55+
};
56+
57+
export const metadata = {
58+
title: 'Best dev tools this week on Product Hunt - Dev Hunt',
59+
metadataBase: new URL('https://devhunt.org'),
60+
alternates: {
61+
canonical: '/best-dev-tools-this-week-on-product-hunt',
62+
},
63+
};
64+
65+
export default async () => {
66+
const origin = process.env.NODE_ENV == 'development' ? 'http://localhost:3001' : 'https://devhunt.org';
67+
const {
68+
data: { posts },
69+
} = await axios.get(`${origin}/api/ph-dev-tools`);
70+
71+
// Create an array to store all the promises for the requests
72+
const requests = posts.map((item: Product) => {
73+
// Return a promise for each request
74+
return new Promise((resolve, reject) => {
75+
// Perform the request asynchronously
76+
request({ url: item.node.website, followRedirect: false }, function (err, res, body) {
77+
if (err) {
78+
reject(err);
79+
} else {
80+
resolve(extractLink(extractHostAndPath(res.headers.location as string).host));
81+
}
82+
});
83+
});
84+
});
85+
86+
// Wait for all promises to resolve
87+
const websites = (await Promise.all(requests)) || [];
88+
89+
return (
90+
<section className="max-w-4xl mt-20 mx-auto px-4 md:px-8">
91+
<div>
92+
<h1 className="text-slate-50 text-3xl font-semibold">Best dev tools this week on Product Hunt</h1>
93+
</div>
94+
<ul className="mt-10 mb-12 divide-y divide-slate-800/60">
95+
{posts?.map((tool: Product, idx: number) => (
96+
<li key={idx} className="py-3">
97+
<ProductHuntCard href={websites[idx] ? `${websites[idx]}/?ref=devhunt` : tool.node.website}>
98+
{/* {console.log(tool.node)} */}
99+
<div className="w-full flex items-center gap-x-4">
100+
<Image
101+
src={tool.node.thumbnail.url}
102+
alt={tool.node.name}
103+
width={64}
104+
height={64}
105+
className="rounded-full object-cover flex-none"
106+
/>
107+
<div className="w-full space-y-1">
108+
<ToolName href={websites[idx] ? websites[idx] : tool.node.website}>{tool.node.name}</ToolName>
109+
<Title className="line-clamp-2">{tool.node.tagline}</Title>
110+
<ToolFooter>
111+
{/* <Tags items={[tool.product_pricing_types?.title ?? 'Free', ...(tool.product_categories || []).map(c => c.name)]} /> */}
112+
</ToolFooter>
113+
</div>
114+
</div>
115+
<div className="px-4 py-1 text-center active:scale-[1.5] duration-200 rounded-md border bg-[linear-gradient(180deg,_#1E293B_0%,_rgba(30,_41,_59,_0.00)_100%)] border-slate-700 text-orange-300">
116+
<span className="text-sm pointer-events-none">#{idx + 1}</span>
117+
</div>
118+
</ProductHuntCard>
119+
</li>
120+
))}
121+
</ul>
122+
</section>
123+
);
124+
};

app/sitemap.xml/route.tsx

+3
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ async function generateSiteMap() {
2121
</url>
2222
<url>
2323
<loc>https://devhunt.org/blog</loc>
24+
</url>
25+
<url>
26+
<loc>https://devhunt.org/best-dev-tools-this-week-on-product-hunt</loc>
2427
</url>
2528
${
2629
tools &&

components/ui/Navbar/Navbar.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,10 @@ export default () => {
5959
className: 'bg-orange-500 hover:bg-orange-600 text-white text-center rounded-lg px-3 p-2 duration-150 btnshake',
6060
},
6161
];
62-
6362
const submenu = [
6463
{ title: 'This Week', path: '/' },
6564
{ title: 'Upcoming Tools', path: '/upcoming' },
65+
{ title: 'Best DevTools On Product Hunt', path: '/best-dev-tools-this-week-on-product-hunt' },
6666
];
6767

6868
const handleSearch = (value: string) => {
+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
'use client';
2+
3+
import mergeTW from '@/utils/mergeTW';
4+
import { MouseEvent, ReactNode } from 'react';
5+
6+
export default ({ href, className, children }: { href: string; className?: string; children?: ReactNode }) => {
7+
const handleClick = (e: MouseEvent) => {
8+
window.open(href, '_blank');
9+
};
10+
return (
11+
<div
12+
onClick={handleClick}
13+
className={mergeTW(`flex items-start gap-x-4 relative py-4 rounded-2xl cursor-pointer group group/card ${className}`)}
14+
>
15+
{children}
16+
<div className="absolute -z-10 -inset-2 rounded-2xl group-hover:bg-slate-800/60 opacity-0 group-hover:opacity-100 duration-150 sm:-inset-3"></div>
17+
</div>
18+
);
19+
};

next.config.js

+6
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ const nextConfig = {
2020
AUTH_TOKEN_PASSWORD: process.env.AUTH_TOKEN_PASSWORD,
2121
AUTH_TOKEN_API_KEY: process.env.AUTH_TOKEN_API_KEY,
2222
CRON_SECRET: process.env.CRON_SECRET,
23+
PH_ACCESS_TOKEN: process.env.PH_ACCESS_TOKEN,
2324
},
2425
images: {
2526
remotePatterns: [
@@ -29,6 +30,11 @@ const nextConfig = {
2930
port: '',
3031
pathname: '/seobot/devhunt.org/**',
3132
},
33+
{
34+
protocol: 'https',
35+
hostname: 'ph-files.imgix.net',
36+
port: '',
37+
},
3238
],
3339
},
3440
};

package.json

+3
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"@usermaven/sdk-js": "^1.2.2",
2626
"axios": "^1.4.0",
2727
"dompurify": "^3.0.3",
28+
"follow-redirects": "^1.15.5",
2829
"framer-motion": "^10.12.16",
2930
"highlight.js": "^11.9.0",
3031
"js-confetti": "^0.11.0",
@@ -38,6 +39,7 @@
3839
"react-cool-onclickoutside": "^1.7.0",
3940
"react-dom": "18.2.0",
4041
"react-hook-form": "^7.44.2",
42+
"request": "^2.88.2",
4143
"seobot": "^1.0.7",
4244
"tailwind-merge": "^1.12.0",
4345
"waypoints": "^4.0.1"
@@ -50,6 +52,7 @@
5052
"@types/node": "20.2.3",
5153
"@types/react": "18.2.6",
5254
"@types/react-dom": "18.2.4",
55+
"@types/request": "^2.48.12",
5356
"@typescript-eslint/eslint-plugin": "^5.59.7",
5457
"autoprefixer": "10.4.14",
5558
"better-supabase-types": "^2.7.1",

0 commit comments

Comments
 (0)