@reallukemanning/folio 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (332) hide show
  1. package/LICENSE +21 -0
  2. package/copy-components.js +31 -0
  3. package/coverage/base.css +224 -0
  4. package/coverage/block-navigation.js +87 -0
  5. package/coverage/core/copy-components.js.html +178 -0
  6. package/coverage/core/eslint.config.js.html +184 -0
  7. package/coverage/core/index.html +146 -0
  8. package/coverage/core/src/__tests__/benchmarks/ProjectCard.bench.tsx.html +364 -0
  9. package/coverage/core/src/__tests__/benchmarks/ProjectView.bench.tsx.html +484 -0
  10. package/coverage/core/src/__tests__/benchmarks/github.bench.ts.html +244 -0
  11. package/coverage/core/src/__tests__/benchmarks/index.html +191 -0
  12. package/coverage/core/src/__tests__/benchmarks/npm.bench.ts.html +271 -0
  13. package/coverage/core/src/__tests__/benchmarks/product-hunt.bench.ts.html +259 -0
  14. package/coverage/core/src/__tests__/benchmarks/utilities.bench.ts.html +478 -0
  15. package/coverage/core/src/components/FeaturedProject.test.tsx.html +697 -0
  16. package/coverage/core/src/components/FeaturedProject.tsx.html +163 -0
  17. package/coverage/core/src/components/ProjectCard/ProjectCard.test.tsx.html +928 -0
  18. package/coverage/core/src/components/ProjectCard/ProjectCard.tsx.html +379 -0
  19. package/coverage/core/src/components/ProjectCard/index.html +146 -0
  20. package/coverage/core/src/components/ProjectCard/index.ts.html +88 -0
  21. package/coverage/core/src/components/ProjectGrid/ProjectGrid.test.tsx.html +292 -0
  22. package/coverage/core/src/components/ProjectGrid/ProjectGrid.tsx.html +103 -0
  23. package/coverage/core/src/components/ProjectGrid/index.html +146 -0
  24. package/coverage/core/src/components/ProjectGrid/index.ts.html +88 -0
  25. package/coverage/core/src/components/ProjectList/ProjectList.test.tsx.html +292 -0
  26. package/coverage/core/src/components/ProjectList/ProjectList.tsx.html +103 -0
  27. package/coverage/core/src/components/ProjectList/index.html +146 -0
  28. package/coverage/core/src/components/ProjectList/index.ts.html +88 -0
  29. package/coverage/core/src/components/ProjectView/ProjectView.test.tsx.html +1108 -0
  30. package/coverage/core/src/components/ProjectView/ProjectView.tsx.html +589 -0
  31. package/coverage/core/src/components/ProjectView/index.html +146 -0
  32. package/coverage/core/src/components/ProjectView/index.ts.html +88 -0
  33. package/coverage/core/src/components/index.html +146 -0
  34. package/coverage/core/src/components/index.ts.html +97 -0
  35. package/coverage/core/src/index.html +116 -0
  36. package/coverage/core/src/index.ts.html +94 -0
  37. package/coverage/core/src/lib/__tests__/defineProjects.test.ts.html +421 -0
  38. package/coverage/core/src/lib/__tests__/filterByFeatured.test.ts.html +523 -0
  39. package/coverage/core/src/lib/__tests__/filterByStatus.test.ts.html +664 -0
  40. package/coverage/core/src/lib/__tests__/filterByType.test.ts.html +631 -0
  41. package/coverage/core/src/lib/__tests__/github.test.ts.html +1783 -0
  42. package/coverage/core/src/lib/__tests__/hybrid-config.test.ts.html +1345 -0
  43. package/coverage/core/src/lib/__tests__/index.html +311 -0
  44. package/coverage/core/src/lib/__tests__/normalise.test.ts.html +1585 -0
  45. package/coverage/core/src/lib/__tests__/npm-config.test.ts.html +385 -0
  46. package/coverage/core/src/lib/__tests__/npm.test.ts.html +1135 -0
  47. package/coverage/core/src/lib/__tests__/product-hunt-config.test.ts.html +397 -0
  48. package/coverage/core/src/lib/__tests__/product-hunt.test.ts.html +505 -0
  49. package/coverage/core/src/lib/__tests__/sortByDate.test.ts.html +751 -0
  50. package/coverage/core/src/lib/__tests__/sortByName.test.ts.html +832 -0
  51. package/coverage/core/src/lib/__tests__/sortByStars.test.ts.html +703 -0
  52. package/coverage/core/src/lib/defineProjects.ts.html +100 -0
  53. package/coverage/core/src/lib/filterByFeatured.ts.html +133 -0
  54. package/coverage/core/src/lib/filterByStatus.ts.html +145 -0
  55. package/coverage/core/src/lib/filterByType.ts.html +133 -0
  56. package/coverage/core/src/lib/github.ts.html +517 -0
  57. package/coverage/core/src/lib/index.html +281 -0
  58. package/coverage/core/src/lib/index.ts.html +130 -0
  59. package/coverage/core/src/lib/normalise.ts.html +868 -0
  60. package/coverage/core/src/lib/npm.ts.html +199 -0
  61. package/coverage/core/src/lib/product-hunt.ts.html +256 -0
  62. package/coverage/core/src/lib/sortByDate.ts.html +175 -0
  63. package/coverage/core/src/lib/sortByName.ts.html +172 -0
  64. package/coverage/core/src/lib/sortByStars.ts.html +172 -0
  65. package/coverage/core/src/types/index.html +116 -0
  66. package/coverage/core/src/types/index.ts.html +517 -0
  67. package/coverage/core/vitest.config.ts.html +178 -0
  68. package/coverage/coverage-final.json +53 -0
  69. package/coverage/coverage-summary.json +54 -0
  70. package/coverage/favicon.png +0 -0
  71. package/coverage/index.html +266 -0
  72. package/coverage/prettify.css +1 -0
  73. package/coverage/prettify.js +2 -0
  74. package/coverage/sort-arrow-sprite.png +0 -0
  75. package/coverage/sorter.js +210 -0
  76. package/dist/cli-components/FeaturedProject/FeaturedProject.d.ts +6 -0
  77. package/dist/cli-components/FeaturedProject/FeaturedProject.d.ts.map +1 -0
  78. package/dist/cli-components/FeaturedProject/FeaturedProject.js +9 -0
  79. package/dist/cli-components/FeaturedProject/FeaturedProject.js.map +1 -0
  80. package/dist/cli-components/FeaturedProject/FeaturedProject.tsx +54 -0
  81. package/dist/cli-components/FeaturedProject/index.d.ts +3 -0
  82. package/dist/cli-components/FeaturedProject/index.d.ts.map +1 -0
  83. package/dist/cli-components/FeaturedProject/index.js +2 -0
  84. package/dist/cli-components/FeaturedProject/index.js.map +1 -0
  85. package/dist/cli-components/FeaturedProject/index.ts +2 -0
  86. package/dist/cli-components/ProjectCard/ProjectCard.d.ts +26 -0
  87. package/dist/cli-components/ProjectCard/ProjectCard.d.ts.map +1 -0
  88. package/dist/cli-components/ProjectCard/ProjectCard.js +33 -0
  89. package/dist/cli-components/ProjectCard/ProjectCard.js.map +1 -0
  90. package/dist/cli-components/ProjectCard/ProjectCard.tsx +90 -0
  91. package/dist/cli-components/ProjectCard/index.d.ts +3 -0
  92. package/dist/cli-components/ProjectCard/index.d.ts.map +1 -0
  93. package/dist/cli-components/ProjectCard/index.js +2 -0
  94. package/dist/cli-components/ProjectCard/index.js.map +1 -0
  95. package/dist/cli-components/ProjectCard/index.ts +2 -0
  96. package/dist/cli-components/ProjectGrid/ProjectGrid.d.ts +5 -0
  97. package/dist/cli-components/ProjectGrid/ProjectGrid.d.ts.map +1 -0
  98. package/dist/cli-components/ProjectGrid/ProjectGrid.js +8 -0
  99. package/dist/cli-components/ProjectGrid/ProjectGrid.js.map +1 -0
  100. package/dist/cli-components/ProjectGrid/ProjectGrid.tsx +6 -0
  101. package/dist/cli-components/ProjectGrid/index.d.ts +3 -0
  102. package/dist/cli-components/ProjectGrid/index.d.ts.map +1 -0
  103. package/dist/cli-components/ProjectGrid/index.js +2 -0
  104. package/dist/cli-components/ProjectGrid/index.js.map +1 -0
  105. package/dist/cli-components/ProjectGrid/index.ts +2 -0
  106. package/dist/cli-components/ProjectList/ProjectList.d.ts +5 -0
  107. package/dist/cli-components/ProjectList/ProjectList.d.ts.map +1 -0
  108. package/dist/cli-components/ProjectList/ProjectList.js +8 -0
  109. package/dist/cli-components/ProjectList/ProjectList.js.map +1 -0
  110. package/dist/cli-components/ProjectList/ProjectList.tsx +6 -0
  111. package/dist/cli-components/ProjectList/index.d.ts +3 -0
  112. package/dist/cli-components/ProjectList/index.d.ts.map +1 -0
  113. package/dist/cli-components/ProjectList/index.js +2 -0
  114. package/dist/cli-components/ProjectList/index.js.map +1 -0
  115. package/dist/cli-components/ProjectList/index.ts +2 -0
  116. package/dist/cli-components/ProjectView/ProjectView.d.ts +17 -0
  117. package/dist/cli-components/ProjectView/ProjectView.d.ts.map +1 -0
  118. package/dist/cli-components/ProjectView/ProjectView.js +39 -0
  119. package/dist/cli-components/ProjectView/ProjectView.js.map +1 -0
  120. package/dist/cli-components/ProjectView/ProjectView.tsx +117 -0
  121. package/dist/cli-components/ProjectView/index.d.ts +3 -0
  122. package/dist/cli-components/ProjectView/index.d.ts.map +1 -0
  123. package/dist/cli-components/ProjectView/index.js +2 -0
  124. package/dist/cli-components/ProjectView/index.js.map +1 -0
  125. package/dist/cli-components/ProjectView/index.ts +2 -0
  126. package/dist/cli-components/types.d.ts +52 -0
  127. package/dist/cli-components/types.d.ts.map +1 -0
  128. package/dist/cli-components/types.js +2 -0
  129. package/dist/cli-components/types.js.map +1 -0
  130. package/dist/cli-components/types.ts +58 -0
  131. package/dist/cli-types.d.ts +52 -0
  132. package/dist/cli-types.d.ts.map +1 -0
  133. package/dist/cli-types.js +2 -0
  134. package/dist/cli-types.js.map +1 -0
  135. package/dist/cli.d.ts +3 -0
  136. package/dist/cli.d.ts.map +1 -0
  137. package/dist/cli.js +21 -0
  138. package/dist/cli.js.map +1 -0
  139. package/dist/commands/add.d.ts +2 -0
  140. package/dist/commands/add.d.ts.map +1 -0
  141. package/dist/commands/add.js +166 -0
  142. package/dist/commands/add.js.map +1 -0
  143. package/dist/commands/init.d.ts +6 -0
  144. package/dist/commands/init.d.ts.map +1 -0
  145. package/dist/commands/init.js +231 -0
  146. package/dist/commands/init.js.map +1 -0
  147. package/dist/components/FeaturedProject.d.ts +7 -0
  148. package/dist/components/FeaturedProject.d.ts.map +1 -0
  149. package/dist/components/FeaturedProject.js +10 -0
  150. package/dist/components/FeaturedProject.js.map +1 -0
  151. package/dist/components/FeaturedProject.test.tsx +204 -0
  152. package/dist/components/FeaturedProject.tsx +26 -0
  153. package/dist/components/ProjectCard/ProjectCard.d.ts +26 -0
  154. package/dist/components/ProjectCard/ProjectCard.d.ts.map +1 -0
  155. package/dist/components/ProjectCard/ProjectCard.js +39 -0
  156. package/dist/components/ProjectCard/ProjectCard.js.map +1 -0
  157. package/dist/components/ProjectCard/ProjectCard.test.tsx +281 -0
  158. package/dist/components/ProjectCard/ProjectCard.tsx +98 -0
  159. package/dist/components/ProjectCard/index.d.ts +2 -0
  160. package/dist/components/ProjectCard/index.d.ts.map +1 -0
  161. package/dist/components/ProjectCard/index.js +2 -0
  162. package/dist/components/ProjectCard/index.js.map +1 -0
  163. package/dist/components/ProjectCard/index.ts +1 -0
  164. package/dist/components/ProjectGrid/ProjectGrid.d.ts +5 -0
  165. package/dist/components/ProjectGrid/ProjectGrid.d.ts.map +1 -0
  166. package/dist/components/ProjectGrid/ProjectGrid.js +8 -0
  167. package/dist/components/ProjectGrid/ProjectGrid.js.map +1 -0
  168. package/dist/components/ProjectGrid/ProjectGrid.test.tsx +69 -0
  169. package/dist/components/ProjectGrid/ProjectGrid.tsx +6 -0
  170. package/dist/components/ProjectGrid/index.d.ts +2 -0
  171. package/dist/components/ProjectGrid/index.d.ts.map +1 -0
  172. package/dist/components/ProjectGrid/index.js +2 -0
  173. package/dist/components/ProjectGrid/index.js.map +1 -0
  174. package/dist/components/ProjectGrid/index.ts +1 -0
  175. package/dist/components/ProjectList/ProjectList.d.ts +5 -0
  176. package/dist/components/ProjectList/ProjectList.d.ts.map +1 -0
  177. package/dist/components/ProjectList/ProjectList.js +8 -0
  178. package/dist/components/ProjectList/ProjectList.js.map +1 -0
  179. package/dist/components/ProjectList/ProjectList.test.tsx +69 -0
  180. package/dist/components/ProjectList/ProjectList.tsx +6 -0
  181. package/dist/components/ProjectList/index.d.ts +2 -0
  182. package/dist/components/ProjectList/index.d.ts.map +1 -0
  183. package/dist/components/ProjectList/index.js +2 -0
  184. package/dist/components/ProjectList/index.js.map +1 -0
  185. package/dist/components/ProjectList/index.ts +1 -0
  186. package/dist/components/ProjectView/ProjectView.d.ts +20 -0
  187. package/dist/components/ProjectView/ProjectView.d.ts.map +1 -0
  188. package/dist/components/ProjectView/ProjectView.js +64 -0
  189. package/dist/components/ProjectView/ProjectView.js.map +1 -0
  190. package/dist/components/ProjectView/ProjectView.test.tsx +341 -0
  191. package/dist/components/ProjectView/ProjectView.tsx +168 -0
  192. package/dist/components/ProjectView/index.d.ts +2 -0
  193. package/dist/components/ProjectView/index.d.ts.map +1 -0
  194. package/dist/components/ProjectView/index.js +2 -0
  195. package/dist/components/ProjectView/index.js.map +1 -0
  196. package/dist/components/ProjectView/index.ts +1 -0
  197. package/dist/components/index.d.ts +5 -0
  198. package/dist/components/index.d.ts.map +1 -0
  199. package/dist/components/index.js +5 -0
  200. package/dist/components/index.js.map +1 -0
  201. package/dist/components/index.ts +4 -0
  202. package/dist/index.d.ts +4 -0
  203. package/dist/index.d.ts.map +1 -0
  204. package/dist/index.js +4 -0
  205. package/dist/index.js.map +1 -0
  206. package/dist/lib/defineProjects.d.ts +3 -0
  207. package/dist/lib/defineProjects.d.ts.map +1 -0
  208. package/dist/lib/defineProjects.js +4 -0
  209. package/dist/lib/defineProjects.js.map +1 -0
  210. package/dist/lib/filterByFeatured.d.ts +3 -0
  211. package/dist/lib/filterByFeatured.d.ts.map +1 -0
  212. package/dist/lib/filterByFeatured.js +10 -0
  213. package/dist/lib/filterByFeatured.js.map +1 -0
  214. package/dist/lib/filterByStatus.d.ts +3 -0
  215. package/dist/lib/filterByStatus.d.ts.map +1 -0
  216. package/dist/lib/filterByStatus.js +13 -0
  217. package/dist/lib/filterByStatus.js.map +1 -0
  218. package/dist/lib/filterByType.d.ts +3 -0
  219. package/dist/lib/filterByType.d.ts.map +1 -0
  220. package/dist/lib/filterByType.js +10 -0
  221. package/dist/lib/filterByType.js.map +1 -0
  222. package/dist/lib/github.d.ts +24 -0
  223. package/dist/lib/github.d.ts.map +1 -0
  224. package/dist/lib/github.js +107 -0
  225. package/dist/lib/github.js.map +1 -0
  226. package/dist/lib/index.d.ts +16 -0
  227. package/dist/lib/index.d.ts.map +1 -0
  228. package/dist/lib/index.js +12 -0
  229. package/dist/lib/index.js.map +1 -0
  230. package/dist/lib/normalise.d.ts +4 -0
  231. package/dist/lib/normalise.d.ts.map +1 -0
  232. package/dist/lib/normalise.js +221 -0
  233. package/dist/lib/normalise.js.map +1 -0
  234. package/dist/lib/npm.d.ts +7 -0
  235. package/dist/lib/npm.d.ts.map +1 -0
  236. package/dist/lib/npm.js +28 -0
  237. package/dist/lib/npm.js.map +1 -0
  238. package/dist/lib/product-hunt.d.ts +12 -0
  239. package/dist/lib/product-hunt.d.ts.map +1 -0
  240. package/dist/lib/product-hunt.js +40 -0
  241. package/dist/lib/product-hunt.js.map +1 -0
  242. package/dist/lib/sortByDate.d.ts +4 -0
  243. package/dist/lib/sortByDate.d.ts.map +1 -0
  244. package/dist/lib/sortByDate.js +21 -0
  245. package/dist/lib/sortByDate.js.map +1 -0
  246. package/dist/lib/sortByName.d.ts +4 -0
  247. package/dist/lib/sortByName.d.ts.map +1 -0
  248. package/dist/lib/sortByName.js +21 -0
  249. package/dist/lib/sortByName.js.map +1 -0
  250. package/dist/lib/sortByStars.d.ts +4 -0
  251. package/dist/lib/sortByStars.d.ts.map +1 -0
  252. package/dist/lib/sortByStars.js +21 -0
  253. package/dist/lib/sortByStars.js.map +1 -0
  254. package/dist/test-setup.d.ts +2 -0
  255. package/dist/test-setup.d.ts.map +1 -0
  256. package/dist/test-setup.js +2 -0
  257. package/dist/test-setup.js.map +1 -0
  258. package/dist/types/index.d.ts +121 -0
  259. package/dist/types/index.d.ts.map +1 -0
  260. package/dist/types/index.js +2 -0
  261. package/dist/types/index.js.map +1 -0
  262. package/eslint.config.js +33 -0
  263. package/package.json +47 -0
  264. package/playwright-report/index.html +85 -0
  265. package/src/__tests__/benchmarks/ProjectCard.bench.tsx +93 -0
  266. package/src/__tests__/benchmarks/ProjectView.bench.tsx +133 -0
  267. package/src/__tests__/benchmarks/github.bench.ts +53 -0
  268. package/src/__tests__/benchmarks/npm.bench.ts +62 -0
  269. package/src/__tests__/benchmarks/product-hunt.bench.ts +58 -0
  270. package/src/__tests__/benchmarks/utilities.bench.ts +131 -0
  271. package/src/cli-components/FeaturedProject/FeaturedProject.tsx +54 -0
  272. package/src/cli-components/FeaturedProject/index.ts +2 -0
  273. package/src/cli-components/ProjectCard/ProjectCard.tsx +90 -0
  274. package/src/cli-components/ProjectCard/index.ts +2 -0
  275. package/src/cli-components/ProjectGrid/ProjectGrid.tsx +6 -0
  276. package/src/cli-components/ProjectGrid/index.ts +2 -0
  277. package/src/cli-components/ProjectList/ProjectList.tsx +6 -0
  278. package/src/cli-components/ProjectList/index.ts +2 -0
  279. package/src/cli-components/ProjectView/ProjectView.tsx +117 -0
  280. package/src/cli-components/ProjectView/index.ts +2 -0
  281. package/src/cli-components/types.ts +58 -0
  282. package/src/cli-types.ts +58 -0
  283. package/src/cli.ts +26 -0
  284. package/src/commands/add.ts +191 -0
  285. package/src/commands/init.ts +260 -0
  286. package/src/components/FeaturedProject.test.tsx +204 -0
  287. package/src/components/FeaturedProject.tsx +26 -0
  288. package/src/components/ProjectCard/ProjectCard.test.tsx +281 -0
  289. package/src/components/ProjectCard/ProjectCard.tsx +98 -0
  290. package/src/components/ProjectCard/index.ts +1 -0
  291. package/src/components/ProjectGrid/ProjectGrid.test.tsx +69 -0
  292. package/src/components/ProjectGrid/ProjectGrid.tsx +6 -0
  293. package/src/components/ProjectGrid/index.ts +1 -0
  294. package/src/components/ProjectList/ProjectList.test.tsx +69 -0
  295. package/src/components/ProjectList/ProjectList.tsx +6 -0
  296. package/src/components/ProjectList/index.ts +1 -0
  297. package/src/components/ProjectView/ProjectView.test.tsx +341 -0
  298. package/src/components/ProjectView/ProjectView.tsx +168 -0
  299. package/src/components/ProjectView/index.ts +1 -0
  300. package/src/components/index.ts +4 -0
  301. package/src/index.ts +3 -0
  302. package/src/lib/__tests__/defineProjects.test.ts +112 -0
  303. package/src/lib/__tests__/filterByFeatured.test.ts +146 -0
  304. package/src/lib/__tests__/filterByStatus.test.ts +193 -0
  305. package/src/lib/__tests__/filterByType.test.ts +182 -0
  306. package/src/lib/__tests__/github.test.ts +566 -0
  307. package/src/lib/__tests__/hybrid-config.test.ts +420 -0
  308. package/src/lib/__tests__/normalise.test.ts +500 -0
  309. package/src/lib/__tests__/npm-config.test.ts +100 -0
  310. package/src/lib/__tests__/npm.test.ts +350 -0
  311. package/src/lib/__tests__/product-hunt-config.test.ts +104 -0
  312. package/src/lib/__tests__/product-hunt.test.ts +140 -0
  313. package/src/lib/__tests__/sortByDate.test.ts +222 -0
  314. package/src/lib/__tests__/sortByName.test.ts +249 -0
  315. package/src/lib/__tests__/sortByStars.test.ts +206 -0
  316. package/src/lib/defineProjects.ts +5 -0
  317. package/src/lib/filterByFeatured.ts +16 -0
  318. package/src/lib/filterByStatus.ts +20 -0
  319. package/src/lib/filterByType.ts +16 -0
  320. package/src/lib/github.ts +144 -0
  321. package/src/lib/index.ts +15 -0
  322. package/src/lib/normalise.ts +261 -0
  323. package/src/lib/npm.ts +38 -0
  324. package/src/lib/product-hunt.ts +57 -0
  325. package/src/lib/sortByDate.ts +30 -0
  326. package/src/lib/sortByName.ts +29 -0
  327. package/src/lib/sortByStars.ts +29 -0
  328. package/src/test-setup.ts +1 -0
  329. package/src/types/index.ts +144 -0
  330. package/test-results/.last-run.json +62 -0
  331. package/tsconfig.json +29 -0
  332. package/vitest.config.ts +31 -0
