Skip to content

Commit 9879c67

Browse files
committed
feat: add automatic timestamps for Product Hunt and YouTube
- Map Product Hunt featured_at to updatedAt (zero API cost) - Map YouTube latestVideoPublishedAt to updatedAt (zero API cost) - Add YouTube project normalization tests - Add Product Hunt timestamp tests - All 699 tests passing Completes the high-priority zero-cost timestamp mappings from issue #2.
1 parent 6a87c70 commit 9879c67

2 files changed

Lines changed: 123 additions & 1 deletion

File tree

packages/core/src/lib/__tests__/normalise.test.ts

Lines changed: 113 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,21 @@
11
import { describe, it, expect, vi, beforeEach } from 'vitest'
22
import { normalise, normalizeStats } from '../normalise'
3-
import type { ManualProjectInput, GitHubProjectInput, NpmProjectInput, ProductHuntProjectInput } from '../../types'
3+
import type { ManualProjectInput, GitHubProjectInput, NpmProjectInput, ProductHuntProjectInput, YouTubeProjectInput } from '../../types'
44
import { fetchGitHubRepo, fetchGitHubCommits } from '../github'
55
import { fetchNpmPackage } from '../npm'
66
import { fetchProductHuntPost } from '../product-hunt'
7+
import { fetchYouTubeChannel } from '../youtube'
78

89
vi.mock('../github')
910
vi.mock('../npm')
1011
vi.mock('../product-hunt')
12+
vi.mock('../youtube')
1113

1214
const mockedFetchGitHubRepo = vi.mocked(fetchGitHubRepo)
1315
const mockedFetchGitHubCommits = vi.mocked(fetchGitHubCommits)
1416
const mockedFetchNpmPackage = vi.mocked(fetchNpmPackage)
1517
const mockedFetchProductHuntPost = vi.mocked(fetchProductHuntPost)
18+
const mockedFetchYouTubeChannel = vi.mocked(fetchYouTubeChannel)
1619

1720
describe('normalise', () => {
1821
beforeEach(() => {
@@ -783,6 +786,115 @@ describe('normalise', () => {
783786
expect(result.name).toBe('Fallback Name')
784787
expect(result.stats).toBeNull()
785788
})
789+
790+
it('should use featured_at as updatedAt for Product Hunt projects', async () => {
791+
mockedFetchProductHuntPost.mockResolvedValue({
792+
name: 'Product Name',
793+
tagline: 'Product tagline',
794+
description: 'Product description',
795+
votes_count: 500,
796+
comments_count: 50,
797+
featured_at: '2024-03-15T00:00:00Z',
798+
website: 'https://example.com',
799+
url: 'https://producthunt.com/posts/product',
800+
})
801+
802+
const input: ProductHuntProjectInput = {
803+
id: 'test-ph-timestamp',
804+
type: 'product-hunt',
805+
slug: 'my-product',
806+
status: 'shipped',
807+
name: 'Input Name',
808+
createdAt: '2024-01-01',
809+
updatedAt: '2024-02-01',
810+
}
811+
812+
const result = await normalise(input)
813+
814+
expect(result.createdAt).toBe('2024-01-01')
815+
expect(result.updatedAt).toBe('2024-03-15T00:00:00Z')
816+
})
817+
818+
it('should fallback to input updatedAt when Product Hunt fetch fails', async () => {
819+
mockedFetchProductHuntPost.mockResolvedValue(null)
820+
821+
const input: ProductHuntProjectInput = {
822+
id: 'test-ph-fallback',
823+
type: 'product-hunt',
824+
slug: 'nonexistent',
825+
status: 'active',
826+
name: 'Fallback Name',
827+
updatedAt: '2024-05-01',
828+
}
829+
830+
const result = await normalise(input)
831+
832+
expect(result.updatedAt).toBe('2024-05-01')
833+
})
834+
})
835+
836+
describe('YouTube project normalization', () => {
837+
beforeEach(() => {
838+
vi.clearAllMocks()
839+
})
840+
841+
it('should use latestVideoPublishedAt as updatedAt for YouTube projects', async () => {
842+
mockedFetchYouTubeChannel.mockResolvedValue({
843+
subscriberCount: 10000,
844+
viewCount: 500000,
845+
latestVideoTitle: 'My Latest Video',
846+
latestVideoUrl: 'https://youtube.com/watch?v=abc123',
847+
latestVideoPublishedAt: '2024-04-20T10:00:00Z',
848+
})
849+
850+
const input: YouTubeProjectInput = {
851+
id: 'test-youtube-timestamp',
852+
type: 'youtube',
853+
channelId: 'UC_x5XG1OV2P6uZZ5FSM9Ttw',
854+
status: 'active',
855+
createdAt: '2024-01-01',
856+
updatedAt: '2024-02-01',
857+
}
858+
859+
const result = await normalise(input)
860+
861+
expect(result.createdAt).toBe('2024-01-01')
862+
expect(result.updatedAt).toBe('2024-04-20T10:00:00Z')
863+
})
864+
865+
it('should fallback to input updatedAt when YouTube fetch fails', async () => {
866+
mockedFetchYouTubeChannel.mockResolvedValue(null)
867+
868+
const input: YouTubeProjectInput = {
869+
id: 'test-youtube-fallback',
870+
type: 'youtube',
871+
channelId: 'UC_x5XG1OV2P6uZZ5FSM9Ttw',
872+
status: 'active',
873+
name: 'My Channel',
874+
updatedAt: '2024-05-15',
875+
}
876+
877+
const result = await normalise(input)
878+
879+
expect(result.updatedAt).toBe('2024-05-15')
880+
})
881+
882+
it('should have no automatic timestamp when both fetch fails and no input', async () => {
883+
mockedFetchYouTubeChannel.mockResolvedValue(null)
884+
885+
const input: YouTubeProjectInput = {
886+
id: 'test-youtube-no-timestamp',
887+
type: 'youtube',
888+
channelId: 'UC_x5XG1OV2P6uZZ5FSM9Ttw',
889+
status: 'active',
890+
name: 'My Channel',
891+
}
892+
893+
const result = await normalise(input)
894+
895+
expect(result.createdAt).toBeNull()
896+
expect(result.updatedAt).toBeNull()
897+
})
786898
})
787899

788900
describe('hybrid project normalization', () => {

packages/core/src/lib/normalise.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,16 @@ export async function normalise(
292292
finalLanguageColor = null
293293
finalCreatedAt = fetchNpmTimestamps ? npmData?.createdAt || inputCreatedAt || null : inputCreatedAt || null
294294
finalUpdatedAt = fetchNpmTimestamps ? npmData?.modifiedAt || inputUpdatedAt || null : inputUpdatedAt || null
295+
} else if (type === 'product-hunt') {
296+
finalLanguage = null
297+
finalLanguageColor = null
298+
finalCreatedAt = inputCreatedAt || null
299+
finalUpdatedAt = productHuntData?.featured_at || inputUpdatedAt || null
300+
} else if (type === 'youtube') {
301+
finalLanguage = null
302+
finalLanguageColor = null
303+
finalCreatedAt = inputCreatedAt || null
304+
finalUpdatedAt = youtubeData?.latestVideoPublishedAt || inputUpdatedAt || null
295305
} else {
296306
finalLanguage = null
297307
finalLanguageColor = null

0 commit comments

Comments
 (0)