@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,124 @@
1
+ <script setup lang="ts">
2
+ definePageMeta({ layout: 'auth' });
3
+
4
+ useSeoMeta({
5
+ title: `Reset Password — ${useSiteName()}`,
6
+ description: 'Set a new password for your CommonPub account.',
7
+ });
8
+
9
+ const route = useRoute();
10
+ const token = computed(() => (route.query.token as string) || '');
11
+
12
+ const password = ref('');
13
+ const confirmPassword = ref('');
14
+ const error = ref('');
15
+ const success = ref(false);
16
+ const loading = ref(false);
17
+
18
+ async function handleSubmit(): Promise<void> {
19
+ error.value = '';
20
+
21
+ if (password.value.length < 8) {
22
+ error.value = 'Password must be at least 8 characters.';
23
+ return;
24
+ }
25
+ if (password.value !== confirmPassword.value) {
26
+ error.value = 'Passwords do not match.';
27
+ return;
28
+ }
29
+ if (!token.value) {
30
+ error.value = 'Invalid or expired reset link.';
31
+ return;
32
+ }
33
+
34
+ loading.value = true;
35
+
36
+ try {
37
+ await $fetch('/api/auth/reset-password', {
38
+ method: 'POST',
39
+ body: { token: token.value, newPassword: password.value },
40
+ });
41
+ success.value = true;
42
+ } catch (err: unknown) {
43
+ const message = (err as { data?: { message?: string } })?.data?.message;
44
+ error.value = message || 'Failed to reset password. The link may have expired.';
45
+ } finally {
46
+ loading.value = false;
47
+ }
48
+ }
49
+ </script>
50
+
51
+ <template>
52
+ <div class="reset-page">
53
+ <h1 class="reset-title">Reset Password</h1>
54
+
55
+ <template v-if="success">
56
+ <div class="reset-success">
57
+ <i class="fa-solid fa-check-circle" style="font-size: 24px; color: var(--green); margin-bottom: 12px;"></i>
58
+ <p class="reset-success-text">Your password has been reset successfully.</p>
59
+ </div>
60
+ <NuxtLink to="/auth/login" class="back-link">
61
+ <i class="fa-solid fa-arrow-right"></i> Go to login
62
+ </NuxtLink>
63
+ </template>
64
+
65
+ <template v-else>
66
+ <p class="reset-desc">Enter your new password below.</p>
67
+
68
+ <form class="reset-form" @submit.prevent="handleSubmit" aria-label="Reset password form">
69
+ <div v-if="error" class="form-error" role="alert">{{ error }}</div>
70
+
71
+ <div class="field">
72
+ <label for="password" class="field-label">New Password</label>
73
+ <input
74
+ id="password"
75
+ v-model="password"
76
+ type="password"
77
+ class="field-input"
78
+ autocomplete="new-password"
79
+ required
80
+ placeholder="At least 8 characters"
81
+ minlength="8"
82
+ />
83
+ </div>
84
+
85
+ <div class="field">
86
+ <label for="confirm" class="field-label">Confirm Password</label>
87
+ <input
88
+ id="confirm"
89
+ v-model="confirmPassword"
90
+ type="password"
91
+ class="field-input"
92
+ autocomplete="new-password"
93
+ required
94
+ placeholder="Confirm your password"
95
+ />
96
+ </div>
97
+
98
+ <button type="submit" class="submit-btn" :disabled="loading">
99
+ {{ loading ? 'Resetting...' : 'Reset Password' }}
100
+ </button>
101
+ </form>
102
+ </template>
103
+ </div>
104
+ </template>
105
+
106
+ <style scoped>
107
+ .reset-page { width: 100%; }
108
+ .reset-title { font-size: 18px; font-weight: 600; margin-bottom: var(--space-3); }
109
+ .reset-desc { font-size: 13px; color: var(--text-dim); margin-bottom: var(--space-5); line-height: 1.6; }
110
+ .reset-form { display: flex; flex-direction: column; gap: var(--space-4); }
111
+ .reset-success { text-align: center; padding: var(--space-5) 0; }
112
+ .reset-success-text { font-size: 13px; color: var(--text-dim); line-height: 1.6; }
113
+ .back-link { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--accent); text-decoration: none; justify-content: center; margin-top: var(--space-4); }
114
+ .back-link:hover { text-decoration: underline; }
115
+ .form-error { padding: var(--space-3); background: var(--red-bg); color: var(--red); border: var(--border-width-default) solid var(--red); border-radius: var(--radius); font-size: 12px; }
116
+ .field { display: flex; flex-direction: column; gap: 4px; }
117
+ .field-label { font-size: 12px; font-weight: 500; font-family: var(--font-mono); text-transform: uppercase; letter-spacing: 0.04em; color: var(--text-dim); }
118
+ .field-input { padding: 8px 12px; border: var(--border-width-default) solid var(--border); border-radius: var(--radius); background: var(--surface); color: var(--text); font-size: 13px; font-family: var(--font-sans); outline: none; width: 100%; transition: border-color 0.15s; }
119
+ .field-input::placeholder { color: var(--text-faint); }
120
+ .field-input:focus { border-color: var(--accent); }
121
+ .submit-btn { padding: 7px 14px; background: var(--accent); color: var(--color-text-inverse); border: var(--border-width-default) solid var(--accent); border-radius: var(--radius); font-size: 13px; font-weight: 500; cursor: pointer; box-shadow: var(--shadow-sm); transition: all 0.15s; }
122
+ .submit-btn:hover:not(:disabled) { box-shadow: var(--shadow-md); transform: translate(-1px, -1px); }
123
+ .submit-btn:disabled { opacity: 0.7; cursor: not-allowed; }
124
+ </style>
@@ -0,0 +1,80 @@
1
+ <script setup lang="ts">
2
+ definePageMeta({ layout: 'auth' });
3
+
4
+ useSeoMeta({
5
+ title: `Verify Email — ${useSiteName()}`,
6
+ description: 'Verify your CommonPub email address.',
7
+ });
8
+
9
+ const route = useRoute();
10
+ const token = computed(() => (route.query.token as string) || '');
11
+
12
+ const status = ref<'verifying' | 'success' | 'error' | 'no-token'>('verifying');
13
+ const errorMessage = ref('');
14
+
15
+ if (!token.value) {
16
+ status.value = 'no-token';
17
+ } else {
18
+ onMounted(async () => {
19
+ try {
20
+ await $fetch('/api/auth/verify-email', {
21
+ method: 'POST',
22
+ body: { token: token.value },
23
+ });
24
+ status.value = 'success';
25
+ } catch (err: unknown) {
26
+ const message = (err as { data?: { message?: string } })?.data?.message;
27
+ errorMessage.value = message || 'Verification failed. The link may have expired.';
28
+ status.value = 'error';
29
+ }
30
+ });
31
+ }
32
+ </script>
33
+
34
+ <template>
35
+ <div class="verify-page">
36
+ <h1 class="verify-title">Email Verification</h1>
37
+
38
+ <!-- Verifying -->
39
+ <div v-if="status === 'verifying'" class="verify-status">
40
+ <i class="fa-solid fa-circle-notch fa-spin" style="font-size: 24px; color: var(--accent); margin-bottom: 12px;"></i>
41
+ <p class="verify-text">Verifying your email address...</p>
42
+ </div>
43
+
44
+ <!-- Success -->
45
+ <div v-else-if="status === 'success'" class="verify-status">
46
+ <i class="fa-solid fa-check-circle" style="font-size: 24px; color: var(--green); margin-bottom: 12px;"></i>
47
+ <p class="verify-text">Your email has been verified successfully!</p>
48
+ <NuxtLink to="/auth/login" class="verify-link">
49
+ <i class="fa-solid fa-arrow-right"></i> Continue to login
50
+ </NuxtLink>
51
+ </div>
52
+
53
+ <!-- Error -->
54
+ <div v-else-if="status === 'error'" class="verify-status">
55
+ <i class="fa-solid fa-circle-xmark" style="font-size: 24px; color: var(--red); margin-bottom: 12px;"></i>
56
+ <p class="verify-text">{{ errorMessage }}</p>
57
+ <NuxtLink to="/auth/login" class="verify-link">
58
+ <i class="fa-solid fa-arrow-left"></i> Back to login
59
+ </NuxtLink>
60
+ </div>
61
+
62
+ <!-- No token -->
63
+ <div v-else class="verify-status">
64
+ <i class="fa-solid fa-circle-exclamation" style="font-size: 24px; color: var(--yellow); margin-bottom: 12px;"></i>
65
+ <p class="verify-text">No verification token found. Please check the link in your email.</p>
66
+ <NuxtLink to="/auth/login" class="verify-link">
67
+ <i class="fa-solid fa-arrow-left"></i> Back to login
68
+ </NuxtLink>
69
+ </div>
70
+ </div>
71
+ </template>
72
+
73
+ <style scoped>
74
+ .verify-page { width: 100%; }
75
+ .verify-title { font-size: 18px; font-weight: 600; margin-bottom: var(--space-5); }
76
+ .verify-status { text-align: center; padding: var(--space-5) 0; display: flex; flex-direction: column; align-items: center; }
77
+ .verify-text { font-size: 13px; color: var(--text-dim); line-height: 1.6; max-width: 300px; }
78
+ .verify-link { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--accent); text-decoration: none; margin-top: var(--space-4); }
79
+ .verify-link:hover { text-decoration: underline; }
80
+ </style>
@@ -0,0 +1,154 @@
1
+ <script setup lang="ts">
2
+ const route = useRoute();
3
+ const code = route.params.code as string;
4
+
5
+ const { data: certData } = useLazyFetch(`/api/cert/${code}`);
6
+
7
+ useSeoMeta({
8
+ title: () => certData.value ? `Certificate — ${certData.value.path.title} — ${useSiteName()}` : `Certificate — ${useSiteName()}`,
9
+ description: () => certData.value ? `Certificate of completion for ${certData.value.path.title}` : '',
10
+ });
11
+ </script>
12
+
13
+ <template>
14
+ <div class="cert-page">
15
+ <div v-if="certData" class="cert-card">
16
+ <!-- Certificate Badge -->
17
+ <div class="cert-badge-wrap">
18
+ <div class="cert-badge">
19
+ <i class="fa-solid fa-award"></i>
20
+ </div>
21
+ </div>
22
+
23
+ <div class="cert-eyebrow">Certificate of Completion</div>
24
+ <h1 class="cert-title">{{ certData.path.title }}</h1>
25
+
26
+ <div class="cert-recipient">
27
+ <div class="cert-recipient-label">Awarded to</div>
28
+ <NuxtLink :to="`/u/${certData.user.username}`" class="cert-recipient-name">
29
+ {{ certData.user.displayName || certData.user.username }}
30
+ </NuxtLink>
31
+ </div>
32
+
33
+ <div class="cert-details">
34
+ <div class="cert-detail">
35
+ <span class="cert-detail-label">Completed</span>
36
+ <span class="cert-detail-value">
37
+ {{ new Date(certData.certificate.issuedAt).toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' }) }}
38
+ </span>
39
+ </div>
40
+ <div class="cert-detail">
41
+ <span class="cert-detail-label">Verification Code</span>
42
+ <span class="cert-detail-value cert-code">{{ certData.certificate.verificationCode }}</span>
43
+ </div>
44
+ </div>
45
+
46
+ <div class="cert-footer">
47
+ <NuxtLink :to="`/learn/${certData.path.slug}`" class="cert-path-link">
48
+ <i class="fa-solid fa-graduation-cap"></i> View Learning Path
49
+ </NuxtLink>
50
+ </div>
51
+
52
+ <!-- Verified stamp -->
53
+ <div class="cert-verified">
54
+ <i class="fa-solid fa-circle-check"></i> Verified
55
+ </div>
56
+ </div>
57
+
58
+ <div v-else class="cert-not-found">
59
+ <div class="cert-not-found-icon"><i class="fa-solid fa-circle-xmark"></i></div>
60
+ <h1>Certificate Not Found</h1>
61
+ <p>The verification code could not be found. Please check the code and try again.</p>
62
+ </div>
63
+ </div>
64
+ </template>
65
+
66
+ <style scoped>
67
+ .cert-page {
68
+ max-width: 600px;
69
+ margin: 0 auto;
70
+ padding: 48px 24px;
71
+ }
72
+
73
+ .cert-card {
74
+ border: var(--border-width-default) solid var(--border);
75
+ background: var(--surface);
76
+ padding: 40px;
77
+ text-align: center;
78
+ position: relative;
79
+ box-shadow: var(--shadow-xl);
80
+ }
81
+
82
+ .cert-badge-wrap { margin-bottom: 20px; }
83
+
84
+ .cert-badge {
85
+ width: 64px;
86
+ height: 64px;
87
+ border-radius: 50%;
88
+ background: var(--yellow-bg);
89
+ border: 3px solid var(--yellow);
90
+ display: inline-flex;
91
+ align-items: center;
92
+ justify-content: center;
93
+ font-size: 28px;
94
+ color: var(--yellow);
95
+ }
96
+
97
+ .cert-eyebrow {
98
+ font-size: 10px;
99
+ font-family: var(--font-mono);
100
+ font-weight: 700;
101
+ letter-spacing: 0.15em;
102
+ text-transform: uppercase;
103
+ color: var(--text-faint);
104
+ margin-bottom: 12px;
105
+ }
106
+
107
+ .cert-title {
108
+ font-size: 24px;
109
+ font-weight: 700;
110
+ letter-spacing: -0.02em;
111
+ margin-bottom: 24px;
112
+ line-height: 1.2;
113
+ }
114
+
115
+ .cert-recipient { margin-bottom: 24px; padding: 16px 0; border-top: var(--border-width-default) solid var(--border); border-bottom: var(--border-width-default) solid var(--border); }
116
+ .cert-recipient-label { font-size: 10px; font-family: var(--font-mono); color: var(--text-faint); text-transform: uppercase; letter-spacing: 0.1em; margin-bottom: 4px; }
117
+ .cert-recipient-name { font-size: 20px; font-weight: 700; color: var(--accent); text-decoration: none; }
118
+ .cert-recipient-name:hover { text-decoration: underline; }
119
+
120
+ .cert-details { display: flex; gap: 24px; justify-content: center; margin-bottom: 24px; }
121
+ .cert-detail { display: flex; flex-direction: column; gap: 4px; }
122
+ .cert-detail-label { font-size: 10px; font-family: var(--font-mono); color: var(--text-faint); text-transform: uppercase; letter-spacing: 0.08em; }
123
+ .cert-detail-value { font-size: 13px; color: var(--text); }
124
+ .cert-code { font-family: var(--font-mono); font-weight: 600; color: var(--accent); background: var(--accent-bg); padding: 2px 8px; border: var(--border-width-default) solid var(--accent-border); }
125
+
126
+ .cert-footer { margin-top: 8px; }
127
+ .cert-path-link { font-size: 12px; color: var(--accent); text-decoration: none; display: inline-flex; align-items: center; gap: 6px; }
128
+ .cert-path-link:hover { text-decoration: underline; }
129
+
130
+ .cert-verified {
131
+ position: absolute;
132
+ top: 16px;
133
+ right: 16px;
134
+ font-size: 11px;
135
+ font-family: var(--font-mono);
136
+ color: var(--green);
137
+ display: flex;
138
+ align-items: center;
139
+ gap: 4px;
140
+ }
141
+
142
+ .cert-not-found { text-align: center; padding: 64px 0; color: var(--text-dim); }
143
+ .cert-not-found-icon { font-size: 40px; color: var(--red); margin-bottom: 16px; }
144
+ .cert-not-found h1 { font-size: 20px; margin-bottom: 8px; }
145
+ .cert-not-found p { font-size: 13px; }
146
+
147
+ @media (max-width: 768px) {
148
+ .cert-page { padding: 24px 16px; }
149
+ .cert-card { padding: 24px 16px; }
150
+ .cert-title { font-size: 1.125rem; }
151
+ .cert-recipient-name { font-size: 1rem; }
152
+ .cert-details { flex-direction: column; gap: 12px; }
153
+ }
154
+ </style>
@@ -0,0 +1,153 @@
1
+ <script setup lang="ts">
2
+ definePageMeta({ middleware: 'auth' });
3
+
4
+ const route = useRoute();
5
+ const slug = route.params.slug as string;
6
+ const toast = useToast();
7
+ const { extract: extractError } = useApiError();
8
+
9
+ const { data: contest, refresh } = useLazyFetch(`/api/contests/${slug}`);
10
+ useSeoMeta({ title: () => `Edit: ${contest.value?.title ?? 'Contest'} — ${useSiteName()}` });
11
+
12
+ const saving = ref(false);
13
+ const title = ref('');
14
+ const description = ref('');
15
+ const rules = ref('');
16
+ const startDate = ref('');
17
+ const endDate = ref('');
18
+
19
+ // Load current data
20
+ watch(contest, (c) => {
21
+ if (!c) return;
22
+ title.value = c.title ?? '';
23
+ description.value = c.description ?? '';
24
+ rules.value = c.rules ?? '';
25
+ startDate.value = c.startDate ? new Date(c.startDate).toISOString().slice(0, 16) : '';
26
+ endDate.value = c.endDate ? new Date(c.endDate).toISOString().slice(0, 16) : '';
27
+ }, { immediate: true });
28
+
29
+ async function handleSave(): Promise<void> {
30
+ saving.value = true;
31
+ try {
32
+ await $fetch(`/api/contests/${slug}`, {
33
+ method: 'PUT',
34
+ body: {
35
+ title: title.value,
36
+ description: description.value || undefined,
37
+ rules: rules.value || undefined,
38
+ startDate: startDate.value ? new Date(startDate.value).toISOString() : undefined,
39
+ endDate: endDate.value ? new Date(endDate.value).toISOString() : undefined,
40
+ },
41
+ });
42
+ toast.success('Contest updated');
43
+ await refresh();
44
+ } catch (err: unknown) {
45
+ toast.error(extractError(err));
46
+ } finally {
47
+ saving.value = false;
48
+ }
49
+ }
50
+
51
+ async function transitionStatus(newStatus: string): Promise<void> {
52
+ if (!confirm(`Change contest status to "${newStatus}"?`)) return;
53
+ try {
54
+ await $fetch(`/api/contests/${slug}/transition`, {
55
+ method: 'POST',
56
+ body: { status: newStatus },
57
+ });
58
+ toast.success(`Status changed to ${newStatus}`);
59
+ await refresh();
60
+ } catch (err: unknown) {
61
+ toast.error(extractError(err));
62
+ }
63
+ }
64
+ </script>
65
+
66
+ <template>
67
+ <div v-if="contest" class="contest-edit">
68
+ <NuxtLink :to="`/contests/${slug}`" class="cpub-back-link"><i class="fa-solid fa-arrow-left"></i> Back to contest</NuxtLink>
69
+ <h1 class="page-title">Edit Contest</h1>
70
+ <p class="page-subtitle">
71
+ Status: <span class="status-badge" :class="`status-${contest.status}`">{{ contest.status }}</span>
72
+ </p>
73
+
74
+ <form class="edit-form" @submit.prevent="handleSave">
75
+ <section class="form-section">
76
+ <h2 class="form-section-title">Details</h2>
77
+ <div class="form-field">
78
+ <label class="form-label">Title</label>
79
+ <input v-model="title" type="text" class="form-input" />
80
+ </div>
81
+ <div class="form-field">
82
+ <label class="form-label">Description</label>
83
+ <textarea v-model="description" class="form-textarea" rows="3" />
84
+ </div>
85
+ <div class="form-field">
86
+ <label class="form-label">Rules</label>
87
+ <textarea v-model="rules" class="form-textarea" rows="4" />
88
+ </div>
89
+ </section>
90
+
91
+ <section class="form-section">
92
+ <h2 class="form-section-title">Schedule</h2>
93
+ <div class="form-row">
94
+ <div class="form-field">
95
+ <label class="form-label">Start Date</label>
96
+ <input v-model="startDate" type="datetime-local" class="form-input" />
97
+ </div>
98
+ <div class="form-field">
99
+ <label class="form-label">End Date</label>
100
+ <input v-model="endDate" type="datetime-local" class="form-input" />
101
+ </div>
102
+ </div>
103
+ </section>
104
+
105
+ <section class="form-section">
106
+ <h2 class="form-section-title">Status Transitions</h2>
107
+ <div class="status-actions">
108
+ <button v-if="contest.status === 'upcoming'" type="button" class="cpub-btn" style="color: var(--green); border-color: var(--green-border);" @click="transitionStatus('active')">
109
+ <i class="fa-solid fa-play"></i> Start Contest
110
+ </button>
111
+ <button v-if="contest.status === 'active'" type="button" class="cpub-btn" style="color: var(--yellow); border-color: var(--yellow-border);" @click="transitionStatus('judging')">
112
+ <i class="fa-solid fa-gavel"></i> Begin Judging
113
+ </button>
114
+ <button v-if="contest.status === 'judging'" type="button" class="cpub-btn" style="color: var(--accent); border-color: var(--accent-border);" @click="transitionStatus('completed')">
115
+ <i class="fa-solid fa-flag-checkered"></i> Complete
116
+ </button>
117
+ </div>
118
+ </section>
119
+
120
+ <button type="submit" class="cpub-btn cpub-btn-primary" :disabled="saving || !title.trim()">
121
+ <i class="fa-solid fa-floppy-disk"></i> {{ saving ? 'Saving...' : 'Save Changes' }}
122
+ </button>
123
+ </form>
124
+ </div>
125
+ <div v-else class="not-found"><p>Contest not found</p></div>
126
+ </template>
127
+
128
+ <style scoped>
129
+ .contest-edit { max-width: 700px; margin: 0 auto; padding: 32px; }
130
+ .cpub-back-link { font-size: 11px; font-family: var(--font-mono); color: var(--text-faint); text-decoration: none; display: inline-flex; align-items: center; gap: 6px; margin-bottom: 16px; }
131
+ .cpub-back-link:hover { color: var(--accent); }
132
+ .page-title { font-size: 22px; font-weight: 700; margin-bottom: 4px; }
133
+ .page-subtitle { font-size: 13px; color: var(--text-dim); margin-bottom: 24px; display: flex; align-items: center; gap: 8px; }
134
+ .status-badge { font-size: 10px; font-family: var(--font-mono); text-transform: uppercase; padding: 2px 8px; border: var(--border-width-default) solid; }
135
+ .status-upcoming { color: var(--yellow); border-color: var(--yellow-border); background: var(--yellow-bg); }
136
+ .status-active { color: var(--green); border-color: var(--green-border); background: var(--green-bg); }
137
+ .status-judging { color: var(--accent); border-color: var(--accent-border); background: var(--accent-bg); }
138
+ .status-completed { color: var(--text-faint); border-color: var(--border2); background: var(--surface2); }
139
+
140
+ .edit-form { display: flex; flex-direction: column; gap: 16px; }
141
+ .form-section { border: var(--border-width-default) solid var(--border); background: var(--surface); padding: 20px; box-shadow: var(--shadow-md); }
142
+ .form-section-title { font-size: 14px; font-weight: 700; margin-bottom: 14px; }
143
+ .form-field { display: flex; flex-direction: column; gap: 4px; margin-bottom: 12px; }
144
+ .form-field:last-child { margin-bottom: 0; }
145
+ .form-label { font-size: 10px; font-weight: 600; font-family: var(--font-mono); text-transform: uppercase; letter-spacing: 0.06em; color: var(--text-faint); }
146
+ .form-input, .form-textarea { padding: 8px 10px; border: var(--border-width-default) solid var(--border); background: var(--surface); color: var(--text); font-size: 13px; font-family: inherit; }
147
+ .form-input:focus, .form-textarea:focus { border-color: var(--accent); outline: none; }
148
+ .form-textarea { resize: vertical; }
149
+ .form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
150
+ .status-actions { display: flex; gap: 8px; }
151
+
152
+ .not-found { text-align: center; padding: 64px; color: var(--text-dim); }
153
+ </style>