@@ -0,0 +1,341 @@
1
+ import { describe, it, expect, afterEach } from 'vitest'
2
+ import { render, screen, cleanup } from '@testing-library/react'
3
+ import { ProjectView } from './ProjectView'
4
+ import type { FolioProject } from '../../types'
5
+
6
+ afterEach(() => {
7
+ cleanup()
8
+ })
9
+
10
+ const createProject = (overrides: Partial<FolioProject> = {}): FolioProject => ({
11
+ id: 'test-project',
12
+ type: 'github',
13
+ status: 'active',
14
+ featured: false,
15
+ name: 'Test Project',
16
+ tagline: 'A test project',
17
+ description: 'This is a test project description',
18
+ background: 'This is the background story of the project.',
19
+ why: 'Why I built this project.',
20
+ image: null,
21
+ struggles: [
22
+ { type: 'warn', text: 'First challenge faced' },
23
+ { type: 'error', text: 'Critical issue encountered' },
24
+ ],
25
+ timeline: [
26
+ { date: '2024-01-01', note: 'Project started' },
27
+ { date: '2024-06-01', note: 'First release' },
28
+ ],
29
+ posts: [
30
+ { title: 'Announcement Post', date: '2024-02-01', url: 'https://blog.com/post1' },
31
+ { title: 'Update Post', date: '2024-07-01' },
32
+ ],
33
+ stack: ['React', 'TypeScript', 'Node.js'],
34
+ links: {
35
+ github: 'https://github.com/test/project',
36
+ live: 'https://test-project.com',
37
+ },
38
+ stats: {
39
+ stars: 100,
40
+ forks: 20,
41
+ },
42
+ language: 'TypeScript',
43
+ languageColor: '#3178c6',
44
+ createdAt: '2024-01-01',
45
+ updatedAt: '2024-06-01',
46
+ ...overrides,
47
+ })
48
+
49
+ describe('ProjectView', () => {
50
+ it('renders with project sections displaying correct content', () => {
51
+ const project = createProject()
52
+
53
+ const { container } = render(
54
+ <ProjectView project={project}>
55
+ <ProjectView.Section project={project} name="background" />
56
+ <ProjectView.Section project={project} name="why" />
57
+ </ProjectView>
58
+ )
59
+
60
+ expect(screen.getByRole('heading', { level: 2 })).toHaveTextContent('Test Project')
61
+ expect(container.querySelector('[data-folio-view-section-name="background"]')).toHaveTextContent(/This is the background story/)
62
+ expect(screen.getByText('Why I built this project.')).toBeInTheDocument()
63
+ })
64
+
65
+ it('renders back button when onBack is provided', () => {
66
+ const project = createProject()
67
+ const onBack = () => {}
68
+
69
+ render(
70
+ <ProjectView project={project} onBack={onBack}>
71
+ <span>Content</span>
72
+ </ProjectView>
73
+ )
74
+
75
+ expect(screen.getByRole('button', { name: 'Back' })).toBeInTheDocument()
76
+ })
77
+
78
+ it('does not render back button when onBack is not provided', () => {
79
+ const project = createProject()
80
+
81
+ const { container } = render(
82
+ <ProjectView project={project}>
83
+ <span>Content</span>
84
+ </ProjectView>
85
+ )
86
+
87
+ expect(container.querySelector('button')).not.toBeInTheDocument()
88
+ })
89
+ })
90
+
91
+ describe('ProjectView sections conditional rendering', () => {
92
+ it('does not render section when data is null', () => {
93
+ const project = createProject({ background: null })
94
+
95
+ const { container } = render(<ProjectView.Section project={project} name="background" />)
96
+
97
+ expect(container.querySelector('[data-folio-view-section]')).not.toBeInTheDocument()
98
+ })
99
+
100
+ it('does not render section when data is empty string', () => {
101
+ const project = createProject({ background: '' })
102
+
103
+ const { container } = render(<ProjectView.Section project={project} name="background" />)
104
+
105
+ expect(container.querySelector('[data-folio-view-section]')).not.toBeInTheDocument()
106
+ })
107
+
108
+ it('renders empty section wrapper when data is empty array', () => {
109
+ const project = createProject({ struggles: [] })
110
+
111
+ const { container } = render(<ProjectView.Section project={project} name="struggles" />)
112
+
113
+ expect(container.querySelector('[data-folio-view-section]')).toBeInTheDocument()
114
+ expect(container.querySelector('[data-folio-struggle]')).not.toBeInTheDocument()
115
+ })
116
+
117
+ it('renders background section when present', () => {
118
+ const project = createProject({ background: 'Background info' })
119
+
120
+ const { container } = render(<ProjectView.Section project={project} name="background" />)
121
+
122
+ expect(container.querySelector('[data-folio-view-section]')).toBeInTheDocument()
123
+ expect(screen.getByText('Background info')).toBeInTheDocument()
124
+ })
125
+
126
+ it('renders why section when present', () => {
127
+ const project = createProject({ why: 'Why this matters' })
128
+
129
+ const { container } = render(<ProjectView.Section project={project} name="why" />)
130
+
131
+ expect(container.querySelector('[data-folio-view-section]')).toBeInTheDocument()
132
+ expect(screen.getByText('Why this matters')).toBeInTheDocument()
133
+ })
134
+
135
+ it('renders struggles with correct attributes', () => {
136
+ const project = createProject()
137
+
138
+ const { container } = render(<ProjectView.Section project={project} name="struggles" />)
139
+
140
+ expect(container.querySelectorAll('[data-folio-struggle]')).toHaveLength(2)
141
+
142
+ const warnStruggle = container.querySelector('[data-folio-struggle-type="warn"]')
143
+ expect(warnStruggle).toBeInTheDocument()
144
+ expect(warnStruggle).toHaveTextContent('First challenge faced')
145
+
146
+ const errorStruggle = container.querySelector('[data-folio-struggle-type="error"]')
147
+ expect(errorStruggle).toBeInTheDocument()
148
+ expect(errorStruggle).toHaveTextContent('Critical issue encountered')
149
+ })
150
+
151
+ it('renders timeline with correct attributes', () => {
152
+ const project = createProject()
153
+
154
+ const { container } = render(<ProjectView.Section project={project} name="timeline" />)
155
+
156
+ expect(container.querySelectorAll('[data-folio-timeline-date]')).toHaveLength(2)
157
+ expect(container.querySelectorAll('[data-folio-timeline-note]')).toHaveLength(2)
158
+ expect(screen.getByText('2024-01-01')).toBeInTheDocument()
159
+ expect(screen.getByText('Project started')).toBeInTheDocument()
160
+ })
161
+
162
+ it('renders posts with correct attributes', () => {
163
+ const project = createProject()
164
+
165
+ const { container } = render(<ProjectView.Section project={project} name="posts" />)
166
+
167
+ expect(container.querySelectorAll('[data-folio-post-title]')).toHaveLength(2)
168
+ expect(container.querySelectorAll('[data-folio-post-date]')).toHaveLength(2)
169
+
170
+ expect(screen.getByText('Announcement Post')).toBeInTheDocument()
171
+
172
+ const postLink = container.querySelector('[data-folio-post-link]')
173
+ expect(postLink).toBeInTheDocument()
174
+ expect(postLink).toHaveAttribute('href', 'https://blog.com/post1')
175
+ })
176
+
177
+ it('renders posts without url without link', () => {
178
+ const project = createProject({
179
+ posts: [{ title: 'No URL Post', date: '2024-01-01' }]
180
+ })
181
+
182
+ const { container } = render(<ProjectView.Section project={project} name="posts" />)
183
+
184
+ expect(container.querySelector('[data-folio-post-link]')).not.toBeInTheDocument()
185
+ expect(screen.getByText('No URL Post')).toBeInTheDocument()
186
+ })
187
+
188
+ it('renders stack with tag attributes', () => {
189
+ const project = createProject()
190
+
191
+ const { container } = render(<ProjectView.Section project={project} name="stack" />)
192
+
193
+ expect(container.querySelectorAll('[data-folio-tag]')).toHaveLength(3)
194
+ })
195
+ })
196
+
197
+ describe('ProjectView Links', () => {
198
+ it('renders links when present', () => {
199
+ const project = createProject()
200
+
201
+ const { container } = render(<ProjectView.Links project={project} />)
202
+
203
+ expect(container.querySelector('[data-folio-view-links]')).toBeInTheDocument()
204
+ expect(container.querySelector('[data-folio-link-type="github"]')).toBeInTheDocument()
205
+ expect(container.querySelector('[data-folio-link-type="live"]')).toBeInTheDocument()
206
+ })
207
+
208
+ it('does not render when no links', () => {
209
+ const project = createProject({ links: {} })
210
+
211
+ const { container } = render(<ProjectView.Links project={project} />)
212
+
213
+ expect(container.querySelector('[data-folio-view-links]')).not.toBeInTheDocument()
214
+ })
215
+
216
+ it('renders app store link', () => {
217
+ const project = createProject({
218
+ links: { appStore: 'https://apps.apple.com/app/test' }
219
+ })
220
+
221
+ const { container } = render(<ProjectView.Links project={project} />)
222
+
223
+ const link = container.querySelector('[data-folio-link-type="app-store"]')
224
+ expect(link).toBeInTheDocument()
225
+ expect(link).toHaveAttribute('href', 'https://apps.apple.com/app/test')
226
+ })
227
+
228
+ it('renders play store link', () => {
229
+ const project = createProject({
230
+ links: { playStore: 'https://play.google.com/store/apps/test' }
231
+ })
232
+
233
+ const { container } = render(<ProjectView.Links project={project} />)
234
+
235
+ const link = container.querySelector('[data-folio-link-type="play-store"]')
236
+ expect(link).toBeInTheDocument()
237
+ expect(link).toHaveAttribute('href', 'https://play.google.com/store/apps/test')
238
+ })
239
+
240
+ it('renders product hunt link', () => {
241
+ const project = createProject({
242
+ links: { productHunt: 'https://producthunt.com/posts/test' }
243
+ })
244
+
245
+ const { container } = render(<ProjectView.Links project={project} />)
246
+
247
+ const link = container.querySelector('[data-folio-link-type="product-hunt"]')
248
+ expect(link).toBeInTheDocument()
249
+ expect(link).toHaveAttribute('href', 'https://producthunt.com/posts/test')
250
+ })
251
+ })
252
+
253
+ describe('ProjectView Stats', () => {
254
+ it('renders stats when present', () => {
255
+ const project = createProject()
256
+
257
+ const { container } = render(<ProjectView.Stats project={project} />)
258
+
259
+ expect(container.querySelector('[data-folio-view-stats]')).toBeInTheDocument()
260
+ expect(screen.getByText('100 stars')).toBeInTheDocument()
261
+ expect(screen.getByText('20 forks')).toBeInTheDocument()
262
+ })
263
+
264
+ it('does not render when stats is null', () => {
265
+ const project = createProject({ stats: null })
266
+
267
+ const { container } = render(<ProjectView.Stats project={project} />)
268
+
269
+ expect(container.querySelector('[data-folio-view-stats]')).not.toBeInTheDocument()
270
+ })
271
+
272
+ it('does not render when stats object has no values', () => {
273
+ const project = createProject({ stats: {} })
274
+
275
+ const { container } = render(<ProjectView.Stats project={project} />)
276
+
277
+ expect(container.querySelector('[data-folio-view-stats]')).not.toBeInTheDocument()
278
+ })
279
+
280
+ it('renders npm stats', () => {
281
+ const project = createProject({
282
+ stats: { downloads: '5000', version: '3.0.0' }
283
+ })
284
+
285
+ render(<ProjectView.Stats project={project} />)
286
+
287
+ expect(screen.getByText('5000 downloads')).toBeInTheDocument()
288
+ expect(screen.getByText('3.0.0')).toBeInTheDocument()
289
+ })
290
+
291
+ it('renders product hunt stats', () => {
292
+ const project = createProject({
293
+ stats: { upvotes: 300, comments: 45 }
294
+ })
295
+
296
+ render(<ProjectView.Stats project={project} />)
297
+
298
+ expect(screen.getByText('300 upvotes')).toBeInTheDocument()
299
+ expect(screen.getByText('45 comments')).toBeInTheDocument()
300
+ })
301
+ })
302
+
303
+ describe('ProjectView data attributes', () => {
304
+ it('has correct data-folio-view attribute on root', () => {
305
+ const project = createProject()
306
+
307
+ const { container } = render(
308
+ <ProjectView project={project}>
309
+ <span>Content</span>
310
+ </ProjectView>
311
+ )
312
+
313
+ expect(container.querySelector('[data-folio-view]')).toBeInTheDocument()
314
+ })
315
+
316
+ it('has correct data-folio-view-section attributes', () => {
317
+ const project = createProject()
318
+
319
+ const { container } = render(<ProjectView.Section project={project} name="background" />)
320
+
321
+ const section = container.querySelector('[data-folio-view-section]')
322
+ expect(section).toBeInTheDocument()
323
+ expect(section).toHaveAttribute('data-folio-view-section-name', 'background')
324
+ })
325
+
326
+ it('has correct data-folio-view-links attribute', () => {
327
+ const project = createProject()
328
+
329
+ const { container } = render(<ProjectView.Links project={project} />)
330
+
331
+ expect(container.querySelector('[data-folio-view-links]')).toBeInTheDocument()
332
+ })
333
+
334
+ it('has correct data-folio-view-stats attribute', () => {
335
+ const project = createProject()
336
+
337
+ const { container } = render(<ProjectView.Stats project={project} />)
338
+
339
+ expect(container.querySelector('[data-folio-view-stats]')).toBeInTheDocument()
340
+ })
341
+ })
@@ -0,0 +1,168 @@
1
+ import type { FolioProject } from '../../types'
2
+
3
+ type SectionName = keyof Pick<FolioProject, 'description' | 'background' | 'why' | 'stack' | 'struggles' | 'timeline' | 'posts'>
4
+
5
+ function isValidSectionName(name: string): name is SectionName {
6
+ return ['description', 'background', 'why', 'stack', 'struggles', 'timeline', 'posts'].includes(name)
7
+ }
8
+
9
+ function ProjectView({
10
+ project,
11
+ onBack,
12
+ children
13
+ }: {
14
+ project: FolioProject
15
+ onBack?: () => void
16
+ children: React.ReactNode
17
+ }) {
18
+ return (
19
+ <div data-folio-view>
20
+ {onBack && <button onClick={onBack}>Back</button>}
21
+ <h2>{project.name}</h2>
22
+ {children}
23
+ </div>
24
+ )
25
+ }
26
+
27
+ ProjectView.Section = function ProjectViewSection({
28
+ project,
29
+ name
30
+ }: {
31
+ project: FolioProject
32
+ name: string
33
+ }) {
34
+ if (!isValidSectionName(name)) return null
35
+
36
+ const sectionData = project[name]
37
+
38
+ if (!sectionData) return null
39
+
40
+ return (
41
+ <div data-folio-view-section data-folio-view-section-name={name}>
42
+ {typeof sectionData === 'string' && <>{sectionData}</>}
43
+ {Array.isArray(sectionData) &&
44
+ sectionData.map((item, index) => {
45
+ if (typeof item === 'string') {
46
+ return <span key={index} data-folio-tag>{item}</span>
47
+ }
48
+ if ('type' in item && 'text' in item) {
49
+ const struggle = item as { type: string; text: string }
50
+ return (
51
+ <div key={index} data-folio-struggle data-folio-struggle-type={struggle.type}>
52
+ {struggle.text}
53
+ </div>
54
+ )
55
+ }
56
+ if ('date' in item && ('note' in item || 'title' in item)) {
57
+ if ('note' in item) {
58
+ const timeline = item as { date: string; note: string }
59
+ return (
60
+ <div key={index}>
61
+ <span data-folio-timeline-date>{timeline.date}</span>
62
+ <span data-folio-timeline-note>{timeline.note}</span>
63
+ </div>
64
+ )
65
+ }
66
+ if ('title' in item) {
67
+ const post = item as { title: string; date: string; url?: string }
68
+ return (
69
+ <div key={index}>
70
+ <span data-folio-post-title>{post.title}</span>
71
+ <span data-folio-post-date>{post.date}</span>
72
+ {post.url && (
73
+ <a href={post.url} data-folio-post-link>
74
+ Link
75
+ </a>
76
+ )}
77
+ </div>
78
+ )
79
+ }
80
+ }
81
+ return null
82
+ })}
83
+ </div>
84
+ )
85
+ }
86
+
87
+ ProjectView.Links = function ProjectViewLinks({ project }: { project: FolioProject }) {
88
+ const links = project.links
89
+
90
+ if (
91
+ !links.github &&
92
+ !links.live &&
93
+ !links.npm &&
94
+ !links.appStore &&
95
+ !links.playStore &&
96
+ !links.productHunt
97
+ ) {
98
+ return null
99
+ }
100
+
101
+ return (
102
+ <div data-folio-view-links>
103
+ {links.github && (
104
+ <a href={links.github} data-folio-link data-folio-link-type="github">
105
+ GitHub
106
+ </a>
107
+ )}
108
+ {links.live && (
109
+ <a href={links.live} data-folio-link data-folio-link-type="live">
110
+ Live
111
+ </a>
112
+ )}
113
+ {links.npm && (
114
+ <a href={links.npm} data-folio-link data-folio-link-type="npm">
115
+ npm
116
+ </a>
117
+ )}
118
+ {links.appStore && (
119
+ <a href={links.appStore} data-folio-link data-folio-link-type="app-store">
120
+ App Store
121
+ </a>
122
+ )}
123
+ {links.playStore && (
124
+ <a href={links.playStore} data-folio-link data-folio-link-type="play-store">
125
+ Play Store
126
+ </a>
127
+ )}
128
+ {links.productHunt && (
129
+ <a href={links.productHunt} data-folio-link data-folio-link-type="product-hunt">
130
+ Product Hunt
131
+ </a>
132
+ )}
133
+ </div>
134
+ )
135
+ }
136
+
137
+ ProjectView.Stats = function ProjectViewStats({ project }: { project: FolioProject }) {
138
+ if (!project.stats) {
139
+ return null
140
+ }
141
+
142
+ const hasStats =
143
+ project.stats.stars ||
144
+ project.stats.forks ||
145
+ project.stats.downloads ||
146
+ project.stats.version ||
147
+ project.stats.upvotes ||
148
+ project.stats.comments
149
+
150
+ if (!hasStats) {
151
+ return null
152
+ }
153
+
154
+ return (
155
+ <div data-folio-view-stats>
156
+ {project.stats.stars && <span data-folio-stat="stars">{project.stats.stars} stars</span>}
157
+ {project.stats.forks && <span data-folio-stat="forks">{project.stats.forks} forks</span>}
158
+ {project.stats.downloads && (
159
+ <span data-folio-stat="downloads">{project.stats.downloads} downloads</span>
160
+ )}
161
+ {project.stats.version && <span data-folio-stat="version">{project.stats.version}</span>}
162
+ {project.stats.upvotes && <span data-folio-stat="upvotes">{project.stats.upvotes} upvotes</span>}
163
+ {project.stats.comments && <span data-folio-stat="comments">{project.stats.comments} comments</span>}
164
+ </div>
165
+ )
166
+ }
167
+
168
+ export { ProjectView }
@@ -0,0 +1 @@
1
+ export { ProjectView } from './ProjectView'
@@ -0,0 +1,4 @@
1
+ export { FeaturedProject } from './FeaturedProject'
2
+ export { ProjectCard } from './ProjectCard'
3
+ export { ProjectList } from './ProjectList'
4
+ export { ProjectView } from './ProjectView'
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export * from './components'
2
+ export * from './lib'
3
+ export * from './types'
@@ -0,0 +1,112 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { defineProjects } from '../defineProjects'
3
+ import type { FolioProjectInput } from '../../types'
4
+
5
+ describe('defineProjects', () => {
6
+ it('should return the input array unchanged', () => {
7
+ const projects: FolioProjectInput[] = [
8
+ {
9
+ id: 'project-1',
10
+ type: 'github',
11
+ repo: 'user/repo1',
12
+ status: 'active',
13
+ },
14
+ {
15
+ id: 'project-2',
16
+ type: 'manual',
17
+ status: 'shipped',
18
+ name: 'Manual Project',
19
+ },
20
+ ]
21
+
22
+ const result = defineProjects(projects)
23
+
24
+ expect(result).toBe(projects)
25
+ expect(result).toHaveLength(2)
26
+ })
27
+
28
+ it('should return an empty array when given an empty array', () => {
29
+ const result = defineProjects([])
30
+
31
+ expect(result).toEqual([])
32
+ expect(result).toHaveLength(0)
33
+ })
34
+
35
+ it('should handle single project', () => {
36
+ const projects: FolioProjectInput[] = [
37
+ {
38
+ id: 'single',
39
+ type: 'npm',
40
+ package: 'my-package',
41
+ status: 'active',
42
+ },
43
+ ]
44
+
45
+ const result = defineProjects(projects)
46
+
47
+ expect(result).toHaveLength(1)
48
+ expect(result[0].id).toBe('single')
49
+ expect(result[0].type).toBe('npm')
50
+ })
51
+
52
+ it('should handle all project types', () => {
53
+ const projects: FolioProjectInput[] = [
54
+ { id: 'gh', type: 'github', repo: 'user/repo', status: 'active' },
55
+ { id: 'man', type: 'manual', status: 'active' },
56
+ { id: 'npm', type: 'npm', package: 'pkg', status: 'active' },
57
+ { id: 'ph', type: 'product-hunt', slug: 'product', status: 'active' },
58
+ { id: 'hyb', type: 'hybrid', repo: 'user/repo', package: 'pkg', status: 'active' },
59
+ ]
60
+
61
+ const result = defineProjects(projects)
62
+
63
+ expect(result).toHaveLength(5)
64
+ expect(result.map(p => p.type)).toEqual(['github', 'manual', 'npm', 'product-hunt', 'hybrid'])
65
+ })
66
+
67
+ it('should preserve all project properties', () => {
68
+ const projects: FolioProjectInput[] = [
69
+ {
70
+ id: 'full-project',
71
+ type: 'github',
72
+ repo: 'user/repo',
73
+ status: 'shipped',
74
+ featured: true,
75
+ name: 'Full Project',
76
+ tagline: 'A complete project',
77
+ description: 'Full description',
78
+ background: 'Background',
79
+ why: 'Why',
80
+ image: 'image.png',
81
+ struggles: [{ type: 'warn', text: 'Warning' }],
82
+ timeline: [{ date: '2024-01-01', note: 'Start' }],
83
+ posts: [{ title: 'Post', date: '2024-01-01' }],
84
+ stack: ['React', 'TypeScript'],
85
+ links: { live: 'https://example.com' },
86
+ stats: { stars: 100 },
87
+ createdAt: '2024-01-01',
88
+ updatedAt: '2024-06-01',
89
+ },
90
+ ]
91
+
92
+ const result = defineProjects(projects)
93
+
94
+ expect(result[0]).toEqual(projects[0])
95
+ })
96
+
97
+ it('should provide type safety for project configs', () => {
98
+ const githubProject: FolioProjectInput = {
99
+ id: 'typed-project',
100
+ type: 'github',
101
+ repo: 'user/repo',
102
+ status: 'active',
103
+ }
104
+
105
+ const result = defineProjects([githubProject])
106
+
107
+ expect(result[0].type).toBe('github')
108
+ if (result[0].type === 'github') {
109
+ expect(result[0].repo).toBe('user/repo')
110
+ }
111
+ })
112
+ })