@commonpub/layer 0.1.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 (436) hide show
  1. package/LICENSE +661 -0
  2. package/app.vue +7 -0
  3. package/components/AnnouncementBand.vue +117 -0
  4. package/components/AppToast.vue +108 -0
  5. package/components/AuthorCard.vue +119 -0
  6. package/components/AuthorRow.vue +81 -0
  7. package/components/CommentSection.vue +330 -0
  8. package/components/ContentCard.vue +340 -0
  9. package/components/ContentPicker.vue +240 -0
  10. package/components/ContentStarterForm.vue +214 -0
  11. package/components/ContentTypeBadge.vue +46 -0
  12. package/components/CountdownTimer.vue +68 -0
  13. package/components/CpubEditor.vue +87 -0
  14. package/components/DiscussionItem.vue +191 -0
  15. package/components/EditorPropertiesPanel.vue +393 -0
  16. package/components/EngagementBar.vue +131 -0
  17. package/components/FederatedContentCard.vue +291 -0
  18. package/components/FeedItem.vue +283 -0
  19. package/components/FilterChip.vue +21 -0
  20. package/components/HeatmapGrid.vue +92 -0
  21. package/components/ImageUpload.vue +219 -0
  22. package/components/MemberCard.vue +163 -0
  23. package/components/MessageThread.vue +120 -0
  24. package/components/NotificationItem.vue +103 -0
  25. package/components/ProgressTracker.vue +41 -0
  26. package/components/PublishErrorsModal.vue +116 -0
  27. package/components/RemoteActorCard.vue +206 -0
  28. package/components/RemoteUserSearch.vue +117 -0
  29. package/components/SearchFilters.vue +188 -0
  30. package/components/SearchSidebar.vue +181 -0
  31. package/components/SectionHeader.vue +17 -0
  32. package/components/ShareToHubModal.vue +189 -0
  33. package/components/SiteLogo.vue +21 -0
  34. package/components/SkillBar.vue +57 -0
  35. package/components/SortSelect.vue +30 -0
  36. package/components/StatBar.vue +14 -0
  37. package/components/TOCNav.vue +69 -0
  38. package/components/TimelineItem.vue +82 -0
  39. package/components/VideoCard.vue +106 -0
  40. package/components/blocks/BlockBuildStepView.vue +92 -0
  41. package/components/blocks/BlockCalloutView.vue +82 -0
  42. package/components/blocks/BlockCheckpointView.vue +50 -0
  43. package/components/blocks/BlockCodeView.vue +212 -0
  44. package/components/blocks/BlockContentRenderer.vue +143 -0
  45. package/components/blocks/BlockDividerView.vue +11 -0
  46. package/components/blocks/BlockDownloadsView.vue +126 -0
  47. package/components/blocks/BlockEmbedView.vue +61 -0
  48. package/components/blocks/BlockGalleryView.vue +57 -0
  49. package/components/blocks/BlockHeadingView.vue +29 -0
  50. package/components/blocks/BlockImageView.vue +34 -0
  51. package/components/blocks/BlockMarkdownView.vue +118 -0
  52. package/components/blocks/BlockMathView.vue +45 -0
  53. package/components/blocks/BlockPartsListView.vue +104 -0
  54. package/components/blocks/BlockQuizView.vue +239 -0
  55. package/components/blocks/BlockQuoteView.vue +41 -0
  56. package/components/blocks/BlockSectionHeaderView.vue +58 -0
  57. package/components/blocks/BlockSliderView.vue +236 -0
  58. package/components/blocks/BlockTextView.vue +41 -0
  59. package/components/blocks/BlockToolListView.vue +87 -0
  60. package/components/blocks/BlockVideoView.vue +89 -0
  61. package/components/editors/ArticleEditor.vue +545 -0
  62. package/components/editors/BlockCanvas.vue +487 -0
  63. package/components/editors/BlockInsertZone.vue +84 -0
  64. package/components/editors/BlockPicker.vue +285 -0
  65. package/components/editors/BlockWrapper.vue +192 -0
  66. package/components/editors/BlogEditor.vue +567 -0
  67. package/components/editors/EditorBlocks.vue +248 -0
  68. package/components/editors/EditorSection.vue +81 -0
  69. package/components/editors/EditorShell.vue +168 -0
  70. package/components/editors/EditorTagInput.vue +114 -0
  71. package/components/editors/EditorVisibility.vue +110 -0
  72. package/components/editors/ExplainerEditor.vue +503 -0
  73. package/components/editors/MarkdownImportDialog.vue +249 -0
  74. package/components/editors/ProjectEditor.vue +446 -0
  75. package/components/editors/blocks/BuildStepBlock.vue +102 -0
  76. package/components/editors/blocks/CalloutBlock.vue +122 -0
  77. package/components/editors/blocks/CheckpointBlock.vue +27 -0
  78. package/components/editors/blocks/CodeBlock.vue +177 -0
  79. package/components/editors/blocks/DividerBlock.vue +22 -0
  80. package/components/editors/blocks/DownloadsBlock.vue +41 -0
  81. package/components/editors/blocks/EmbedBlock.vue +20 -0
  82. package/components/editors/blocks/GalleryBlock.vue +236 -0
  83. package/components/editors/blocks/HeadingBlock.vue +96 -0
  84. package/components/editors/blocks/ImageBlock.vue +271 -0
  85. package/components/editors/blocks/MarkdownBlock.vue +258 -0
  86. package/components/editors/blocks/MathBlock.vue +37 -0
  87. package/components/editors/blocks/PartsListBlock.vue +358 -0
  88. package/components/editors/blocks/QuizBlock.vue +47 -0
  89. package/components/editors/blocks/QuoteBlock.vue +101 -0
  90. package/components/editors/blocks/SectionHeaderBlock.vue +130 -0
  91. package/components/editors/blocks/SliderBlock.vue +318 -0
  92. package/components/editors/blocks/TextBlock.vue +201 -0
  93. package/components/editors/blocks/ToolListBlock.vue +70 -0
  94. package/components/editors/blocks/VideoBlock.vue +22 -0
  95. package/components/hub/HubDiscussions.vue +47 -0
  96. package/components/hub/HubFeed.vue +199 -0
  97. package/components/hub/HubHero.vue +185 -0
  98. package/components/hub/HubLayout.vue +103 -0
  99. package/components/hub/HubMembers.vue +40 -0
  100. package/components/hub/HubProducts.vue +93 -0
  101. package/components/hub/HubProjects.vue +207 -0
  102. package/components/hub/HubSidebar.vue +12 -0
  103. package/components/hub/HubSidebarCard.vue +35 -0
  104. package/components/views/ArticleView.vue +771 -0
  105. package/components/views/BlogView.vue +667 -0
  106. package/components/views/ExplainerView.vue +688 -0
  107. package/components/views/ProjectView.vue +1500 -0
  108. package/composables/useApiError.ts +39 -0
  109. package/composables/useAuth.ts +87 -0
  110. package/composables/useBlockEditor.ts +187 -0
  111. package/composables/useContentSave.ts +253 -0
  112. package/composables/useContentTypes.ts +37 -0
  113. package/composables/useEngagement.ts +196 -0
  114. package/composables/useFeatures.ts +33 -0
  115. package/composables/useFederation.ts +72 -0
  116. package/composables/useJsonLd.ts +183 -0
  117. package/composables/useMarkdownImport.ts +77 -0
  118. package/composables/useMirrorContent.ts +105 -0
  119. package/composables/useNotifications.ts +73 -0
  120. package/composables/usePublishValidation.ts +65 -0
  121. package/composables/useSanitize.ts +34 -0
  122. package/composables/useSiteName.ts +4 -0
  123. package/composables/useTheme.ts +34 -0
  124. package/composables/useToast.ts +35 -0
  125. package/error.vue +129 -0
  126. package/layouts/admin.vue +213 -0
  127. package/layouts/auth.vue +63 -0
  128. package/layouts/default.vue +269 -0
  129. package/layouts/editor.vue +129 -0
  130. package/middleware/auth.ts +6 -0
  131. package/nuxt.config.ts +83 -0
  132. package/package.json +59 -0
  133. package/pages/[type]/[slug]/edit.vue +676 -0
  134. package/pages/[type]/[slug]/index.vue +313 -0
  135. package/pages/[type]/index.vue +118 -0
  136. package/pages/about.vue +100 -0
  137. package/pages/admin/audit.vue +66 -0
  138. package/pages/admin/content.vue +116 -0
  139. package/pages/admin/federation.vue +446 -0
  140. package/pages/admin/index.vue +62 -0
  141. package/pages/admin/reports.vue +88 -0
  142. package/pages/admin/settings.vue +167 -0
  143. package/pages/admin/users.vue +145 -0
  144. package/pages/auth/forgot-password.vue +103 -0
  145. package/pages/auth/login.vue +216 -0
  146. package/pages/auth/oauth/authorize.vue +178 -0
  147. package/pages/auth/register.vue +246 -0
  148. package/pages/auth/reset-password.vue +124 -0
  149. package/pages/auth/verify-email.vue +80 -0
  150. package/pages/cert/[code].vue +154 -0
  151. package/pages/contests/[slug]/edit.vue +153 -0
  152. package/pages/contests/[slug]/index.vue +556 -0
  153. package/pages/contests/[slug]/judge.vue +160 -0
  154. package/pages/contests/create.vue +192 -0
  155. package/pages/contests/index.vue +69 -0
  156. package/pages/create.vue +219 -0
  157. package/pages/dashboard.vue +521 -0
  158. package/pages/docs/[siteSlug]/[...pagePath].vue +704 -0
  159. package/pages/docs/[siteSlug]/edit.vue +480 -0
  160. package/pages/docs/[siteSlug]/index.vue +380 -0
  161. package/pages/docs/create.vue +60 -0
  162. package/pages/docs/index.vue +181 -0
  163. package/pages/explore.vue +422 -0
  164. package/pages/federated-hubs/[id]/index.vue +385 -0
  165. package/pages/federated-hubs/[id]/posts/[postId].vue +309 -0
  166. package/pages/federation/index.vue +157 -0
  167. package/pages/federation/search.vue +43 -0
  168. package/pages/federation/users/[handle].vue +221 -0
  169. package/pages/feed.vue +135 -0
  170. package/pages/hubs/[slug]/index.vue +513 -0
  171. package/pages/hubs/[slug]/members.vue +134 -0
  172. package/pages/hubs/[slug]/posts/[postId].vue +352 -0
  173. package/pages/hubs/[slug]/settings.vue +254 -0
  174. package/pages/hubs/create.vue +201 -0
  175. package/pages/hubs/index.vue +207 -0
  176. package/pages/index.vue +1005 -0
  177. package/pages/learn/[slug]/[lessonSlug]/edit.vue +413 -0
  178. package/pages/learn/[slug]/[lessonSlug]/index.vue +438 -0
  179. package/pages/learn/[slug]/edit.vue +414 -0
  180. package/pages/learn/[slug]/index.vue +341 -0
  181. package/pages/learn/create.vue +71 -0
  182. package/pages/learn/index.vue +360 -0
  183. package/pages/messages/[conversationId].vue +113 -0
  184. package/pages/messages/index.vue +303 -0
  185. package/pages/mirror/[id].vue +115 -0
  186. package/pages/notifications.vue +91 -0
  187. package/pages/products/[slug].vue +128 -0
  188. package/pages/products/index.vue +122 -0
  189. package/pages/search.vue +692 -0
  190. package/pages/settings/account.vue +170 -0
  191. package/pages/settings/appearance.vue +80 -0
  192. package/pages/settings/index.vue +81 -0
  193. package/pages/settings/notifications.vue +68 -0
  194. package/pages/settings/profile.vue +838 -0
  195. package/pages/tags/[slug].vue +111 -0
  196. package/pages/tags/index.vue +73 -0
  197. package/pages/u/[username]/followers.vue +86 -0
  198. package/pages/u/[username]/following.vue +94 -0
  199. package/pages/u/[username]/index.vue +837 -0
  200. package/pages/videos/[id].vue +212 -0
  201. package/pages/videos/index.vue +327 -0
  202. package/pages/videos/submit.vue +112 -0
  203. package/plugins/auth.ts +23 -0
  204. package/server/api/admin/audit.get.ts +17 -0
  205. package/server/api/admin/content/[id].delete.ts +15 -0
  206. package/server/api/admin/content/[id].patch.ts +37 -0
  207. package/server/api/admin/federation/activity.get.ts +31 -0
  208. package/server/api/admin/federation/clients.get.ts +9 -0
  209. package/server/api/admin/federation/clients.post.ts +16 -0
  210. package/server/api/admin/federation/hub-mirrors/index.get.ts +10 -0
  211. package/server/api/admin/federation/hub-mirrors/index.post.ts +42 -0
  212. package/server/api/admin/federation/mirrors/[id]/backfill.post.ts +39 -0
  213. package/server/api/admin/federation/mirrors/[id].delete.ts +11 -0
  214. package/server/api/admin/federation/mirrors/[id].get.ts +15 -0
  215. package/server/api/admin/federation/mirrors/[id].put.ts +22 -0
  216. package/server/api/admin/federation/mirrors/index.get.ts +9 -0
  217. package/server/api/admin/federation/mirrors/index.post.ts +30 -0
  218. package/server/api/admin/federation/pending.get.ts +22 -0
  219. package/server/api/admin/federation/refederate.post.ts +92 -0
  220. package/server/api/admin/federation/repair-types.post.ts +57 -0
  221. package/server/api/admin/federation/retry.post.ts +41 -0
  222. package/server/api/admin/federation/stats.get.ts +26 -0
  223. package/server/api/admin/reports/[id]/resolve.post.ts +12 -0
  224. package/server/api/admin/reports.get.ts +17 -0
  225. package/server/api/admin/settings.get.ts +22 -0
  226. package/server/api/admin/settings.put.ts +11 -0
  227. package/server/api/admin/stats.get.ts +9 -0
  228. package/server/api/admin/users/[id]/role.put.ts +12 -0
  229. package/server/api/admin/users/[id]/status.put.ts +12 -0
  230. package/server/api/admin/users/[id].delete.ts +10 -0
  231. package/server/api/admin/users.get.ts +18 -0
  232. package/server/api/auth/federated/callback.get.ts +67 -0
  233. package/server/api/auth/federated/login.post.ts +60 -0
  234. package/server/api/auth/oauth2/authorize.get.ts +30 -0
  235. package/server/api/auth/oauth2/authorize.post.ts +51 -0
  236. package/server/api/auth/oauth2/register.post.ts +41 -0
  237. package/server/api/auth/oauth2/token.post.ts +48 -0
  238. package/server/api/cert/[code].get.ts +13 -0
  239. package/server/api/content/[id]/build.post.ts +9 -0
  240. package/server/api/content/[id]/fork.post.ts +10 -0
  241. package/server/api/content/[id]/index.delete.ts +18 -0
  242. package/server/api/content/[id]/index.get.ts +15 -0
  243. package/server/api/content/[id]/index.put.ts +23 -0
  244. package/server/api/content/[id]/products/[productId].delete.ts +28 -0
  245. package/server/api/content/[id]/products-sync.post.ts +29 -0
  246. package/server/api/content/[id]/products.get.ts +9 -0
  247. package/server/api/content/[id]/products.post.ts +31 -0
  248. package/server/api/content/[id]/publish.post.ts +17 -0
  249. package/server/api/content/[id]/report.post.ts +17 -0
  250. package/server/api/content/[id]/versions.get.ts +9 -0
  251. package/server/api/content/[id]/view.post.ts +34 -0
  252. package/server/api/content/index.get.ts +23 -0
  253. package/server/api/content/index.post.ts +11 -0
  254. package/server/api/contests/[slug]/entries.get.ts +18 -0
  255. package/server/api/contests/[slug]/entries.post.ts +23 -0
  256. package/server/api/contests/[slug]/index.delete.ts +21 -0
  257. package/server/api/contests/[slug]/index.get.ts +11 -0
  258. package/server/api/contests/[slug]/index.put.ts +15 -0
  259. package/server/api/contests/[slug]/judge.post.ts +12 -0
  260. package/server/api/contests/[slug]/transition.post.ts +24 -0
  261. package/server/api/contests/index.get.ts +10 -0
  262. package/server/api/contests/index.post.ts +28 -0
  263. package/server/api/docs/[siteSlug]/index.delete.ts +14 -0
  264. package/server/api/docs/[siteSlug]/index.get.ts +17 -0
  265. package/server/api/docs/[siteSlug]/index.put.ts +20 -0
  266. package/server/api/docs/[siteSlug]/nav.get.ts +26 -0
  267. package/server/api/docs/[siteSlug]/pages/[pageId].delete.ts +14 -0
  268. package/server/api/docs/[siteSlug]/pages/[pageId].get.ts +31 -0
  269. package/server/api/docs/[siteSlug]/pages/[pageId].put.ts +15 -0
  270. package/server/api/docs/[siteSlug]/pages/index.get.ts +34 -0
  271. package/server/api/docs/[siteSlug]/pages/index.post.ts +28 -0
  272. package/server/api/docs/[siteSlug]/pages/reorder.post.ts +26 -0
  273. package/server/api/docs/[siteSlug]/search.get.ts +20 -0
  274. package/server/api/docs/[siteSlug]/versions.post.ts +11 -0
  275. package/server/api/docs/index.get.ts +6 -0
  276. package/server/api/docs/index.post.ts +10 -0
  277. package/server/api/federated-hubs/[id]/posts/[postId].get.ts +16 -0
  278. package/server/api/federated-hubs/[id]/posts.get.ts +15 -0
  279. package/server/api/federated-hubs/[id].get.ts +16 -0
  280. package/server/api/federation/boost.post.ts +21 -0
  281. package/server/api/federation/content/[id].get.ts +15 -0
  282. package/server/api/federation/follow.post.ts +16 -0
  283. package/server/api/federation/health.get.ts +56 -0
  284. package/server/api/federation/hub-follow.post.ts +27 -0
  285. package/server/api/federation/hub-post-like.post.ts +115 -0
  286. package/server/api/federation/hub-post-likes.get.ts +24 -0
  287. package/server/api/federation/hub-post-reply.post.ts +42 -0
  288. package/server/api/federation/hub-post.post.ts +33 -0
  289. package/server/api/federation/like.post.ts +21 -0
  290. package/server/api/federation/remote-actor.get.ts +22 -0
  291. package/server/api/federation/reply.post.ts +22 -0
  292. package/server/api/federation/search.post.ts +17 -0
  293. package/server/api/federation/timeline.get.ts +22 -0
  294. package/server/api/federation/unfollow.post.ts +17 -0
  295. package/server/api/files/[id].delete.ts +35 -0
  296. package/server/api/files/mine.get.ts +31 -0
  297. package/server/api/files/upload-from-url.post.ts +68 -0
  298. package/server/api/files/upload.post.ts +105 -0
  299. package/server/api/health.get.ts +4 -0
  300. package/server/api/hubs/[slug]/bans/[userId].delete.ts +13 -0
  301. package/server/api/hubs/[slug]/bans.get.ts +20 -0
  302. package/server/api/hubs/[slug]/bans.post.ts +23 -0
  303. package/server/api/hubs/[slug]/feed.xml.get.ts +60 -0
  304. package/server/api/hubs/[slug]/gallery.get.ts +21 -0
  305. package/server/api/hubs/[slug]/index.delete.ts +18 -0
  306. package/server/api/hubs/[slug]/index.get.ts +14 -0
  307. package/server/api/hubs/[slug]/index.put.ts +22 -0
  308. package/server/api/hubs/[slug]/invites.get.ts +20 -0
  309. package/server/api/hubs/[slug]/invites.post.ts +27 -0
  310. package/server/api/hubs/[slug]/join.post.ts +17 -0
  311. package/server/api/hubs/[slug]/leave.post.ts +13 -0
  312. package/server/api/hubs/[slug]/members/[userId].delete.ts +13 -0
  313. package/server/api/hubs/[slug]/members/[userId].put.ts +16 -0
  314. package/server/api/hubs/[slug]/members.get.ts +20 -0
  315. package/server/api/hubs/[slug]/posts/[postId]/like.post.ts +21 -0
  316. package/server/api/hubs/[slug]/posts/[postId]/lock.post.ts +17 -0
  317. package/server/api/hubs/[slug]/posts/[postId]/pin.post.ts +17 -0
  318. package/server/api/hubs/[slug]/posts/[postId]/replies.get.ts +16 -0
  319. package/server/api/hubs/[slug]/posts/[postId]/replies.post.ts +21 -0
  320. package/server/api/hubs/[slug]/posts/[postId].delete.ts +23 -0
  321. package/server/api/hubs/[slug]/posts/[postId].get.ts +16 -0
  322. package/server/api/hubs/[slug]/posts/index.get.ts +16 -0
  323. package/server/api/hubs/[slug]/posts/index.post.ts +36 -0
  324. package/server/api/hubs/[slug]/products.get.ts +26 -0
  325. package/server/api/hubs/[slug]/products.post.ts +18 -0
  326. package/server/api/hubs/[slug]/share.post.ts +39 -0
  327. package/server/api/hubs/index.get.ts +15 -0
  328. package/server/api/hubs/index.post.ts +11 -0
  329. package/server/api/image-proxy.get.ts +91 -0
  330. package/server/api/learn/[slug]/[lessonSlug]/complete.post.ts +13 -0
  331. package/server/api/learn/[slug]/[lessonSlug]/index.get.ts +68 -0
  332. package/server/api/learn/[slug]/enroll.post.ts +12 -0
  333. package/server/api/learn/[slug]/index.delete.ts +12 -0
  334. package/server/api/learn/[slug]/index.get.ts +14 -0
  335. package/server/api/learn/[slug]/index.put.ts +19 -0
  336. package/server/api/learn/[slug]/lessons/[lessonId].delete.ts +14 -0
  337. package/server/api/learn/[slug]/lessons/[lessonId].put.ts +24 -0
  338. package/server/api/learn/[slug]/lessons.post.ts +10 -0
  339. package/server/api/learn/[slug]/modules/[moduleId].delete.ts +14 -0
  340. package/server/api/learn/[slug]/modules/[moduleId].put.ts +15 -0
  341. package/server/api/learn/[slug]/modules.post.ts +14 -0
  342. package/server/api/learn/[slug]/publish.post.ts +13 -0
  343. package/server/api/learn/[slug]/unenroll.post.ts +12 -0
  344. package/server/api/learn/certificates.get.ts +9 -0
  345. package/server/api/learn/enrollments.get.ts +9 -0
  346. package/server/api/learn/index.get.ts +17 -0
  347. package/server/api/learn/index.post.ts +11 -0
  348. package/server/api/me.get.ts +13 -0
  349. package/server/api/messages/[conversationId]/info.get.ts +43 -0
  350. package/server/api/messages/[conversationId]/stream.get.ts +73 -0
  351. package/server/api/messages/[conversationId].get.ts +13 -0
  352. package/server/api/messages/[conversationId].post.ts +12 -0
  353. package/server/api/messages/index.get.ts +39 -0
  354. package/server/api/messages/index.post.ts +58 -0
  355. package/server/api/notifications/[id].delete.ts +11 -0
  356. package/server/api/notifications/count.get.ts +10 -0
  357. package/server/api/notifications/index.get.ts +24 -0
  358. package/server/api/notifications/read.post.ts +15 -0
  359. package/server/api/notifications/stream.get.ts +59 -0
  360. package/server/api/openapi.get.ts +5 -0
  361. package/server/api/products/[id].delete.ts +15 -0
  362. package/server/api/products/[id].put.ts +18 -0
  363. package/server/api/products/[slug]/content.get.ts +21 -0
  364. package/server/api/products/[slug].get.ts +16 -0
  365. package/server/api/products/index.get.ts +28 -0
  366. package/server/api/profile.get.ts +15 -0
  367. package/server/api/profile.put.ts +24 -0
  368. package/server/api/resolve-identity.post.ts +34 -0
  369. package/server/api/search/index.get.ts +24 -0
  370. package/server/api/search/trending.get.ts +22 -0
  371. package/server/api/social/bookmark.get.ts +16 -0
  372. package/server/api/social/bookmark.post.ts +15 -0
  373. package/server/api/social/bookmarks.get.ts +16 -0
  374. package/server/api/social/comments/[id].delete.ts +13 -0
  375. package/server/api/social/comments.get.ts +18 -0
  376. package/server/api/social/comments.post.ts +11 -0
  377. package/server/api/social/like.get.ts +17 -0
  378. package/server/api/social/like.post.ts +35 -0
  379. package/server/api/stats.get.ts +8 -0
  380. package/server/api/user/hubs.get.ts +22 -0
  381. package/server/api/users/[username]/content.get.ts +21 -0
  382. package/server/api/users/[username]/feed.xml.get.ts +65 -0
  383. package/server/api/users/[username]/follow.delete.ts +15 -0
  384. package/server/api/users/[username]/follow.post.ts +15 -0
  385. package/server/api/users/[username]/followers.get.ts +22 -0
  386. package/server/api/users/[username]/following.get.ts +22 -0
  387. package/server/api/users/[username]/learning.get.ts +18 -0
  388. package/server/api/users/[username].get.ts +25 -0
  389. package/server/api/users/index.get.ts +78 -0
  390. package/server/api/videos/[id].get.ts +18 -0
  391. package/server/api/videos/categories/[id].delete.ts +15 -0
  392. package/server/api/videos/categories/[id].put.ts +17 -0
  393. package/server/api/videos/categories.get.ts +7 -0
  394. package/server/api/videos/categories.post.ts +11 -0
  395. package/server/api/videos/index.get.ts +20 -0
  396. package/server/api/videos/index.post.ts +11 -0
  397. package/server/middleware/auth.ts +180 -0
  398. package/server/middleware/features.ts +31 -0
  399. package/server/middleware/security.ts +54 -0
  400. package/server/plugins/auto-admin.ts +69 -0
  401. package/server/plugins/federation-delivery.ts +74 -0
  402. package/server/routes/.well-known/nodeinfo.ts +15 -0
  403. package/server/routes/.well-known/webfinger.ts +86 -0
  404. package/server/routes/actor/followers.ts +34 -0
  405. package/server/routes/actor/following.ts +34 -0
  406. package/server/routes/actor/outbox.ts +31 -0
  407. package/server/routes/actor.ts +30 -0
  408. package/server/routes/feed.xml.ts +59 -0
  409. package/server/routes/hubs/[slug]/followers.ts +39 -0
  410. package/server/routes/hubs/[slug]/inbox.ts +35 -0
  411. package/server/routes/hubs/[slug]/outbox.ts +43 -0
  412. package/server/routes/hubs/[slug]/posts/[postId].ts +63 -0
  413. package/server/routes/hubs/[slug].ts +29 -0
  414. package/server/routes/inbox.ts +34 -0
  415. package/server/routes/nodeinfo/2.1.ts +27 -0
  416. package/server/routes/robots.txt.ts +19 -0
  417. package/server/routes/sitemap.xml.ts +105 -0
  418. package/server/routes/users/[username]/followers.ts +33 -0
  419. package/server/routes/users/[username]/following.ts +33 -0
  420. package/server/routes/users/[username]/inbox.ts +34 -0
  421. package/server/routes/users/[username]/outbox.ts +35 -0
  422. package/server/routes/users/[username].ts +62 -0
  423. package/server/utils/auth.ts +36 -0
  424. package/server/utils/db.ts +34 -0
  425. package/server/utils/errors.ts +24 -0
  426. package/server/utils/inbox.ts +122 -0
  427. package/server/utils/validate.ts +82 -0
  428. package/theme/base.css +283 -0
  429. package/theme/components.css +322 -0
  430. package/theme/dark.css +87 -0
  431. package/theme/editor-panels.css +63 -0
  432. package/theme/forms.css +216 -0
  433. package/theme/generics.css +68 -0
  434. package/theme/layouts.css +415 -0
  435. package/theme/prose.css +342 -0
  436. package/types/hub.ts +61 -0
