From 22b7e7ab810d2ca73e9b17a1fa8db8ffc937ce51 Mon Sep 17 00:00:00 2001 From: SoSweetHam Date: Tue, 10 Mar 2026 22:15:52 +0530 Subject: [PATCH 01/27] feat: profile editor --- packages/auth/package.json | 33 +++ packages/auth/src/auth-offer.ts | 15 + packages/auth/src/guard.ts | 8 + packages/auth/src/index.ts | 14 + packages/auth/src/jwt.ts | 20 ++ packages/auth/src/middleware.ts | 32 +++ packages/auth/src/types.ts | 34 +++ packages/auth/src/verify-login.ts | 22 ++ packages/auth/tsconfig.json | 18 ++ packages/ui/.gitignore | 3 + packages/ui/.prettierignore | 1 + packages/ui/.prettierrc | 8 + packages/ui/components.json | 16 ++ packages/ui/eslint.config.js | 48 ++++ packages/ui/package.json | 205 +++++++++++++ packages/ui/src/app.css | 123 ++++++++ .../ui/accordion/accordion-content.svelte | 22 ++ .../ui/accordion/accordion-item.svelte | 17 ++ .../ui/accordion/accordion-trigger.svelte | 32 +++ .../components/ui/accordion/accordion.svelte | 16 ++ .../src/lib/components/ui/accordion/index.ts | 16 ++ .../ui/avatar/avatar-fallback.svelte | 17 ++ .../components/ui/avatar/avatar-image.svelte | 17 ++ .../lib/components/ui/avatar/avatar.svelte | 19 ++ .../ui/src/lib/components/ui/avatar/index.ts | 13 + .../src/lib/components/ui/badge/badge.svelte | 49 ++++ .../ui/src/lib/components/ui/badge/index.ts | 2 + .../lib/components/ui/button/button.svelte | 82 ++++++ .../ui/src/lib/components/ui/button/index.ts | 17 ++ .../lib/components/ui/card/card-action.svelte | 20 ++ .../components/ui/card/card-content.svelte | 15 + .../ui/card/card-description.svelte | 20 ++ .../lib/components/ui/card/card-footer.svelte | 20 ++ .../lib/components/ui/card/card-header.svelte | 23 ++ .../lib/components/ui/card/card-title.svelte | 20 ++ .../ui/src/lib/components/ui/card/card.svelte | 23 ++ .../ui/src/lib/components/ui/card/index.ts | 25 ++ .../ui/carousel/carousel-content.svelte | 43 +++ .../ui/carousel/carousel-item.svelte | 30 ++ .../ui/carousel/carousel-next.svelte | 38 +++ .../ui/carousel/carousel-previous.svelte | 38 +++ .../components/ui/carousel/carousel.svelte | 93 ++++++ .../src/lib/components/ui/carousel/context.ts | 58 ++++ .../src/lib/components/ui/carousel/index.ts | 19 ++ .../components/ui/dialog/dialog-close.svelte | 7 + .../ui/dialog/dialog-content.svelte | 45 +++ .../ui/dialog/dialog-description.svelte | 17 ++ .../components/ui/dialog/dialog-footer.svelte | 20 ++ .../components/ui/dialog/dialog-header.svelte | 20 ++ .../ui/dialog/dialog-overlay.svelte | 20 ++ .../components/ui/dialog/dialog-portal.svelte | 7 + .../components/ui/dialog/dialog-title.svelte | 17 ++ .../ui/dialog/dialog-trigger.svelte | 7 + .../lib/components/ui/dialog/dialog.svelte | 7 + .../ui/src/lib/components/ui/dialog/index.ts | 34 +++ .../dropdown-menu-checkbox-item.svelte | 41 +++ .../dropdown-menu-content.svelte | 27 ++ .../dropdown-menu-group-heading.svelte | 22 ++ .../dropdown-menu/dropdown-menu-group.svelte | 7 + .../dropdown-menu/dropdown-menu-item.svelte | 27 ++ .../dropdown-menu/dropdown-menu-label.svelte | 24 ++ .../dropdown-menu-radio-group.svelte | 16 ++ .../dropdown-menu-radio-item.svelte | 31 ++ .../dropdown-menu-separator.svelte | 17 ++ .../dropdown-menu-shortcut.svelte | 20 ++ .../dropdown-menu-sub-content.svelte | 20 ++ .../dropdown-menu-sub-trigger.svelte | 29 ++ .../dropdown-menu-trigger.svelte | 7 + .../lib/components/ui/dropdown-menu/index.ts | 50 ++++ .../components/ui/field/field-content.svelte | 14 + .../ui/field/field-description.svelte | 19 ++ .../components/ui/field/field-error.svelte | 56 ++++ .../components/ui/field/field-group.svelte | 17 ++ .../components/ui/field/field-label.svelte | 26 ++ .../components/ui/field/field-legend.svelte | 27 ++ .../ui/field/field-separator.svelte | 33 +++ .../lib/components/ui/field/field-set.svelte | 18 ++ .../components/ui/field/field-title.svelte | 17 ++ .../src/lib/components/ui/field/field.svelte | 51 ++++ .../ui/src/lib/components/ui/field/index.ts | 33 +++ .../lib/components/ui/form/form-button.svelte | 7 + .../ui/form/form-description.svelte | 17 ++ .../ui/form/form-element-field.svelte | 24 ++ .../ui/form/form-field-errors.svelte | 30 ++ .../lib/components/ui/form/form-field.svelte | 24 ++ .../components/ui/form/form-fieldset.svelte | 15 + .../lib/components/ui/form/form-label.svelte | 24 ++ .../lib/components/ui/form/form-legend.svelte | 16 ++ .../ui/src/lib/components/ui/form/index.ts | 33 +++ .../ui/src/lib/components/ui/input/index.ts | 7 + .../src/lib/components/ui/input/input.svelte | 52 ++++ .../ui/src/lib/components/ui/label/index.ts | 7 + .../src/lib/components/ui/label/label.svelte | 20 ++ .../src/lib/components/ui/pagination/index.ts | 25 ++ .../ui/pagination/pagination-content.svelte | 20 ++ .../ui/pagination/pagination-ellipsis.svelte | 22 ++ .../ui/pagination/pagination-item.svelte | 14 + .../ui/pagination/pagination-link.svelte | 39 +++ .../pagination/pagination-next-button.svelte | 33 +++ .../pagination/pagination-prev-button.svelte | 33 +++ .../ui/pagination/pagination.svelte | 28 ++ .../src/lib/components/ui/progress/index.ts | 7 + .../components/ui/progress/progress.svelte | 27 ++ .../ui/src/lib/components/ui/select/index.ts | 37 +++ .../ui/select/select-content.svelte | 45 +++ .../ui/select/select-group-heading.svelte | 21 ++ .../components/ui/select/select-group.svelte | 7 + .../components/ui/select/select-item.svelte | 38 +++ .../components/ui/select/select-label.svelte | 20 ++ .../components/ui/select/select-portal.svelte | 7 + .../select/select-scroll-down-button.svelte | 20 ++ .../ui/select/select-scroll-up-button.svelte | 20 ++ .../ui/select/select-separator.svelte | 18 ++ .../ui/select/select-trigger.svelte | 29 ++ .../lib/components/ui/select/select.svelte | 11 + .../src/lib/components/ui/separator/index.ts | 7 + .../components/ui/separator/separator.svelte | 21 ++ .../ui/src/lib/components/ui/sheet/index.ts | 36 +++ .../components/ui/sheet/sheet-close.svelte | 7 + .../components/ui/sheet/sheet-content.svelte | 60 ++++ .../ui/sheet/sheet-description.svelte | 17 ++ .../components/ui/sheet/sheet-footer.svelte | 20 ++ .../components/ui/sheet/sheet-header.svelte | 20 ++ .../components/ui/sheet/sheet-overlay.svelte | 20 ++ .../components/ui/sheet/sheet-title.svelte | 17 ++ .../components/ui/sheet/sheet-trigger.svelte | 7 + .../lib/components/ui/sidebar/constants.ts | 6 + .../components/ui/sidebar/context.svelte.ts | 79 +++++ .../ui/src/lib/components/ui/sidebar/index.ts | 77 +++++ .../components/ui/sidebar/is-mobile.svelte.ts | 25 ++ .../ui/sidebar/sidebar-content.svelte | 24 ++ .../ui/sidebar/sidebar-footer.svelte | 21 ++ .../ui/sidebar/sidebar-group-action.svelte | 36 +++ .../ui/sidebar/sidebar-group-content.svelte | 21 ++ .../ui/sidebar/sidebar-group-label.svelte | 34 +++ .../ui/sidebar/sidebar-group.svelte | 21 ++ .../ui/sidebar/sidebar-header.svelte | 21 ++ .../ui/sidebar/sidebar-input.svelte | 21 ++ .../ui/sidebar/sidebar-inset.svelte | 24 ++ .../ui/sidebar/sidebar-menu-action.svelte | 43 +++ .../ui/sidebar/sidebar-menu-badge.svelte | 29 ++ .../ui/sidebar/sidebar-menu-button.svelte | 101 +++++++ .../ui/sidebar/sidebar-menu-item.svelte | 21 ++ .../ui/sidebar/sidebar-menu-skeleton.svelte | 36 +++ .../ui/sidebar/sidebar-menu-sub-button.svelte | 43 +++ .../ui/sidebar/sidebar-menu-sub-item.svelte | 21 ++ .../ui/sidebar/sidebar-menu-sub.svelte | 25 ++ .../components/ui/sidebar/sidebar-menu.svelte | 21 ++ .../ui/sidebar/sidebar-provider.svelte | 53 ++++ .../components/ui/sidebar/sidebar-rail.svelte | 36 +++ .../ui/sidebar/sidebar-separator.svelte | 19 ++ .../ui/sidebar/sidebar-trigger.svelte | 35 +++ .../lib/components/ui/sidebar/sidebar.svelte | 101 +++++++ .../src/lib/components/ui/skeleton/index.ts | 7 + .../components/ui/skeleton/skeleton.svelte | 17 ++ .../ui/src/lib/components/ui/sonner/index.ts | 1 + .../lib/components/ui/sonner/sonner.svelte | 13 + .../ui/src/lib/components/ui/table/index.ts | 28 ++ .../lib/components/ui/table/table-body.svelte | 20 ++ .../components/ui/table/table-caption.svelte | 20 ++ .../lib/components/ui/table/table-cell.svelte | 23 ++ .../components/ui/table/table-footer.svelte | 20 ++ .../lib/components/ui/table/table-head.svelte | 23 ++ .../components/ui/table/table-header.svelte | 20 ++ .../lib/components/ui/table/table-row.svelte | 23 ++ .../src/lib/components/ui/table/table.svelte | 22 ++ .../ui/src/lib/components/ui/tabs/index.ts | 16 ++ .../components/ui/tabs/tabs-content.svelte | 17 ++ .../lib/components/ui/tabs/tabs-list.svelte | 16 ++ .../components/ui/tabs/tabs-trigger.svelte | 20 ++ .../ui/src/lib/components/ui/tabs/tabs.svelte | 19 ++ .../src/lib/components/ui/textarea/index.ts | 7 + .../components/ui/textarea/textarea.svelte | 23 ++ .../ui/src/lib/components/ui/tooltip/index.ts | 21 ++ .../ui/tooltip/tooltip-content.svelte | 47 +++ .../ui/tooltip/tooltip-trigger.svelte | 7 + packages/ui/src/lib/index.ts | 34 +++ packages/ui/src/lib/utils.ts | 13 + packages/ui/src/routes/+page.svelte | 1 + packages/ui/svelte.config.js | 8 + packages/ui/tsconfig.json | 18 ++ platforms/profile-editor/api/package.json | 44 +++ .../api/src/controllers/AuthController.ts | 134 +++++++++ .../src/controllers/DiscoveryController.ts | 40 +++ .../src/controllers/FileProxyController.ts | 69 +++++ .../api/src/controllers/ProfileController.ts | 269 ++++++++++++++++++ .../api/src/controllers/WebhookController.ts | 129 +++++++++ .../api/src/database/data-source.ts | 32 +++ .../api/src/database/entities/Session.ts | 29 ++ .../api/src/database/entities/User.ts | 55 ++++ .../migrations/1773143278029-InitialSchema.ts | 18 ++ platforms/profile-editor/api/src/index.ts | 103 +++++++ .../profile-editor/api/src/middleware/auth.ts | 49 ++++ .../api/src/services/EVaultProfileService.ts | 246 ++++++++++++++++ .../api/src/services/EVaultSyncService.ts | 106 +++++++ .../api/src/services/RegistryService.ts | 60 ++++ .../api/src/services/UserSearchService.ts | 133 +++++++++ .../profile-editor/api/src/types/express.d.ts | 13 + .../profile-editor/api/src/types/profile.ts | 83 ++++++ .../api/src/utils/file-proxy.ts | 49 ++++ .../professional-profile.mapping.json | 18 ++ .../web3adapter/mappings/user.mapping.json | 20 ++ .../src/web3adapter/watchers/subscriber.ts | 131 +++++++++ platforms/profile-editor/api/tsconfig.json | 26 ++ platforms/profile-editor/client/package.json | 38 +++ platforms/profile-editor/client/src/app.css | 1 + platforms/profile-editor/client/src/app.html | 12 + .../src/lib/components/layout/Header.svelte | 27 ++ .../src/lib/components/layout/Sidebar.svelte | 60 ++++ .../components/profile/AboutSection.svelte | 49 ++++ .../components/profile/ContactSection.svelte | 83 ++++++ .../profile/DocumentsSection.svelte | 147 ++++++++++ .../profile/EducationSection.svelte | 138 +++++++++ .../profile/ExperienceSection.svelte | 139 +++++++++ .../lib/components/profile/ProfileCard.svelte | 58 ++++ .../components/profile/ProfileHeader.svelte | 167 +++++++++++ .../components/profile/SkillsSection.svelte | 73 +++++ .../profile/SocialLinksSection.svelte | 103 +++++++ .../client/src/lib/stores/auth.ts | 54 ++++ .../client/src/lib/stores/discovery.ts | 56 ++++ .../client/src/lib/stores/profile.ts | 118 ++++++++ .../client/src/lib/utils/axios.ts | 47 +++ .../client/src/lib/utils/file-manager.ts | 29 ++ .../src/routes/(auth)/auth/+page.svelte | 179 ++++++++++++ .../routes/(auth)/deeplink-login/+page.svelte | 63 ++++ .../src/routes/(protected)/+layout.svelte | 28 ++ .../routes/(protected)/discover/+page.svelte | 141 +++++++++ .../routes/(protected)/profile/+page.svelte | 97 +++++++ .../(protected)/profile/[ename]/+page.svelte | 99 +++++++ .../(protected)/profile/[ename]/+page.ts | 1 + .../routes/(protected)/settings/+page.svelte | 134 +++++++++ .../client/src/routes/+layout.svelte | 29 ++ .../client/src/routes/+layout.ts | 2 + .../client/src/routes/+page.svelte | 12 + .../profile-editor/client/svelte.config.js | 14 + platforms/profile-editor/client/tsconfig.json | 14 + .../profile-editor/client/vite.config.ts | 15 + 237 files changed, 8864 insertions(+) create mode 100644 packages/auth/package.json create mode 100644 packages/auth/src/auth-offer.ts create mode 100644 packages/auth/src/guard.ts create mode 100644 packages/auth/src/index.ts create mode 100644 packages/auth/src/jwt.ts create mode 100644 packages/auth/src/middleware.ts create mode 100644 packages/auth/src/types.ts create mode 100644 packages/auth/src/verify-login.ts create mode 100644 packages/auth/tsconfig.json create mode 100644 packages/ui/.gitignore create mode 100644 packages/ui/.prettierignore create mode 100644 packages/ui/.prettierrc create mode 100644 packages/ui/components.json create mode 100644 packages/ui/eslint.config.js create mode 100644 packages/ui/package.json create mode 100644 packages/ui/src/app.css create mode 100644 packages/ui/src/lib/components/ui/accordion/accordion-content.svelte create mode 100644 packages/ui/src/lib/components/ui/accordion/accordion-item.svelte create mode 100644 packages/ui/src/lib/components/ui/accordion/accordion-trigger.svelte create mode 100644 packages/ui/src/lib/components/ui/accordion/accordion.svelte create mode 100644 packages/ui/src/lib/components/ui/accordion/index.ts create mode 100644 packages/ui/src/lib/components/ui/avatar/avatar-fallback.svelte create mode 100644 packages/ui/src/lib/components/ui/avatar/avatar-image.svelte create mode 100644 packages/ui/src/lib/components/ui/avatar/avatar.svelte create mode 100644 packages/ui/src/lib/components/ui/avatar/index.ts create mode 100644 packages/ui/src/lib/components/ui/badge/badge.svelte create mode 100644 packages/ui/src/lib/components/ui/badge/index.ts create mode 100644 packages/ui/src/lib/components/ui/button/button.svelte create mode 100644 packages/ui/src/lib/components/ui/button/index.ts create mode 100644 packages/ui/src/lib/components/ui/card/card-action.svelte create mode 100644 packages/ui/src/lib/components/ui/card/card-content.svelte create mode 100644 packages/ui/src/lib/components/ui/card/card-description.svelte create mode 100644 packages/ui/src/lib/components/ui/card/card-footer.svelte create mode 100644 packages/ui/src/lib/components/ui/card/card-header.svelte create mode 100644 packages/ui/src/lib/components/ui/card/card-title.svelte create mode 100644 packages/ui/src/lib/components/ui/card/card.svelte create mode 100644 packages/ui/src/lib/components/ui/card/index.ts create mode 100644 packages/ui/src/lib/components/ui/carousel/carousel-content.svelte create mode 100644 packages/ui/src/lib/components/ui/carousel/carousel-item.svelte create mode 100644 packages/ui/src/lib/components/ui/carousel/carousel-next.svelte create mode 100644 packages/ui/src/lib/components/ui/carousel/carousel-previous.svelte create mode 100644 packages/ui/src/lib/components/ui/carousel/carousel.svelte create mode 100644 packages/ui/src/lib/components/ui/carousel/context.ts create mode 100644 packages/ui/src/lib/components/ui/carousel/index.ts create mode 100644 packages/ui/src/lib/components/ui/dialog/dialog-close.svelte create mode 100644 packages/ui/src/lib/components/ui/dialog/dialog-content.svelte create mode 100644 packages/ui/src/lib/components/ui/dialog/dialog-description.svelte create mode 100644 packages/ui/src/lib/components/ui/dialog/dialog-footer.svelte create mode 100644 packages/ui/src/lib/components/ui/dialog/dialog-header.svelte create mode 100644 packages/ui/src/lib/components/ui/dialog/dialog-overlay.svelte create mode 100644 packages/ui/src/lib/components/ui/dialog/dialog-portal.svelte create mode 100644 packages/ui/src/lib/components/ui/dialog/dialog-title.svelte create mode 100644 packages/ui/src/lib/components/ui/dialog/dialog-trigger.svelte create mode 100644 packages/ui/src/lib/components/ui/dialog/dialog.svelte create mode 100644 packages/ui/src/lib/components/ui/dialog/index.ts create mode 100644 packages/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte create mode 100644 packages/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte create mode 100644 packages/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte create mode 100644 packages/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-group.svelte create mode 100644 packages/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte create mode 100644 packages/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte create mode 100644 packages/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte create mode 100644 packages/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte create mode 100644 packages/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte create mode 100644 packages/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte create mode 100644 packages/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte create mode 100644 packages/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte create mode 100644 packages/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-trigger.svelte create mode 100644 packages/ui/src/lib/components/ui/dropdown-menu/index.ts create mode 100644 packages/ui/src/lib/components/ui/field/field-content.svelte create mode 100644 packages/ui/src/lib/components/ui/field/field-description.svelte create mode 100644 packages/ui/src/lib/components/ui/field/field-error.svelte create mode 100644 packages/ui/src/lib/components/ui/field/field-group.svelte create mode 100644 packages/ui/src/lib/components/ui/field/field-label.svelte create mode 100644 packages/ui/src/lib/components/ui/field/field-legend.svelte create mode 100644 packages/ui/src/lib/components/ui/field/field-separator.svelte create mode 100644 packages/ui/src/lib/components/ui/field/field-set.svelte create mode 100644 packages/ui/src/lib/components/ui/field/field-title.svelte create mode 100644 packages/ui/src/lib/components/ui/field/field.svelte create mode 100644 packages/ui/src/lib/components/ui/field/index.ts create mode 100644 packages/ui/src/lib/components/ui/form/form-button.svelte create mode 100644 packages/ui/src/lib/components/ui/form/form-description.svelte create mode 100644 packages/ui/src/lib/components/ui/form/form-element-field.svelte create mode 100644 packages/ui/src/lib/components/ui/form/form-field-errors.svelte create mode 100644 packages/ui/src/lib/components/ui/form/form-field.svelte create mode 100644 packages/ui/src/lib/components/ui/form/form-fieldset.svelte create mode 100644 packages/ui/src/lib/components/ui/form/form-label.svelte create mode 100644 packages/ui/src/lib/components/ui/form/form-legend.svelte create mode 100644 packages/ui/src/lib/components/ui/form/index.ts create mode 100644 packages/ui/src/lib/components/ui/input/index.ts create mode 100644 packages/ui/src/lib/components/ui/input/input.svelte create mode 100644 packages/ui/src/lib/components/ui/label/index.ts create mode 100644 packages/ui/src/lib/components/ui/label/label.svelte create mode 100644 packages/ui/src/lib/components/ui/pagination/index.ts create mode 100644 packages/ui/src/lib/components/ui/pagination/pagination-content.svelte create mode 100644 packages/ui/src/lib/components/ui/pagination/pagination-ellipsis.svelte create mode 100644 packages/ui/src/lib/components/ui/pagination/pagination-item.svelte create mode 100644 packages/ui/src/lib/components/ui/pagination/pagination-link.svelte create mode 100644 packages/ui/src/lib/components/ui/pagination/pagination-next-button.svelte create mode 100644 packages/ui/src/lib/components/ui/pagination/pagination-prev-button.svelte create mode 100644 packages/ui/src/lib/components/ui/pagination/pagination.svelte create mode 100644 packages/ui/src/lib/components/ui/progress/index.ts create mode 100644 packages/ui/src/lib/components/ui/progress/progress.svelte create mode 100644 packages/ui/src/lib/components/ui/select/index.ts create mode 100644 packages/ui/src/lib/components/ui/select/select-content.svelte create mode 100644 packages/ui/src/lib/components/ui/select/select-group-heading.svelte create mode 100644 packages/ui/src/lib/components/ui/select/select-group.svelte create mode 100644 packages/ui/src/lib/components/ui/select/select-item.svelte create mode 100644 packages/ui/src/lib/components/ui/select/select-label.svelte create mode 100644 packages/ui/src/lib/components/ui/select/select-portal.svelte create mode 100644 packages/ui/src/lib/components/ui/select/select-scroll-down-button.svelte create mode 100644 packages/ui/src/lib/components/ui/select/select-scroll-up-button.svelte create mode 100644 packages/ui/src/lib/components/ui/select/select-separator.svelte create mode 100644 packages/ui/src/lib/components/ui/select/select-trigger.svelte create mode 100644 packages/ui/src/lib/components/ui/select/select.svelte create mode 100644 packages/ui/src/lib/components/ui/separator/index.ts create mode 100644 packages/ui/src/lib/components/ui/separator/separator.svelte create mode 100644 packages/ui/src/lib/components/ui/sheet/index.ts create mode 100644 packages/ui/src/lib/components/ui/sheet/sheet-close.svelte create mode 100644 packages/ui/src/lib/components/ui/sheet/sheet-content.svelte create mode 100644 packages/ui/src/lib/components/ui/sheet/sheet-description.svelte create mode 100644 packages/ui/src/lib/components/ui/sheet/sheet-footer.svelte create mode 100644 packages/ui/src/lib/components/ui/sheet/sheet-header.svelte create mode 100644 packages/ui/src/lib/components/ui/sheet/sheet-overlay.svelte create mode 100644 packages/ui/src/lib/components/ui/sheet/sheet-title.svelte create mode 100644 packages/ui/src/lib/components/ui/sheet/sheet-trigger.svelte create mode 100644 packages/ui/src/lib/components/ui/sidebar/constants.ts create mode 100644 packages/ui/src/lib/components/ui/sidebar/context.svelte.ts create mode 100644 packages/ui/src/lib/components/ui/sidebar/index.ts create mode 100644 packages/ui/src/lib/components/ui/sidebar/is-mobile.svelte.ts create mode 100644 packages/ui/src/lib/components/ui/sidebar/sidebar-content.svelte create mode 100644 packages/ui/src/lib/components/ui/sidebar/sidebar-footer.svelte create mode 100644 packages/ui/src/lib/components/ui/sidebar/sidebar-group-action.svelte create mode 100644 packages/ui/src/lib/components/ui/sidebar/sidebar-group-content.svelte create mode 100644 packages/ui/src/lib/components/ui/sidebar/sidebar-group-label.svelte create mode 100644 packages/ui/src/lib/components/ui/sidebar/sidebar-group.svelte create mode 100644 packages/ui/src/lib/components/ui/sidebar/sidebar-header.svelte create mode 100644 packages/ui/src/lib/components/ui/sidebar/sidebar-input.svelte create mode 100644 packages/ui/src/lib/components/ui/sidebar/sidebar-inset.svelte create mode 100644 packages/ui/src/lib/components/ui/sidebar/sidebar-menu-action.svelte create mode 100644 packages/ui/src/lib/components/ui/sidebar/sidebar-menu-badge.svelte create mode 100644 packages/ui/src/lib/components/ui/sidebar/sidebar-menu-button.svelte create mode 100644 packages/ui/src/lib/components/ui/sidebar/sidebar-menu-item.svelte create mode 100644 packages/ui/src/lib/components/ui/sidebar/sidebar-menu-skeleton.svelte create mode 100644 packages/ui/src/lib/components/ui/sidebar/sidebar-menu-sub-button.svelte create mode 100644 packages/ui/src/lib/components/ui/sidebar/sidebar-menu-sub-item.svelte create mode 100644 packages/ui/src/lib/components/ui/sidebar/sidebar-menu-sub.svelte create mode 100644 packages/ui/src/lib/components/ui/sidebar/sidebar-menu.svelte create mode 100644 packages/ui/src/lib/components/ui/sidebar/sidebar-provider.svelte create mode 100644 packages/ui/src/lib/components/ui/sidebar/sidebar-rail.svelte create mode 100644 packages/ui/src/lib/components/ui/sidebar/sidebar-separator.svelte create mode 100644 packages/ui/src/lib/components/ui/sidebar/sidebar-trigger.svelte create mode 100644 packages/ui/src/lib/components/ui/sidebar/sidebar.svelte create mode 100644 packages/ui/src/lib/components/ui/skeleton/index.ts create mode 100644 packages/ui/src/lib/components/ui/skeleton/skeleton.svelte create mode 100644 packages/ui/src/lib/components/ui/sonner/index.ts create mode 100644 packages/ui/src/lib/components/ui/sonner/sonner.svelte create mode 100644 packages/ui/src/lib/components/ui/table/index.ts create mode 100644 packages/ui/src/lib/components/ui/table/table-body.svelte create mode 100644 packages/ui/src/lib/components/ui/table/table-caption.svelte create mode 100644 packages/ui/src/lib/components/ui/table/table-cell.svelte create mode 100644 packages/ui/src/lib/components/ui/table/table-footer.svelte create mode 100644 packages/ui/src/lib/components/ui/table/table-head.svelte create mode 100644 packages/ui/src/lib/components/ui/table/table-header.svelte create mode 100644 packages/ui/src/lib/components/ui/table/table-row.svelte create mode 100644 packages/ui/src/lib/components/ui/table/table.svelte create mode 100644 packages/ui/src/lib/components/ui/tabs/index.ts create mode 100644 packages/ui/src/lib/components/ui/tabs/tabs-content.svelte create mode 100644 packages/ui/src/lib/components/ui/tabs/tabs-list.svelte create mode 100644 packages/ui/src/lib/components/ui/tabs/tabs-trigger.svelte create mode 100644 packages/ui/src/lib/components/ui/tabs/tabs.svelte create mode 100644 packages/ui/src/lib/components/ui/textarea/index.ts create mode 100644 packages/ui/src/lib/components/ui/textarea/textarea.svelte create mode 100644 packages/ui/src/lib/components/ui/tooltip/index.ts create mode 100644 packages/ui/src/lib/components/ui/tooltip/tooltip-content.svelte create mode 100644 packages/ui/src/lib/components/ui/tooltip/tooltip-trigger.svelte create mode 100644 packages/ui/src/lib/index.ts create mode 100644 packages/ui/src/lib/utils.ts create mode 100644 packages/ui/src/routes/+page.svelte create mode 100644 packages/ui/svelte.config.js create mode 100644 packages/ui/tsconfig.json create mode 100644 platforms/profile-editor/api/package.json create mode 100644 platforms/profile-editor/api/src/controllers/AuthController.ts create mode 100644 platforms/profile-editor/api/src/controllers/DiscoveryController.ts create mode 100644 platforms/profile-editor/api/src/controllers/FileProxyController.ts create mode 100644 platforms/profile-editor/api/src/controllers/ProfileController.ts create mode 100644 platforms/profile-editor/api/src/controllers/WebhookController.ts create mode 100644 platforms/profile-editor/api/src/database/data-source.ts create mode 100644 platforms/profile-editor/api/src/database/entities/Session.ts create mode 100644 platforms/profile-editor/api/src/database/entities/User.ts create mode 100644 platforms/profile-editor/api/src/database/migrations/1773143278029-InitialSchema.ts create mode 100644 platforms/profile-editor/api/src/index.ts create mode 100644 platforms/profile-editor/api/src/middleware/auth.ts create mode 100644 platforms/profile-editor/api/src/services/EVaultProfileService.ts create mode 100644 platforms/profile-editor/api/src/services/EVaultSyncService.ts create mode 100644 platforms/profile-editor/api/src/services/RegistryService.ts create mode 100644 platforms/profile-editor/api/src/services/UserSearchService.ts create mode 100644 platforms/profile-editor/api/src/types/express.d.ts create mode 100644 platforms/profile-editor/api/src/types/profile.ts create mode 100644 platforms/profile-editor/api/src/utils/file-proxy.ts create mode 100644 platforms/profile-editor/api/src/web3adapter/mappings/professional-profile.mapping.json create mode 100644 platforms/profile-editor/api/src/web3adapter/mappings/user.mapping.json create mode 100644 platforms/profile-editor/api/src/web3adapter/watchers/subscriber.ts create mode 100644 platforms/profile-editor/api/tsconfig.json create mode 100644 platforms/profile-editor/client/package.json create mode 100644 platforms/profile-editor/client/src/app.css create mode 100644 platforms/profile-editor/client/src/app.html create mode 100644 platforms/profile-editor/client/src/lib/components/layout/Header.svelte create mode 100644 platforms/profile-editor/client/src/lib/components/layout/Sidebar.svelte create mode 100644 platforms/profile-editor/client/src/lib/components/profile/AboutSection.svelte create mode 100644 platforms/profile-editor/client/src/lib/components/profile/ContactSection.svelte create mode 100644 platforms/profile-editor/client/src/lib/components/profile/DocumentsSection.svelte create mode 100644 platforms/profile-editor/client/src/lib/components/profile/EducationSection.svelte create mode 100644 platforms/profile-editor/client/src/lib/components/profile/ExperienceSection.svelte create mode 100644 platforms/profile-editor/client/src/lib/components/profile/ProfileCard.svelte create mode 100644 platforms/profile-editor/client/src/lib/components/profile/ProfileHeader.svelte create mode 100644 platforms/profile-editor/client/src/lib/components/profile/SkillsSection.svelte create mode 100644 platforms/profile-editor/client/src/lib/components/profile/SocialLinksSection.svelte create mode 100644 platforms/profile-editor/client/src/lib/stores/auth.ts create mode 100644 platforms/profile-editor/client/src/lib/stores/discovery.ts create mode 100644 platforms/profile-editor/client/src/lib/stores/profile.ts create mode 100644 platforms/profile-editor/client/src/lib/utils/axios.ts create mode 100644 platforms/profile-editor/client/src/lib/utils/file-manager.ts create mode 100644 platforms/profile-editor/client/src/routes/(auth)/auth/+page.svelte create mode 100644 platforms/profile-editor/client/src/routes/(auth)/deeplink-login/+page.svelte create mode 100644 platforms/profile-editor/client/src/routes/(protected)/+layout.svelte create mode 100644 platforms/profile-editor/client/src/routes/(protected)/discover/+page.svelte create mode 100644 platforms/profile-editor/client/src/routes/(protected)/profile/+page.svelte create mode 100644 platforms/profile-editor/client/src/routes/(protected)/profile/[ename]/+page.svelte create mode 100644 platforms/profile-editor/client/src/routes/(protected)/profile/[ename]/+page.ts create mode 100644 platforms/profile-editor/client/src/routes/(protected)/settings/+page.svelte create mode 100644 platforms/profile-editor/client/src/routes/+layout.svelte create mode 100644 platforms/profile-editor/client/src/routes/+layout.ts create mode 100644 platforms/profile-editor/client/src/routes/+page.svelte create mode 100644 platforms/profile-editor/client/svelte.config.js create mode 100644 platforms/profile-editor/client/tsconfig.json create mode 100644 platforms/profile-editor/client/vite.config.ts diff --git a/packages/auth/package.json b/packages/auth/package.json new file mode 100644 index 000000000..612124ac0 --- /dev/null +++ b/packages/auth/package.json @@ -0,0 +1,33 @@ +{ + "name": "@metastate-foundation/auth", + "version": "0.1.0", + "description": "Shared authentication utilities for w3ds platform APIs", + "type": "module", + "scripts": { + "build": "tsc -p tsconfig.json", + "check-types": "tsc --noEmit", + "postinstall": "npm run build" + }, + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "require": "./dist/index.js", + "default": "./dist/index.js" + } + }, + "files": ["dist"], + "dependencies": { + "jsonwebtoken": "^9.0.2", + "signature-validator": "workspace:*", + "uuid": "^11.1.0" + }, + "devDependencies": { + "@types/jsonwebtoken": "^9.0.9", + "@types/node": "^20.11.24", + "@types/uuid": "^10.0.0", + "typescript": "~5.6.2" + } +} diff --git a/packages/auth/src/auth-offer.ts b/packages/auth/src/auth-offer.ts new file mode 100644 index 000000000..afd103af8 --- /dev/null +++ b/packages/auth/src/auth-offer.ts @@ -0,0 +1,15 @@ +import { v4 as uuidv4 } from "uuid"; +import type { AuthOfferConfig } from "./types.js"; + +export interface AuthOffer { + uri: string; + session: string; +} + +export function buildAuthOffer(config: AuthOfferConfig): AuthOffer { + const callbackPath = config.callbackPath ?? "/api/auth"; + const url = new URL(callbackPath, config.baseUrl).toString(); + const session = uuidv4(); + const uri = `w3ds://auth?redirect=${url}&session=${session}&platform=${config.platform}`; + return { uri, session }; +} diff --git a/packages/auth/src/guard.ts b/packages/auth/src/guard.ts new file mode 100644 index 000000000..a53d4bfad --- /dev/null +++ b/packages/auth/src/guard.ts @@ -0,0 +1,8 @@ +export function createAuthGuard() { + return (req: any, res: any, next: any): void => { + if (!req.user) { + return res.status(401).json({ error: "Authentication required" }); + } + next(); + }; +} diff --git a/packages/auth/src/index.ts b/packages/auth/src/index.ts new file mode 100644 index 000000000..36cc6998c --- /dev/null +++ b/packages/auth/src/index.ts @@ -0,0 +1,14 @@ +export { signToken, verifyToken } from "./jwt.js"; +export { createAuthMiddleware } from "./middleware.js"; +export { createAuthGuard } from "./guard.js"; +export { buildAuthOffer } from "./auth-offer.js"; +export type { AuthOffer } from "./auth-offer.js"; +export { verifyLoginSignature } from "./verify-login.js"; +export type { + AuthUser, + JwtPayload, + AuthMiddlewareConfig, + AuthOfferConfig, + LoginVerificationConfig, + LoginVerificationResult, +} from "./types.js"; diff --git a/packages/auth/src/jwt.ts b/packages/auth/src/jwt.ts new file mode 100644 index 000000000..3704135b2 --- /dev/null +++ b/packages/auth/src/jwt.ts @@ -0,0 +1,20 @@ +import jwt from "jsonwebtoken"; +import type { JwtPayload } from "./types.js"; + +export function signToken( + payload: JwtPayload, + secret: string, + options?: { expiresIn?: string | number }, +): string { + return jwt.sign(payload as object, secret, { + expiresIn: (options?.expiresIn ?? "7d") as any, + }); +} + +export function verifyToken(token: string, secret: string): JwtPayload { + try { + return jwt.verify(token, secret) as JwtPayload; + } catch { + throw new Error("Invalid token"); + } +} diff --git a/packages/auth/src/middleware.ts b/packages/auth/src/middleware.ts new file mode 100644 index 000000000..b3812f7fb --- /dev/null +++ b/packages/auth/src/middleware.ts @@ -0,0 +1,32 @@ +import { verifyToken } from "./jwt.js"; +import type { AuthMiddlewareConfig } from "./types.js"; + +export function createAuthMiddleware(config: AuthMiddlewareConfig) { + return async (req: any, res: any, next: any): Promise => { + try { + const authHeader = req.headers.authorization; + if (!authHeader?.startsWith("Bearer ")) { + return next(); + } + + const token = authHeader.split(" ")[1]; + const decoded = verifyToken(token, config.secret); + + if (!decoded?.userId) { + return res.status(401).json({ error: "Invalid token" }); + } + + const user = await config.findUser(decoded.userId); + + if (!user) { + return res.status(401).json({ error: "User not found" }); + } + + req.user = user; + next(); + } catch (error) { + console.error("Auth middleware error:", error); + res.status(401).json({ error: "Invalid token" }); + } + }; +} diff --git a/packages/auth/src/types.ts b/packages/auth/src/types.ts new file mode 100644 index 000000000..21495e93b --- /dev/null +++ b/packages/auth/src/types.ts @@ -0,0 +1,34 @@ +export interface AuthUser { + id: string; + ename: string; + [key: string]: unknown; +} + +export interface JwtPayload { + userId: string; + [key: string]: unknown; +} + +export interface AuthMiddlewareConfig { + secret: string; + findUser: (userId: string) => Promise; +} + +export interface AuthOfferConfig { + baseUrl: string; + platform: string; + callbackPath?: string; +} + +export interface LoginVerificationConfig { + eName: string; + signature: string; + session: string; + registryBaseUrl: string; +} + +export interface LoginVerificationResult { + valid: boolean; + error?: string; + publicKey?: string; +} diff --git a/packages/auth/src/verify-login.ts b/packages/auth/src/verify-login.ts new file mode 100644 index 000000000..2904aa0f0 --- /dev/null +++ b/packages/auth/src/verify-login.ts @@ -0,0 +1,22 @@ +import { verifySignature } from "signature-validator"; +import type { + LoginVerificationConfig, + LoginVerificationResult, +} from "./types.js"; + +export async function verifyLoginSignature( + config: LoginVerificationConfig, +): Promise { + const result = await verifySignature({ + eName: config.eName, + signature: config.signature, + payload: config.session, + registryBaseUrl: config.registryBaseUrl, + }); + + return { + valid: result.valid, + error: result.error, + publicKey: result.publicKey, + }; +} diff --git a/packages/auth/tsconfig.json b/packages/auth/tsconfig.json new file mode 100644 index 000000000..b6d15c93b --- /dev/null +++ b/packages/auth/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/ui/.gitignore b/packages/ui/.gitignore new file mode 100644 index 000000000..407df197b --- /dev/null +++ b/packages/ui/.gitignore @@ -0,0 +1,3 @@ +node_modules +.svelte-kit +dist diff --git a/packages/ui/.prettierignore b/packages/ui/.prettierignore new file mode 100644 index 000000000..1521c8b76 --- /dev/null +++ b/packages/ui/.prettierignore @@ -0,0 +1 @@ +dist diff --git a/packages/ui/.prettierrc b/packages/ui/.prettierrc new file mode 100644 index 000000000..95730232b --- /dev/null +++ b/packages/ui/.prettierrc @@ -0,0 +1,8 @@ +{ + "useTabs": true, + "singleQuote": true, + "trailingComma": "none", + "printWidth": 100, + "plugins": ["prettier-plugin-svelte"], + "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }] +} diff --git a/packages/ui/components.json b/packages/ui/components.json new file mode 100644 index 000000000..c5d91b458 --- /dev/null +++ b/packages/ui/components.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://shadcn-svelte.com/schema.json", + "tailwind": { + "css": "src/app.css", + "baseColor": "slate" + }, + "aliases": { + "components": "$lib/components", + "utils": "$lib/utils", + "ui": "$lib/components/ui", + "hooks": "$lib/hooks", + "lib": "$lib" + }, + "typescript": true, + "registry": "https://shadcn-svelte.com/registry" +} diff --git a/packages/ui/eslint.config.js b/packages/ui/eslint.config.js new file mode 100644 index 000000000..56232c02e --- /dev/null +++ b/packages/ui/eslint.config.js @@ -0,0 +1,48 @@ +import prettier from 'eslint-config-prettier'; +import { fileURLToPath } from 'node:url'; +import { includeIgnoreFile } from '@eslint/compat'; +import js from '@eslint/js'; +import svelte from 'eslint-plugin-svelte'; +import { defineConfig } from 'eslint/config'; +import globals from 'globals'; +import ts from 'typescript-eslint'; +import svelteConfig from './svelte.config.js'; + +const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url)); + +export default defineConfig( + includeIgnoreFile(gitignorePath), + js.configs.recommended, + ...ts.configs.recommended, + ...svelte.configs.recommended, + prettier, + ...svelte.configs.prettier, + { + languageOptions: { + globals: { ...globals.browser, ...globals.node } + }, + rules: { + 'no-undef': 'off', + 'svelte/no-navigation-without-resolve': ['error', { ignoreLinks: true }], + '@typescript-eslint/no-unused-vars': [ + 'error', + { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + caughtErrorsIgnorePattern: '^_' + } + ] + } + }, + { + files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'], + languageOptions: { + parserOptions: { + projectService: true, + extraFileExtensions: ['.svelte'], + parser: ts.parser, + svelteConfig + } + } + } +); diff --git a/packages/ui/package.json b/packages/ui/package.json new file mode 100644 index 000000000..f01bd8717 --- /dev/null +++ b/packages/ui/package.json @@ -0,0 +1,205 @@ +{ + "name": "@metastate-foundation/ui", + "version": "0.0.1", + "type": "module", + "svelte": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "svelte": "./dist/index.js", + "default": "./dist/index.js" + }, + "./styles": { + "default": "./src/app.css" + }, + "./accordion": { + "types": "./dist/components/ui/accordion/index.d.ts", + "svelte": "./dist/components/ui/accordion/index.js", + "default": "./dist/components/ui/accordion/index.js" + }, + "./avatar": { + "types": "./dist/components/ui/avatar/index.d.ts", + "svelte": "./dist/components/ui/avatar/index.js", + "default": "./dist/components/ui/avatar/index.js" + }, + "./badge": { + "types": "./dist/components/ui/badge/index.d.ts", + "svelte": "./dist/components/ui/badge/index.js", + "default": "./dist/components/ui/badge/index.js" + }, + "./button": { + "types": "./dist/components/ui/button/index.d.ts", + "svelte": "./dist/components/ui/button/index.js", + "default": "./dist/components/ui/button/index.js" + }, + "./card": { + "types": "./dist/components/ui/card/index.d.ts", + "svelte": "./dist/components/ui/card/index.js", + "default": "./dist/components/ui/card/index.js" + }, + "./carousel": { + "types": "./dist/components/ui/carousel/index.d.ts", + "svelte": "./dist/components/ui/carousel/index.js", + "default": "./dist/components/ui/carousel/index.js" + }, + "./carousel/context": { + "types": "./dist/components/ui/carousel/context.d.ts", + "default": "./dist/components/ui/carousel/context.js" + }, + "./dialog": { + "types": "./dist/components/ui/dialog/index.d.ts", + "svelte": "./dist/components/ui/dialog/index.js", + "default": "./dist/components/ui/dialog/index.js" + }, + "./dropdown-menu": { + "types": "./dist/components/ui/dropdown-menu/index.d.ts", + "svelte": "./dist/components/ui/dropdown-menu/index.js", + "default": "./dist/components/ui/dropdown-menu/index.js" + }, + "./field": { + "types": "./dist/components/ui/field/index.d.ts", + "svelte": "./dist/components/ui/field/index.js", + "default": "./dist/components/ui/field/index.js" + }, + "./form": { + "types": "./dist/components/ui/form/index.d.ts", + "svelte": "./dist/components/ui/form/index.js", + "default": "./dist/components/ui/form/index.js" + }, + "./input": { + "types": "./dist/components/ui/input/index.d.ts", + "svelte": "./dist/components/ui/input/index.js", + "default": "./dist/components/ui/input/index.js" + }, + "./label": { + "types": "./dist/components/ui/label/index.d.ts", + "svelte": "./dist/components/ui/label/index.js", + "default": "./dist/components/ui/label/index.js" + }, + "./pagination": { + "types": "./dist/components/ui/pagination/index.d.ts", + "svelte": "./dist/components/ui/pagination/index.js", + "default": "./dist/components/ui/pagination/index.js" + }, + "./progress": { + "types": "./dist/components/ui/progress/index.d.ts", + "svelte": "./dist/components/ui/progress/index.js", + "default": "./dist/components/ui/progress/index.js" + }, + "./select": { + "types": "./dist/components/ui/select/index.d.ts", + "svelte": "./dist/components/ui/select/index.js", + "default": "./dist/components/ui/select/index.js" + }, + "./separator": { + "types": "./dist/components/ui/separator/index.d.ts", + "svelte": "./dist/components/ui/separator/index.js", + "default": "./dist/components/ui/separator/index.js" + }, + "./sheet": { + "types": "./dist/components/ui/sheet/index.d.ts", + "svelte": "./dist/components/ui/sheet/index.js", + "default": "./dist/components/ui/sheet/index.js" + }, + "./sidebar": { + "types": "./dist/components/ui/sidebar/index.d.ts", + "svelte": "./dist/components/ui/sidebar/index.js", + "default": "./dist/components/ui/sidebar/index.js" + }, + "./skeleton": { + "types": "./dist/components/ui/skeleton/index.d.ts", + "svelte": "./dist/components/ui/skeleton/index.js", + "default": "./dist/components/ui/skeleton/index.js" + }, + "./sonner": { + "types": "./dist/components/ui/sonner/index.d.ts", + "svelte": "./dist/components/ui/sonner/index.js", + "default": "./dist/components/ui/sonner/index.js" + }, + "./tabs": { + "types": "./dist/components/ui/tabs/index.d.ts", + "svelte": "./dist/components/ui/tabs/index.js", + "default": "./dist/components/ui/tabs/index.js" + }, + "./table": { + "types": "./dist/components/ui/table/index.d.ts", + "svelte": "./dist/components/ui/table/index.js", + "default": "./dist/components/ui/table/index.js" + }, + "./textarea": { + "types": "./dist/components/ui/textarea/index.d.ts", + "svelte": "./dist/components/ui/textarea/index.js", + "default": "./dist/components/ui/textarea/index.js" + }, + "./tooltip": { + "types": "./dist/components/ui/tooltip/index.d.ts", + "svelte": "./dist/components/ui/tooltip/index.js", + "default": "./dist/components/ui/tooltip/index.js" + }, + "./utils": { + "types": "./dist/utils.d.ts", + "import": "./dist/utils.js", + "default": "./dist/utils.js" + }, + "./hooks/*": { + "types": "./dist/hooks/*.d.ts", + "svelte": "./dist/hooks/*.js", + "default": "./dist/hooks/*.js" + } + }, + "files": [ + "dist", + "src", + "!dist/**/*.test.*" + ], + "scripts": { + "dev": "chokidar \"src/**/*\" -c \"pnpm build\" --initial", + "build": "svelte-kit sync && svelte-package", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "format": "prettier --write .", + "format:check": "prettier --check .", + "lint": "prettier --check . && eslint .", + "prepare": "svelte-kit sync || echo ''", + "add": "shadcn-svelte add" + }, + "peerDependencies": { + "svelte": "^5.0.0" + }, + "dependencies": { + "bits-ui": "^2.15.4", + "clsx": "^2.1.1", + "embla-carousel-svelte": "^8.6.0", + "formsnap": "^2.0.1", + "mode-watcher": "^1.1.0", + "svelte-sonner": "^1.0.5", + "sveltekit-superforms": "^2.27.2" + }, + "devDependencies": { + "@eslint/compat": "^1.4.0", + "@eslint/js": "^9.36.0", + "@internationalized/date": "^3.10.0", + "@lucide/svelte": "^0.564.0", + "@sveltejs/kit": "^2.43.2", + "@sveltejs/package": "^2.3.10", + "@sveltejs/vite-plugin-svelte": "^6.2.0", + "@tailwindcss/vite": "^4.1.14", + "chokidar-cli": "^3.0.0", + "eslint": "^9.36.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-svelte": "^3.12.4", + "globals": "^16.4.0", + "prettier": "^3.6.2", + "prettier-plugin-svelte": "^3.4.0", + "shadcn-svelte": "^1.1.1", + "svelte": "^5.39.5", + "svelte-check": "^4.3.2", + "tailwind-merge": "^3.4.1", + "tailwind-variants": "^3.2.2", + "tailwindcss": "^4.1.14", + "tw-animate-css": "^1.4.0", + "typescript": "^5.9.2", + "typescript-eslint": "^8.44.1", + "vite": "^7.1.7" + } +} diff --git a/packages/ui/src/app.css b/packages/ui/src/app.css new file mode 100644 index 000000000..fdbba7ef8 --- /dev/null +++ b/packages/ui/src/app.css @@ -0,0 +1,123 @@ +@import 'tailwindcss'; +@import 'tw-animate-css'; + +/* Scan UI component output for Tailwind classes */ +@source "../dist"; + +@custom-variant dark (&:is(.dark *)); + +:root { + --radius: 0.625rem; + --background: oklch(1 0 0); + --foreground: oklch(0.129 0.042 264.695); + --card: oklch(1 0 0); + --card-foreground: oklch(0.129 0.042 264.695); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.129 0.042 264.695); + --primary: oklch(0.208 0.042 265.755); + --primary-foreground: oklch(0.984 0.003 247.858); + --secondary: oklch(0.968 0.007 247.896); + --secondary-foreground: oklch(0.208 0.042 265.755); + --muted: oklch(0.968 0.007 247.896); + --muted-foreground: oklch(0.554 0.046 257.417); + --accent: oklch(0.968 0.007 247.896); + --accent-foreground: oklch(0.208 0.042 265.755); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.929 0.013 255.508); + --input: oklch(0.929 0.013 255.508); + --ring: oklch(0.704 0.04 256.788); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --sidebar: oklch(0.984 0.003 247.858); + --sidebar-foreground: oklch(0.129 0.042 264.695); + --sidebar-primary: oklch(0.208 0.042 265.755); + --sidebar-primary-foreground: oklch(0.984 0.003 247.858); + --sidebar-accent: oklch(0.968 0.007 247.896); + --sidebar-accent-foreground: oklch(0.208 0.042 265.755); + --sidebar-border: oklch(0.929 0.013 255.508); + --sidebar-ring: oklch(0.704 0.04 256.788); +} + +.dark { + --background: oklch(0.129 0.042 264.695); + --foreground: oklch(0.984 0.003 247.858); + --card: oklch(0.208 0.042 265.755); + --card-foreground: oklch(0.984 0.003 247.858); + --popover: oklch(0.208 0.042 265.755); + --popover-foreground: oklch(0.984 0.003 247.858); + --primary: oklch(0.929 0.013 255.508); + --primary-foreground: oklch(0.208 0.042 265.755); + --secondary: oklch(0.279 0.041 260.031); + --secondary-foreground: oklch(0.984 0.003 247.858); + --muted: oklch(0.279 0.041 260.031); + --muted-foreground: oklch(0.704 0.04 256.788); + --accent: oklch(0.279 0.041 260.031); + --accent-foreground: oklch(0.984 0.003 247.858); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.551 0.027 264.364); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.208 0.042 265.755); + --sidebar-foreground: oklch(0.984 0.003 247.858); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.984 0.003 247.858); + --sidebar-accent: oklch(0.279 0.041 260.031); + --sidebar-accent-foreground: oklch(0.984 0.003 247.858); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.551 0.027 264.364); +} + +@theme inline { + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/packages/ui/src/lib/components/ui/accordion/accordion-content.svelte b/packages/ui/src/lib/components/ui/accordion/accordion-content.svelte new file mode 100644 index 000000000..06a3855ba --- /dev/null +++ b/packages/ui/src/lib/components/ui/accordion/accordion-content.svelte @@ -0,0 +1,22 @@ + + + +
+ {@render children?.()} +
+
diff --git a/packages/ui/src/lib/components/ui/accordion/accordion-item.svelte b/packages/ui/src/lib/components/ui/accordion/accordion-item.svelte new file mode 100644 index 000000000..e8b5fb757 --- /dev/null +++ b/packages/ui/src/lib/components/ui/accordion/accordion-item.svelte @@ -0,0 +1,17 @@ + + + diff --git a/packages/ui/src/lib/components/ui/accordion/accordion-trigger.svelte b/packages/ui/src/lib/components/ui/accordion/accordion-trigger.svelte new file mode 100644 index 000000000..9f1e34c63 --- /dev/null +++ b/packages/ui/src/lib/components/ui/accordion/accordion-trigger.svelte @@ -0,0 +1,32 @@ + + + + svg]:rotate-180', + className + )} + {...restProps} + > + {@render children?.()} + + + diff --git a/packages/ui/src/lib/components/ui/accordion/accordion.svelte b/packages/ui/src/lib/components/ui/accordion/accordion.svelte new file mode 100644 index 000000000..eafc3e071 --- /dev/null +++ b/packages/ui/src/lib/components/ui/accordion/accordion.svelte @@ -0,0 +1,16 @@ + + + diff --git a/packages/ui/src/lib/components/ui/accordion/index.ts b/packages/ui/src/lib/components/ui/accordion/index.ts new file mode 100644 index 000000000..660e6e670 --- /dev/null +++ b/packages/ui/src/lib/components/ui/accordion/index.ts @@ -0,0 +1,16 @@ +import Root from './accordion.svelte'; +import Content from './accordion-content.svelte'; +import Item from './accordion-item.svelte'; +import Trigger from './accordion-trigger.svelte'; + +export { + Root, + Content, + Item, + Trigger, + // + Root as Accordion, + Content as AccordionContent, + Item as AccordionItem, + Trigger as AccordionTrigger +}; diff --git a/packages/ui/src/lib/components/ui/avatar/avatar-fallback.svelte b/packages/ui/src/lib/components/ui/avatar/avatar-fallback.svelte new file mode 100644 index 000000000..4a452e9db --- /dev/null +++ b/packages/ui/src/lib/components/ui/avatar/avatar-fallback.svelte @@ -0,0 +1,17 @@ + + + diff --git a/packages/ui/src/lib/components/ui/avatar/avatar-image.svelte b/packages/ui/src/lib/components/ui/avatar/avatar-image.svelte new file mode 100644 index 000000000..7ccc3ce38 --- /dev/null +++ b/packages/ui/src/lib/components/ui/avatar/avatar-image.svelte @@ -0,0 +1,17 @@ + + + diff --git a/packages/ui/src/lib/components/ui/avatar/avatar.svelte b/packages/ui/src/lib/components/ui/avatar/avatar.svelte new file mode 100644 index 000000000..3fd4dc2aa --- /dev/null +++ b/packages/ui/src/lib/components/ui/avatar/avatar.svelte @@ -0,0 +1,19 @@ + + + diff --git a/packages/ui/src/lib/components/ui/avatar/index.ts b/packages/ui/src/lib/components/ui/avatar/index.ts new file mode 100644 index 000000000..9585f8ad6 --- /dev/null +++ b/packages/ui/src/lib/components/ui/avatar/index.ts @@ -0,0 +1,13 @@ +import Root from './avatar.svelte'; +import Image from './avatar-image.svelte'; +import Fallback from './avatar-fallback.svelte'; + +export { + Root, + Image, + Fallback, + // + Root as Avatar, + Image as AvatarImage, + Fallback as AvatarFallback +}; diff --git a/packages/ui/src/lib/components/ui/badge/badge.svelte b/packages/ui/src/lib/components/ui/badge/badge.svelte new file mode 100644 index 000000000..0acc5eb2d --- /dev/null +++ b/packages/ui/src/lib/components/ui/badge/badge.svelte @@ -0,0 +1,49 @@ + + + + + + {@render children?.()} + diff --git a/packages/ui/src/lib/components/ui/badge/index.ts b/packages/ui/src/lib/components/ui/badge/index.ts new file mode 100644 index 000000000..f05fb87fa --- /dev/null +++ b/packages/ui/src/lib/components/ui/badge/index.ts @@ -0,0 +1,2 @@ +export { default as Badge } from './badge.svelte'; +export { badgeVariants, type BadgeVariant } from './badge.svelte'; diff --git a/packages/ui/src/lib/components/ui/button/button.svelte b/packages/ui/src/lib/components/ui/button/button.svelte new file mode 100644 index 000000000..fad512e85 --- /dev/null +++ b/packages/ui/src/lib/components/ui/button/button.svelte @@ -0,0 +1,82 @@ + + + + +{#if href} + + {@render children?.()} + +{:else} + +{/if} diff --git a/packages/ui/src/lib/components/ui/button/index.ts b/packages/ui/src/lib/components/ui/button/index.ts new file mode 100644 index 000000000..5414d9d34 --- /dev/null +++ b/packages/ui/src/lib/components/ui/button/index.ts @@ -0,0 +1,17 @@ +import Root, { + type ButtonProps, + type ButtonSize, + type ButtonVariant, + buttonVariants +} from './button.svelte'; + +export { + Root, + type ButtonProps as Props, + // + Root as Button, + buttonVariants, + type ButtonProps, + type ButtonSize, + type ButtonVariant +}; diff --git a/packages/ui/src/lib/components/ui/card/card-action.svelte b/packages/ui/src/lib/components/ui/card/card-action.svelte new file mode 100644 index 000000000..bbaafd224 --- /dev/null +++ b/packages/ui/src/lib/components/ui/card/card-action.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/packages/ui/src/lib/components/ui/card/card-content.svelte b/packages/ui/src/lib/components/ui/card/card-content.svelte new file mode 100644 index 000000000..1d60124ad --- /dev/null +++ b/packages/ui/src/lib/components/ui/card/card-content.svelte @@ -0,0 +1,15 @@ + + +
+ {@render children?.()} +
diff --git a/packages/ui/src/lib/components/ui/card/card-description.svelte b/packages/ui/src/lib/components/ui/card/card-description.svelte new file mode 100644 index 000000000..b46a1adee --- /dev/null +++ b/packages/ui/src/lib/components/ui/card/card-description.svelte @@ -0,0 +1,20 @@ + + +

+ {@render children?.()} +

diff --git a/packages/ui/src/lib/components/ui/card/card-footer.svelte b/packages/ui/src/lib/components/ui/card/card-footer.svelte new file mode 100644 index 000000000..4e390bfd3 --- /dev/null +++ b/packages/ui/src/lib/components/ui/card/card-footer.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/packages/ui/src/lib/components/ui/card/card-header.svelte b/packages/ui/src/lib/components/ui/card/card-header.svelte new file mode 100644 index 000000000..9cdc602ba --- /dev/null +++ b/packages/ui/src/lib/components/ui/card/card-header.svelte @@ -0,0 +1,23 @@ + + +
+ {@render children?.()} +
diff --git a/packages/ui/src/lib/components/ui/card/card-title.svelte b/packages/ui/src/lib/components/ui/card/card-title.svelte new file mode 100644 index 000000000..0eae8ec1a --- /dev/null +++ b/packages/ui/src/lib/components/ui/card/card-title.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/packages/ui/src/lib/components/ui/card/card.svelte b/packages/ui/src/lib/components/ui/card/card.svelte new file mode 100644 index 000000000..10304357f --- /dev/null +++ b/packages/ui/src/lib/components/ui/card/card.svelte @@ -0,0 +1,23 @@ + + +
+ {@render children?.()} +
diff --git a/packages/ui/src/lib/components/ui/card/index.ts b/packages/ui/src/lib/components/ui/card/index.ts new file mode 100644 index 000000000..77d367477 --- /dev/null +++ b/packages/ui/src/lib/components/ui/card/index.ts @@ -0,0 +1,25 @@ +import Root from './card.svelte'; +import Content from './card-content.svelte'; +import Description from './card-description.svelte'; +import Footer from './card-footer.svelte'; +import Header from './card-header.svelte'; +import Title from './card-title.svelte'; +import Action from './card-action.svelte'; + +export { + Root, + Content, + Description, + Footer, + Header, + Title, + Action, + // + Root as Card, + Content as CardContent, + Description as CardDescription, + Footer as CardFooter, + Header as CardHeader, + Title as CardTitle, + Action as CardAction +}; diff --git a/packages/ui/src/lib/components/ui/carousel/carousel-content.svelte b/packages/ui/src/lib/components/ui/carousel/carousel-content.svelte new file mode 100644 index 000000000..72570b69e --- /dev/null +++ b/packages/ui/src/lib/components/ui/carousel/carousel-content.svelte @@ -0,0 +1,43 @@ + + +
+
+ {@render children?.()} +
+
diff --git a/packages/ui/src/lib/components/ui/carousel/carousel-item.svelte b/packages/ui/src/lib/components/ui/carousel/carousel-item.svelte new file mode 100644 index 000000000..1cf361e90 --- /dev/null +++ b/packages/ui/src/lib/components/ui/carousel/carousel-item.svelte @@ -0,0 +1,30 @@ + + +
+ {@render children?.()} +
diff --git a/packages/ui/src/lib/components/ui/carousel/carousel-next.svelte b/packages/ui/src/lib/components/ui/carousel/carousel-next.svelte new file mode 100644 index 000000000..647234802 --- /dev/null +++ b/packages/ui/src/lib/components/ui/carousel/carousel-next.svelte @@ -0,0 +1,38 @@ + + + diff --git a/packages/ui/src/lib/components/ui/carousel/carousel-previous.svelte b/packages/ui/src/lib/components/ui/carousel/carousel-previous.svelte new file mode 100644 index 000000000..7b355a27a --- /dev/null +++ b/packages/ui/src/lib/components/ui/carousel/carousel-previous.svelte @@ -0,0 +1,38 @@ + + + diff --git a/packages/ui/src/lib/components/ui/carousel/carousel.svelte b/packages/ui/src/lib/components/ui/carousel/carousel.svelte new file mode 100644 index 000000000..499ac8c72 --- /dev/null +++ b/packages/ui/src/lib/components/ui/carousel/carousel.svelte @@ -0,0 +1,93 @@ + + +
+ {@render children?.()} +
diff --git a/packages/ui/src/lib/components/ui/carousel/context.ts b/packages/ui/src/lib/components/ui/carousel/context.ts new file mode 100644 index 000000000..fe68bd9c9 --- /dev/null +++ b/packages/ui/src/lib/components/ui/carousel/context.ts @@ -0,0 +1,58 @@ +import type { WithElementRef } from '$lib/utils.js'; +import type { + EmblaCarouselSvelteType, + default as emblaCarouselSvelte +} from 'embla-carousel-svelte'; +import { getContext, hasContext, setContext } from 'svelte'; +import type { HTMLAttributes } from 'svelte/elements'; + +export type CarouselAPI = + NonNullable['on:emblaInit']> extends ( + evt: CustomEvent + ) => void + ? CarouselAPI + : never; + +type EmblaCarouselConfig = NonNullable[1]>; + +export type CarouselOptions = EmblaCarouselConfig['options']; +export type CarouselPlugins = EmblaCarouselConfig['plugins']; + +//// + +export type CarouselProps = { + opts?: CarouselOptions; + plugins?: CarouselPlugins; + setApi?: (api: CarouselAPI | undefined) => void; + orientation?: 'horizontal' | 'vertical'; +} & WithElementRef>; + +const EMBLA_CAROUSEL_CONTEXT = Symbol('EMBLA_CAROUSEL_CONTEXT'); + +export type EmblaContext = { + api: CarouselAPI | undefined; + orientation: 'horizontal' | 'vertical'; + scrollNext: () => void; + scrollPrev: () => void; + canScrollNext: boolean; + canScrollPrev: boolean; + handleKeyDown: (e: KeyboardEvent) => void; + options: CarouselOptions; + plugins: CarouselPlugins; + onInit: (e: CustomEvent) => void; + scrollTo: (index: number, jump?: boolean) => void; + scrollSnaps: number[]; + selectedIndex: number; +}; + +export function setEmblaContext(config: EmblaContext): EmblaContext { + setContext(EMBLA_CAROUSEL_CONTEXT, config); + return config; +} + +export function getEmblaContext(name = 'This component') { + if (!hasContext(EMBLA_CAROUSEL_CONTEXT)) { + throw new Error(`${name} must be used within a component`); + } + return getContext>(EMBLA_CAROUSEL_CONTEXT); +} diff --git a/packages/ui/src/lib/components/ui/carousel/index.ts b/packages/ui/src/lib/components/ui/carousel/index.ts new file mode 100644 index 000000000..2d92c7039 --- /dev/null +++ b/packages/ui/src/lib/components/ui/carousel/index.ts @@ -0,0 +1,19 @@ +import Root from './carousel.svelte'; +import Content from './carousel-content.svelte'; +import Item from './carousel-item.svelte'; +import Previous from './carousel-previous.svelte'; +import Next from './carousel-next.svelte'; + +export { + Root, + Content, + Item, + Previous, + Next, + // + Root as Carousel, + Content as CarouselContent, + Item as CarouselItem, + Previous as CarouselPrevious, + Next as CarouselNext +}; diff --git a/packages/ui/src/lib/components/ui/dialog/dialog-close.svelte b/packages/ui/src/lib/components/ui/dialog/dialog-close.svelte new file mode 100644 index 000000000..e8a96a773 --- /dev/null +++ b/packages/ui/src/lib/components/ui/dialog/dialog-close.svelte @@ -0,0 +1,7 @@ + + + diff --git a/packages/ui/src/lib/components/ui/dialog/dialog-content.svelte b/packages/ui/src/lib/components/ui/dialog/dialog-content.svelte new file mode 100644 index 000000000..451133318 --- /dev/null +++ b/packages/ui/src/lib/components/ui/dialog/dialog-content.svelte @@ -0,0 +1,45 @@ + + + + + + {@render children?.()} + {#if showCloseButton} + + + Close + + {/if} + + diff --git a/packages/ui/src/lib/components/ui/dialog/dialog-description.svelte b/packages/ui/src/lib/components/ui/dialog/dialog-description.svelte new file mode 100644 index 000000000..7539190db --- /dev/null +++ b/packages/ui/src/lib/components/ui/dialog/dialog-description.svelte @@ -0,0 +1,17 @@ + + + diff --git a/packages/ui/src/lib/components/ui/dialog/dialog-footer.svelte b/packages/ui/src/lib/components/ui/dialog/dialog-footer.svelte new file mode 100644 index 000000000..c457d5632 --- /dev/null +++ b/packages/ui/src/lib/components/ui/dialog/dialog-footer.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/packages/ui/src/lib/components/ui/dialog/dialog-header.svelte b/packages/ui/src/lib/components/ui/dialog/dialog-header.svelte new file mode 100644 index 000000000..9b2701ca3 --- /dev/null +++ b/packages/ui/src/lib/components/ui/dialog/dialog-header.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/packages/ui/src/lib/components/ui/dialog/dialog-overlay.svelte b/packages/ui/src/lib/components/ui/dialog/dialog-overlay.svelte new file mode 100644 index 000000000..34166e133 --- /dev/null +++ b/packages/ui/src/lib/components/ui/dialog/dialog-overlay.svelte @@ -0,0 +1,20 @@ + + + diff --git a/packages/ui/src/lib/components/ui/dialog/dialog-portal.svelte b/packages/ui/src/lib/components/ui/dialog/dialog-portal.svelte new file mode 100644 index 000000000..a60a85e7a --- /dev/null +++ b/packages/ui/src/lib/components/ui/dialog/dialog-portal.svelte @@ -0,0 +1,7 @@ + + + diff --git a/packages/ui/src/lib/components/ui/dialog/dialog-title.svelte b/packages/ui/src/lib/components/ui/dialog/dialog-title.svelte new file mode 100644 index 000000000..7073699d6 --- /dev/null +++ b/packages/ui/src/lib/components/ui/dialog/dialog-title.svelte @@ -0,0 +1,17 @@ + + + diff --git a/packages/ui/src/lib/components/ui/dialog/dialog-trigger.svelte b/packages/ui/src/lib/components/ui/dialog/dialog-trigger.svelte new file mode 100644 index 000000000..ac04d9fe4 --- /dev/null +++ b/packages/ui/src/lib/components/ui/dialog/dialog-trigger.svelte @@ -0,0 +1,7 @@ + + + diff --git a/packages/ui/src/lib/components/ui/dialog/dialog.svelte b/packages/ui/src/lib/components/ui/dialog/dialog.svelte new file mode 100644 index 000000000..c1444b2a4 --- /dev/null +++ b/packages/ui/src/lib/components/ui/dialog/dialog.svelte @@ -0,0 +1,7 @@ + + + diff --git a/packages/ui/src/lib/components/ui/dialog/index.ts b/packages/ui/src/lib/components/ui/dialog/index.ts new file mode 100644 index 000000000..9143f72a0 --- /dev/null +++ b/packages/ui/src/lib/components/ui/dialog/index.ts @@ -0,0 +1,34 @@ +import Root from './dialog.svelte'; +import Portal from './dialog-portal.svelte'; +import Title from './dialog-title.svelte'; +import Footer from './dialog-footer.svelte'; +import Header from './dialog-header.svelte'; +import Overlay from './dialog-overlay.svelte'; +import Content from './dialog-content.svelte'; +import Description from './dialog-description.svelte'; +import Trigger from './dialog-trigger.svelte'; +import Close from './dialog-close.svelte'; + +export { + Root, + Title, + Portal, + Footer, + Header, + Trigger, + Overlay, + Content, + Description, + Close, + // + Root as Dialog, + Title as DialogTitle, + Portal as DialogPortal, + Footer as DialogFooter, + Header as DialogHeader, + Trigger as DialogTrigger, + Overlay as DialogOverlay, + Content as DialogContent, + Description as DialogDescription, + Close as DialogClose +}; diff --git a/packages/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte b/packages/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte new file mode 100644 index 000000000..ed52f6783 --- /dev/null +++ b/packages/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte @@ -0,0 +1,41 @@ + + + + {#snippet children({ checked, indeterminate })} + + {#if indeterminate} + + {:else} + + {/if} + + {@render childrenProp?.()} + {/snippet} + diff --git a/packages/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte b/packages/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte new file mode 100644 index 000000000..305557bdd --- /dev/null +++ b/packages/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte @@ -0,0 +1,27 @@ + + + + + diff --git a/packages/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte b/packages/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte new file mode 100644 index 000000000..920848ee4 --- /dev/null +++ b/packages/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte @@ -0,0 +1,22 @@ + + + diff --git a/packages/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-group.svelte b/packages/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-group.svelte new file mode 100644 index 000000000..261ab7e63 --- /dev/null +++ b/packages/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-group.svelte @@ -0,0 +1,7 @@ + + + diff --git a/packages/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte b/packages/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte new file mode 100644 index 000000000..b0d4ed31c --- /dev/null +++ b/packages/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte @@ -0,0 +1,27 @@ + + + diff --git a/packages/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte b/packages/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte new file mode 100644 index 000000000..14e40f751 --- /dev/null +++ b/packages/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte @@ -0,0 +1,24 @@ + + +
+ {@render children?.()} +
diff --git a/packages/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte b/packages/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte new file mode 100644 index 000000000..3e9874919 --- /dev/null +++ b/packages/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte @@ -0,0 +1,16 @@ + + + diff --git a/packages/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte b/packages/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte new file mode 100644 index 000000000..ca8ccafb9 --- /dev/null +++ b/packages/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte @@ -0,0 +1,31 @@ + + + + {#snippet children({ checked })} + + {#if checked} + + {/if} + + {@render childrenProp?.({ checked })} + {/snippet} + diff --git a/packages/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte b/packages/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte new file mode 100644 index 000000000..a076e43c1 --- /dev/null +++ b/packages/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte @@ -0,0 +1,17 @@ + + + diff --git a/packages/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte b/packages/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte new file mode 100644 index 000000000..eb86e670a --- /dev/null +++ b/packages/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte @@ -0,0 +1,20 @@ + + + + {@render children?.()} + diff --git a/packages/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte b/packages/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte new file mode 100644 index 000000000..8528d25a3 --- /dev/null +++ b/packages/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte @@ -0,0 +1,20 @@ + + + diff --git a/packages/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte b/packages/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte new file mode 100644 index 000000000..96bb81087 --- /dev/null +++ b/packages/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte @@ -0,0 +1,29 @@ + + + + {@render children?.()} + + diff --git a/packages/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-trigger.svelte b/packages/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-trigger.svelte new file mode 100644 index 000000000..032b64531 --- /dev/null +++ b/packages/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-trigger.svelte @@ -0,0 +1,7 @@ + + + diff --git a/packages/ui/src/lib/components/ui/dropdown-menu/index.ts b/packages/ui/src/lib/components/ui/dropdown-menu/index.ts new file mode 100644 index 000000000..653290a07 --- /dev/null +++ b/packages/ui/src/lib/components/ui/dropdown-menu/index.ts @@ -0,0 +1,50 @@ +import type { Component } from 'svelte'; +import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui'; +import CheckboxItem from './dropdown-menu-checkbox-item.svelte'; +import Content from './dropdown-menu-content.svelte'; +import Group from './dropdown-menu-group.svelte'; +import Item from './dropdown-menu-item.svelte'; +import Label from './dropdown-menu-label.svelte'; +import RadioGroup from './dropdown-menu-radio-group.svelte'; +import RadioItem from './dropdown-menu-radio-item.svelte'; +import Separator from './dropdown-menu-separator.svelte'; +import Shortcut from './dropdown-menu-shortcut.svelte'; +import Trigger from './dropdown-menu-trigger.svelte'; +import SubContent from './dropdown-menu-sub-content.svelte'; +import SubTrigger from './dropdown-menu-sub-trigger.svelte'; +import GroupHeading from './dropdown-menu-group-heading.svelte'; +const Sub = DropdownMenuPrimitive.Sub; +const Root: Component = DropdownMenuPrimitive.Root; + +export { + CheckboxItem, + Content, + Root as DropdownMenu, + CheckboxItem as DropdownMenuCheckboxItem, + Content as DropdownMenuContent, + Group as DropdownMenuGroup, + Item as DropdownMenuItem, + Label as DropdownMenuLabel, + RadioGroup as DropdownMenuRadioGroup, + RadioItem as DropdownMenuRadioItem, + Separator as DropdownMenuSeparator, + Shortcut as DropdownMenuShortcut, + Sub as DropdownMenuSub, + SubContent as DropdownMenuSubContent, + SubTrigger as DropdownMenuSubTrigger, + Trigger as DropdownMenuTrigger, + GroupHeading as DropdownMenuGroupHeading, + Group, + GroupHeading, + Item, + Label, + RadioGroup, + RadioItem, + Root, + Separator, + Shortcut, + Sub, + SubContent, + SubTrigger, + Trigger +}; diff --git a/packages/ui/src/lib/components/ui/field/field-content.svelte b/packages/ui/src/lib/components/ui/field/field-content.svelte new file mode 100644 index 000000000..622b37b07 --- /dev/null +++ b/packages/ui/src/lib/components/ui/field/field-content.svelte @@ -0,0 +1,14 @@ + + +
+ {@render children?.()} +
diff --git a/packages/ui/src/lib/components/ui/field/field-description.svelte b/packages/ui/src/lib/components/ui/field/field-description.svelte new file mode 100644 index 000000000..c803f28d1 --- /dev/null +++ b/packages/ui/src/lib/components/ui/field/field-description.svelte @@ -0,0 +1,19 @@ + + +

a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary', + className + )} + {...restProps} +> + {@render children?.()} +

diff --git a/packages/ui/src/lib/components/ui/field/field-error.svelte b/packages/ui/src/lib/components/ui/field/field-error.svelte new file mode 100644 index 000000000..3fe85f262 --- /dev/null +++ b/packages/ui/src/lib/components/ui/field/field-error.svelte @@ -0,0 +1,56 @@ + + +{#if hasContent} + +{/if} diff --git a/packages/ui/src/lib/components/ui/field/field-group.svelte b/packages/ui/src/lib/components/ui/field/field-group.svelte new file mode 100644 index 000000000..58cf54e1c --- /dev/null +++ b/packages/ui/src/lib/components/ui/field/field-group.svelte @@ -0,0 +1,17 @@ + + +
[data-slot=field-group]]:gap-4', + className + )} + {...restProps} +> + {@render children?.()} +
diff --git a/packages/ui/src/lib/components/ui/field/field-label.svelte b/packages/ui/src/lib/components/ui/field/field-label.svelte new file mode 100644 index 000000000..4de55749c --- /dev/null +++ b/packages/ui/src/lib/components/ui/field/field-label.svelte @@ -0,0 +1,26 @@ + + + diff --git a/packages/ui/src/lib/components/ui/field/field-legend.svelte b/packages/ui/src/lib/components/ui/field/field-legend.svelte new file mode 100644 index 000000000..ad3dfcd59 --- /dev/null +++ b/packages/ui/src/lib/components/ui/field/field-legend.svelte @@ -0,0 +1,27 @@ + + + + {@render children?.()} + diff --git a/packages/ui/src/lib/components/ui/field/field-separator.svelte b/packages/ui/src/lib/components/ui/field/field-separator.svelte new file mode 100644 index 000000000..5a193331a --- /dev/null +++ b/packages/ui/src/lib/components/ui/field/field-separator.svelte @@ -0,0 +1,33 @@ + + +
+ + {#if children} + + {@render children()} + + {/if} +
diff --git a/packages/ui/src/lib/components/ui/field/field-set.svelte b/packages/ui/src/lib/components/ui/field/field-set.svelte new file mode 100644 index 000000000..f2a613162 --- /dev/null +++ b/packages/ui/src/lib/components/ui/field/field-set.svelte @@ -0,0 +1,18 @@ + + +
[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3', + className + )} + {...restProps} +> + {@render children?.()} +
diff --git a/packages/ui/src/lib/components/ui/field/field-title.svelte b/packages/ui/src/lib/components/ui/field/field-title.svelte new file mode 100644 index 000000000..e7aa1eb02 --- /dev/null +++ b/packages/ui/src/lib/components/ui/field/field-title.svelte @@ -0,0 +1,17 @@ + + +
+ {@render children?.()} +
diff --git a/packages/ui/src/lib/components/ui/field/field.svelte b/packages/ui/src/lib/components/ui/field/field.svelte new file mode 100644 index 000000000..483e8ab49 --- /dev/null +++ b/packages/ui/src/lib/components/ui/field/field.svelte @@ -0,0 +1,51 @@ + + + + +
+ {@render children?.()} +
diff --git a/packages/ui/src/lib/components/ui/field/index.ts b/packages/ui/src/lib/components/ui/field/index.ts new file mode 100644 index 000000000..0c207d8a0 --- /dev/null +++ b/packages/ui/src/lib/components/ui/field/index.ts @@ -0,0 +1,33 @@ +import Field from './field.svelte'; +import Set from './field-set.svelte'; +import Legend from './field-legend.svelte'; +import Group from './field-group.svelte'; +import Content from './field-content.svelte'; +import Label from './field-label.svelte'; +import Title from './field-title.svelte'; +import Description from './field-description.svelte'; +import Separator from './field-separator.svelte'; +import Error from './field-error.svelte'; + +export { + Field, + Set, + Legend, + Group, + Content, + Label, + Title, + Description, + Separator, + Error, + // + Set as FieldSet, + Legend as FieldLegend, + Group as FieldGroup, + Content as FieldContent, + Label as FieldLabel, + Title as FieldTitle, + Description as FieldDescription, + Separator as FieldSeparator, + Error as FieldError +}; diff --git a/packages/ui/src/lib/components/ui/form/form-button.svelte b/packages/ui/src/lib/components/ui/form/form-button.svelte new file mode 100644 index 000000000..0b3100643 --- /dev/null +++ b/packages/ui/src/lib/components/ui/form/form-button.svelte @@ -0,0 +1,7 @@ + + + +{/if} diff --git a/packages/ui/src/lib/components/ui/sidebar/sidebar-group-content.svelte b/packages/ui/src/lib/components/ui/sidebar/sidebar-group-content.svelte new file mode 100644 index 000000000..fafaceefe --- /dev/null +++ b/packages/ui/src/lib/components/ui/sidebar/sidebar-group-content.svelte @@ -0,0 +1,21 @@ + + +
+ {@render children?.()} +
diff --git a/packages/ui/src/lib/components/ui/sidebar/sidebar-group-label.svelte b/packages/ui/src/lib/components/ui/sidebar/sidebar-group-label.svelte new file mode 100644 index 000000000..1de5e8473 --- /dev/null +++ b/packages/ui/src/lib/components/ui/sidebar/sidebar-group-label.svelte @@ -0,0 +1,34 @@ + + +{#if child} + {@render child({ props: mergedProps })} +{:else} +
+ {@render children?.()} +
+{/if} diff --git a/packages/ui/src/lib/components/ui/sidebar/sidebar-group.svelte b/packages/ui/src/lib/components/ui/sidebar/sidebar-group.svelte new file mode 100644 index 000000000..897445de9 --- /dev/null +++ b/packages/ui/src/lib/components/ui/sidebar/sidebar-group.svelte @@ -0,0 +1,21 @@ + + +
+ {@render children?.()} +
diff --git a/packages/ui/src/lib/components/ui/sidebar/sidebar-header.svelte b/packages/ui/src/lib/components/ui/sidebar/sidebar-header.svelte new file mode 100644 index 000000000..a54647464 --- /dev/null +++ b/packages/ui/src/lib/components/ui/sidebar/sidebar-header.svelte @@ -0,0 +1,21 @@ + + +
+ {@render children?.()} +
diff --git a/packages/ui/src/lib/components/ui/sidebar/sidebar-input.svelte b/packages/ui/src/lib/components/ui/sidebar/sidebar-input.svelte new file mode 100644 index 000000000..1a3610ba3 --- /dev/null +++ b/packages/ui/src/lib/components/ui/sidebar/sidebar-input.svelte @@ -0,0 +1,21 @@ + + + diff --git a/packages/ui/src/lib/components/ui/sidebar/sidebar-inset.svelte b/packages/ui/src/lib/components/ui/sidebar/sidebar-inset.svelte new file mode 100644 index 000000000..e2a30a235 --- /dev/null +++ b/packages/ui/src/lib/components/ui/sidebar/sidebar-inset.svelte @@ -0,0 +1,24 @@ + + +
+ {@render children?.()} +
diff --git a/packages/ui/src/lib/components/ui/sidebar/sidebar-menu-action.svelte b/packages/ui/src/lib/components/ui/sidebar/sidebar-menu-action.svelte new file mode 100644 index 000000000..3f178c8f5 --- /dev/null +++ b/packages/ui/src/lib/components/ui/sidebar/sidebar-menu-action.svelte @@ -0,0 +1,43 @@ + + +{#if child} + {@render child({ props: mergedProps })} +{:else} + +{/if} diff --git a/packages/ui/src/lib/components/ui/sidebar/sidebar-menu-badge.svelte b/packages/ui/src/lib/components/ui/sidebar/sidebar-menu-badge.svelte new file mode 100644 index 000000000..aef8db865 --- /dev/null +++ b/packages/ui/src/lib/components/ui/sidebar/sidebar-menu-badge.svelte @@ -0,0 +1,29 @@ + + +
+ {@render children?.()} +
diff --git a/packages/ui/src/lib/components/ui/sidebar/sidebar-menu-button.svelte b/packages/ui/src/lib/components/ui/sidebar/sidebar-menu-button.svelte new file mode 100644 index 000000000..c358b7082 --- /dev/null +++ b/packages/ui/src/lib/components/ui/sidebar/sidebar-menu-button.svelte @@ -0,0 +1,101 @@ + + + + +{#snippet Button({ props }: { props?: Record })} + {@const mergedProps = mergeProps(buttonProps, props)} + {#if child} + {@render child({ props: mergedProps })} + {:else} + + {/if} +{/snippet} + +{#if !tooltipContent} + {@render Button({})} +{:else} + + + {#snippet child({ props })} + {@render Button({ props })} + {/snippet} + + + +{/if} diff --git a/packages/ui/src/lib/components/ui/sidebar/sidebar-menu-item.svelte b/packages/ui/src/lib/components/ui/sidebar/sidebar-menu-item.svelte new file mode 100644 index 000000000..acd286066 --- /dev/null +++ b/packages/ui/src/lib/components/ui/sidebar/sidebar-menu-item.svelte @@ -0,0 +1,21 @@ + + +
  • + {@render children?.()} +
  • diff --git a/packages/ui/src/lib/components/ui/sidebar/sidebar-menu-skeleton.svelte b/packages/ui/src/lib/components/ui/sidebar/sidebar-menu-skeleton.svelte new file mode 100644 index 000000000..49a18f127 --- /dev/null +++ b/packages/ui/src/lib/components/ui/sidebar/sidebar-menu-skeleton.svelte @@ -0,0 +1,36 @@ + + +
    + {#if showIcon} + + {/if} + + {@render children?.()} +
    diff --git a/packages/ui/src/lib/components/ui/sidebar/sidebar-menu-sub-button.svelte b/packages/ui/src/lib/components/ui/sidebar/sidebar-menu-sub-button.svelte new file mode 100644 index 000000000..95191c2a3 --- /dev/null +++ b/packages/ui/src/lib/components/ui/sidebar/sidebar-menu-sub-button.svelte @@ -0,0 +1,43 @@ + + +{#if child} + {@render child({ props: mergedProps })} +{:else} + + {@render children?.()} + +{/if} diff --git a/packages/ui/src/lib/components/ui/sidebar/sidebar-menu-sub-item.svelte b/packages/ui/src/lib/components/ui/sidebar/sidebar-menu-sub-item.svelte new file mode 100644 index 000000000..4274f0e09 --- /dev/null +++ b/packages/ui/src/lib/components/ui/sidebar/sidebar-menu-sub-item.svelte @@ -0,0 +1,21 @@ + + +
  • + {@render children?.()} +
  • diff --git a/packages/ui/src/lib/components/ui/sidebar/sidebar-menu-sub.svelte b/packages/ui/src/lib/components/ui/sidebar/sidebar-menu-sub.svelte new file mode 100644 index 000000000..609ca6408 --- /dev/null +++ b/packages/ui/src/lib/components/ui/sidebar/sidebar-menu-sub.svelte @@ -0,0 +1,25 @@ + + +
      + {@render children?.()} +
    diff --git a/packages/ui/src/lib/components/ui/sidebar/sidebar-menu.svelte b/packages/ui/src/lib/components/ui/sidebar/sidebar-menu.svelte new file mode 100644 index 000000000..094f0f07b --- /dev/null +++ b/packages/ui/src/lib/components/ui/sidebar/sidebar-menu.svelte @@ -0,0 +1,21 @@ + + +
      + {@render children?.()} +
    diff --git a/packages/ui/src/lib/components/ui/sidebar/sidebar-provider.svelte b/packages/ui/src/lib/components/ui/sidebar/sidebar-provider.svelte new file mode 100644 index 000000000..2e615f4c5 --- /dev/null +++ b/packages/ui/src/lib/components/ui/sidebar/sidebar-provider.svelte @@ -0,0 +1,53 @@ + + + + + +
    + {@render children?.()} +
    +
    diff --git a/packages/ui/src/lib/components/ui/sidebar/sidebar-rail.svelte b/packages/ui/src/lib/components/ui/sidebar/sidebar-rail.svelte new file mode 100644 index 000000000..62ebb55bd --- /dev/null +++ b/packages/ui/src/lib/components/ui/sidebar/sidebar-rail.svelte @@ -0,0 +1,36 @@ + + + diff --git a/packages/ui/src/lib/components/ui/sidebar/sidebar-separator.svelte b/packages/ui/src/lib/components/ui/sidebar/sidebar-separator.svelte new file mode 100644 index 000000000..45c1ebcad --- /dev/null +++ b/packages/ui/src/lib/components/ui/sidebar/sidebar-separator.svelte @@ -0,0 +1,19 @@ + + + diff --git a/packages/ui/src/lib/components/ui/sidebar/sidebar-trigger.svelte b/packages/ui/src/lib/components/ui/sidebar/sidebar-trigger.svelte new file mode 100644 index 000000000..285e6c4fc --- /dev/null +++ b/packages/ui/src/lib/components/ui/sidebar/sidebar-trigger.svelte @@ -0,0 +1,35 @@ + + + diff --git a/packages/ui/src/lib/components/ui/sidebar/sidebar.svelte b/packages/ui/src/lib/components/ui/sidebar/sidebar.svelte new file mode 100644 index 000000000..0eee3bb5b --- /dev/null +++ b/packages/ui/src/lib/components/ui/sidebar/sidebar.svelte @@ -0,0 +1,101 @@ + + +{#if collapsible === 'none'} +
    + {@render children?.()} +
    +{:else if sidebar.isMobile} + sidebar.openMobile, (v) => sidebar.setOpenMobile(v)} {...restProps}> + + + Sidebar + Displays the mobile sidebar. + +
    + {@render children?.()} +
    +
    +
    +{:else} + +{/if} diff --git a/packages/ui/src/lib/components/ui/skeleton/index.ts b/packages/ui/src/lib/components/ui/skeleton/index.ts new file mode 100644 index 000000000..3120ce123 --- /dev/null +++ b/packages/ui/src/lib/components/ui/skeleton/index.ts @@ -0,0 +1,7 @@ +import Root from './skeleton.svelte'; + +export { + Root, + // + Root as Skeleton +}; diff --git a/packages/ui/src/lib/components/ui/skeleton/skeleton.svelte b/packages/ui/src/lib/components/ui/skeleton/skeleton.svelte new file mode 100644 index 000000000..cdd10e003 --- /dev/null +++ b/packages/ui/src/lib/components/ui/skeleton/skeleton.svelte @@ -0,0 +1,17 @@ + + +
    diff --git a/packages/ui/src/lib/components/ui/sonner/index.ts b/packages/ui/src/lib/components/ui/sonner/index.ts new file mode 100644 index 000000000..fcaf06bfb --- /dev/null +++ b/packages/ui/src/lib/components/ui/sonner/index.ts @@ -0,0 +1 @@ +export { default as Toaster } from './sonner.svelte'; diff --git a/packages/ui/src/lib/components/ui/sonner/sonner.svelte b/packages/ui/src/lib/components/ui/sonner/sonner.svelte new file mode 100644 index 000000000..cb1f7c19e --- /dev/null +++ b/packages/ui/src/lib/components/ui/sonner/sonner.svelte @@ -0,0 +1,13 @@ + + + diff --git a/packages/ui/src/lib/components/ui/table/index.ts b/packages/ui/src/lib/components/ui/table/index.ts new file mode 100644 index 000000000..99239aeea --- /dev/null +++ b/packages/ui/src/lib/components/ui/table/index.ts @@ -0,0 +1,28 @@ +import Root from './table.svelte'; +import Body from './table-body.svelte'; +import Caption from './table-caption.svelte'; +import Cell from './table-cell.svelte'; +import Footer from './table-footer.svelte'; +import Head from './table-head.svelte'; +import Header from './table-header.svelte'; +import Row from './table-row.svelte'; + +export { + Root, + Body, + Caption, + Cell, + Footer, + Head, + Header, + Row, + // + Root as Table, + Body as TableBody, + Caption as TableCaption, + Cell as TableCell, + Footer as TableFooter, + Head as TableHead, + Header as TableHeader, + Row as TableRow +}; diff --git a/packages/ui/src/lib/components/ui/table/table-body.svelte b/packages/ui/src/lib/components/ui/table/table-body.svelte new file mode 100644 index 000000000..cf720f47c --- /dev/null +++ b/packages/ui/src/lib/components/ui/table/table-body.svelte @@ -0,0 +1,20 @@ + + + + {@render children?.()} + diff --git a/packages/ui/src/lib/components/ui/table/table-caption.svelte b/packages/ui/src/lib/components/ui/table/table-caption.svelte new file mode 100644 index 000000000..7ad3aeed5 --- /dev/null +++ b/packages/ui/src/lib/components/ui/table/table-caption.svelte @@ -0,0 +1,20 @@ + + + + {@render children?.()} + diff --git a/packages/ui/src/lib/components/ui/table/table-cell.svelte b/packages/ui/src/lib/components/ui/table/table-cell.svelte new file mode 100644 index 000000000..d7699150b --- /dev/null +++ b/packages/ui/src/lib/components/ui/table/table-cell.svelte @@ -0,0 +1,23 @@ + + + + {@render children?.()} + diff --git a/packages/ui/src/lib/components/ui/table/table-footer.svelte b/packages/ui/src/lib/components/ui/table/table-footer.svelte new file mode 100644 index 000000000..565b2441e --- /dev/null +++ b/packages/ui/src/lib/components/ui/table/table-footer.svelte @@ -0,0 +1,20 @@ + + +tr]:last:border-b-0', className)} + {...restProps} +> + {@render children?.()} + diff --git a/packages/ui/src/lib/components/ui/table/table-head.svelte b/packages/ui/src/lib/components/ui/table/table-head.svelte new file mode 100644 index 000000000..e68b74ff5 --- /dev/null +++ b/packages/ui/src/lib/components/ui/table/table-head.svelte @@ -0,0 +1,23 @@ + + + + {@render children?.()} + diff --git a/packages/ui/src/lib/components/ui/table/table-header.svelte b/packages/ui/src/lib/components/ui/table/table-header.svelte new file mode 100644 index 000000000..615b5d8e4 --- /dev/null +++ b/packages/ui/src/lib/components/ui/table/table-header.svelte @@ -0,0 +1,20 @@ + + + + {@render children?.()} + diff --git a/packages/ui/src/lib/components/ui/table/table-row.svelte b/packages/ui/src/lib/components/ui/table/table-row.svelte new file mode 100644 index 000000000..251bbd9c6 --- /dev/null +++ b/packages/ui/src/lib/components/ui/table/table-row.svelte @@ -0,0 +1,23 @@ + + +svelte-css-wrapper]:[&>th,td]:bg-muted/50', + className + )} + {...restProps} +> + {@render children?.()} + diff --git a/packages/ui/src/lib/components/ui/table/table.svelte b/packages/ui/src/lib/components/ui/table/table.svelte new file mode 100644 index 000000000..38b0176b6 --- /dev/null +++ b/packages/ui/src/lib/components/ui/table/table.svelte @@ -0,0 +1,22 @@ + + +
    + + {@render children?.()} +
    +
    diff --git a/packages/ui/src/lib/components/ui/tabs/index.ts b/packages/ui/src/lib/components/ui/tabs/index.ts new file mode 100644 index 000000000..d2a7939e3 --- /dev/null +++ b/packages/ui/src/lib/components/ui/tabs/index.ts @@ -0,0 +1,16 @@ +import Root from './tabs.svelte'; +import Content from './tabs-content.svelte'; +import List from './tabs-list.svelte'; +import Trigger from './tabs-trigger.svelte'; + +export { + Root, + Content, + List, + Trigger, + // + Root as Tabs, + Content as TabsContent, + List as TabsList, + Trigger as TabsTrigger +}; diff --git a/packages/ui/src/lib/components/ui/tabs/tabs-content.svelte b/packages/ui/src/lib/components/ui/tabs/tabs-content.svelte new file mode 100644 index 000000000..92044c867 --- /dev/null +++ b/packages/ui/src/lib/components/ui/tabs/tabs-content.svelte @@ -0,0 +1,17 @@ + + + diff --git a/packages/ui/src/lib/components/ui/tabs/tabs-list.svelte b/packages/ui/src/lib/components/ui/tabs/tabs-list.svelte new file mode 100644 index 000000000..e875fe475 --- /dev/null +++ b/packages/ui/src/lib/components/ui/tabs/tabs-list.svelte @@ -0,0 +1,16 @@ + + + diff --git a/packages/ui/src/lib/components/ui/tabs/tabs-trigger.svelte b/packages/ui/src/lib/components/ui/tabs/tabs-trigger.svelte new file mode 100644 index 000000000..8dac6c531 --- /dev/null +++ b/packages/ui/src/lib/components/ui/tabs/tabs-trigger.svelte @@ -0,0 +1,20 @@ + + + diff --git a/packages/ui/src/lib/components/ui/tabs/tabs.svelte b/packages/ui/src/lib/components/ui/tabs/tabs.svelte new file mode 100644 index 000000000..b275bdafd --- /dev/null +++ b/packages/ui/src/lib/components/ui/tabs/tabs.svelte @@ -0,0 +1,19 @@ + + + diff --git a/packages/ui/src/lib/components/ui/textarea/index.ts b/packages/ui/src/lib/components/ui/textarea/index.ts new file mode 100644 index 000000000..9ccb3bff3 --- /dev/null +++ b/packages/ui/src/lib/components/ui/textarea/index.ts @@ -0,0 +1,7 @@ +import Root from './textarea.svelte'; + +export { + Root, + // + Root as Textarea +}; diff --git a/packages/ui/src/lib/components/ui/textarea/textarea.svelte b/packages/ui/src/lib/components/ui/textarea/textarea.svelte new file mode 100644 index 000000000..5adc8f69e --- /dev/null +++ b/packages/ui/src/lib/components/ui/textarea/textarea.svelte @@ -0,0 +1,23 @@ + + + diff --git a/packages/ui/src/lib/components/ui/tooltip/index.ts b/packages/ui/src/lib/components/ui/tooltip/index.ts new file mode 100644 index 000000000..273d831e6 --- /dev/null +++ b/packages/ui/src/lib/components/ui/tooltip/index.ts @@ -0,0 +1,21 @@ +import { Tooltip as TooltipPrimitive } from 'bits-ui'; +import Trigger from './tooltip-trigger.svelte'; +import Content from './tooltip-content.svelte'; + +const Root = TooltipPrimitive.Root; +const Provider = TooltipPrimitive.Provider; +const Portal = TooltipPrimitive.Portal; + +export { + Root, + Trigger, + Content, + Provider, + Portal, + // + Root as Tooltip, + Content as TooltipContent, + Trigger as TooltipTrigger, + Provider as TooltipProvider, + Portal as TooltipPortal +}; diff --git a/packages/ui/src/lib/components/ui/tooltip/tooltip-content.svelte b/packages/ui/src/lib/components/ui/tooltip/tooltip-content.svelte new file mode 100644 index 000000000..bc33b1824 --- /dev/null +++ b/packages/ui/src/lib/components/ui/tooltip/tooltip-content.svelte @@ -0,0 +1,47 @@ + + + + + {@render children?.()} + + {#snippet child({ props })} +
    + {/snippet} +
    +
    +
    diff --git a/packages/ui/src/lib/components/ui/tooltip/tooltip-trigger.svelte b/packages/ui/src/lib/components/ui/tooltip/tooltip-trigger.svelte new file mode 100644 index 000000000..5631d1b4c --- /dev/null +++ b/packages/ui/src/lib/components/ui/tooltip/tooltip-trigger.svelte @@ -0,0 +1,7 @@ + + + diff --git a/packages/ui/src/lib/index.ts b/packages/ui/src/lib/index.ts new file mode 100644 index 000000000..e1a820acd --- /dev/null +++ b/packages/ui/src/lib/index.ts @@ -0,0 +1,34 @@ +// @metastate-foundation/ui — Shared UI Components +// Re-export utilities +export { + cn, + type WithoutChild, + type WithoutChildren, + type WithoutChildrenOrChild, + type WithElementRef +} from './utils.js'; + +// Re-export all component modules +export * as Accordion from './components/ui/accordion/index.js'; +export * as Avatar from './components/ui/avatar/index.js'; +export * as Badge from './components/ui/badge/index.js'; +export * as Button from './components/ui/button/index.js'; +export * as Card from './components/ui/card/index.js'; +export * as Carousel from './components/ui/carousel/index.js'; +export * as Dialog from './components/ui/dialog/index.js'; +export * as DropdownMenu from './components/ui/dropdown-menu/index.js'; +export * as Field from './components/ui/field/index.js'; +export * as Form from './components/ui/form/index.js'; +export * as Input from './components/ui/input/index.js'; +export * as Label from './components/ui/label/index.js'; +export * as Pagination from './components/ui/pagination/index.js'; +export * as Progress from './components/ui/progress/index.js'; +export * as Select from './components/ui/select/index.js'; +export * as Separator from './components/ui/separator/index.js'; +export * as Sheet from './components/ui/sheet/index.js'; +export * as Sidebar from './components/ui/sidebar/index.js'; +export * as Skeleton from './components/ui/skeleton/index.js'; +export * as Sonner from './components/ui/sonner/index.js'; +export * as Table from './components/ui/table/index.js'; +export * as Textarea from './components/ui/textarea/index.js'; +export * as Tooltip from './components/ui/tooltip/index.js'; diff --git a/packages/ui/src/lib/utils.ts b/packages/ui/src/lib/utils.ts new file mode 100644 index 000000000..f92bfcbb3 --- /dev/null +++ b/packages/ui/src/lib/utils.ts @@ -0,0 +1,13 @@ +import { clsx, type ClassValue } from 'clsx'; +import { twMerge } from 'tailwind-merge'; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type WithoutChild = T extends { child?: any } ? Omit : T; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type WithoutChildren = T extends { children?: any } ? Omit : T; +export type WithoutChildrenOrChild = WithoutChildren>; +export type WithElementRef = T & { ref?: U | null }; diff --git a/packages/ui/src/routes/+page.svelte b/packages/ui/src/routes/+page.svelte new file mode 100644 index 000000000..154f225a3 --- /dev/null +++ b/packages/ui/src/routes/+page.svelte @@ -0,0 +1 @@ + diff --git a/packages/ui/svelte.config.js b/packages/ui/svelte.config.js new file mode 100644 index 000000000..91f85a6b3 --- /dev/null +++ b/packages/ui/svelte.config.js @@ -0,0 +1,8 @@ +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + preprocess: [vitePreprocess()] +}; + +export default config; diff --git a/packages/ui/tsconfig.json b/packages/ui/tsconfig.json new file mode 100644 index 000000000..19cb1544f --- /dev/null +++ b/packages/ui/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "bundler", + "strict": true, + "declaration": true, + "sourceMap": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "isolatedModules": true, + "skipLibCheck": true + }, + "include": ["src/**/*.ts", "src/**/*.svelte"] +} diff --git a/platforms/profile-editor/api/package.json b/platforms/profile-editor/api/package.json new file mode 100644 index 000000000..2320e88a5 --- /dev/null +++ b/platforms/profile-editor/api/package.json @@ -0,0 +1,44 @@ +{ + "name": "profile-editor-api", + "version": "1.0.0", + "description": "Profile Editor API for the w3ds ecosystem", + "main": "src/index.ts", + "scripts": { + "start": "ts-node src/index.ts", + "dev": "nodemon --exec ts-node src/index.ts", + "build": "tsc && cp -r src/web3adapter/mappings dist/web3adapter/", + "typeorm": "typeorm-ts-node-commonjs", + "migration:generate": "bash -c 'read -p \"Migration name: \" name && npx typeorm-ts-node-commonjs migration:generate src/database/migrations/$name -d src/database/data-source.ts'", + "migration:run": "npm run typeorm migration:run -- -d src/database/data-source.ts", + "migration:revert": "npm run typeorm migration:revert -- -d src/database/data-source.ts" + }, + "dependencies": { + "@metastate-foundation/auth": "workspace:*", + "axios": "^1.6.7", + "cors": "^2.8.5", + "dotenv": "^16.4.5", + "express": "^4.18.2", + "form-data": "^4.0.5", + "graphql-request": "^6.1.0", + "jsonwebtoken": "^9.0.3", + "multer": "^2.1.1", + "pg": "^8.20.0", + "reflect-metadata": "^0.2.2", + "signature-validator": "workspace:*", + "web3-adapter": "workspace:*", + "typeorm": "^0.3.28", + "uuid": "^9.0.1" + }, + "devDependencies": { + "@types/cors": "^2.8.17", + "@types/express": "^4.17.21", + "@types/jsonwebtoken": "^9.0.10", + "@types/multer": "^2.1.0", + "@types/node": "^20.11.24", + "@types/pg": "^8.18.0", + "@types/uuid": "^9.0.8", + "nodemon": "^3.0.3", + "ts-node": "^10.9.2", + "typescript": "^5.3.3" + } +} diff --git a/platforms/profile-editor/api/src/controllers/AuthController.ts b/platforms/profile-editor/api/src/controllers/AuthController.ts new file mode 100644 index 000000000..47b5bd56f --- /dev/null +++ b/platforms/profile-editor/api/src/controllers/AuthController.ts @@ -0,0 +1,134 @@ +import { Request, Response } from "express"; +import { EventEmitter } from "events"; +import { + buildAuthOffer, + signToken, + verifyLoginSignature, +} from "@metastate-foundation/auth"; +import { registerSession } from "../middleware/auth"; +import type { EVaultProfileService } from "../services/EVaultProfileService"; + +const JWT_SECRET = process.env.PROFILE_EDITOR_JWT_SECRET!; + +export class AuthController { + private eventEmitter: EventEmitter; + private evaultService: EVaultProfileService; + + constructor(evaultService: EVaultProfileService) { + this.eventEmitter = new EventEmitter(); + this.evaultService = evaultService; + } + + sseStream = async (req: Request, res: Response) => { + const { id } = req.params; + + res.writeHead(200, { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache", + Connection: "keep-alive", + "Access-Control-Allow-Origin": "*", + }); + + const handler = (data: any) => { + res.write(`data: ${JSON.stringify(data)}\n\n`); + }; + + this.eventEmitter.on(id, handler); + + const heartbeatInterval = setInterval(() => { + try { + res.write(`: heartbeat\n\n`); + } catch { + clearInterval(heartbeatInterval); + } + }, 30000); + + req.on("close", () => { + clearInterval(heartbeatInterval); + this.eventEmitter.off(id, handler); + res.end(); + }); + + req.on("error", () => { + clearInterval(heartbeatInterval); + this.eventEmitter.off(id, handler); + res.end(); + }); + }; + + getOffer = async (_req: Request, res: Response) => { + const baseUrl = process.env.PUBLIC_PROFILE_EDITOR_BASE_URL; + if (!baseUrl) { + return res + .status(500) + .json({ error: "PUBLIC_PROFILE_EDITOR_BASE_URL not configured" }); + } + + const offer = buildAuthOffer({ + baseUrl, + platform: "profile-editor", + }); + + res.json({ uri: offer.uri }); + }; + + login = async (req: Request, res: Response) => { + try { + const { ename, session, signature } = req.body; + + if (!ename) { + return res.status(400).json({ error: "ename is required" }); + } + if (!session) { + return res.status(400).json({ error: "session is required" }); + } + if (!signature) { + return res.status(400).json({ error: "signature is required" }); + } + + const registryBaseUrl = process.env.PUBLIC_REGISTRY_URL; + if (!registryBaseUrl) { + return res + .status(500) + .json({ error: "Server configuration error" }); + } + + const result = await verifyLoginSignature({ + eName: ename, + signature, + session, + registryBaseUrl, + }); + + if (!result.valid) { + return res.status(401).json({ + error: "Invalid signature", + message: result.error, + }); + } + + const userId = ename; + const token = signToken({ userId }, JWT_SECRET); + await registerSession(userId, ename, token); + + let name: string | undefined; + try { + const profile = await this.evaultService.getProfile(ename); + name = profile.name ?? ename; + } catch { + name = ename; + } + + const data = { + user: { id: userId, ename, name }, + token, + }; + + this.eventEmitter.emit(session, data); + res.status(200).json(data); + } catch (error) { + console.error("Error during login:", error); + res.status(500).json({ error: "Internal server error" }); + } + }; +} diff --git a/platforms/profile-editor/api/src/controllers/DiscoveryController.ts b/platforms/profile-editor/api/src/controllers/DiscoveryController.ts new file mode 100644 index 000000000..2ef4baf4b --- /dev/null +++ b/platforms/profile-editor/api/src/controllers/DiscoveryController.ts @@ -0,0 +1,40 @@ +import { Request, Response } from "express"; +import { UserSearchService } from "../services/UserSearchService"; + +export class DiscoveryController { + private userSearchService: UserSearchService; + + constructor() { + this.userSearchService = new UserSearchService(); + } + + discover = async (req: Request, res: Response) => { + try { + const { q, page, limit, sortBy } = req.query; + + if (!q) { + return res + .status(400) + .json({ error: 'Query parameter "q" is required' }); + } + + const pageNum = Math.max(1, parseInt(page as string) || 1); + const limitNum = Math.min( + 100, + Math.max(1, parseInt(limit as string) || 10), + ); + + const results = await this.userSearchService.searchUsers( + q as string, + pageNum, + limitNum, + (sortBy as string) || "relevance", + ); + + res.json(results); + } catch (error: any) { + console.error("Discovery error:", error.message); + res.status(500).json({ error: "Search service unavailable" }); + } + }; +} diff --git a/platforms/profile-editor/api/src/controllers/FileProxyController.ts b/platforms/profile-editor/api/src/controllers/FileProxyController.ts new file mode 100644 index 000000000..f8a8510bb --- /dev/null +++ b/platforms/profile-editor/api/src/controllers/FileProxyController.ts @@ -0,0 +1,69 @@ +import { Request, Response, RequestHandler } from "express"; +import multer from "multer"; +import FormData from "form-data"; +import axios from "axios"; +import jwt from "jsonwebtoken"; + +const upload = multer({ + limits: { fileSize: 100 * 1024 * 1024 }, + storage: multer.memoryStorage(), +}); + +function mintFmToken(userId: string): string { + const secret = process.env.FILE_MANAGER_JWT_SECRET; + if (!secret) throw new Error("FILE_MANAGER_JWT_SECRET not configured"); + return jwt.sign({ userId }, secret, { expiresIn: "1h" }); +} + +function getFmBaseUrl(): string { + const url = process.env.PUBLIC_FILE_MANAGER_BASE_URL; + if (!url) throw new Error("PUBLIC_FILE_MANAGER_BASE_URL not configured"); + return url; +} + +async function handleUpload(req: Request, res: Response): Promise { + try { + if (!req.file) { + res.status(400).json({ error: "No file provided" }); + return; + } + + const userId = req.user?.ename; + if (!userId) { + res.status(401).json({ error: "Authentication required" }); + return; + } + + const token = mintFmToken(userId); + const form = new FormData(); + form.append("file", req.file.buffer, { + filename: req.file.originalname, + contentType: req.file.mimetype, + }); + + const response = await axios.post( + `${getFmBaseUrl()}/api/files`, + form, + { + headers: { + ...form.getHeaders(), + Authorization: `Bearer ${token}`, + }, + maxContentLength: Infinity, + maxBodyLength: Infinity, + }, + ); + + res.json(response.data); + } catch (error: any) { + console.error("File proxy error:", error?.response?.data ?? error.message); + const status = error?.response?.status ?? 500; + const message = error?.response?.data?.error ?? "Failed to upload file"; + res.status(status).json({ error: message }); + } +} + +export const fileProxyUpload: RequestHandler[] = [ + upload.single("file") as RequestHandler, + handleUpload as RequestHandler, +]; diff --git a/platforms/profile-editor/api/src/controllers/ProfileController.ts b/platforms/profile-editor/api/src/controllers/ProfileController.ts new file mode 100644 index 000000000..f18ab0a11 --- /dev/null +++ b/platforms/profile-editor/api/src/controllers/ProfileController.ts @@ -0,0 +1,269 @@ +import { Request, Response } from "express"; +import { EVaultProfileService } from "../services/EVaultProfileService"; +import type { + ProfileUpdatePayload, + WorkExperience, + Education, + SocialLink, +} from "../types/profile"; + +export class ProfileController { + private evaultService: EVaultProfileService; + + constructor(evaultService: EVaultProfileService) { + this.evaultService = evaultService; + } + + getProfile = async (req: Request, res: Response) => { + try { + const ename = req.user?.ename; + if (!ename) { + return res.status(401).json({ error: "Authentication required" }); + } + + const profile = await this.evaultService.getProfile(ename); + res.json(profile); + } catch (error: any) { + console.error("Error fetching profile:", error.message); + res.status(500).json({ error: "Failed to fetch profile" }); + } + }; + + updateProfile = async (req: Request, res: Response) => { + try { + const ename = req.user?.ename; + if (!ename) { + return res.status(401).json({ error: "Authentication required" }); + } + + const payload: ProfileUpdatePayload = req.body; + const profile = await this.evaultService.upsertProfile( + ename, + payload, + ); + res.json(profile); + } catch (error: any) { + console.error("Error updating profile:", error.message); + res.status(500).json({ error: "Failed to update profile" }); + } + }; + + updateWorkExperience = async (req: Request, res: Response) => { + try { + const ename = req.user?.ename; + if (!ename) { + return res.status(401).json({ error: "Authentication required" }); + } + + const workExperience: WorkExperience[] = req.body; + if (!Array.isArray(workExperience)) { + return res + .status(400) + .json({ error: "Body must be an array of work experience entries" }); + } + + const profile = await this.evaultService.upsertProfile(ename, { + workExperience, + }); + res.json(profile); + } catch (error: any) { + console.error("Error updating work experience:", error.message); + res.status(500).json({ error: "Failed to update work experience" }); + } + }; + + updateEducation = async (req: Request, res: Response) => { + try { + const ename = req.user?.ename; + if (!ename) { + return res.status(401).json({ error: "Authentication required" }); + } + + const education: Education[] = req.body; + if (!Array.isArray(education)) { + return res + .status(400) + .json({ error: "Body must be an array of education entries" }); + } + + const profile = await this.evaultService.upsertProfile(ename, { + education, + }); + res.json(profile); + } catch (error: any) { + console.error("Error updating education:", error.message); + res.status(500).json({ error: "Failed to update education" }); + } + }; + + updateSkills = async (req: Request, res: Response) => { + try { + const ename = req.user?.ename; + if (!ename) { + return res.status(401).json({ error: "Authentication required" }); + } + + const skills: string[] = req.body; + if (!Array.isArray(skills)) { + return res + .status(400) + .json({ error: "Body must be an array of skill strings" }); + } + + const profile = await this.evaultService.upsertProfile(ename, { + skills, + }); + res.json(profile); + } catch (error: any) { + console.error("Error updating skills:", error.message); + res.status(500).json({ error: "Failed to update skills" }); + } + }; + + updateSocialLinks = async (req: Request, res: Response) => { + try { + const ename = req.user?.ename; + if (!ename) { + return res.status(401).json({ error: "Authentication required" }); + } + + const socialLinks: SocialLink[] = req.body; + if (!Array.isArray(socialLinks)) { + return res + .status(400) + .json({ error: "Body must be an array of social link entries" }); + } + + const profile = await this.evaultService.upsertProfile(ename, { + socialLinks, + }); + res.json(profile); + } catch (error: any) { + console.error("Error updating social links:", error.message); + res.status(500).json({ error: "Failed to update social links" }); + } + }; + + getPublicProfile = async (req: Request, res: Response) => { + try { + const { ename } = req.params; + if (!ename) { + return res.status(400).json({ error: "ename is required" }); + } + + const profile = await this.evaultService.getPublicProfile(ename); + if (!profile) { + return res.status(403).json({ error: "This profile is private" }); + } + + res.json(profile); + } catch (error: any) { + console.error("Error fetching public profile:", error.message); + res.status(500).json({ error: "Failed to fetch profile" }); + } + }; + + private canAccessProfile( + profile: { professional: { isPublic?: boolean }; ename: string }, + req: Request, + ): boolean { + if (profile.professional.isPublic) return true; + return req.user?.ename === profile.ename; + } + + getProfileAvatar = async (req: Request, res: Response) => { + try { + const { ename } = req.params; + const profile = await this.evaultService.getProfile(ename); + + if (!this.canAccessProfile(profile, req)) { + return res.status(403).json({ error: "This profile is private" }); + } + + const fileId = profile.professional.avatarFileId; + if (!fileId) { + return res.status(404).json({ error: "No avatar set" }); + } + + const { proxyFileFromFileManager } = await import( + "../utils/file-proxy" + ); + await proxyFileFromFileManager(fileId, ename, res); + } catch (error: any) { + console.error("Error proxying avatar:", error.message); + res.status(500).json({ error: "Failed to fetch avatar" }); + } + }; + + getProfileBanner = async (req: Request, res: Response) => { + try { + const { ename } = req.params; + const profile = await this.evaultService.getProfile(ename); + + if (!this.canAccessProfile(profile, req)) { + return res.status(403).json({ error: "This profile is private" }); + } + + const fileId = profile.professional.bannerFileId; + if (!fileId) { + return res.status(404).json({ error: "No banner set" }); + } + + const { proxyFileFromFileManager } = await import( + "../utils/file-proxy" + ); + await proxyFileFromFileManager(fileId, ename, res); + } catch (error: any) { + console.error("Error proxying banner:", error.message); + res.status(500).json({ error: "Failed to fetch banner" }); + } + }; + + getProfileCv = async (req: Request, res: Response) => { + try { + const { ename } = req.params; + const profile = await this.evaultService.getProfile(ename); + + if (!this.canAccessProfile(profile, req)) { + return res.status(403).json({ error: "This profile is private" }); + } + + const fileId = profile.professional.cvFileId; + if (!fileId) { + return res.status(404).json({ error: "No CV uploaded" }); + } + + const { proxyFileFromFileManager } = await import( + "../utils/file-proxy" + ); + await proxyFileFromFileManager(fileId, ename, res, "download"); + } catch (error: any) { + console.error("Error proxying CV:", error.message); + res.status(500).json({ error: "Failed to fetch CV" }); + } + }; + + getProfileVideo = async (req: Request, res: Response) => { + try { + const { ename } = req.params; + const profile = await this.evaultService.getProfile(ename); + + if (!this.canAccessProfile(profile, req)) { + return res.status(403).json({ error: "This profile is private" }); + } + + const fileId = profile.professional.videoIntroFileId; + if (!fileId) { + return res.status(404).json({ error: "No video uploaded" }); + } + + const { proxyFileFromFileManager } = await import( + "../utils/file-proxy" + ); + await proxyFileFromFileManager(fileId, ename, res, "preview"); + } catch (error: any) { + console.error("Error proxying video:", error.message); + res.status(500).json({ error: "Failed to fetch video" }); + } + }; +} diff --git a/platforms/profile-editor/api/src/controllers/WebhookController.ts b/platforms/profile-editor/api/src/controllers/WebhookController.ts new file mode 100644 index 000000000..a76be80b2 --- /dev/null +++ b/platforms/profile-editor/api/src/controllers/WebhookController.ts @@ -0,0 +1,129 @@ +import { Request, Response } from "express"; +import { Web3Adapter } from "web3-adapter"; +import { UserSearchService } from "../services/UserSearchService"; + +export class WebhookController { + private userSearchService: UserSearchService; + private adapter: Web3Adapter; + + constructor(adapter: Web3Adapter) { + this.userSearchService = new UserSearchService(); + this.adapter = adapter; + } + + handleWebhook = async (req: Request, res: Response) => { + try { + const schemaId = req.body.schemaId; + const globalId = req.body.id; + const mapping = Object.values(this.adapter.mapping).find( + (m) => m.schemaId === schemaId, + ); + + this.adapter.addToLockedIds(globalId); + + if (!mapping) { + return res.status(200).send("Unknown schema, skipping"); + } + + const local = await this.adapter.fromGlobal({ + data: req.body.data, + mapping, + }); + + const localId = await this.adapter.mappingDb.getLocalId(globalId); + + if (mapping.tableName === "users") { + await this.handleUserWebhook( + local.data, + localId, + globalId, + req.body, + ); + } else if (mapping.tableName === "professional_profiles") { + await this.handleProfessionalProfileWebhook( + local.data, + localId, + globalId, + req.body, + ); + } + + res.status(200).send(); + } catch (e) { + console.error("Webhook error:", e); + res.status(200).send(); + } + }; + + private async handleUserWebhook( + localData: any, + localId: string | null, + globalId: string, + rawBody: any, + ) { + const ename = localData.ename || rawBody.w3id; + if (!ename) return; + + const userData: any = { + ename, + handle: localData.handle, + name: rawBody.data?.displayName || localData.name, + bio: localData.bio, + isVerified: localData.isVerified ?? false, + isPublic: localData.isPublic !== false, + isArchived: localData.isArchived ?? false, + }; + + if (localData.avatarFileId) userData.avatarFileId = localData.avatarFileId; + if (localData.bannerFileId) userData.bannerFileId = localData.bannerFileId; + if (localData.location) userData.location = localData.location; + + const user = await this.userSearchService.upsertFromWebhook(userData); + + await this.adapter.mappingDb.storeMapping({ + localId: user.id, + globalId, + }); + this.adapter.addToLockedIds(user.id); + this.adapter.addToLockedIds(globalId); + } + + private async handleProfessionalProfileWebhook( + localData: any, + localId: string | null, + globalId: string, + rawBody: any, + ) { + const ename = rawBody.w3id; + if (!ename) return; + + const profileData: any = { ename }; + + if (localData.name || rawBody.data?.displayName) { + profileData.name = rawBody.data?.displayName || localData.name; + } + if (localData.headline) profileData.headline = localData.headline; + if (localData.bio) profileData.bio = localData.bio; + if (localData.avatarFileId) + profileData.avatarFileId = localData.avatarFileId; + if (localData.bannerFileId) + profileData.bannerFileId = localData.bannerFileId; + if (localData.cvFileId) profileData.cvFileId = localData.cvFileId; + if (localData.videoIntroFileId) + profileData.videoIntroFileId = localData.videoIntroFileId; + if (localData.location) profileData.location = localData.location; + if (localData.skills) profileData.skills = localData.skills; + if (localData.isPublic !== undefined) + profileData.isPublic = localData.isPublic; + + const user = + await this.userSearchService.upsertFromWebhook(profileData); + + await this.adapter.mappingDb.storeMapping({ + localId: user.id, + globalId, + }); + this.adapter.addToLockedIds(user.id); + this.adapter.addToLockedIds(globalId); + } +} diff --git a/platforms/profile-editor/api/src/database/data-source.ts b/platforms/profile-editor/api/src/database/data-source.ts new file mode 100644 index 000000000..5303ddc0f --- /dev/null +++ b/platforms/profile-editor/api/src/database/data-source.ts @@ -0,0 +1,32 @@ +import "reflect-metadata"; +import { DataSource } from "typeorm"; +import { config } from "dotenv"; +import path from "path"; +import { User } from "./entities/User"; +import { Session } from "./entities/Session"; +import { PostgresSubscriber } from "../web3adapter/watchers/subscriber"; + +config({ path: path.resolve(__dirname, "../../../../.env") }); + +export const AppDataSource = new DataSource({ + type: "postgres", + url: process.env.PROFILE_EDITOR_DATABASE_URL, + synchronize: false, + logging: process.env.NODE_ENV === "development", + entities: [User, Session], + migrations: [path.join(__dirname, "migrations", "*.ts")], + subscribers: [PostgresSubscriber], + ssl: process.env.DB_CA_CERT + ? { + rejectUnauthorized: false, + ca: process.env.DB_CA_CERT, + } + : false, + extra: { + max: 10, + min: 2, + idleTimeoutMillis: 30000, + connectionTimeoutMillis: 5000, + statement_timeout: 10000, + }, +}); diff --git a/platforms/profile-editor/api/src/database/entities/Session.ts b/platforms/profile-editor/api/src/database/entities/Session.ts new file mode 100644 index 000000000..575d0df1a --- /dev/null +++ b/platforms/profile-editor/api/src/database/entities/Session.ts @@ -0,0 +1,29 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + Index, +} from "typeorm"; + +@Entity("sessions") +export class Session { + @PrimaryGeneratedColumn("uuid") + id!: string; + + @Index() + @Column() + userId!: string; + + @Column() + ename!: string; + + @Column({ unique: true }) + token!: string; + + @CreateDateColumn() + createdAt!: Date; + + @Column({ type: "timestamp" }) + expiresAt!: Date; +} diff --git a/platforms/profile-editor/api/src/database/entities/User.ts b/platforms/profile-editor/api/src/database/entities/User.ts new file mode 100644 index 000000000..33bb3fea8 --- /dev/null +++ b/platforms/profile-editor/api/src/database/entities/User.ts @@ -0,0 +1,55 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, +} from "typeorm"; + +@Entity("users") +export class User { + @PrimaryGeneratedColumn("uuid") + id!: string; + + @Column({ nullable: true, unique: true }) + ename!: string; + + @Column({ nullable: true }) + name!: string; + + @Column({ nullable: true }) + handle!: string; + + @Column({ nullable: true, type: "text" }) + bio!: string; + + @Column({ nullable: true }) + avatarFileId!: string; + + @Column({ nullable: true }) + bannerFileId!: string; + + @Column({ nullable: true }) + headline!: string; + + @Column({ nullable: true }) + location!: string; + + @Column("text", { array: true, nullable: true }) + skills!: string[]; + + @Column({ default: false }) + isVerified!: boolean; + + @Column({ default: true }) + isPublic!: boolean; + + @Column({ default: false }) + isArchived!: boolean; + + @CreateDateColumn() + createdAt!: Date; + + @UpdateDateColumn() + updatedAt!: Date; +} diff --git a/platforms/profile-editor/api/src/database/migrations/1773143278029-InitialSchema.ts b/platforms/profile-editor/api/src/database/migrations/1773143278029-InitialSchema.ts new file mode 100644 index 000000000..5ebef285d --- /dev/null +++ b/platforms/profile-editor/api/src/database/migrations/1773143278029-InitialSchema.ts @@ -0,0 +1,18 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class InitialSchema1773143278029 implements MigrationInterface { + name = 'InitialSchema1773143278029' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TABLE "users" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "ename" character varying, "name" character varying, "handle" character varying, "bio" text, "avatarFileId" character varying, "bannerFileId" character varying, "headline" character varying, "location" character varying, "skills" text array, "isVerified" boolean NOT NULL DEFAULT false, "isPublic" boolean NOT NULL DEFAULT true, "isArchived" boolean NOT NULL DEFAULT false, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "UQ_0759b745d50a467c0319c4cb284" UNIQUE ("ename"), CONSTRAINT "PK_a3ffb1c0c8416b9fc6f907b7433" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE TABLE "sessions" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "userId" character varying NOT NULL, "ename" character varying NOT NULL, "token" character varying NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "expiresAt" TIMESTAMP NOT NULL, CONSTRAINT "UQ_e9f62f5dcb8a54b84234c9e7a06" UNIQUE ("token"), CONSTRAINT "PK_3238ef96f18b355b671619111bc" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE INDEX "IDX_57de40bc620f456c7311aa3a1e" ON "sessions" ("userId") `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "public"."IDX_57de40bc620f456c7311aa3a1e"`); + await queryRunner.query(`DROP TABLE "sessions"`); + await queryRunner.query(`DROP TABLE "users"`); + } + +} diff --git a/platforms/profile-editor/api/src/index.ts b/platforms/profile-editor/api/src/index.ts new file mode 100644 index 000000000..b2e516512 --- /dev/null +++ b/platforms/profile-editor/api/src/index.ts @@ -0,0 +1,103 @@ +import "reflect-metadata"; +import express from "express"; +import cors from "cors"; +import { config } from "dotenv"; +import path from "path"; + +config({ path: path.resolve(__dirname, "../../../../.env") }); + +import { AppDataSource } from "./database/data-source"; +import { AuthController } from "./controllers/AuthController"; +import { ProfileController } from "./controllers/ProfileController"; +import { DiscoveryController } from "./controllers/DiscoveryController"; +import { WebhookController } from "./controllers/WebhookController"; +import { EVaultProfileService } from "./services/EVaultProfileService"; +import { RegistryService } from "./services/RegistryService"; +import { authMiddleware, authGuard } from "./middleware/auth"; +import { fileProxyUpload } from "./controllers/FileProxyController"; +import { EVaultSyncService } from "./services/EVaultSyncService"; +import { adapter } from "./web3adapter/watchers/subscriber"; + +const app = express(); +const PORT = process.env.PROFILE_EDITOR_API_PORT || 3006; + +app.use(cors()); +app.use(express.json()); + +const registryService = new RegistryService(); +const evaultService = new EVaultProfileService(registryService); + +const authController = new AuthController(evaultService); +const profileController = new ProfileController(evaultService); +const discoveryController = new DiscoveryController(); +const webhookController = new WebhookController(adapter); + +// Webhook route (no auth, receives eVault events) +app.post("/api/webhook", webhookController.handleWebhook); + +// Public auth routes +app.get("/api/auth/offer", authController.getOffer); +app.post("/api/auth", authController.login); +app.get("/api/auth/sessions/:id", authController.sseStream); + +// Public discovery routes +app.get("/api/discover", discoveryController.discover); + +// Public profile view + file proxy routes (authMiddleware is optional here — populates req.user if logged in) +app.get("/api/profiles/:ename", profileController.getPublicProfile); +app.get( + "/api/profiles/:ename/avatar", + authMiddleware, + profileController.getProfileAvatar, +); +app.get( + "/api/profiles/:ename/banner", + authMiddleware, + profileController.getProfileBanner, +); +app.get( + "/api/profiles/:ename/cv", + authMiddleware, + profileController.getProfileCv, +); +app.get( + "/api/profiles/:ename/video", + authMiddleware, + profileController.getProfileVideo, +); + +// Protected routes +app.use(authMiddleware); + +app.post("/api/files", authGuard, ...fileProxyUpload); + +app.get("/api/profile", authGuard, profileController.getProfile); +app.patch("/api/profile", authGuard, profileController.updateProfile); +app.put( + "/api/profile/work-experience", + authGuard, + profileController.updateWorkExperience, +); +app.put("/api/profile/education", authGuard, profileController.updateEducation); +app.put("/api/profile/skills", authGuard, profileController.updateSkills); +app.put( + "/api/profile/social-links", + authGuard, + profileController.updateSocialLinks, +); + +AppDataSource.initialize() + .then(() => { + console.log("Database connection established"); + + const syncService = new EVaultSyncService(evaultService); + syncService.start(5 * 60 * 1000); + + app.listen(PORT, () => { + console.log(`Profile Editor API running on port ${PORT}`); + }); + }) + .catch((error: any) => { + console.error("Database connection failed:", error); + process.exit(1); + }); diff --git a/platforms/profile-editor/api/src/middleware/auth.ts b/platforms/profile-editor/api/src/middleware/auth.ts new file mode 100644 index 000000000..7220c9aab --- /dev/null +++ b/platforms/profile-editor/api/src/middleware/auth.ts @@ -0,0 +1,49 @@ +import { + createAuthMiddleware, + createAuthGuard, +} from "@metastate-foundation/auth"; +import { AppDataSource } from "../database/data-source"; +import { Session } from "../database/entities/Session"; +import { MoreThan } from "typeorm"; + +const JWT_SECRET = process.env.PROFILE_EDITOR_JWT_SECRET; +if (!JWT_SECRET) throw new Error("PROFILE_EDITOR_JWT_SECRET not configured"); + +export async function registerSession( + userId: string, + ename: string, + token: string, +): Promise { + const repo = AppDataSource.getRepository(Session); + + await repo.delete({ userId }); + + const session = repo.create({ + userId, + ename, + token, + expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), + }); + + await repo.save(session); +} + +export const authMiddleware = createAuthMiddleware({ + secret: JWT_SECRET, + findUser: async (userId: string) => { + if (!AppDataSource.isInitialized) { + return null; + } + + const repo = AppDataSource.getRepository(Session); + const session = await repo.findOneBy({ + userId, + expiresAt: MoreThan(new Date()), + }); + + if (!session) return null; + return { id: session.userId, ename: session.ename }; + }, +}); + +export const authGuard = createAuthGuard(); diff --git a/platforms/profile-editor/api/src/services/EVaultProfileService.ts b/platforms/profile-editor/api/src/services/EVaultProfileService.ts new file mode 100644 index 000000000..379a2fe0e --- /dev/null +++ b/platforms/profile-editor/api/src/services/EVaultProfileService.ts @@ -0,0 +1,246 @@ +import { GraphQLClient } from "graphql-request"; +import { RegistryService } from "./RegistryService"; +import type { + ProfessionalProfile, + FullProfile, + UserOntologyData, +} from "../types/profile"; + +const PROFESSIONAL_PROFILE_ONTOLOGY = "ProfessionalProfile"; +const USER_ONTOLOGY = "User"; + +const META_ENVELOPES_QUERY = ` + query MetaEnvelopes($filter: MetaEnvelopeFilterInput, $first: Int, $after: String) { + metaEnvelopes(filter: $filter, first: $first, after: $after) { + edges { + cursor + node { + id + ontology + parsed + } + } + pageInfo { + hasNextPage + endCursor + } + totalCount + } + } +`; + +const META_ENVELOPE_QUERY = ` + query MetaEnvelope($id: ID!) { + metaEnvelope(id: $id) { + id + ontology + parsed + } + } +`; + +const CREATE_MUTATION = ` + mutation CreateMetaEnvelope($input: MetaEnvelopeInput!) { + createMetaEnvelope(input: $input) { + metaEnvelope { + id + ontology + parsed + } + errors { field message code } + } + } +`; + +const UPDATE_MUTATION = ` + mutation UpdateMetaEnvelope($id: ID!, $input: MetaEnvelopeInput!) { + updateMetaEnvelope(id: $id, input: $input) { + metaEnvelope { + id + ontology + parsed + } + errors { message code } + } + } +`; + +type MetaEnvelopeNode = { + id: string; + ontology: string; + parsed: Record; +}; + +type MetaEnvelopesResult = { + metaEnvelopes: { + edges: Array<{ cursor: string; node: MetaEnvelopeNode }>; + pageInfo: { hasNextPage: boolean; endCursor: string }; + totalCount: number; + }; +}; + +type MetaEnvelopeResult = { + metaEnvelope: MetaEnvelopeNode | null; +}; + +type CreateResult = { + createMetaEnvelope: { + metaEnvelope: MetaEnvelopeNode | null; + errors: Array<{ field?: string; message: string; code?: string }>; + }; +}; + +type UpdateResult = { + updateMetaEnvelope: { + metaEnvelope: MetaEnvelopeNode | null; + errors: Array<{ message: string; code?: string }>; + }; +}; + +export class EVaultProfileService { + private registryService: RegistryService; + + constructor(registryService: RegistryService) { + this.registryService = registryService; + } + + private async getClient(eName: string): Promise { + const endpoint = await this.registryService.getEvaultGraphqlUrl(eName); + const token = await this.registryService.ensurePlatformToken(); + return new GraphQLClient(endpoint, { + headers: { + Authorization: `Bearer ${token}`, + "X-ENAME": eName, + }, + }); + } + + private async findMetaEnvelopeByOntology( + client: GraphQLClient, + ontologyId: string, + ): Promise { + const result = await client.request( + META_ENVELOPES_QUERY, + { + filter: { ontologyId }, + first: 1, + }, + ); + const edge = result.metaEnvelopes.edges[0]; + return edge?.node ?? null; + } + + async getProfile(eName: string): Promise { + const client = await this.getClient(eName); + + const [professionalNode, userNode] = await Promise.all([ + this.findMetaEnvelopeByOntology(client, PROFESSIONAL_PROFILE_ONTOLOGY), + this.findMetaEnvelopeByOntology(client, USER_ONTOLOGY), + ]); + + const userData = (userNode?.parsed ?? {}) as UserOntologyData; + const profData = (professionalNode?.parsed ?? {}) as ProfessionalProfile; + + const name = + profData.displayName ?? userData.displayName ?? eName; + + return { + ename: eName, + name, + handle: userData.username, + isVerified: userData.isVerified, + professional: { + displayName: profData.displayName, + headline: profData.headline, + bio: profData.bio, + avatarFileId: profData.avatarFileId, + bannerFileId: profData.bannerFileId, + cvFileId: profData.cvFileId, + videoIntroFileId: profData.videoIntroFileId, + email: profData.email, + phone: profData.phone, + website: profData.website, + location: profData.location, + isPublic: profData.isPublic ?? true, + workExperience: profData.workExperience ?? [], + education: profData.education ?? [], + skills: profData.skills ?? [], + socialLinks: profData.socialLinks ?? [], + }, + }; + } + + async getPublicProfile(eName: string): Promise { + const profile = await this.getProfile(eName); + if (!profile.professional.isPublic) { + return null; + } + return profile; + } + + async upsertProfile( + eName: string, + data: Partial, + ): Promise { + const client = await this.getClient(eName); + + const existing = await this.findMetaEnvelopeByOntology( + client, + PROFESSIONAL_PROFILE_ONTOLOGY, + ); + + const merged: ProfessionalProfile = { + ...(existing?.parsed as ProfessionalProfile | undefined), + ...data, + }; + + if (existing) { + const result = await client.request(UPDATE_MUTATION, { + id: existing.id, + input: { + ontology: PROFESSIONAL_PROFILE_ONTOLOGY, + payload: merged, + acl: ["*"], + }, + }); + + if (result.updateMetaEnvelope.errors?.length) { + throw new Error( + result.updateMetaEnvelope.errors + .map((e) => e.message) + .join("; "), + ); + } + } else { + const result = await client.request(CREATE_MUTATION, { + input: { + ontology: PROFESSIONAL_PROFILE_ONTOLOGY, + payload: merged, + acl: ["*"], + }, + }); + + if (result.createMetaEnvelope.errors?.length) { + throw new Error( + result.createMetaEnvelope.errors + .map((e) => e.message) + .join("; "), + ); + } + } + + return this.getProfile(eName); + } + + async getProfileByEnvelope( + eName: string, + id: string, + ): Promise { + const client = await this.getClient(eName); + const result = await client.request( + META_ENVELOPE_QUERY, + { id }, + ); + return result.metaEnvelope; + } +} diff --git a/platforms/profile-editor/api/src/services/EVaultSyncService.ts b/platforms/profile-editor/api/src/services/EVaultSyncService.ts new file mode 100644 index 000000000..bf83fdc9a --- /dev/null +++ b/platforms/profile-editor/api/src/services/EVaultSyncService.ts @@ -0,0 +1,106 @@ +import axios from "axios"; +import { EVaultProfileService } from "./EVaultProfileService"; +import { UserSearchService } from "./UserSearchService"; + +interface VaultEntry { + ename: string; + uri: string; + evault: string; +} + +export class EVaultSyncService { + private evaultService: EVaultProfileService; + private userSearchService: UserSearchService; + private intervalId: ReturnType | null = null; + private syncing = false; + + private get registryUrl(): string { + return process.env.PUBLIC_REGISTRY_URL || "http://localhost:4321"; + } + + constructor(evaultService: EVaultProfileService) { + this.evaultService = evaultService; + this.userSearchService = new UserSearchService(); + } + + start(intervalMs: number = 5 * 60 * 1000): void { + this.syncAll(); + + this.intervalId = setInterval(() => { + this.syncAll(); + }, intervalMs); + + console.log( + `eVault sync started (every ${Math.round(intervalMs / 1000)}s)`, + ); + } + + stop(): void { + if (this.intervalId) { + clearInterval(this.intervalId); + this.intervalId = null; + console.log("eVault sync stopped"); + } + } + + private async syncAll(): Promise { + if (this.syncing) { + console.log("Sync already in progress, skipping"); + return; + } + + this.syncing = true; + const startTime = Date.now(); + + try { + const response = await axios.get( + `${this.registryUrl}/list`, + { timeout: 10000 }, + ); + const vaults = response.data; + + let synced = 0; + let failed = 0; + + for (const vault of vaults) { + try { + await this.syncUser(vault.ename); + synced++; + } catch { + failed++; + } + } + + const elapsed = ((Date.now() - startTime) / 1000).toFixed(1); + console.log( + `eVault sync complete: ${synced} synced, ${failed} failed, ${elapsed}s`, + ); + } catch (error: any) { + console.error( + "eVault sync failed to fetch registry list:", + error.message, + ); + } finally { + this.syncing = false; + } + } + + private async syncUser(ename: string): Promise { + const profile = await this.evaultService.getProfile(ename); + + await this.userSearchService.upsertFromWebhook({ + ename, + name: profile.name, + handle: profile.handle, + isVerified: profile.isVerified ?? false, + bio: profile.professional.bio, + headline: profile.professional.headline, + location: profile.professional.location, + avatarFileId: profile.professional.avatarFileId, + bannerFileId: profile.professional.bannerFileId, + skills: profile.professional.skills, + isPublic: profile.professional.isPublic ?? true, + isArchived: false, + }); + } +} diff --git a/platforms/profile-editor/api/src/services/RegistryService.ts b/platforms/profile-editor/api/src/services/RegistryService.ts new file mode 100644 index 000000000..2da00ca2b --- /dev/null +++ b/platforms/profile-editor/api/src/services/RegistryService.ts @@ -0,0 +1,60 @@ +import axios from "axios"; + +interface PlatformTokenResponse { + token: string; + expiresAt?: number; +} + +interface ResolveResponse { + uri: string; + evault: string; +} + +export class RegistryService { + private platformToken: string | null = null; + private tokenExpiresAt: number = 0; + + private get registryUrl(): string { + const url = process.env.PUBLIC_REGISTRY_URL; + if (!url) throw new Error("PUBLIC_REGISTRY_URL not configured"); + return url; + } + + private get platformBaseUrl(): string { + const url = process.env.PUBLIC_PROFILE_EDITOR_BASE_URL; + if (!url) + throw new Error("PUBLIC_PROFILE_EDITOR_BASE_URL not configured"); + return url; + } + + async ensurePlatformToken(): Promise { + const now = Date.now(); + if (this.platformToken && this.tokenExpiresAt > now + 5 * 60 * 1000) { + return this.platformToken; + } + + const response = await axios.post( + new URL("/platforms/certification", this.registryUrl).toString(), + { platform: this.platformBaseUrl }, + { headers: { "Content-Type": "application/json" } }, + ); + + this.platformToken = response.data.token; + this.tokenExpiresAt = response.data.expiresAt || now + 3600000; + return this.platformToken; + } + + async resolveEName(eName: string): Promise<{ uri: string; evault: string }> { + const response = await axios.get( + `${this.registryUrl}/resolve`, + { params: { w3id: eName } }, + ); + return { uri: response.data.uri, evault: response.data.evault }; + } + + async getEvaultGraphqlUrl(eName: string): Promise { + const { uri } = await this.resolveEName(eName); + const base = uri.replace(/\/$/, ""); + return `${base}/graphql`; + } +} diff --git a/platforms/profile-editor/api/src/services/UserSearchService.ts b/platforms/profile-editor/api/src/services/UserSearchService.ts new file mode 100644 index 000000000..5925ce9ef --- /dev/null +++ b/platforms/profile-editor/api/src/services/UserSearchService.ts @@ -0,0 +1,133 @@ +import { Repository } from "typeorm"; +import { AppDataSource } from "../database/data-source"; +import { User } from "../database/entities/User"; + +export class UserSearchService { + private userRepository: Repository; + + constructor() { + this.userRepository = AppDataSource.getRepository(User); + } + + async searchUsers( + query: string, + page: number = 1, + limit: number = 10, + sortBy: string = "relevance", + ) { + const searchQuery = query.trim(); + + if (searchQuery.length < 1) { + return { results: [], total: 0, page, limit }; + } + + if (page < 1 || limit < 1 || limit > 100) { + return { results: [], total: 0, page, limit }; + } + + const queryBuilder = this.userRepository + .createQueryBuilder("user") + .select([ + "user.id", + "user.ename", + "user.name", + "user.handle", + "user.bio", + "user.avatarFileId", + "user.headline", + "user.location", + "user.skills", + "user.isVerified", + ]) + .addSelect( + ` + CASE + WHEN user.ename ILIKE :exactQuery THEN 100 + WHEN user.name ILIKE :exactQuery THEN 90 + WHEN user.handle ILIKE :exactQuery THEN 80 + WHEN user.ename ILIKE :query THEN 70 + WHEN user.name ILIKE :query THEN 60 + WHEN user.handle ILIKE :query THEN 50 + WHEN user.headline ILIKE :query THEN 45 + WHEN user.bio ILIKE :query THEN 30 + WHEN user.location ILIKE :query THEN 25 + WHEN user.ename ILIKE :fuzzyQuery THEN 40 + WHEN user.name ILIKE :fuzzyQuery THEN 35 + WHEN user.handle ILIKE :fuzzyQuery THEN 30 + ELSE 0 + END`, + "relevance_score", + ) + .where( + `(user.name ILIKE :query OR user.ename ILIKE :query OR user.handle ILIKE :query + OR user.headline ILIKE :query OR user.bio ILIKE :query OR user.location ILIKE :query + OR user.ename ILIKE :fuzzyQuery OR user.name ILIKE :fuzzyQuery OR user.handle ILIKE :fuzzyQuery)`, + { + query: `%${searchQuery}%`, + exactQuery: searchQuery, + fuzzyQuery: `%${searchQuery.split("").join("%")}%`, + }, + ) + .andWhere("user.isPublic = :isPublic", { isPublic: true }) + .andWhere("user.isArchived = :archived", { archived: false }); + + switch (sortBy) { + case "name": + queryBuilder.orderBy("user.name", "ASC"); + break; + case "newest": + queryBuilder.orderBy("user.createdAt", "DESC"); + break; + case "relevance": + default: + queryBuilder + .orderBy("relevance_score", "DESC") + .addOrderBy("user.isVerified", "DESC") + .addOrderBy("user.name", "ASC"); + break; + } + + const offset = (page - 1) * limit; + queryBuilder.skip(offset).take(limit); + + const [results, total] = await queryBuilder.getManyAndCount(); + + return { + results: results.map((user) => ({ + id: user.id, + ename: user.ename, + name: user.name, + handle: user.handle, + bio: user.bio, + avatarFileId: user.avatarFileId, + headline: user.headline, + location: user.location, + skills: user.skills, + isVerified: user.isVerified, + })), + total, + page, + limit, + }; + } + + async findByEname(ename: string): Promise { + return this.userRepository.findOneBy({ ename }); + } + + async upsertFromWebhook(data: Partial & { ename: string }): Promise { + let user = await this.userRepository.findOneBy({ ename: data.ename }); + + if (user) { + for (const key of Object.keys(data)) { + if (data[key as keyof User] !== undefined) { + (user as any)[key] = data[key as keyof User]; + } + } + return this.userRepository.save(user); + } + + user = this.userRepository.create(data); + return this.userRepository.save(user); + } +} diff --git a/platforms/profile-editor/api/src/types/express.d.ts b/platforms/profile-editor/api/src/types/express.d.ts new file mode 100644 index 000000000..d593d2448 --- /dev/null +++ b/platforms/profile-editor/api/src/types/express.d.ts @@ -0,0 +1,13 @@ +declare global { + namespace Express { + interface Request { + user?: { + id: string; + ename: string; + [key: string]: unknown; + }; + } + } +} + +export {}; diff --git a/platforms/profile-editor/api/src/types/profile.ts b/platforms/profile-editor/api/src/types/profile.ts new file mode 100644 index 000000000..ee437aee5 --- /dev/null +++ b/platforms/profile-editor/api/src/types/profile.ts @@ -0,0 +1,83 @@ +export interface WorkExperience { + id?: string; + company: string; + role: string; + description?: string; + startDate: string; + endDate?: string; + location?: string; + sortOrder: number; +} + +export interface Education { + id?: string; + institution: string; + degree: string; + fieldOfStudy?: string; + startDate: string; + endDate?: string; + description?: string; + sortOrder: number; +} + +export interface SocialLink { + id?: string; + platform: string; + url: string; + label?: string; +} + +export interface ProfessionalProfile { + displayName?: string; + headline?: string; + bio?: string; + avatarFileId?: string; + bannerFileId?: string; + cvFileId?: string; + videoIntroFileId?: string; + email?: string; + phone?: string; + website?: string; + location?: string; + isPublic?: boolean; + workExperience?: WorkExperience[]; + education?: Education[]; + skills?: string[]; + socialLinks?: SocialLink[]; +} + +export interface UserOntologyData { + username?: string; + displayName?: string; + bio?: string; + avatarUrl?: string; + bannerUrl?: string; + ename?: string; + isVerified?: boolean; + isPrivate?: boolean; + location?: string; + website?: string; +} + +export interface FullProfile { + ename: string; + name?: string; + handle?: string; + isVerified?: boolean; + professional: ProfessionalProfile; +} + +export interface ProfileUpdatePayload { + displayName?: string; + headline?: string; + bio?: string; + avatarFileId?: string; + bannerFileId?: string; + cvFileId?: string; + videoIntroFileId?: string; + email?: string; + phone?: string; + website?: string; + location?: string; + isPublic?: boolean; +} diff --git a/platforms/profile-editor/api/src/utils/file-proxy.ts b/platforms/profile-editor/api/src/utils/file-proxy.ts new file mode 100644 index 000000000..3facf194c --- /dev/null +++ b/platforms/profile-editor/api/src/utils/file-proxy.ts @@ -0,0 +1,49 @@ +import axios from "axios"; +import jwt from "jsonwebtoken"; +import type { Response } from "express"; + +const FILE_MANAGER_BASE_URL = () => + process.env.PUBLIC_FILE_MANAGER_BASE_URL || "http://localhost:3005"; + +function mintFmToken(userId: string): string { + const secret = process.env.FILE_MANAGER_JWT_SECRET; + if (!secret) throw new Error("FILE_MANAGER_JWT_SECRET not configured"); + return jwt.sign({ userId }, secret, { expiresIn: "1h" }); +} + +export async function proxyFileFromFileManager( + fileId: string, + ename: string, + res: Response, + mode: "preview" | "download" = "preview", +): Promise { + try { + const token = mintFmToken(ename); + const endpoint = mode === "download" ? "download" : "preview"; + const url = `${FILE_MANAGER_BASE_URL()}/api/files/${fileId}/${endpoint}`; + const response = await axios.get(url, { + responseType: "arraybuffer", + timeout: 30000, + headers: { + Authorization: `Bearer ${token}`, + }, + }); + + const contentType = + response.headers["content-type"] || "application/octet-stream"; + const contentDisposition = response.headers["content-disposition"]; + res.set("Content-Type", contentType); + if (contentDisposition) { + res.set("Content-Disposition", contentDisposition); + } + res.set("Cache-Control", "public, max-age=3600"); + res.send(response.data); + } catch (error: any) { + if (error?.response?.status === 404) { + res.status(404).json({ error: "File not found" }); + } else { + console.error("File proxy error:", error?.response?.data?.toString?.() ?? error.message); + res.status(502).json({ error: "Failed to fetch file" }); + } + } +} diff --git a/platforms/profile-editor/api/src/web3adapter/mappings/professional-profile.mapping.json b/platforms/profile-editor/api/src/web3adapter/mappings/professional-profile.mapping.json new file mode 100644 index 000000000..721f0c357 --- /dev/null +++ b/platforms/profile-editor/api/src/web3adapter/mappings/professional-profile.mapping.json @@ -0,0 +1,18 @@ +{ + "tableName": "professional_profiles", + "schemaId": "ProfessionalProfile", + "ownerEnamePath": "ename", + "ownedJunctionTables": [], + "localToUniversalMap": { + "name": "displayName", + "headline": "headline", + "bio": "bio", + "avatarFileId": "avatarFileId", + "bannerFileId": "bannerFileId", + "cvFileId": "cvFileId", + "videoIntroFileId": "videoIntroFileId", + "location": "location", + "skills": "skills", + "isPublic": "isPublic" + } +} diff --git a/platforms/profile-editor/api/src/web3adapter/mappings/user.mapping.json b/platforms/profile-editor/api/src/web3adapter/mappings/user.mapping.json new file mode 100644 index 000000000..1188e4f6b --- /dev/null +++ b/platforms/profile-editor/api/src/web3adapter/mappings/user.mapping.json @@ -0,0 +1,20 @@ +{ + "tableName": "users", + "schemaId": "550e8400-e29b-41d4-a716-446655440000", + "ownerEnamePath": "ename", + "ownedJunctionTables": [], + "localToUniversalMap": { + "handle": "username", + "name": "displayName", + "bio": "bio", + "avatarFileId": "avatarUrl", + "bannerFileId": "bannerUrl", + "ename": "ename", + "isVerified": "isVerified", + "isPublic": "isPublic", + "location": "location", + "createdAt": "createdAt", + "updatedAt": "updatedAt", + "isArchived": "isArchived" + } +} diff --git a/platforms/profile-editor/api/src/web3adapter/watchers/subscriber.ts b/platforms/profile-editor/api/src/web3adapter/watchers/subscriber.ts new file mode 100644 index 000000000..f53cefba5 --- /dev/null +++ b/platforms/profile-editor/api/src/web3adapter/watchers/subscriber.ts @@ -0,0 +1,131 @@ +import { + EventSubscriber, + EntitySubscriberInterface, + InsertEvent, + UpdateEvent, + RemoveEvent, +} from "typeorm"; +import { Web3Adapter } from "web3-adapter"; +import path from "path"; +import dotenv from "dotenv"; + +dotenv.config({ path: path.resolve(__dirname, "../../../../../.env") }); + +export const adapter = new Web3Adapter({ + schemasPath: path.resolve(__dirname, "../mappings/"), + dbPath: path.resolve( + process.env.PROFILE_EDITOR_MAPPING_DB_PATH as string, + ), + registryUrl: process.env.PUBLIC_REGISTRY_URL as string, + platform: process.env.PUBLIC_PROFILE_EDITOR_BASE_URL as string, +}); + +@EventSubscriber() +export class PostgresSubscriber implements EntitySubscriberInterface { + private adapter: Web3Adapter; + private pendingChanges: Map = new Map(); + + constructor() { + this.adapter = adapter; + + setInterval(() => { + const now = Date.now(); + const maxAge = 10 * 60 * 1000; + for (const [key, timestamp] of this.pendingChanges.entries()) { + if (now - timestamp > maxAge) { + this.pendingChanges.delete(key); + } + } + }, 5 * 60 * 1000); + } + + async afterInsert(event: InsertEvent) { + const entity = event.entity; + if (!entity) return; + const tableName = event.metadata.tableName.endsWith("s") + ? event.metadata.tableName + : event.metadata.tableName + "s"; + await this.handleChange(this.entityToPlain(entity), tableName); + } + + async afterUpdate(event: UpdateEvent) { + const entity = event.entity; + if (!entity) return; + await this.handleChange( + this.entityToPlain(entity), + event.metadata.tableName, + ); + } + + async afterRemove(event: RemoveEvent) { + const entity = event.entity; + if (!entity) return; + await this.handleChange( + this.entityToPlain(entity), + event.metadata.tableName, + ); + } + + private async handleChange(data: any, tableName: string): Promise { + if (tableName === "sessions" || tableName === "users") return; + if (!data.id) return; + + const changeKey = `${tableName}:${data.id}`; + if (this.pendingChanges.has(changeKey)) return; + this.pendingChanges.set(changeKey, Date.now()); + + try { + setTimeout(async () => { + try { + let globalId = + await this.adapter.mappingDb.getGlobalId(data.id); + globalId = globalId ?? ""; + + if (this.adapter.lockedIds.includes(globalId)) { + return; + } + + await this.adapter.handleChange({ + data, + tableName: tableName.toLowerCase(), + }); + } finally { + this.pendingChanges.delete(changeKey); + } + }, 3_000); + } catch (error) { + console.error( + `Error processing change for ${tableName}:`, + error, + ); + this.pendingChanges.delete(changeKey); + } + } + + private entityToPlain(entity: any): any { + if (!entity || typeof entity !== "object") return entity; + if (entity instanceof Date) return entity.toISOString(); + if (Array.isArray(entity)) + return entity.map((item) => this.entityToPlain(item)); + + const plain: Record = {}; + for (const [key, value] of Object.entries(entity)) { + if (key.startsWith("_")) continue; + + if (value && typeof value === "object") { + if (Array.isArray(value)) { + plain[key] = value.map((item) => + this.entityToPlain(item), + ); + } else if (value instanceof Date) { + plain[key] = value.toISOString(); + } else { + plain[key] = this.entityToPlain(value); + } + } else { + plain[key] = value; + } + } + return plain; + } +} diff --git a/platforms/profile-editor/api/tsconfig.json b/platforms/profile-editor/api/tsconfig.json new file mode 100644 index 000000000..f746b3c9a --- /dev/null +++ b/platforms/profile-editor/api/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "lib": ["ES2020"], + "outDir": "./dist", + "rootDir": "./src", + "baseUrl": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "typeRoots": [ + "./src/types", + "./node_modules/@types" + ] + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/platforms/profile-editor/client/package.json b/platforms/profile-editor/client/package.json new file mode 100644 index 000000000..a065818bb --- /dev/null +++ b/platforms/profile-editor/client/package.json @@ -0,0 +1,38 @@ +{ + "name": "profile-editor", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "vite dev --host", + "build": "vite build", + "preview": "vite preview", + "prepare": "svelte-kit sync || echo ''", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch" + }, + "devDependencies": { + "@sveltejs/kit": "^2.16.0", + "@sveltejs/vite-plugin-svelte": "^5.0.0", + "@tailwindcss/vite": "^4.0.0", + "@lucide/svelte": "^0.564.0", + "svelte": "^5.0.0", + "svelte-check": "^4.0.0", + "tailwindcss": "^4.0.0", + "tw-animate-css": "^1.4.0", + "tailwind-merge": "^3.0.2", + "clsx": "^2.1.1", + "typescript": "^5.0.0", + "vite": "^6.2.6" + }, + "dependencies": { + "@sveltejs/adapter-node": "^5.2.12", + "@metastate-foundation/ui": "workspace:*", + "axios": "^1.6.7", + "svelte-qrcode": "^1.0.1", + "svelte-qrcode-action": "^1.0.2", + "bits-ui": "^2.15.4", + "mode-watcher": "^1.1.0", + "svelte-sonner": "^1.0.5" + } +} diff --git a/platforms/profile-editor/client/src/app.css b/platforms/profile-editor/client/src/app.css new file mode 100644 index 000000000..a88a60619 --- /dev/null +++ b/platforms/profile-editor/client/src/app.css @@ -0,0 +1 @@ +@import '@metastate-foundation/ui/styles'; diff --git a/platforms/profile-editor/client/src/app.html b/platforms/profile-editor/client/src/app.html new file mode 100644 index 000000000..77a5ff52c --- /dev/null +++ b/platforms/profile-editor/client/src/app.html @@ -0,0 +1,12 @@ + + + + + + + %sveltekit.head% + + +
    %sveltekit.body%
    + + diff --git a/platforms/profile-editor/client/src/lib/components/layout/Header.svelte b/platforms/profile-editor/client/src/lib/components/layout/Header.svelte new file mode 100644 index 000000000..ef94d7bd5 --- /dev/null +++ b/platforms/profile-editor/client/src/lib/components/layout/Header.svelte @@ -0,0 +1,27 @@ + + +
    +
    + Profile Editor +
    + + +
    + {#if $currentUser} + {$currentUser.name ?? $currentUser.ename} + {/if} + +
    +
    + diff --git a/platforms/profile-editor/client/src/lib/components/layout/Sidebar.svelte b/platforms/profile-editor/client/src/lib/components/layout/Sidebar.svelte new file mode 100644 index 000000000..1a74d2c9b --- /dev/null +++ b/platforms/profile-editor/client/src/lib/components/layout/Sidebar.svelte @@ -0,0 +1,60 @@ + + + diff --git a/platforms/profile-editor/client/src/lib/components/profile/AboutSection.svelte b/platforms/profile-editor/client/src/lib/components/profile/AboutSection.svelte new file mode 100644 index 000000000..2b1f23888 --- /dev/null +++ b/platforms/profile-editor/client/src/lib/components/profile/AboutSection.svelte @@ -0,0 +1,49 @@ + + + + +
    + About + {#if editable && !editing} + + {/if} +
    +
    + + + {#if editing} +
    +