@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.
- package/LICENSE +661 -0
- package/app.vue +7 -0
- package/components/AnnouncementBand.vue +117 -0
- package/components/AppToast.vue +108 -0
- package/components/AuthorCard.vue +119 -0
- package/components/AuthorRow.vue +81 -0
- package/components/CommentSection.vue +330 -0
- package/components/ContentCard.vue +340 -0
- package/components/ContentPicker.vue +240 -0
- package/components/ContentStarterForm.vue +214 -0
- package/components/ContentTypeBadge.vue +46 -0
- package/components/CountdownTimer.vue +68 -0
- package/components/CpubEditor.vue +87 -0
- package/components/DiscussionItem.vue +191 -0
- package/components/EditorPropertiesPanel.vue +393 -0
- package/components/EngagementBar.vue +131 -0
- package/components/FederatedContentCard.vue +291 -0
- package/components/FeedItem.vue +283 -0
- package/components/FilterChip.vue +21 -0
- package/components/HeatmapGrid.vue +92 -0
- package/components/ImageUpload.vue +219 -0
- package/components/MemberCard.vue +163 -0
- package/components/MessageThread.vue +120 -0
- package/components/NotificationItem.vue +103 -0
- package/components/ProgressTracker.vue +41 -0
- package/components/PublishErrorsModal.vue +116 -0
- package/components/RemoteActorCard.vue +206 -0
- package/components/RemoteUserSearch.vue +117 -0
- package/components/SearchFilters.vue +188 -0
- package/components/SearchSidebar.vue +181 -0
- package/components/SectionHeader.vue +17 -0
- package/components/ShareToHubModal.vue +189 -0
- package/components/SiteLogo.vue +21 -0
- package/components/SkillBar.vue +57 -0
- package/components/SortSelect.vue +30 -0
- package/components/StatBar.vue +14 -0
- package/components/TOCNav.vue +69 -0
- package/components/TimelineItem.vue +82 -0
- package/components/VideoCard.vue +106 -0
- package/components/blocks/BlockBuildStepView.vue +92 -0
- package/components/blocks/BlockCalloutView.vue +82 -0
- package/components/blocks/BlockCheckpointView.vue +50 -0
- package/components/blocks/BlockCodeView.vue +212 -0
- package/components/blocks/BlockContentRenderer.vue +143 -0
- package/components/blocks/BlockDividerView.vue +11 -0
- package/components/blocks/BlockDownloadsView.vue +126 -0
- package/components/blocks/BlockEmbedView.vue +61 -0
- package/components/blocks/BlockGalleryView.vue +57 -0
- package/components/blocks/BlockHeadingView.vue +29 -0
- package/components/blocks/BlockImageView.vue +34 -0
- package/components/blocks/BlockMarkdownView.vue +118 -0
- package/components/blocks/BlockMathView.vue +45 -0
- package/components/blocks/BlockPartsListView.vue +104 -0
- package/components/blocks/BlockQuizView.vue +239 -0
- package/components/blocks/BlockQuoteView.vue +41 -0
- package/components/blocks/BlockSectionHeaderView.vue +58 -0
- package/components/blocks/BlockSliderView.vue +236 -0
- package/components/blocks/BlockTextView.vue +41 -0
- package/components/blocks/BlockToolListView.vue +87 -0
- package/components/blocks/BlockVideoView.vue +89 -0
- package/components/editors/ArticleEditor.vue +545 -0
- package/components/editors/BlockCanvas.vue +487 -0
- package/components/editors/BlockInsertZone.vue +84 -0
- package/components/editors/BlockPicker.vue +285 -0
- package/components/editors/BlockWrapper.vue +192 -0
- package/components/editors/BlogEditor.vue +567 -0
- package/components/editors/EditorBlocks.vue +248 -0
- package/components/editors/EditorSection.vue +81 -0
- package/components/editors/EditorShell.vue +168 -0
- package/components/editors/EditorTagInput.vue +114 -0
- package/components/editors/EditorVisibility.vue +110 -0
- package/components/editors/ExplainerEditor.vue +503 -0
- package/components/editors/MarkdownImportDialog.vue +249 -0
- package/components/editors/ProjectEditor.vue +446 -0
- package/components/editors/blocks/BuildStepBlock.vue +102 -0
- package/components/editors/blocks/CalloutBlock.vue +122 -0
- package/components/editors/blocks/CheckpointBlock.vue +27 -0
- package/components/editors/blocks/CodeBlock.vue +177 -0
- package/components/editors/blocks/DividerBlock.vue +22 -0
- package/components/editors/blocks/DownloadsBlock.vue +41 -0
- package/components/editors/blocks/EmbedBlock.vue +20 -0
- package/components/editors/blocks/GalleryBlock.vue +236 -0
- package/components/editors/blocks/HeadingBlock.vue +96 -0
- package/components/editors/blocks/ImageBlock.vue +271 -0
- package/components/editors/blocks/MarkdownBlock.vue +258 -0
- package/components/editors/blocks/MathBlock.vue +37 -0
- package/components/editors/blocks/PartsListBlock.vue +358 -0
- package/components/editors/blocks/QuizBlock.vue +47 -0
- package/components/editors/blocks/QuoteBlock.vue +101 -0
- package/components/editors/blocks/SectionHeaderBlock.vue +130 -0
- package/components/editors/blocks/SliderBlock.vue +318 -0
- package/components/editors/blocks/TextBlock.vue +201 -0
- package/components/editors/blocks/ToolListBlock.vue +70 -0
- package/components/editors/blocks/VideoBlock.vue +22 -0
- package/components/hub/HubDiscussions.vue +47 -0
- package/components/hub/HubFeed.vue +199 -0
- package/components/hub/HubHero.vue +185 -0
- package/components/hub/HubLayout.vue +103 -0
- package/components/hub/HubMembers.vue +40 -0
- package/components/hub/HubProducts.vue +93 -0
- package/components/hub/HubProjects.vue +207 -0
- package/components/hub/HubSidebar.vue +12 -0
- package/components/hub/HubSidebarCard.vue +35 -0
- package/components/views/ArticleView.vue +771 -0
- package/components/views/BlogView.vue +667 -0
- package/components/views/ExplainerView.vue +688 -0
- package/components/views/ProjectView.vue +1500 -0
- package/composables/useApiError.ts +39 -0
- package/composables/useAuth.ts +87 -0
- package/composables/useBlockEditor.ts +187 -0
- package/composables/useContentSave.ts +253 -0
- package/composables/useContentTypes.ts +37 -0
- package/composables/useEngagement.ts +196 -0
- package/composables/useFeatures.ts +33 -0
- package/composables/useFederation.ts +72 -0
- package/composables/useJsonLd.ts +183 -0
- package/composables/useMarkdownImport.ts +77 -0
- package/composables/useMirrorContent.ts +105 -0
- package/composables/useNotifications.ts +73 -0
- package/composables/usePublishValidation.ts +65 -0
- package/composables/useSanitize.ts +34 -0
- package/composables/useSiteName.ts +4 -0
- package/composables/useTheme.ts +34 -0
- package/composables/useToast.ts +35 -0
- package/error.vue +129 -0
- package/layouts/admin.vue +213 -0
- package/layouts/auth.vue +63 -0
- package/layouts/default.vue +269 -0
- package/layouts/editor.vue +129 -0
- package/middleware/auth.ts +6 -0
- package/nuxt.config.ts +83 -0
- package/package.json +59 -0
- package/pages/[type]/[slug]/edit.vue +676 -0
- package/pages/[type]/[slug]/index.vue +313 -0
- package/pages/[type]/index.vue +118 -0
- package/pages/about.vue +100 -0
- package/pages/admin/audit.vue +66 -0
- package/pages/admin/content.vue +116 -0
- package/pages/admin/federation.vue +446 -0
- package/pages/admin/index.vue +62 -0
- package/pages/admin/reports.vue +88 -0
- package/pages/admin/settings.vue +167 -0
- package/pages/admin/users.vue +145 -0
- package/pages/auth/forgot-password.vue +103 -0
- package/pages/auth/login.vue +216 -0
- package/pages/auth/oauth/authorize.vue +178 -0
- package/pages/auth/register.vue +246 -0
- package/pages/auth/reset-password.vue +124 -0
- package/pages/auth/verify-email.vue +80 -0
- package/pages/cert/[code].vue +154 -0
- package/pages/contests/[slug]/edit.vue +153 -0
- package/pages/contests/[slug]/index.vue +556 -0
- package/pages/contests/[slug]/judge.vue +160 -0
- package/pages/contests/create.vue +192 -0
- package/pages/contests/index.vue +69 -0
- package/pages/create.vue +219 -0
- package/pages/dashboard.vue +521 -0
- package/pages/docs/[siteSlug]/[...pagePath].vue +704 -0
- package/pages/docs/[siteSlug]/edit.vue +480 -0
- package/pages/docs/[siteSlug]/index.vue +380 -0
- package/pages/docs/create.vue +60 -0
- package/pages/docs/index.vue +181 -0
- package/pages/explore.vue +422 -0
- package/pages/federated-hubs/[id]/index.vue +385 -0
- package/pages/federated-hubs/[id]/posts/[postId].vue +309 -0
- package/pages/federation/index.vue +157 -0
- package/pages/federation/search.vue +43 -0
- package/pages/federation/users/[handle].vue +221 -0
- package/pages/feed.vue +135 -0
- package/pages/hubs/[slug]/index.vue +513 -0
- package/pages/hubs/[slug]/members.vue +134 -0
- package/pages/hubs/[slug]/posts/[postId].vue +352 -0
- package/pages/hubs/[slug]/settings.vue +254 -0
- package/pages/hubs/create.vue +201 -0
- package/pages/hubs/index.vue +207 -0
- package/pages/index.vue +1005 -0
- package/pages/learn/[slug]/[lessonSlug]/edit.vue +413 -0
- package/pages/learn/[slug]/[lessonSlug]/index.vue +438 -0
- package/pages/learn/[slug]/edit.vue +414 -0
- package/pages/learn/[slug]/index.vue +341 -0
- package/pages/learn/create.vue +71 -0
- package/pages/learn/index.vue +360 -0
- package/pages/messages/[conversationId].vue +113 -0
- package/pages/messages/index.vue +303 -0
- package/pages/mirror/[id].vue +115 -0
- package/pages/notifications.vue +91 -0
- package/pages/products/[slug].vue +128 -0
- package/pages/products/index.vue +122 -0
- package/pages/search.vue +692 -0
- package/pages/settings/account.vue +170 -0
- package/pages/settings/appearance.vue +80 -0
- package/pages/settings/index.vue +81 -0
- package/pages/settings/notifications.vue +68 -0
- package/pages/settings/profile.vue +838 -0
- package/pages/tags/[slug].vue +111 -0
- package/pages/tags/index.vue +73 -0
- package/pages/u/[username]/followers.vue +86 -0
- package/pages/u/[username]/following.vue +94 -0
- package/pages/u/[username]/index.vue +837 -0
- package/pages/videos/[id].vue +212 -0
- package/pages/videos/index.vue +327 -0
- package/pages/videos/submit.vue +112 -0
- package/plugins/auth.ts +23 -0
- package/server/api/admin/audit.get.ts +17 -0
- package/server/api/admin/content/[id].delete.ts +15 -0
- package/server/api/admin/content/[id].patch.ts +37 -0
- package/server/api/admin/federation/activity.get.ts +31 -0
- package/server/api/admin/federation/clients.get.ts +9 -0
- package/server/api/admin/federation/clients.post.ts +16 -0
- package/server/api/admin/federation/hub-mirrors/index.get.ts +10 -0
- package/server/api/admin/federation/hub-mirrors/index.post.ts +42 -0
- package/server/api/admin/federation/mirrors/[id]/backfill.post.ts +39 -0
- package/server/api/admin/federation/mirrors/[id].delete.ts +11 -0
- package/server/api/admin/federation/mirrors/[id].get.ts +15 -0
- package/server/api/admin/federation/mirrors/[id].put.ts +22 -0
- package/server/api/admin/federation/mirrors/index.get.ts +9 -0
- package/server/api/admin/federation/mirrors/index.post.ts +30 -0
- package/server/api/admin/federation/pending.get.ts +22 -0
- package/server/api/admin/federation/refederate.post.ts +92 -0
- package/server/api/admin/federation/repair-types.post.ts +57 -0
- package/server/api/admin/federation/retry.post.ts +41 -0
- package/server/api/admin/federation/stats.get.ts +26 -0
- package/server/api/admin/reports/[id]/resolve.post.ts +12 -0
- package/server/api/admin/reports.get.ts +17 -0
- package/server/api/admin/settings.get.ts +22 -0
- package/server/api/admin/settings.put.ts +11 -0
- package/server/api/admin/stats.get.ts +9 -0
- package/server/api/admin/users/[id]/role.put.ts +12 -0
- package/server/api/admin/users/[id]/status.put.ts +12 -0
- package/server/api/admin/users/[id].delete.ts +10 -0
- package/server/api/admin/users.get.ts +18 -0
- package/server/api/auth/federated/callback.get.ts +67 -0
- package/server/api/auth/federated/login.post.ts +60 -0
- package/server/api/auth/oauth2/authorize.get.ts +30 -0
- package/server/api/auth/oauth2/authorize.post.ts +51 -0
- package/server/api/auth/oauth2/register.post.ts +41 -0
- package/server/api/auth/oauth2/token.post.ts +48 -0
- package/server/api/cert/[code].get.ts +13 -0
- package/server/api/content/[id]/build.post.ts +9 -0
- package/server/api/content/[id]/fork.post.ts +10 -0
- package/server/api/content/[id]/index.delete.ts +18 -0
- package/server/api/content/[id]/index.get.ts +15 -0
- package/server/api/content/[id]/index.put.ts +23 -0
- package/server/api/content/[id]/products/[productId].delete.ts +28 -0
- package/server/api/content/[id]/products-sync.post.ts +29 -0
- package/server/api/content/[id]/products.get.ts +9 -0
- package/server/api/content/[id]/products.post.ts +31 -0
- package/server/api/content/[id]/publish.post.ts +17 -0
- package/server/api/content/[id]/report.post.ts +17 -0
- package/server/api/content/[id]/versions.get.ts +9 -0
- package/server/api/content/[id]/view.post.ts +34 -0
- package/server/api/content/index.get.ts +23 -0
- package/server/api/content/index.post.ts +11 -0
- package/server/api/contests/[slug]/entries.get.ts +18 -0
- package/server/api/contests/[slug]/entries.post.ts +23 -0
- package/server/api/contests/[slug]/index.delete.ts +21 -0
- package/server/api/contests/[slug]/index.get.ts +11 -0
- package/server/api/contests/[slug]/index.put.ts +15 -0
- package/server/api/contests/[slug]/judge.post.ts +12 -0
- package/server/api/contests/[slug]/transition.post.ts +24 -0
- package/server/api/contests/index.get.ts +10 -0
- package/server/api/contests/index.post.ts +28 -0
- package/server/api/docs/[siteSlug]/index.delete.ts +14 -0
- package/server/api/docs/[siteSlug]/index.get.ts +17 -0
- package/server/api/docs/[siteSlug]/index.put.ts +20 -0
- package/server/api/docs/[siteSlug]/nav.get.ts +26 -0
- package/server/api/docs/[siteSlug]/pages/[pageId].delete.ts +14 -0
- package/server/api/docs/[siteSlug]/pages/[pageId].get.ts +31 -0
- package/server/api/docs/[siteSlug]/pages/[pageId].put.ts +15 -0
- package/server/api/docs/[siteSlug]/pages/index.get.ts +34 -0
- package/server/api/docs/[siteSlug]/pages/index.post.ts +28 -0
- package/server/api/docs/[siteSlug]/pages/reorder.post.ts +26 -0
- package/server/api/docs/[siteSlug]/search.get.ts +20 -0
- package/server/api/docs/[siteSlug]/versions.post.ts +11 -0
- package/server/api/docs/index.get.ts +6 -0
- package/server/api/docs/index.post.ts +10 -0
- package/server/api/federated-hubs/[id]/posts/[postId].get.ts +16 -0
- package/server/api/federated-hubs/[id]/posts.get.ts +15 -0
- package/server/api/federated-hubs/[id].get.ts +16 -0
- package/server/api/federation/boost.post.ts +21 -0
- package/server/api/federation/content/[id].get.ts +15 -0
- package/server/api/federation/follow.post.ts +16 -0
- package/server/api/federation/health.get.ts +56 -0
- package/server/api/federation/hub-follow.post.ts +27 -0
- package/server/api/federation/hub-post-like.post.ts +115 -0
- package/server/api/federation/hub-post-likes.get.ts +24 -0
- package/server/api/federation/hub-post-reply.post.ts +42 -0
- package/server/api/federation/hub-post.post.ts +33 -0
- package/server/api/federation/like.post.ts +21 -0
- package/server/api/federation/remote-actor.get.ts +22 -0
- package/server/api/federation/reply.post.ts +22 -0
- package/server/api/federation/search.post.ts +17 -0
- package/server/api/federation/timeline.get.ts +22 -0
- package/server/api/federation/unfollow.post.ts +17 -0
- package/server/api/files/[id].delete.ts +35 -0
- package/server/api/files/mine.get.ts +31 -0
- package/server/api/files/upload-from-url.post.ts +68 -0
- package/server/api/files/upload.post.ts +105 -0
- package/server/api/health.get.ts +4 -0
- package/server/api/hubs/[slug]/bans/[userId].delete.ts +13 -0
- package/server/api/hubs/[slug]/bans.get.ts +20 -0
- package/server/api/hubs/[slug]/bans.post.ts +23 -0
- package/server/api/hubs/[slug]/feed.xml.get.ts +60 -0
- package/server/api/hubs/[slug]/gallery.get.ts +21 -0
- package/server/api/hubs/[slug]/index.delete.ts +18 -0
- package/server/api/hubs/[slug]/index.get.ts +14 -0
- package/server/api/hubs/[slug]/index.put.ts +22 -0
- package/server/api/hubs/[slug]/invites.get.ts +20 -0
- package/server/api/hubs/[slug]/invites.post.ts +27 -0
- package/server/api/hubs/[slug]/join.post.ts +17 -0
- package/server/api/hubs/[slug]/leave.post.ts +13 -0
- package/server/api/hubs/[slug]/members/[userId].delete.ts +13 -0
- package/server/api/hubs/[slug]/members/[userId].put.ts +16 -0
- package/server/api/hubs/[slug]/members.get.ts +20 -0
- package/server/api/hubs/[slug]/posts/[postId]/like.post.ts +21 -0
- package/server/api/hubs/[slug]/posts/[postId]/lock.post.ts +17 -0
- package/server/api/hubs/[slug]/posts/[postId]/pin.post.ts +17 -0
- package/server/api/hubs/[slug]/posts/[postId]/replies.get.ts +16 -0
- package/server/api/hubs/[slug]/posts/[postId]/replies.post.ts +21 -0
- package/server/api/hubs/[slug]/posts/[postId].delete.ts +23 -0
- package/server/api/hubs/[slug]/posts/[postId].get.ts +16 -0
- package/server/api/hubs/[slug]/posts/index.get.ts +16 -0
- package/server/api/hubs/[slug]/posts/index.post.ts +36 -0
- package/server/api/hubs/[slug]/products.get.ts +26 -0
- package/server/api/hubs/[slug]/products.post.ts +18 -0
- package/server/api/hubs/[slug]/share.post.ts +39 -0
- package/server/api/hubs/index.get.ts +15 -0
- package/server/api/hubs/index.post.ts +11 -0
- package/server/api/image-proxy.get.ts +91 -0
- package/server/api/learn/[slug]/[lessonSlug]/complete.post.ts +13 -0
- package/server/api/learn/[slug]/[lessonSlug]/index.get.ts +68 -0
- package/server/api/learn/[slug]/enroll.post.ts +12 -0
- package/server/api/learn/[slug]/index.delete.ts +12 -0
- package/server/api/learn/[slug]/index.get.ts +14 -0
- package/server/api/learn/[slug]/index.put.ts +19 -0
- package/server/api/learn/[slug]/lessons/[lessonId].delete.ts +14 -0
- package/server/api/learn/[slug]/lessons/[lessonId].put.ts +24 -0
- package/server/api/learn/[slug]/lessons.post.ts +10 -0
- package/server/api/learn/[slug]/modules/[moduleId].delete.ts +14 -0
- package/server/api/learn/[slug]/modules/[moduleId].put.ts +15 -0
- package/server/api/learn/[slug]/modules.post.ts +14 -0
- package/server/api/learn/[slug]/publish.post.ts +13 -0
- package/server/api/learn/[slug]/unenroll.post.ts +12 -0
- package/server/api/learn/certificates.get.ts +9 -0
- package/server/api/learn/enrollments.get.ts +9 -0
- package/server/api/learn/index.get.ts +17 -0
- package/server/api/learn/index.post.ts +11 -0
- package/server/api/me.get.ts +13 -0
- package/server/api/messages/[conversationId]/info.get.ts +43 -0
- package/server/api/messages/[conversationId]/stream.get.ts +73 -0
- package/server/api/messages/[conversationId].get.ts +13 -0
- package/server/api/messages/[conversationId].post.ts +12 -0
- package/server/api/messages/index.get.ts +39 -0
- package/server/api/messages/index.post.ts +58 -0
- package/server/api/notifications/[id].delete.ts +11 -0
- package/server/api/notifications/count.get.ts +10 -0
- package/server/api/notifications/index.get.ts +24 -0
- package/server/api/notifications/read.post.ts +15 -0
- package/server/api/notifications/stream.get.ts +59 -0
- package/server/api/openapi.get.ts +5 -0
- package/server/api/products/[id].delete.ts +15 -0
- package/server/api/products/[id].put.ts +18 -0
- package/server/api/products/[slug]/content.get.ts +21 -0
- package/server/api/products/[slug].get.ts +16 -0
- package/server/api/products/index.get.ts +28 -0
- package/server/api/profile.get.ts +15 -0
- package/server/api/profile.put.ts +24 -0
- package/server/api/resolve-identity.post.ts +34 -0
- package/server/api/search/index.get.ts +24 -0
- package/server/api/search/trending.get.ts +22 -0
- package/server/api/social/bookmark.get.ts +16 -0
- package/server/api/social/bookmark.post.ts +15 -0
- package/server/api/social/bookmarks.get.ts +16 -0
- package/server/api/social/comments/[id].delete.ts +13 -0
- package/server/api/social/comments.get.ts +18 -0
- package/server/api/social/comments.post.ts +11 -0
- package/server/api/social/like.get.ts +17 -0
- package/server/api/social/like.post.ts +35 -0
- package/server/api/stats.get.ts +8 -0
- package/server/api/user/hubs.get.ts +22 -0
- package/server/api/users/[username]/content.get.ts +21 -0
- package/server/api/users/[username]/feed.xml.get.ts +65 -0
- package/server/api/users/[username]/follow.delete.ts +15 -0
- package/server/api/users/[username]/follow.post.ts +15 -0
- package/server/api/users/[username]/followers.get.ts +22 -0
- package/server/api/users/[username]/following.get.ts +22 -0
- package/server/api/users/[username]/learning.get.ts +18 -0
- package/server/api/users/[username].get.ts +25 -0
- package/server/api/users/index.get.ts +78 -0
- package/server/api/videos/[id].get.ts +18 -0
- package/server/api/videos/categories/[id].delete.ts +15 -0
- package/server/api/videos/categories/[id].put.ts +17 -0
- package/server/api/videos/categories.get.ts +7 -0
- package/server/api/videos/categories.post.ts +11 -0
- package/server/api/videos/index.get.ts +20 -0
- package/server/api/videos/index.post.ts +11 -0
- package/server/middleware/auth.ts +180 -0
- package/server/middleware/features.ts +31 -0
- package/server/middleware/security.ts +54 -0
- package/server/plugins/auto-admin.ts +69 -0
- package/server/plugins/federation-delivery.ts +74 -0
- package/server/routes/.well-known/nodeinfo.ts +15 -0
- package/server/routes/.well-known/webfinger.ts +86 -0
- package/server/routes/actor/followers.ts +34 -0
- package/server/routes/actor/following.ts +34 -0
- package/server/routes/actor/outbox.ts +31 -0
- package/server/routes/actor.ts +30 -0
- package/server/routes/feed.xml.ts +59 -0
- package/server/routes/hubs/[slug]/followers.ts +39 -0
- package/server/routes/hubs/[slug]/inbox.ts +35 -0
- package/server/routes/hubs/[slug]/outbox.ts +43 -0
- package/server/routes/hubs/[slug]/posts/[postId].ts +63 -0
- package/server/routes/hubs/[slug].ts +29 -0
- package/server/routes/inbox.ts +34 -0
- package/server/routes/nodeinfo/2.1.ts +27 -0
- package/server/routes/robots.txt.ts +19 -0
- package/server/routes/sitemap.xml.ts +105 -0
- package/server/routes/users/[username]/followers.ts +33 -0
- package/server/routes/users/[username]/following.ts +33 -0
- package/server/routes/users/[username]/inbox.ts +34 -0
- package/server/routes/users/[username]/outbox.ts +35 -0
- package/server/routes/users/[username].ts +62 -0
- package/server/utils/auth.ts +36 -0
- package/server/utils/db.ts +34 -0
- package/server/utils/errors.ts +24 -0
- package/server/utils/inbox.ts +122 -0
- package/server/utils/validate.ts +82 -0
- package/theme/base.css +283 -0
- package/theme/components.css +322 -0
- package/theme/dark.css +87 -0
- package/theme/editor-panels.css +63 -0
- package/theme/forms.css +216 -0
- package/theme/generics.css +68 -0
- package/theme/layouts.css +415 -0
- package/theme/prose.css +342 -0
- package/types/hub.ts +61 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { findOrCreateConversation } from '@commonpub/server';
|
|
2
|
+
import { users } from '@commonpub/schema';
|
|
3
|
+
import { and, inArray, isNull } from 'drizzle-orm';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
|
|
6
|
+
// Accept either UUIDs or usernames — resolve usernames to IDs server-side
|
|
7
|
+
const createConversationBodySchema = z.object({
|
|
8
|
+
participants: z.array(z.string().min(1)).min(1).max(50),
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
12
|
+
|
|
13
|
+
export default defineEventHandler(async (event) => {
|
|
14
|
+
const db = useDB();
|
|
15
|
+
const user = requireAuth(event);
|
|
16
|
+
const input = await parseBody(event, createConversationBodySchema);
|
|
17
|
+
|
|
18
|
+
// Separate UUIDs from usernames, deduplicating each
|
|
19
|
+
const uuidSet = new Set<string>();
|
|
20
|
+
const usernameSet = new Set<string>();
|
|
21
|
+
for (const p of input.participants) {
|
|
22
|
+
if (UUID_REGEX.test(p)) {
|
|
23
|
+
uuidSet.add(p);
|
|
24
|
+
} else {
|
|
25
|
+
usernameSet.add(p);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const uuids = [...uuidSet];
|
|
30
|
+
const usernames = [...usernameSet];
|
|
31
|
+
|
|
32
|
+
// Resolve usernames to UUIDs (exclude soft-deleted users)
|
|
33
|
+
if (usernames.length > 0) {
|
|
34
|
+
const resolved = await db
|
|
35
|
+
.select({ id: users.id })
|
|
36
|
+
.from(users)
|
|
37
|
+
.where(and(inArray(users.username, usernames), isNull(users.deletedAt)));
|
|
38
|
+
|
|
39
|
+
if (resolved.length !== usernames.length) {
|
|
40
|
+
throw createError({ statusCode: 400, statusMessage: 'One or more usernames not found' });
|
|
41
|
+
}
|
|
42
|
+
uuids.push(...resolved.map((r) => r.id));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Ensure current user is included
|
|
46
|
+
if (!uuids.includes(user.id)) {
|
|
47
|
+
uuids.push(user.id);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Deduplicate (a username could resolve to a UUID already in the list)
|
|
51
|
+
const participants = [...new Set(uuids)];
|
|
52
|
+
|
|
53
|
+
if (participants.length < 2) {
|
|
54
|
+
throw createError({ statusCode: 400, statusMessage: 'A conversation requires at least one other participant' });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return findOrCreateConversation(db, participants);
|
|
58
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { deleteNotification } from '@commonpub/server';
|
|
2
|
+
|
|
3
|
+
export default defineEventHandler(async (event): Promise<{ success: boolean }> => {
|
|
4
|
+
const user = requireAuth(event);
|
|
5
|
+
const db = useDB();
|
|
6
|
+
const { id } = parseParams(event, { id: 'uuid' });
|
|
7
|
+
|
|
8
|
+
await deleteNotification(db, id, user.id);
|
|
9
|
+
|
|
10
|
+
return { success: true };
|
|
11
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { getUnreadCount } from '@commonpub/server';
|
|
2
|
+
|
|
3
|
+
export default defineEventHandler(async (event): Promise<{ count: number }> => {
|
|
4
|
+
const user = requireAuth(event);
|
|
5
|
+
const db = useDB();
|
|
6
|
+
|
|
7
|
+
const count = await getUnreadCount(db, user.id);
|
|
8
|
+
|
|
9
|
+
return { count };
|
|
10
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { listNotifications } from '@commonpub/server';
|
|
2
|
+
import type { PaginatedResponse, NotificationItem, NotificationType } from '@commonpub/server';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
|
|
5
|
+
const notificationsQuerySchema = z.object({
|
|
6
|
+
type: z.string().max(64).optional(),
|
|
7
|
+
read: z.enum(['true', 'false']).optional(),
|
|
8
|
+
limit: z.coerce.number().int().positive().max(100).optional(),
|
|
9
|
+
offset: z.coerce.number().int().min(0).optional(),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export default defineEventHandler(async (event): Promise<PaginatedResponse<NotificationItem>> => {
|
|
13
|
+
const user = requireAuth(event);
|
|
14
|
+
const db = useDB();
|
|
15
|
+
const query = parseQueryParams(event, notificationsQuerySchema);
|
|
16
|
+
|
|
17
|
+
return listNotifications(db, {
|
|
18
|
+
userId: user.id,
|
|
19
|
+
type: query.type as NotificationType | undefined,
|
|
20
|
+
read: query.read !== undefined ? query.read === 'true' : undefined,
|
|
21
|
+
limit: query.limit,
|
|
22
|
+
offset: query.offset,
|
|
23
|
+
});
|
|
24
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { markNotificationRead, markAllNotificationsRead } from '@commonpub/server';
|
|
2
|
+
|
|
3
|
+
export default defineEventHandler(async (event): Promise<{ success: boolean }> => {
|
|
4
|
+
const user = requireAuth(event);
|
|
5
|
+
const db = useDB();
|
|
6
|
+
const body = await readBody(event);
|
|
7
|
+
|
|
8
|
+
if (body.notificationId) {
|
|
9
|
+
await markNotificationRead(db, body.notificationId, user.id);
|
|
10
|
+
} else {
|
|
11
|
+
await markAllNotificationsRead(db, user.id);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return { success: true };
|
|
15
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { getUnreadCount } from '@commonpub/server';
|
|
2
|
+
|
|
3
|
+
export default defineEventHandler(async (event) => {
|
|
4
|
+
const user = requireAuth(event);
|
|
5
|
+
const userId = user.id;
|
|
6
|
+
const db = useDB();
|
|
7
|
+
|
|
8
|
+
setResponseHeader(event, 'Content-Type', 'text/event-stream');
|
|
9
|
+
setResponseHeader(event, 'Cache-Control', 'no-cache');
|
|
10
|
+
setResponseHeader(event, 'Connection', 'keep-alive');
|
|
11
|
+
|
|
12
|
+
const encoder = new TextEncoder();
|
|
13
|
+
const stream = new ReadableStream({
|
|
14
|
+
async start(controller) {
|
|
15
|
+
let closed = false;
|
|
16
|
+
function cleanup(): void {
|
|
17
|
+
if (closed) return;
|
|
18
|
+
closed = true;
|
|
19
|
+
clearInterval(interval);
|
|
20
|
+
clearInterval(keepalive);
|
|
21
|
+
try { controller.close(); } catch { /* already closed */ }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Send initial count
|
|
25
|
+
const count = await getUnreadCount(db, userId);
|
|
26
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify({ type: 'count', count })}\n\n`));
|
|
27
|
+
|
|
28
|
+
// Poll every 10 seconds for new notifications
|
|
29
|
+
const interval = setInterval(async () => {
|
|
30
|
+
try {
|
|
31
|
+
const currentCount = await getUnreadCount(db, userId);
|
|
32
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify({ type: 'count', count: currentCount })}\n\n`));
|
|
33
|
+
} catch {
|
|
34
|
+
cleanup();
|
|
35
|
+
}
|
|
36
|
+
}, 10000);
|
|
37
|
+
|
|
38
|
+
// Send keepalive every 30 seconds
|
|
39
|
+
const keepalive = setInterval(() => {
|
|
40
|
+
try {
|
|
41
|
+
controller.enqueue(encoder.encode(': keepalive\n\n'));
|
|
42
|
+
} catch {
|
|
43
|
+
cleanup();
|
|
44
|
+
}
|
|
45
|
+
}, 30000);
|
|
46
|
+
|
|
47
|
+
// Clean up on close
|
|
48
|
+
event.node.req.on('close', cleanup);
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
return new Response(stream, {
|
|
53
|
+
headers: {
|
|
54
|
+
'Content-Type': 'text/event-stream',
|
|
55
|
+
'Cache-Control': 'no-cache',
|
|
56
|
+
'Connection': 'keep-alive',
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { deleteProduct } from '@commonpub/server';
|
|
2
|
+
|
|
3
|
+
export default defineEventHandler(async (event): Promise<{ deleted: boolean }> => {
|
|
4
|
+
const db = useDB();
|
|
5
|
+
requireAdmin(event);
|
|
6
|
+
const { id } = parseParams(event, { id: 'uuid' });
|
|
7
|
+
|
|
8
|
+
const deleted = await deleteProduct(db, id);
|
|
9
|
+
|
|
10
|
+
if (!deleted) {
|
|
11
|
+
throw createError({ statusCode: 404, statusMessage: 'Product not found' });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return { deleted: true };
|
|
15
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { updateProduct } from '@commonpub/server';
|
|
2
|
+
import type { ProductDetail } from '@commonpub/server';
|
|
3
|
+
import { updateProductSchema } from '@commonpub/schema';
|
|
4
|
+
|
|
5
|
+
export default defineEventHandler(async (event): Promise<ProductDetail> => {
|
|
6
|
+
const db = useDB();
|
|
7
|
+
const user = requireAuth(event);
|
|
8
|
+
const { id } = parseParams(event, { id: 'uuid' });
|
|
9
|
+
const input = await parseBody(event, updateProductSchema);
|
|
10
|
+
|
|
11
|
+
const product = await updateProduct(db, id, user.id, input);
|
|
12
|
+
|
|
13
|
+
if (!product) {
|
|
14
|
+
throw createError({ statusCode: 404, statusMessage: 'Product not found' });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return product;
|
|
18
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { getProductBySlug, listProductContent } from '@commonpub/server';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
const querySchema = z.object({
|
|
5
|
+
limit: z.coerce.number().int().positive().max(100).optional(),
|
|
6
|
+
offset: z.coerce.number().int().min(0).optional(),
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
export default defineEventHandler(async (event) => {
|
|
10
|
+
const db = useDB();
|
|
11
|
+
const { slug } = parseParams(event, { slug: 'string' });
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
const product = await getProductBySlug(db, slug);
|
|
15
|
+
if (!product) {
|
|
16
|
+
throw createError({ statusCode: 404, statusMessage: 'Product not found' });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const query = parseQueryParams(event, querySchema);
|
|
20
|
+
return listProductContent(db, product.id, query);
|
|
21
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { getProductBySlug } from '@commonpub/server';
|
|
2
|
+
import type { ProductDetail } from '@commonpub/server';
|
|
3
|
+
|
|
4
|
+
export default defineEventHandler(async (event): Promise<ProductDetail> => {
|
|
5
|
+
const db = useDB();
|
|
6
|
+
const { slug } = parseParams(event, { slug: 'string' });
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
const product = await getProductBySlug(db, slug);
|
|
10
|
+
|
|
11
|
+
if (!product) {
|
|
12
|
+
throw createError({ statusCode: 404, statusMessage: 'Product not found' });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return product;
|
|
16
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { searchProducts } from '@commonpub/server';
|
|
2
|
+
import type { PaginatedResponse, ProductListItem } from '@commonpub/server';
|
|
3
|
+
import { productStatusSchema, productCategorySchema } from '@commonpub/schema';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
|
|
6
|
+
const productSearchSchema = z.object({
|
|
7
|
+
q: z.string().max(200).optional(),
|
|
8
|
+
search: z.string().max(200).optional(),
|
|
9
|
+
category: productCategorySchema.optional(),
|
|
10
|
+
status: productStatusSchema.optional(),
|
|
11
|
+
hubId: z.string().uuid().optional(),
|
|
12
|
+
limit: z.coerce.number().int().positive().max(100).optional(),
|
|
13
|
+
offset: z.coerce.number().int().min(0).optional(),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export default defineEventHandler(async (event): Promise<PaginatedResponse<ProductListItem>> => {
|
|
17
|
+
const db = useDB();
|
|
18
|
+
const query = parseQueryParams(event, productSearchSchema);
|
|
19
|
+
|
|
20
|
+
return searchProducts(db, {
|
|
21
|
+
search: query.q ?? query.search,
|
|
22
|
+
category: query.category,
|
|
23
|
+
status: query.status,
|
|
24
|
+
hubId: query.hubId,
|
|
25
|
+
limit: query.limit,
|
|
26
|
+
offset: query.offset,
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { getUserByUsername } from '@commonpub/server';
|
|
2
|
+
import type { UserProfile } from '@commonpub/server';
|
|
3
|
+
|
|
4
|
+
export default defineEventHandler(async (event): Promise<UserProfile> => {
|
|
5
|
+
const db = useDB();
|
|
6
|
+
const user = requireAuth(event);
|
|
7
|
+
|
|
8
|
+
const profile = await getUserByUsername(db, user.username);
|
|
9
|
+
|
|
10
|
+
if (!profile) {
|
|
11
|
+
throw createError({ statusCode: 404, statusMessage: 'Profile not found' });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return profile;
|
|
15
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { updateUserProfile } from '@commonpub/server';
|
|
2
|
+
import type { UserProfile } from '@commonpub/server';
|
|
3
|
+
import { updateProfileSchema } from '@commonpub/schema';
|
|
4
|
+
|
|
5
|
+
export default defineEventHandler(async (event): Promise<UserProfile> => {
|
|
6
|
+
const db = useDB();
|
|
7
|
+
const user = requireAuth(event);
|
|
8
|
+
const raw = await parseBody(event, updateProfileSchema);
|
|
9
|
+
// Convert empty strings to undefined for URL fields (avoids <img src="">)
|
|
10
|
+
const input = {
|
|
11
|
+
...raw,
|
|
12
|
+
avatarUrl: raw.avatarUrl || undefined,
|
|
13
|
+
bannerUrl: raw.bannerUrl || undefined,
|
|
14
|
+
website: raw.website || undefined,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const profile = await updateUserProfile(db, user.id, input);
|
|
18
|
+
|
|
19
|
+
if (!profile) {
|
|
20
|
+
throw createError({ statusCode: 404, statusMessage: 'Profile not found' });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return profile;
|
|
24
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { eq, and, isNull } from 'drizzle-orm';
|
|
2
|
+
import { users } from '@commonpub/schema';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Resolve identity (username or email) to an email address.
|
|
6
|
+
* The client then uses the resolved email to call Better Auth's sign-in directly.
|
|
7
|
+
*/
|
|
8
|
+
export default defineEventHandler(async (event) => {
|
|
9
|
+
const body = await readBody(event);
|
|
10
|
+
const { identity } = body as { identity: string };
|
|
11
|
+
|
|
12
|
+
if (!identity) {
|
|
13
|
+
throw createError({ statusCode: 400, statusMessage: 'Username or email required' });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// If it looks like an email, return it directly
|
|
17
|
+
if (identity.includes('@')) {
|
|
18
|
+
return { email: identity };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Otherwise resolve username to email
|
|
22
|
+
const db = useDB();
|
|
23
|
+
const [user] = await db
|
|
24
|
+
.select({ email: users.email })
|
|
25
|
+
.from(users)
|
|
26
|
+
.where(and(eq(users.username, identity), isNull(users.deletedAt)))
|
|
27
|
+
.limit(1);
|
|
28
|
+
|
|
29
|
+
if (!user) {
|
|
30
|
+
throw createError({ statusCode: 401, statusMessage: 'Invalid credentials' });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return { email: user.email };
|
|
34
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { listContent } from '@commonpub/server';
|
|
2
|
+
import type { PaginatedResponse, ContentListItem } from '@commonpub/server';
|
|
3
|
+
import { contentFiltersSchema } from '@commonpub/schema';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
|
|
6
|
+
const searchQuerySchema = contentFiltersSchema.extend({
|
|
7
|
+
q: z.string().max(200).optional(),
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
export default defineEventHandler(async (event): Promise<PaginatedResponse<ContentListItem>> => {
|
|
11
|
+
const db = useDB();
|
|
12
|
+
const filters = parseQueryParams(event, searchQuerySchema);
|
|
13
|
+
const q = filters.q || filters.search;
|
|
14
|
+
|
|
15
|
+
if (!q) {
|
|
16
|
+
return { items: [], total: 0 };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return listContent(db, {
|
|
20
|
+
...filters,
|
|
21
|
+
status: 'published',
|
|
22
|
+
search: q,
|
|
23
|
+
});
|
|
24
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { sql } from 'drizzle-orm';
|
|
2
|
+
import { contentItems } from '@commonpub/schema';
|
|
3
|
+
|
|
4
|
+
export default defineEventHandler(async (): Promise<Array<{ query: string; trend: number }>> => {
|
|
5
|
+
const db = useDB();
|
|
6
|
+
|
|
7
|
+
// Return the most-viewed content titles as "trending searches"
|
|
8
|
+
const rows = await db
|
|
9
|
+
.select({
|
|
10
|
+
title: contentItems.title,
|
|
11
|
+
viewCount: contentItems.viewCount,
|
|
12
|
+
})
|
|
13
|
+
.from(contentItems)
|
|
14
|
+
.where(sql`${contentItems.status} = 'published'`)
|
|
15
|
+
.orderBy(sql`${contentItems.viewCount} DESC`)
|
|
16
|
+
.limit(8);
|
|
17
|
+
|
|
18
|
+
return rows.map((r) => ({
|
|
19
|
+
query: r.title,
|
|
20
|
+
trend: r.viewCount,
|
|
21
|
+
}));
|
|
22
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { isBookmarked } from '@commonpub/server';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
const checkSchema = z.object({
|
|
5
|
+
targetType: z.enum(['project', 'article', 'blog', 'explainer', 'learning_path']),
|
|
6
|
+
targetId: z.string().uuid(),
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
export default defineEventHandler(async (event): Promise<{ bookmarked: boolean }> => {
|
|
10
|
+
const user = requireAuth(event);
|
|
11
|
+
const db = useDB();
|
|
12
|
+
const query = parseQueryParams(event, checkSchema);
|
|
13
|
+
|
|
14
|
+
const bookmarked = await isBookmarked(db, user.id, query.targetType, query.targetId);
|
|
15
|
+
return { bookmarked };
|
|
16
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { toggleBookmark } from '@commonpub/server';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
const toggleBookmarkSchema = z.object({
|
|
5
|
+
targetType: z.enum(['project', 'article', 'blog', 'explainer', 'learning_path']),
|
|
6
|
+
targetId: z.string().uuid(),
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
export default defineEventHandler(async (event): Promise<{ bookmarked: boolean }> => {
|
|
10
|
+
const user = requireAuth(event);
|
|
11
|
+
const db = useDB();
|
|
12
|
+
const input = await parseBody(event, toggleBookmarkSchema);
|
|
13
|
+
|
|
14
|
+
return toggleBookmark(db, user.id, input.targetType, input.targetId);
|
|
15
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { listUserBookmarks } from '@commonpub/server';
|
|
2
|
+
import type { PaginatedResponse, BookmarkItem } from '@commonpub/server';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
|
|
5
|
+
const bookmarksQuerySchema = z.object({
|
|
6
|
+
limit: z.coerce.number().int().positive().max(100).optional(),
|
|
7
|
+
offset: z.coerce.number().int().min(0).optional(),
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
export default defineEventHandler(async (event): Promise<PaginatedResponse<BookmarkItem>> => {
|
|
11
|
+
const user = requireAuth(event);
|
|
12
|
+
const db = useDB();
|
|
13
|
+
const query = parseQueryParams(event, bookmarksQuerySchema);
|
|
14
|
+
|
|
15
|
+
return listUserBookmarks(db, user.id, query);
|
|
16
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { deleteComment } from '@commonpub/server';
|
|
2
|
+
|
|
3
|
+
export default defineEventHandler(async (event): Promise<{ success: boolean }> => {
|
|
4
|
+
const user = requireAuth(event);
|
|
5
|
+
const db = useDB();
|
|
6
|
+
const { id } = parseParams(event, { id: 'uuid' });
|
|
7
|
+
|
|
8
|
+
const deleted = await deleteComment(db, id, user.id);
|
|
9
|
+
if (!deleted) {
|
|
10
|
+
throw createError({ statusCode: 404, statusMessage: 'Comment not found or not owned by you' });
|
|
11
|
+
}
|
|
12
|
+
return { success: true };
|
|
13
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { listComments } from '@commonpub/server';
|
|
2
|
+
import type { CommentItem } from '@commonpub/server';
|
|
3
|
+
import { commentTargetTypeSchema } from '@commonpub/schema';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
|
|
6
|
+
const commentsQuerySchema = z.object({
|
|
7
|
+
targetType: commentTargetTypeSchema,
|
|
8
|
+
targetId: z.string().uuid(),
|
|
9
|
+
limit: z.coerce.number().int().min(1).max(100).optional().default(20),
|
|
10
|
+
offset: z.coerce.number().int().min(0).optional().default(0),
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
export default defineEventHandler(async (event): Promise<CommentItem[]> => {
|
|
14
|
+
const db = useDB();
|
|
15
|
+
const query = parseQueryParams(event, commentsQuerySchema);
|
|
16
|
+
|
|
17
|
+
return listComments(db, query.targetType, query.targetId, query.limit, query.offset);
|
|
18
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { createComment } from '@commonpub/server';
|
|
2
|
+
import type { CommentItem } from '@commonpub/server';
|
|
3
|
+
import { createCommentSchema } from '@commonpub/schema';
|
|
4
|
+
|
|
5
|
+
export default defineEventHandler(async (event): Promise<CommentItem> => {
|
|
6
|
+
const user = requireAuth(event);
|
|
7
|
+
const db = useDB();
|
|
8
|
+
const input = await parseBody(event, createCommentSchema);
|
|
9
|
+
|
|
10
|
+
return createComment(db, user.id, input);
|
|
11
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { isLiked } from '@commonpub/server';
|
|
2
|
+
import { likeTargetTypeSchema } from '@commonpub/schema';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
|
|
5
|
+
const likeQuerySchema = z.object({
|
|
6
|
+
targetType: likeTargetTypeSchema,
|
|
7
|
+
targetId: z.string().uuid(),
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
export default defineEventHandler(async (event): Promise<{ liked: boolean }> => {
|
|
11
|
+
const user = requireAuth(event);
|
|
12
|
+
const db = useDB();
|
|
13
|
+
const query = parseQueryParams(event, likeQuerySchema);
|
|
14
|
+
|
|
15
|
+
const liked = await isLiked(db, user.id, query.targetType, query.targetId);
|
|
16
|
+
return { liked };
|
|
17
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { toggleLike, onContentLiked, onContentUnliked, getContentSlugById, buildContentUri } from '@commonpub/server';
|
|
2
|
+
import { likeTargetTypeSchema } from '@commonpub/schema';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
|
|
5
|
+
const toggleLikeSchema = z.object({
|
|
6
|
+
targetType: likeTargetTypeSchema,
|
|
7
|
+
targetId: z.string().uuid(),
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
/** Content types that should federate likes (not posts or comments) */
|
|
11
|
+
const FEDERABLE_LIKE_TYPES = new Set(['project', 'article', 'blog', 'explainer']);
|
|
12
|
+
|
|
13
|
+
export default defineEventHandler(async (event): Promise<{ liked: boolean }> => {
|
|
14
|
+
const user = requireAuth(event);
|
|
15
|
+
const db = useDB();
|
|
16
|
+
const config = useConfig();
|
|
17
|
+
const input = await parseBody(event, toggleLikeSchema);
|
|
18
|
+
|
|
19
|
+
const result = await toggleLike(db, user.id, input.targetType, input.targetId);
|
|
20
|
+
|
|
21
|
+
// Federate likes on content items (not posts/comments)
|
|
22
|
+
if (FEDERABLE_LIKE_TYPES.has(input.targetType)) {
|
|
23
|
+
const slug = await getContentSlugById(db, input.targetId);
|
|
24
|
+
if (slug) {
|
|
25
|
+
const contentUri = buildContentUri(config.instance.domain, slug);
|
|
26
|
+
if (result.liked) {
|
|
27
|
+
await onContentLiked(db, user.id, contentUri, config);
|
|
28
|
+
} else {
|
|
29
|
+
await onContentUnliked(db, user.id, contentUri, config);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return result;
|
|
35
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { getPlatformStats } from '@commonpub/server';
|
|
2
|
+
import type { PlatformStats } from '@commonpub/server';
|
|
3
|
+
|
|
4
|
+
export default defineEventHandler(async (_event): Promise<PlatformStats> => {
|
|
5
|
+
const db = useDB();
|
|
6
|
+
const stats = await getPlatformStats(db);
|
|
7
|
+
return stats;
|
|
8
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { eq } from 'drizzle-orm';
|
|
2
|
+
import { hubs, hubMembers } from '@commonpub/schema';
|
|
3
|
+
|
|
4
|
+
/** GET /api/user/hubs — List hubs the current user is a member of */
|
|
5
|
+
export default defineEventHandler(async (event) => {
|
|
6
|
+
const user = requireAuth(event);
|
|
7
|
+
const db = useDB();
|
|
8
|
+
|
|
9
|
+
const rows = await db
|
|
10
|
+
.select({
|
|
11
|
+
id: hubs.id,
|
|
12
|
+
name: hubs.name,
|
|
13
|
+
slug: hubs.slug,
|
|
14
|
+
iconUrl: hubs.iconUrl,
|
|
15
|
+
role: hubMembers.role,
|
|
16
|
+
})
|
|
17
|
+
.from(hubMembers)
|
|
18
|
+
.innerJoin(hubs, eq(hubMembers.hubId, hubs.id))
|
|
19
|
+
.where(eq(hubMembers.userId, user.id));
|
|
20
|
+
|
|
21
|
+
return { items: rows };
|
|
22
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { getUserByUsername, getUserContent } from '@commonpub/server';
|
|
2
|
+
import type { PaginatedResponse, ContentListItem } from '@commonpub/server';
|
|
3
|
+
import { contentTypeSchema } from '@commonpub/schema';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
|
|
6
|
+
const userContentQuerySchema = z.object({
|
|
7
|
+
type: contentTypeSchema.optional(),
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
export default defineEventHandler(async (event): Promise<PaginatedResponse<ContentListItem>> => {
|
|
11
|
+
const db = useDB();
|
|
12
|
+
const { username } = parseParams(event, { username: 'string' });
|
|
13
|
+
const query = parseQueryParams(event, userContentQuerySchema);
|
|
14
|
+
|
|
15
|
+
const user = await getUserByUsername(db, username);
|
|
16
|
+
if (!user) {
|
|
17
|
+
throw createError({ statusCode: 404, statusMessage: 'User not found' });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return getUserContent(db, user.id, query.type);
|
|
21
|
+
});
|