@@ -0,0 +1,556 @@
1
+ <script setup lang="ts">
2
+ const route = useRoute();
3
+ const slug = route.params.slug as string;
4
+
5
+ const { data: contest } = useLazyFetch(`/api/contests/${slug}`);
6
+
7
+ useSeoMeta({
8
+ title: () => `${contest.value?.title || 'Contest'} — ${useSiteName()}`,
9
+ ogTitle: () => `${contest.value?.title || 'Contest'} — ${useSiteName()}`,
10
+ ogImage: '/og-default.png',
11
+ });
12
+
13
+ // Fetch entries from API
14
+ const { data: apiEntriesData } = useLazyFetch<{ items: any[]; total: number }>(`/api/contests/${slug}/entries`);
15
+
16
+ const c = computed(() => contest.value);
17
+
18
+ // Countdown timer
19
+ const countdown = ref({ days: '00', hours: '00', mins: '00', secs: '00' });
20
+ let countdownInterval: ReturnType<typeof setInterval> | null = null;
21
+
22
+ function pad(n: number): string { return String(n).padStart(2, '0'); }
23
+
24
+ function updateCountdown(): void {
25
+ const target = c.value?.endDate ? new Date(c.value.endDate) : new Date();
26
+ const now = new Date();
27
+ let diff = Math.max(0, Math.floor((target.getTime() - now.getTime()) / 1000));
28
+ const days = Math.floor(diff / 86400); diff %= 86400;
29
+ const hours = Math.floor(diff / 3600); diff %= 3600;
30
+ const mins = Math.floor(diff / 60);
31
+ const secs = diff % 60;
32
+ countdown.value = { days: pad(days), hours: pad(hours), mins: pad(mins), secs: pad(secs) };
33
+ }
34
+
35
+ onMounted(() => {
36
+ updateCountdown();
37
+ countdownInterval = setInterval(updateCountdown, 1000);
38
+ });
39
+
40
+ onUnmounted(() => {
41
+ if (countdownInterval) clearInterval(countdownInterval);
42
+ });
43
+
44
+ // FAQ accordion
45
+ const openFaq = ref<number>(0);
46
+ function toggleFaq(i: number): void {
47
+ openFaq.value = openFaq.value === i ? -1 : i;
48
+ }
49
+
50
+ // Vote state
51
+ const toast = useToast();
52
+ const votedEntries = ref<Set<string>>(new Set());
53
+ async function toggleVote(entryId: string): Promise<void> {
54
+ if (!isAuthenticated.value) {
55
+ toast.error('Log in to vote');
56
+ return;
57
+ }
58
+ try {
59
+ await $fetch('/api/social/like', {
60
+ method: 'POST',
61
+ body: { targetId: entryId, targetType: 'contestEntry' },
62
+ });
63
+ if (votedEntries.value.has(entryId)) votedEntries.value.delete(entryId);
64
+ else votedEntries.value.add(entryId);
65
+ } catch {
66
+ toast.error('Failed to vote');
67
+ }
68
+ }
69
+
70
+ const entries = computed(() => {
71
+ return apiEntriesData.value?.items ?? [];
72
+ });
73
+
74
+ const entryFilter = ref('all');
75
+ const filters = ['all', 'newest'];
76
+
77
+ // Admin contest management
78
+ const { isAuthenticated, isAdmin } = useAuth();
79
+ const transitioning = ref(false);
80
+
81
+ async function transitionStatus(newStatus: string): Promise<void> {
82
+ transitioning.value = true;
83
+ try {
84
+ await $fetch(`/api/contests/${slug}/transition`, {
85
+ method: 'POST',
86
+ body: { status: newStatus },
87
+ });
88
+ toast.success(`Contest ${newStatus}`);
89
+ refreshNuxtData();
90
+ } catch {
91
+ toast.error(`Failed to transition to ${newStatus}`);
92
+ } finally {
93
+ transitioning.value = false;
94
+ }
95
+ }
96
+
97
+ // Entry submission
98
+ const showSubmitDialog = ref(false);
99
+ const submitContentId = ref('');
100
+ const submitting = ref(false);
101
+ const { data: userContent } = useFetch('/api/content', {
102
+ query: { status: 'published', limit: 50 },
103
+ immediate: isAuthenticated.value,
104
+ });
105
+
106
+ function copyLink(): void {
107
+ if (typeof window !== 'undefined' && window.navigator?.clipboard) {
108
+ window.navigator.clipboard.writeText(window.location.href);
109
+ }
110
+ }
111
+
112
+ async function submitEntry(): Promise<void> {
113
+ if (!submitContentId.value) return;
114
+ submitting.value = true;
115
+ try {
116
+ await $fetch(`/api/contests/${slug}/entries`, {
117
+ method: 'POST',
118
+ body: { contentId: submitContentId.value },
119
+ });
120
+ showSubmitDialog.value = false;
121
+ submitContentId.value = '';
122
+ toast.success('Entry submitted!');
123
+ refreshNuxtData();
124
+ } catch {
125
+ toast.error('Failed to submit entry');
126
+ } finally {
127
+ submitting.value = false;
128
+ }
129
+ }
130
+ </script>
131
+
132
+ <template>
133
+ <div class="cpub-contest">
134
+
135
+ <!-- HERO -->
136
+ <div class="cpub-hero">
137
+ <div class="cpub-hero-pattern">
138
+ <div class="cpub-hero-dots"></div>
139
+ <div class="cpub-hero-lines"></div>
140
+ </div>
141
+
142
+ <div class="cpub-hero-inner">
143
+ <div class="cpub-hero-eyebrow">
144
+ <span class="cpub-contest-badge"><i class="fa fa-trophy" style="margin-right:5px;font-size:8px;"></i>Contest</span>
145
+ <span class="cpub-hero-host">
146
+ Hosted by
147
+ <span class="cpub-av cpub-av-sm" style="background:var(--accent-bg);border-color:var(--accent);color:var(--accent);">CP</span>
148
+ <strong style="color:var(--hero-text);">CommonPub</strong>
149
+ </span>
150
+ </div>
151
+
152
+ <div class="cpub-hero-title">{{ c?.title || 'Contest' }}</div>
153
+ <div class="cpub-hero-tagline">
154
+ {{ c?.description || 'No description available.' }}
155
+ </div>
156
+
157
+ <div class="cpub-hero-meta">
158
+ <span v-if="c?.startDate || c?.endDate" class="cpub-hero-meta-item"><i class="fa fa-calendar"></i> {{ c?.startDate ? new Date(c.startDate).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) : '' }}{{ c?.startDate && c?.endDate ? ' — ' : '' }}{{ c?.endDate ? new Date(c.endDate).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) : '' }}</span>
159
+ <span v-if="c?.startDate || c?.endDate" class="cpub-hero-meta-sep">|</span>
160
+ <span class="cpub-hero-meta-item"><i class="fa fa-folder-open"></i> {{ c?.entryCount ?? 0 }} entries</span>
161
+ </div>
162
+
163
+ <!-- COUNTDOWN -->
164
+ <div class="cpub-countdown-section">
165
+ <div class="cpub-countdown-label"><i class="fa fa-clock" style="margin-right:4px;color:var(--accent);"></i>{{ c?.status === 'judging' ? 'Judging ends in' : c?.status === 'completed' ? 'Contest ended' : 'Submissions close in' }}</div>
166
+ <div class="cpub-countdown-row">
167
+ <div class="cpub-countdown-block">
168
+ <div class="cpub-countdown-val">{{ countdown.days }}</div>
169
+ <div class="cpub-countdown-unit">Days</div>
170
+ </div>
171
+ <div class="cpub-countdown-sep">:</div>
172
+ <div class="cpub-countdown-block">
173
+ <div class="cpub-countdown-val">{{ countdown.hours }}</div>
174
+ <div class="cpub-countdown-unit">Hours</div>
175
+ </div>
176
+ <div class="cpub-countdown-sep">:</div>
177
+ <div class="cpub-countdown-block">
178
+ <div class="cpub-countdown-val">{{ countdown.mins }}</div>
179
+ <div class="cpub-countdown-unit">Minutes</div>
180
+ </div>
181
+ <div class="cpub-countdown-sep">:</div>
182
+ <div class="cpub-countdown-block">
183
+ <div class="cpub-countdown-val">{{ countdown.secs }}</div>
184
+ <div class="cpub-countdown-unit">Seconds</div>
185
+ </div>
186
+ </div>
187
+ </div>
188
+
189
+ <div class="cpub-hero-cta">
190
+ <button v-if="isAuthenticated && c?.status === 'active'" class="cpub-btn cpub-btn-primary cpub-btn-lg" @click="showSubmitDialog = true"><i class="fa fa-upload"></i> Submit Entry</button>
191
+ <button class="cpub-btn cpub-btn-lg cpub-btn-dark"><i class="fa fa-file-lines"></i> View Rules</button>
192
+ <button class="cpub-btn cpub-btn-sm cpub-btn-dark" style="margin-left:4px;"><i class="fa fa-bell"></i> Notify Me</button>
193
+ </div>
194
+
195
+ <!-- Admin controls -->
196
+ <div v-if="isAdmin && c" class="cpub-admin-controls">
197
+ <span class="cpub-admin-controls-label"><i class="fa-solid fa-shield-halved"></i> Admin</span>
198
+ <button v-if="c.status === 'upcoming'" class="cpub-btn cpub-btn-sm" :disabled="transitioning" @click="transitionStatus('active')"><i class="fa-solid fa-play"></i> Activate</button>
199
+ <button v-if="c.status === 'active'" class="cpub-btn cpub-btn-sm" :disabled="transitioning" @click="transitionStatus('judging')"><i class="fa-solid fa-gavel"></i> Start Judging</button>
200
+ <button v-if="c.status === 'judging'" class="cpub-btn cpub-btn-sm" :disabled="transitioning" @click="transitionStatus('completed')"><i class="fa-solid fa-check"></i> Complete</button>
201
+ <span class="cpub-admin-status">Status: <strong>{{ c.status }}</strong></span>
202
+ </div>
203
+
204
+ <div class="cpub-hero-stats">
205
+ <div class="cpub-hero-stat">
206
+ <div class="cpub-hero-stat-val">{{ c?.entryCount ?? 0 }}</div>
207
+ <div class="cpub-hero-stat-label">Entries</div>
208
+ </div>
209
+ <div class="cpub-hero-stat">
210
+ <div class="cpub-hero-stat-val">{{ c?.status ?? 'draft' }}</div>
211
+ <div class="cpub-hero-stat-label">Status</div>
212
+ </div>
213
+ </div>
214
+ </div>
215
+ </div>
216
+
217
+ <!-- SUBMIT ENTRY DIALOG -->
218
+ <div v-if="showSubmitDialog" class="cpub-submit-overlay" @click.self="showSubmitDialog = false">
219
+ <div class="cpub-submit-dialog" role="dialog" aria-label="Submit entry">
220
+ <div class="cpub-submit-header">
221
+ <h2 style="font-size: 14px; font-weight: 700;">Submit Entry</h2>
222
+ <button style="background:none;border:none;color:var(--text-faint);cursor:pointer;font-size:14px;" @click="showSubmitDialog = false"><i class="fa-solid fa-times"></i></button>
223
+ </div>
224
+ <div class="cpub-submit-body">
225
+ <p style="font-size: 12px; color: var(--text-dim); margin-bottom: 12px;">Select one of your published projects to submit as an entry.</p>
226
+ <select v-model="submitContentId" class="cpub-submit-select">
227
+ <option value="">Select a project...</option>
228
+ <option v-for="item in (userContent?.items ?? [])" :key="item.id" :value="item.id">
229
+ {{ item.title }} ({{ item.type }})
230
+ </option>
231
+ </select>
232
+ </div>
233
+ <div class="cpub-submit-footer">
234
+ <button class="cpub-btn cpub-btn-sm" @click="showSubmitDialog = false">Cancel</button>
235
+ <button class="cpub-btn cpub-btn-sm cpub-btn-primary" :disabled="!submitContentId || submitting" @click="submitEntry">
236
+ {{ submitting ? 'Submitting...' : 'Submit' }}
237
+ </button>
238
+ </div>
239
+ </div>
240
+ </div>
241
+
242
+ <!-- MAIN CONTENT -->
243
+ <div class="cpub-contest-main">
244
+ <div class="cpub-contest-layout">
245
+
246
+ <!-- MAIN COLUMN -->
247
+ <div>
248
+
249
+ <!-- ABOUT -->
250
+ <div style="margin-bottom:20px;">
251
+ <div class="cpub-sec-head">
252
+ <h2><i class="fa fa-circle-info" style="color:var(--accent);margin-right:6px;"></i>About This Contest</h2>
253
+ </div>
254
+ <div class="cpub-about-card">
255
+ <div class="cpub-about-body">
256
+ <p>{{ c?.description || 'No description available for this contest.' }}</p>
257
+ </div>
258
+ </div>
259
+ </div>
260
+
261
+ <!-- RULES -->
262
+ <div v-if="c?.rules" style="margin-bottom:20px;">
263
+ <div class="cpub-sec-head">
264
+ <h2><i class="fa fa-file-lines" style="color:var(--purple);margin-right:6px;"></i>Rules</h2>
265
+ </div>
266
+ <div class="cpub-rules-card">
267
+ <div class="cpub-about-body" style="white-space: pre-line;">{{ c.rules }}</div>
268
+ </div>
269
+ </div>
270
+
271
+ <!-- ENTRIES -->
272
+ <div style="margin-bottom:20px;">
273
+ <div class="cpub-sec-head">
274
+ <h2><i class="fa fa-box-open" style="color:var(--teal);margin-right:6px;"></i>Submitted Entries</h2>
275
+ <span class="cpub-sec-sub">{{ c?.entryCount ?? entries.length }} entries</span>
276
+ </div>
277
+ <div v-if="entries.length" class="cpub-entry-grid">
278
+ <div
279
+ v-for="(entry, i) in entries"
280
+ :key="entry.id"
281
+ class="cpub-entry-card"
282
+ >
283
+ <div class="cpub-entry-thumb" :class="i % 2 === 0 ? 'cpub-entry-bg-light' : 'cpub-entry-bg-dark'">
284
+ <img v-if="entry.contentCoverImageUrl" :src="entry.contentCoverImageUrl" :alt="entry.contentTitle" class="cpub-entry-cover-img" />
285
+ <template v-else>
286
+ <div class="cpub-entry-grid-pat"></div>
287
+ <div class="cpub-entry-icon" style="color: var(--accent)"><i class="fa-solid fa-microchip"></i></div>
288
+ </template>
289
+ <span v-if="entry.rank" class="cpub-entry-rank" :class="`cpub-rank-${entry.rank}`">#{{ entry.rank }}</span>
290
+ </div>
291
+ <div class="cpub-entry-body">
292
+ <NuxtLink :to="`/${entry.contentType}/${entry.contentSlug}`" class="cpub-entry-title">{{ entry.contentTitle || `Entry #${i + 1}` }}</NuxtLink>
293
+ <div class="cpub-entry-author">
294
+ <div class="cpub-entry-av">
295
+ <img v-if="entry.authorAvatarUrl" :src="entry.authorAvatarUrl" :alt="entry.authorName || entry.authorUsername" class="cpub-entry-av-img" />
296
+ <span v-else>{{ (entry.authorName || entry.authorUsername || '?').charAt(0).toUpperCase() }}</span>
297
+ </div>
298
+ <NuxtLink v-if="entry.authorUsername" :to="`/u/${entry.authorUsername}`" style="color: var(--text-dim); text-decoration: none;">{{ entry.authorName }}</NuxtLink>
299
+ <span class="cpub-entry-meta">{{ new Date(entry.submittedAt).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) }}</span>
300
+ </div>
301
+ <div class="cpub-entry-footer">
302
+ <button
303
+ class="cpub-vote-btn"
304
+ :class="{ 'cpub-voted': votedEntries.has(entry.id) }"
305
+ @click.prevent="toggleVote(entry.id)"
306
+ ><i class="fa fa-arrow-up"></i> Vote</button>
307
+ <span v-if="entry.score != null" class="cpub-entry-views">Score: {{ entry.score }}</span>
308
+ </div>
309
+ </div>
310
+ </div>
311
+ </div>
312
+ <div v-else class="cpub-empty-state" style="padding: 32px 0;">
313
+ <div class="cpub-empty-state-icon"><i class="fa-solid fa-box-open"></i></div>
314
+ <p class="cpub-empty-state-title">No entries yet</p>
315
+ <p class="cpub-empty-state-desc">Be the first to submit an entry!</p>
316
+ </div>
317
+ </div>
318
+
319
+ </div>
320
+
321
+ <!-- SIDEBAR -->
322
+ <div>
323
+
324
+ <!-- STATUS -->
325
+ <div class="cpub-sb-card">
326
+ <div class="cpub-sb-title"><i class="fa-solid fa-circle-info" style="margin-right:5px;"></i>Status</div>
327
+ <div style="font-size: 12px; color: var(--text-dim); display: flex; flex-direction: column; gap: 8px;">
328
+ <div><strong>Status:</strong> {{ c?.status ?? 'unknown' }}</div>
329
+ <div v-if="c?.startDate"><strong>Starts:</strong> {{ new Date(c.startDate).toLocaleDateString() }}</div>
330
+ <div v-if="c?.endDate"><strong>Ends:</strong> {{ new Date(c.endDate).toLocaleDateString() }}</div>
331
+ <div><strong>Entries:</strong> {{ c?.entryCount ?? 0 }}</div>
332
+ </div>
333
+ </div>
334
+
335
+ <!-- SHARE -->
336
+ <div class="cpub-sb-card">
337
+ <div class="cpub-sb-title"><i class="fa-solid fa-share-nodes" style="margin-right:5px;"></i>Share This Contest</div>
338
+ <div style="display:flex;gap:6px;flex-wrap:wrap;">
339
+ <button class="cpub-btn cpub-btn-sm" style="flex:1;justify-content:center;" @click="copyLink()"><i class="fa fa-link"></i> Copy Link</button>
340
+ </div>
341
+ </div>
342
+
343
+ <NuxtLink to="/contests" class="cpub-btn" style="width: 100%; text-align: center; display: block; margin-top: 12px;"><i class="fa fa-arrow-left"></i> All Contests</NuxtLink>
344
+ </div>
345
+
346
+ </div>
347
+ </div>
348
+
349
+ </div>
350
+ </template>
351
+
352
+ <style scoped>
353
+ /* Hero uses a dark context — local custom properties for dark-bg values */
354
+ .cpub-hero {
355
+ --hero-bg: var(--text);
356
+ --hero-text: var(--color-text-inverse);
357
+ --hero-text-dim: var(--text-faint);
358
+ --hero-border: rgba(255, 255, 255, 0.15);
359
+ --hero-surface: rgba(255, 255, 255, 0.06);
360
+ }
361
+
362
+ /* Metallic prize colors — no token equivalents */
363
+ .cpub-contest {
364
+ --silver: var(--text-faint);
365
+ --bronze: #a0724a;
366
+ }
367
+
368
+ /* SUBMIT DIALOG */
369
+ .cpub-submit-overlay { position: fixed; inset: 0; z-index: 200; background: var(--color-surface-overlay-light); display: flex; align-items: center; justify-content: center; }
370
+ .cpub-submit-dialog { background: var(--surface); border: var(--border-width-default) solid var(--border); box-shadow: var(--shadow-xl); width: 420px; max-width: 90vw; }
371
+ .cpub-submit-header { display: flex; align-items: center; justify-content: space-between; padding: 14px 16px; border-bottom: var(--border-width-default) solid var(--border); }
372
+ .cpub-submit-body { padding: 16px; }
373
+ .cpub-submit-select { width: 100%; padding: 8px 10px; border: var(--border-width-default) solid var(--border); background: var(--surface); color: var(--text); font-size: 13px; }
374
+ .cpub-submit-select:focus { border-color: var(--accent); outline: none; }
375
+ .cpub-submit-footer { display: flex; justify-content: flex-end; gap: 8px; padding: 12px 16px; border-top: var(--border-width-default) solid var(--border); }
376
+
377
+ /* HERO */
378
+ .cpub-hero { position: relative; overflow: hidden; background: var(--hero-bg); padding: 56px 0 48px; }
379
+ .cpub-hero-pattern { position: absolute; inset: 0; }
380
+ .cpub-hero-dots { position: absolute; inset: 0; background-image: radial-gradient(var(--accent-border) 1.5px, transparent 1.5px); background-size: 28px 28px; opacity: .3; }
381
+ .cpub-hero-lines { position: absolute; inset: 0; background-image: linear-gradient(var(--accent-bg) 1px, transparent 1px), linear-gradient(90deg, var(--accent-bg) 1px, transparent 1px); background-size: 56px 56px; }
382
+ .cpub-hero-inner { max-width: 1100px; margin: 0 auto; padding: 0 32px; position: relative; z-index: 1; }
383
+ .cpub-hero-eyebrow { display: flex; align-items: center; gap: 10px; margin-bottom: 16px; }
384
+ .cpub-contest-badge { font-size: 9px; font-weight: 700; letter-spacing: .16em; text-transform: uppercase; font-family: var(--font-mono); color: var(--accent); background: var(--accent-bg); border: var(--border-width-default) solid var(--accent); padding: 3px 10px; border-radius: var(--radius); }
385
+ .cpub-hero-host { font-size: 11px; color: var(--hero-text-dim); font-family: var(--font-mono); display: flex; align-items: center; gap: 6px; }
386
+ .cpub-hero-title { font-size: 36px; font-weight: 800; letter-spacing: -.03em; line-height: 1.1; margin-bottom: 10px; color: var(--hero-text); }
387
+ .cpub-hero-highlight { color: var(--accent); }
388
+ .cpub-hero-tagline { font-size: 14px; color: var(--hero-text-dim); line-height: 1.55; max-width: 580px; margin-bottom: 28px; }
389
+ .cpub-hero-meta { display: flex; align-items: center; gap: 20px; font-size: 11px; color: var(--hero-text-dim); font-family: var(--font-mono); margin-bottom: 28px; }
390
+ .cpub-hero-meta-item { display: flex; align-items: center; gap: 5px; }
391
+ .cpub-hero-meta-sep { color: var(--hero-border); }
392
+
393
+ /* COUNTDOWN */
394
+ .cpub-countdown-section { margin-bottom: 28px; }
395
+ .cpub-countdown-label { font-size: 10px; font-family: var(--font-mono); color: var(--hero-text-dim); letter-spacing: .1em; text-transform: uppercase; margin-bottom: 10px; }
396
+ .cpub-countdown-row { display: flex; align-items: center; gap: 8px; }
397
+ .cpub-countdown-block { display: flex; flex-direction: column; align-items: center; background: var(--hero-surface); border: var(--border-width-default) solid var(--hero-border); border-radius: var(--radius); padding: 10px 16px; min-width: 60px; box-shadow: 4px 4px 0 var(--hero-surface); }
398
+ .cpub-countdown-val { font-size: 26px; font-weight: 700; font-family: var(--font-mono); color: var(--hero-text); line-height: 1; margin-bottom: 4px; }
399
+ .cpub-countdown-unit { font-size: 9px; text-transform: uppercase; letter-spacing: .1em; color: var(--hero-text-dim); font-family: var(--font-mono); }
400
+ .cpub-countdown-sep { font-size: 20px; font-weight: 700; color: var(--hero-border); margin-top: -8px; font-family: var(--font-mono); }
401
+
402
+ /* HERO CTA & STATS */
403
+ .cpub-hero-cta { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
404
+ .cpub-total-prize { font-size: 12px; color: var(--hero-text-dim); font-family: var(--font-mono); display: flex; align-items: center; gap: 6px; padding-left: 10px; border-left: var(--border-width-default) solid var(--hero-border); }
405
+ .cpub-total-prize strong { color: var(--yellow); font-size: 15px; }
406
+ .cpub-hero-stats { display: flex; gap: 24px; margin-top: 28px; padding-top: 24px; border-top: var(--border-width-default) solid var(--hero-border); }
407
+ .cpub-hero-stat { display: flex; flex-direction: column; }
408
+ .cpub-hero-stat-val { font-size: 20px; font-weight: 700; font-family: var(--font-mono); color: var(--hero-text); }
409
+ .cpub-hero-stat-label { font-size: 10px; color: var(--hero-text-dim); text-transform: uppercase; letter-spacing: .1em; font-family: var(--font-mono); }
410
+
411
+ /* BUTTONS (page-specific) */
412
+ .cpub-btn-lg { padding: 10px 22px; font-size: 13px; }
413
+ .cpub-btn-dark { background: var(--hero-surface); color: var(--hero-text); border-color: var(--hero-border); }
414
+ .cpub-btn-dark:hover { background: var(--hero-surface); }
415
+
416
+ /* AVATARS */
417
+ .cpub-av { display: flex; align-items: center; justify-content: center; border-radius: 50%; font-weight: 600; font-family: var(--font-mono); flex-shrink: 0; background: var(--surface3); border: var(--border-width-default) solid var(--border); color: var(--text-dim); }
418
+ .cpub-av-sm { width: 24px; height: 24px; font-size: 9px; }
419
+
420
+ /* LAYOUT */
421
+ .cpub-contest-main { max-width: 1100px; margin: 0 auto; padding: 32px; }
422
+ .cpub-contest-layout { display: grid; grid-template-columns: 1fr 300px; gap: 28px; align-items: start; }
423
+
424
+ /* SECTION HEADERS (page-specific) */
425
+ .cpub-sec-sub { font-size: 11px; color: var(--text-faint); margin-left: auto; font-family: var(--font-mono); }
426
+
427
+ /* ABOUT */
428
+ .cpub-about-card { background: var(--surface); border: var(--border-width-default) solid var(--border); border-radius: var(--radius); padding: 20px; margin-bottom: 20px; box-shadow: var(--shadow-md); }
429
+ .cpub-about-body { font-size: 12px; color: var(--text-dim); line-height: 1.7; }
430
+ .cpub-about-body p { margin-bottom: 10px; }
431
+ .cpub-about-body p:last-child { margin-bottom: 0; }
432
+ .cpub-highlight-box { background: var(--accent-bg); border: var(--border-width-default) solid var(--accent); border-radius: var(--radius); padding: 12px 14px; margin: 12px 0; font-size: 11px; color: var(--text-dim); }
433
+ .cpub-highlight-box strong { color: var(--accent); }
434
+
435
+ /* PRIZES */
436
+ .cpub-prize-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; margin-bottom: 20px; }
437
+ .cpub-prize-card { border-radius: var(--radius); padding: 20px; position: relative; overflow: hidden; text-align: center; background: var(--surface); border: var(--border-width-default) solid var(--border); }
438
+ .cpub-prize-gold { box-shadow: var(--shadow-accent); }
439
+ .cpub-prize-silver { box-shadow: var(--shadow-md); }
440
+ .cpub-prize-bronze { box-shadow: var(--shadow-md); }
441
+ .cpub-prize-rank { font-size: 11px; font-family: var(--font-mono); font-weight: 600; letter-spacing: .08em; margin-bottom: 8px; position: relative; z-index: 1; }
442
+ .cpub-prize-rank-gold { color: var(--yellow); }
443
+ .cpub-prize-rank-silver { color: var(--silver); }
444
+ .cpub-prize-rank-bronze { color: var(--bronze); }
445
+ .cpub-prize-icon { font-size: 28px; margin-bottom: 8px; position: relative; z-index: 1; }
446
+ .cpub-prize-icon-gold { color: var(--yellow); }
447
+ .cpub-prize-icon-silver { color: var(--silver); }
448
+ .cpub-prize-icon-bronze { color: var(--bronze); }
449
+ .cpub-prize-amount { font-size: 24px; font-weight: 800; font-family: var(--font-mono); margin-bottom: 4px; position: relative; z-index: 1; }
450
+ .cpub-prize-amount-gold { color: var(--yellow); }
451
+ .cpub-prize-amount-silver { color: var(--silver); }
452
+ .cpub-prize-amount-bronze { color: var(--bronze); }
453
+ .cpub-prize-label { font-size: 10px; color: var(--text-faint); margin-bottom: 10px; font-family: var(--font-mono); position: relative; z-index: 1; }
454
+ .cpub-prize-perks { text-align: left; position: relative; z-index: 1; }
455
+ .cpub-prize-perk { font-size: 10px; color: var(--text-dim); display: flex; align-items: center; gap: 5px; margin-bottom: 3px; font-family: var(--font-mono); }
456
+ .cpub-prize-perk i { font-size: 8px; color: var(--green); }
457
+ .cpub-prize-additional { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 12px; }
458
+ .cpub-prize-extra { background: var(--surface); border: var(--border-width-default) solid var(--border); border-radius: var(--radius); padding: 12px; text-align: center; box-shadow: var(--shadow-md); }
459
+ .cpub-prize-extra-title { font-size: 11px; font-weight: 600; margin-bottom: 2px; }
460
+ .cpub-prize-extra-val { font-size: 14px; font-weight: 700; font-family: var(--font-mono); color: var(--teal); }
461
+ .cpub-prize-extra-label { font-size: 9px; color: var(--text-faint); font-family: var(--font-mono); }
462
+
463
+ /* RULES */
464
+ .cpub-rules-card { background: var(--surface); border: var(--border-width-default) solid var(--border); border-radius: var(--radius); padding: 20px; margin-bottom: 20px; box-shadow: var(--shadow-md); }
465
+ .cpub-rule-item { display: flex; align-items: flex-start; gap: 10px; margin-bottom: 10px; font-size: 12px; color: var(--text-dim); line-height: 1.55; }
466
+ .cpub-rule-item:last-child { margin-bottom: 0; }
467
+ .cpub-rule-icon { font-size: 11px; color: var(--accent); margin-top: 2px; flex-shrink: 0; width: 14px; }
468
+
469
+ /* ENTRIES */
470
+ .cpub-entries-filter { display: flex; gap: 6px; margin-bottom: 14px; }
471
+ .cpub-entry-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 12px; }
472
+ .cpub-entry-card { background: var(--surface); border: var(--border-width-default) solid var(--border); border-radius: var(--radius); overflow: hidden; cursor: pointer; box-shadow: var(--shadow-md); }
473
+ .cpub-entry-card:hover { box-shadow: var(--shadow-accent); }
474
+ .cpub-entry-thumb { height: 110px; position: relative; overflow: hidden; display: flex; align-items: center; justify-content: center; }
475
+ .cpub-entry-bg-light { background: var(--surface2); }
476
+ .cpub-entry-bg-dark { background: var(--surface3); }
477
+ .cpub-entry-grid-pat { position: absolute; inset: 0; background-image: linear-gradient(var(--border2) 1px, transparent 1px), linear-gradient(90deg, var(--border2) 1px, transparent 1px); background-size: 20px 20px; opacity: .3; }
478
+ .cpub-entry-icon { position: relative; z-index: 1; font-size: 22px; opacity: .65; }
479
+ .cpub-entry-rank { position: absolute; top: 8px; left: 8px; z-index: 2; font-size: 10px; font-family: var(--font-mono); font-weight: 700; padding: 2px 7px; border-radius: var(--radius); }
480
+ .cpub-rank-1 { background: var(--yellow-bg); color: var(--yellow); border: var(--border-width-default) solid var(--yellow); }
481
+ .cpub-rank-2 { background: var(--surface2); color: var(--silver); border: var(--border-width-default) solid var(--silver); }
482
+ .cpub-rank-3 { background: var(--surface2); color: var(--bronze); border: var(--border-width-default) solid var(--bronze); }
483
+ .cpub-entry-body { padding: 10px 12px; }
484
+ .cpub-entry-title { font-size: 12px; font-weight: 600; margin-bottom: 3px; line-height: 1.3; }
485
+ .cpub-entry-cover-img { width: 100%; height: 100%; object-fit: cover; }
486
+ .cpub-entry-av { width: 18px; height: 18px; border-radius: 50%; background: var(--surface3); border: var(--border-width-default) solid var(--border); display: flex; align-items: center; justify-content: center; font-size: 7px; font-family: var(--font-mono); color: var(--text-faint); flex-shrink: 0; overflow: hidden; }
487
+ .cpub-entry-av-img { width: 100%; height: 100%; object-fit: cover; border-radius: inherit; }
488
+ .cpub-entry-author { font-size: 10px; color: var(--text-dim); font-family: var(--font-mono); margin-bottom: 6px; display: flex; align-items: center; gap: 5px; }
489
+ .cpub-entry-footer { display: flex; align-items: center; gap: 6px; }
490
+ .cpub-vote-btn { display: flex; align-items: center; gap: 4px; font-size: 10px; font-family: var(--font-mono); padding: 3px 8px; border-radius: var(--radius); border: var(--border-width-default) solid var(--border); background: var(--surface); color: var(--text-dim); cursor: pointer; }
491
+ .cpub-vote-btn:hover { background: var(--surface2); }
492
+ .cpub-vote-btn i { font-size: 9px; }
493
+ .cpub-voted { background: var(--accent-bg); border-color: var(--accent); color: var(--accent); }
494
+ .cpub-entry-views { font-size: 10px; color: var(--text-faint); font-family: var(--font-mono); margin-left: auto; display: flex; align-items: center; gap: 3px; }
495
+
496
+ /* JUDGES */
497
+ .cpub-judges-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-bottom: 20px; }
498
+ .cpub-judge-card { background: var(--surface); border: var(--border-width-default) solid var(--border); border-radius: var(--radius); padding: 14px; text-align: center; box-shadow: var(--shadow-md); }
499
+ .cpub-judge-av { width: 44px; height: 44px; border-radius: 50%; margin: 0 auto 8px; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: 700; font-family: var(--font-mono); border: var(--border-width-default) solid var(--border); }
500
+ .cpub-judge-name { font-size: 11px; font-weight: 600; margin-bottom: 2px; }
501
+ .cpub-judge-title { font-size: 10px; color: var(--text-dim); line-height: 1.35; font-family: var(--font-mono); }
502
+ .cpub-judge-org { font-size: 10px; color: var(--accent); font-family: var(--font-mono); margin-top: 2px; }
503
+
504
+ /* TIMELINE */
505
+ .cpub-tl-item { display: flex; gap: 12px; margin-bottom: 14px; position: relative; }
506
+ .cpub-tl-item:not(.cpub-tl-last)::before { content: ''; position: absolute; left: 10px; top: 20px; bottom: -14px; width: 2px; background: var(--border); }
507
+ .cpub-tl-last { margin-bottom: 0; }
508
+ .cpub-tl-icon { width: 20px; height: 20px; border-radius: 50%; display: flex; align-items: center; justify-content: center; flex-shrink: 0; font-size: 8px; margin-top: 1px; }
509
+ .cpub-tl-done { background: var(--green-bg); border: var(--border-width-default) solid var(--green); color: var(--green); }
510
+ .cpub-tl-active { background: var(--accent-bg); border: var(--border-width-default) solid var(--accent); color: var(--accent); }
511
+ .cpub-tl-upcoming { background: var(--surface2); border: var(--border-width-default) solid var(--border2); color: var(--text-faint); }
512
+ .cpub-tl-info { flex: 1; padding-top: 1px; }
513
+ .cpub-tl-name { font-size: 11px; font-weight: 600; margin-bottom: 1px; }
514
+ .cpub-tl-name-done { color: var(--green); }
515
+ .cpub-tl-name-active { color: var(--text); }
516
+ .cpub-tl-name-upcoming { color: var(--text-faint); }
517
+ .cpub-tl-date { font-size: 10px; color: var(--text-faint); font-family: var(--font-mono); }
518
+ .cpub-tl-status { font-size: 9px; font-family: var(--font-mono); padding: 1px 5px; border-radius: var(--radius); }
519
+ .cpub-status-done { color: var(--green); background: var(--green-bg); border: var(--border-width-default) solid var(--green); }
520
+ .cpub-status-active { color: var(--accent); background: var(--accent-bg); border: var(--border-width-default) solid var(--accent); }
521
+
522
+ /* SPONSORS */
523
+ .cpub-sponsor-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
524
+ .cpub-sponsor-card { background: var(--surface2); border: var(--border-width-default) solid var(--border); border-radius: var(--radius); padding: 10px; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px; }
525
+ .cpub-sponsor-icon { font-size: 16px; margin-bottom: 2px; color: var(--text-dim); }
526
+ .cpub-sponsor-name { font-size: 10px; font-weight: 600; font-family: var(--font-mono); color: var(--text); }
527
+ .cpub-sponsor-tier { font-size: 8px; font-family: var(--font-mono); color: var(--text-faint); }
528
+ .cpub-sponsor-link { font-size: 11px; color: var(--accent); text-decoration: none; font-family: var(--font-mono); }
529
+ .cpub-sponsor-link:hover { text-decoration: underline; }
530
+
531
+ /* FAQ */
532
+ .cpub-faq-wrap { box-shadow: none; padding: 0; border: none; background: transparent; }
533
+ .cpub-faq-item { border: var(--border-width-default) solid var(--border); margin-bottom: -2px; overflow: hidden; }
534
+ .cpub-faq-item:first-of-type { border-top: var(--border-width-default) solid var(--border); }
535
+ .cpub-faq-q { display: flex; align-items: center; justify-content: space-between; padding: 10px 12px; cursor: pointer; font-size: 11px; font-weight: 500; gap: 8px; background: var(--surface); }
536
+ .cpub-faq-q:hover { background: var(--surface2); color: var(--accent); }
537
+ .cpub-faq-q i { font-size: 10px; color: var(--text-faint); flex-shrink: 0; transition: transform .15s; }
538
+ .cpub-faq-open .cpub-faq-q i { transform: rotate(180deg); }
539
+ .cpub-faq-open .cpub-faq-q { background: var(--surface2); border-bottom: var(--border-width-default) solid var(--border2); }
540
+ .cpub-faq-a { font-size: 11px; color: var(--text-dim); line-height: 1.55; padding: 10px 12px; display: none; background: var(--surface); }
541
+ .cpub-faq-open .cpub-faq-a { display: block; }
542
+
543
+ /* Admin controls */
544
+ .cpub-admin-controls {
545
+ display: flex; align-items: center; gap: 8px; margin-top: 16px;
546
+ padding: 10px 14px; background: var(--accent-bg); border: var(--border-width-default) solid var(--accent-border);
547
+ }
548
+ .cpub-admin-controls-label {
549
+ font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em;
550
+ color: var(--accent); margin-right: 4px; font-family: var(--font-mono);
551
+ }
552
+ .cpub-admin-status {
553
+ font-size: 11px; color: var(--text-dim); margin-left: auto; font-family: var(--font-mono);
554
+ }
555
+ .cpub-admin-status strong { color: var(--accent); text-transform: capitalize; }
556
+ </style>