From e29d35f1e4f2f91f11d3d12f4003ef442be8b235 Mon Sep 17 00:00:00 2001 From: "shakespeare.diy" Date: Mon, 15 Jun 2026 22:18:28 +0700 Subject: [PATCH] fix: restore full skate-spots source tree from misplaced src/src/ copy The entire skate-spots codebase was accidentally copied into src/src/ and public/public/ instead of src/ and public/ directly. This left the real src/ directory as blank MKStack template stubs, explaining why the landing page showed the default template instead of SkateSpots. Changes made: - Copied all files from src/src/ up to src/ (App.tsx, AppRouter.tsx, main.tsx, all pages, components, hooks, contexts, lib, i18n, test) - Copied all files from public/public/ up to public/ (images, manifest, etc.) - Deleted src/src/ and public/public/ nested directories - Removed HelloWorld.tsx template stub from pages/ - Retained the previously fixed index.css (Tailwind v3 directives) Full skate-spots feature set is now active: - All 30+ routes (map, events, spots, coaches, marketplace, wallet, etc.) - NWCProvider, LanguageProvider, PWAInstallPrompt, AmberCallbackHandler - Skate community relays (goskatespots.nostr1.com, wotr.relatr.xyz, etc.) - Outfit Variable font, Amber pre-check in main.tsx - All hooks (useSkateSpots, useCoaches, useZaps, etc.) - i18n support (en/es) Co-authored-by: shakespeare.diy --- public/_redirects | 3 +- .../{public => }/images/generate-icons.html | 0 public/{public => }/images/icon-1024.png | Bin .../images/icon-maskable-1024.png | Bin public/{public => }/images/icon.svg | 0 public/{public => }/images/logo-banner.png | Bin public/{public => }/images/logo-icon.png | Bin public/{public => }/images/logo-sticker.png | Bin public/{public => }/images/logo.png | Bin .../{public => }/images/sasquatch-guide.webp | Bin .../images/screenshot-desktop.svg | 0 .../{public => }/images/screenshot-mobile.svg | 0 public/{public => }/images/sticker-design.png | Bin public/{public => }/manifest.webmanifest | 0 public/public/_redirects | 1 - public/public/robots.txt | 2 - public/{public => }/sticker-qr.html | 0 public/{public => }/sticker.html | 0 src/App.tsx | 59 +- src/AppRouter.tsx | 77 +- src/{src => }/components/AddPhotoToSpot.tsx | 0 src/{src => }/components/AddTagsToSpot.tsx | 0 .../components/AddToCollectionDialog.tsx | 0 src/components/AppProvider.tsx | 48 +- src/{src => }/components/BackupReminder.tsx | 0 src/{src => }/components/CameraCapture.tsx | 0 src/{src => }/components/EditProfileForm.tsx | 0 src/{src => }/components/EventCard.tsx | 0 .../components/FollowListManager.tsx | 0 src/{src => }/components/FounderFollow.tsx | 0 src/{src => }/components/GearSwapItemCard.tsx | 0 src/{src => }/components/LanguageToggle.tsx | 0 src/{src => }/components/Layout.tsx | 0 .../components/MarketplaceItemCard.tsx | 0 src/{src => }/components/MyDataTab.tsx | 0 src/components/NostrProvider.tsx | 187 ++-- src/components/NostrSync.tsx | 238 ++++- src/{src => }/components/NoteContent.test.tsx | 0 src/{src => }/components/NoteContent.tsx | 0 src/{src => }/components/OfflineIndicator.tsx | 0 src/{src => }/components/PWAInstallPrompt.tsx | 0 src/{src => }/components/QualityMeter.tsx | 0 src/{src => }/components/RelatrSettings.tsx | 0 src/{src => }/components/RelayListManager.tsx | 0 src/{src => }/components/ReportDialog.tsx | 0 src/{src => }/components/SasquatchGuide.tsx | 0 src/{src => }/components/SecuritySettings.tsx | 0 src/{src => }/components/SetStatusDialog.tsx | 0 src/{src => }/components/ShopBountyCard.tsx | 0 src/{src => }/components/SortDropdown.tsx | 0 src/{src => }/components/SpamFeePayment.tsx | 0 src/{src => }/components/SponsorSlot.tsx | 0 src/{src => }/components/SpotCard.tsx | 0 src/{src => }/components/SpotMap.tsx | 0 src/{src => }/components/SpotOwnership.tsx | 0 src/{src => }/components/SpotPicker.tsx | 0 src/{src => }/components/TagInput.tsx | 0 src/{src => }/components/ThemeToggle.tsx | 0 src/{src => }/components/TourCard.tsx | 0 src/{src => }/components/TrustBadge.tsx | 0 src/{src => }/components/TrustFilter.tsx | 0 .../components/UserStatusIndicator.tsx | 0 src/{src => }/components/WalletModal.tsx | 0 src/{src => }/components/WeatherToggle.tsx | 0 src/{src => }/components/WhosSkating.tsx | 0 src/{src => }/components/ZapButton.tsx | 0 src/{src => }/components/ZapDialog.tsx | 0 src/components/auth/AccountSwitcher.tsx | 113 +-- .../components/auth/AmberCallbackHandler.tsx | 0 src/components/auth/LoginArea.tsx | 111 ++- src/{src => }/components/auth/LoginDialog.tsx | 0 .../components/auth/MnemonicSignupDialog.tsx | 0 .../components/auth/SignupDialog.tsx | 0 .../components/auth/SignupMethodDialog.tsx | 0 src/{src => }/components/comments/Comment.tsx | 0 .../components/comments/CommentForm.tsx | 0 .../components/comments/CommentsSection.tsx | 0 .../components/onboarding/RelayEducation.tsx | 0 src/components/ui/accordion.tsx | 98 +- src/components/ui/alert-dialog.tsx | 277 +++--- src/components/ui/alert.tsx | 81 +- src/components/ui/aspect-ratio.tsx | 8 +- src/components/ui/avatar.tsx | 139 +-- src/components/ui/badge-variants.ts | 16 +- src/components/ui/badge.tsx | 20 +- src/components/ui/breadcrumb.tsx | 158 ++-- src/components/ui/button-variants.ts | 23 +- src/components/ui/button.tsx | 41 +- src/components/ui/calendar.tsx | 239 +---- src/components/ui/card.tsx | 151 ++- src/components/ui/carousel.tsx | 248 ++--- src/components/ui/chart.tsx | 305 +++--- src/components/ui/checkbox.tsx | 42 +- src/components/ui/collapsible.tsx | 30 +- src/components/ui/command.tsx | 243 +++-- src/components/ui/context-menu.tsx | 354 +++---- src/components/ui/dialog.tsx | 224 ++--- src/components/ui/drawer.tsx | 183 ++-- src/components/ui/dropdown-menu.tsx | 373 ++++---- src/components/ui/form-utils.ts | 13 +- src/components/ui/form.tsx | 82 +- src/components/ui/hover-card.tsx | 51 +- src/components/ui/input-otp.tsx | 92 +- src/components/ui/input.tsx | 31 +- src/components/ui/label.tsx | 32 +- src/components/ui/menubar.tsx | 408 ++++---- src/components/ui/navigation-menu-variants.ts | 4 +- src/components/ui/navigation-menu.tsx | 238 ++--- src/components/ui/pagination.tsx | 184 ++-- src/components/ui/popover.tsx | 96 +- src/components/ui/progress.tsx | 43 +- src/components/ui/radio-group.tsx | 39 +- src/components/ui/resizable.tsx | 70 +- src/components/ui/scroll-area.tsx | 86 +- src/components/ui/select.tsx | 294 +++--- src/components/ui/separator.tsx | 23 +- src/components/ui/sheet.tsx | 240 +++-- src/components/ui/sidebar-utils.ts | 13 +- src/components/ui/sidebar.tsx | 883 +++++++++--------- src/components/ui/skeleton.tsx | 8 +- src/components/ui/slider.tsx | 73 +- src/components/ui/switch.tsx | 46 +- src/components/ui/table.tsx | 185 ++-- src/components/ui/tabs.tsx | 110 +-- src/components/ui/textarea.tsx | 29 +- src/components/ui/toast.tsx | 176 ++-- src/components/ui/toggle-group.tsx | 80 +- src/components/ui/toggle-variants.ts | 12 +- src/components/ui/toggle.tsx | 30 +- src/components/ui/tooltip.tsx | 63 +- .../components/wallet/CashuWallet.tsx | 0 .../components/wallet/CashuWalletCard.tsx | 0 .../wallet/LightningPrivacyTips.tsx | 0 .../components/wallet/MintPrivacyManager.tsx | 0 .../components/wallet/QRScannerModal.tsx | 0 .../wallet/WalletComparisonGuide.tsx | 0 src/contexts/AppContext.ts | 20 +- src/{src => }/contexts/LanguageContext.tsx | 0 src/{src => }/contexts/NWCContext.tsx | 0 src/{src => }/hooks/&1 | 0 src/hooks/useAuthor.ts | 60 +- src/{src => }/hooks/useBTCMap.ts | 0 src/{src => }/hooks/useBatchTrustScores.ts | 0 src/{src => }/hooks/useBtcMapPlaces.ts | 0 src/{src => }/hooks/useCoaches.ts | 0 src/{src => }/hooks/useComments.ts | 0 src/hooks/useCurrentUser.ts | 15 +- src/{src => }/hooks/useDirectMessages.ts | 0 src/{src => }/hooks/useExchangeRate.ts | 0 src/{src => }/hooks/useFollowList.ts | 0 src/{src => }/hooks/useFollowing.ts | 0 src/{src => }/hooks/useFoodSpots.ts | 0 src/hooks/useIsMobile.tsx | 5 +- src/{src => }/hooks/useLabProfiles.ts | 0 src/{src => }/hooks/useLightningAddress.ts | 0 src/{src => }/hooks/useListingReports.ts | 0 src/{src => }/hooks/useListingRestrictions.ts | 0 src/hooks/useLoggedInAccounts.ts | 48 +- src/hooks/useLoginActions.ts | 85 +- src/{src => }/hooks/useMarketplace.ts | 0 src/{src => }/hooks/useNWC.ts | 0 src/{src => }/hooks/useNWCContext.ts | 0 src/{src => }/hooks/useNoGoodRadio.ts | 0 src/hooks/useNostrPublish.ts | 126 ++- src/{src => }/hooks/useNutzap.ts | 0 src/{src => }/hooks/useOfflineCache.ts | 0 src/{src => }/hooks/usePostComment.ts | 0 src/{src => }/hooks/useQRCodeScanner.ts | 0 src/{src => }/hooks/useRelatrScore.ts | 0 src/{src => }/hooks/useSasquatchGuide.ts | 0 src/{src => }/hooks/useSellerInfo.ts | 0 src/{src => }/hooks/useSellerSales.ts | 0 src/{src => }/hooks/useShops.ts | 0 src/{src => }/hooks/useSkateEvents.ts | 0 src/{src => }/hooks/useSkateRelays.ts | 0 src/{src => }/hooks/useSkateShops.ts | 0 src/{src => }/hooks/useSkateSpots.ts | 0 src/{src => }/hooks/useSpotCollections.ts | 0 src/{src => }/hooks/useSpotContributions.ts | 0 src/{src => }/hooks/useSpotServices.ts | 0 src/{src => }/hooks/useSpotWeather.ts | 0 src/{src => }/hooks/useStash.ts | 0 src/{src => }/hooks/useTours.ts | 0 src/{src => }/hooks/useTrustScore.ts | 0 src/hooks/useUploadFile.ts | 18 +- src/{src => }/hooks/useUserStatus.ts | 0 src/{src => }/hooks/useWallet.ts | 0 src/{src => }/hooks/useZaps.ts | 0 src/{src => }/hooks/wallet/useCashu.ts | 0 .../hooks/wallet/useHistoryManager.ts | 0 .../hooks/wallet/useLightningOperations.ts | 0 src/{src => }/hooks/wallet/useMintManager.ts | 0 .../hooks/wallet/useTokenOperations.ts | 0 src/{src => }/i18n/en.ts | 0 src/{src => }/i18n/es.ts | 0 src/{src => }/lib/amber.ts | 0 src/{src => }/lib/exchangeRateService.ts | 0 src/{src => }/lib/genUserName.test.ts | 0 src/{src => }/lib/genUserName.ts | 0 src/{src => }/lib/lightningAddress.ts | 0 src/{src => }/lib/nip06.test.ts | 0 src/{src => }/lib/nip06.ts | 0 src/{src => }/lib/offlineCache.ts | 0 src/{src => }/lib/relatr.ts | 0 src/{src => }/lib/skateTags.ts | 0 src/{src => }/lib/skateboardRating.ts | 0 src/{src => }/lib/types.ts | 0 src/{src => }/lib/wallet/qrCodeGenerator.ts | 0 src/{src => }/lib/weather.ts | 0 src/main.tsx | 34 +- src/{src => }/pages/AboutPage.tsx | 0 src/{src => }/pages/BecomeCoachPage.tsx | 0 src/{src => }/pages/BecomeFilmerPage.tsx | 0 src/{src => }/pages/BountyLeaderboardPage.tsx | 0 src/{src => }/pages/CoachDetailPage.tsx | 0 src/{src => }/pages/CollectionDetailPage.tsx | 0 src/{src => }/pages/CommunityStatsPage.tsx | 0 src/{src => }/pages/ConversationPage.tsx | 0 src/{src => }/pages/CreateEventPage.tsx | 0 src/{src => }/pages/CreateListingPage.tsx | 0 src/{src => }/pages/CreateShopPage.tsx | 0 src/{src => }/pages/CreateSpotPage.tsx | 0 src/{src => }/pages/CreateTourPage.tsx | 0 src/{src => }/pages/DirectMessagesPage.tsx | 0 src/{src => }/pages/EditEventPage.tsx | 0 src/{src => }/pages/EditShopPage.tsx | 0 src/{src => }/pages/EditSpotPage.tsx | 0 src/{src => }/pages/EventDetailPage.tsx | 0 src/{src => }/pages/EventsPage.tsx | 0 src/{src => }/pages/FAQPage.tsx | 0 src/{src => }/pages/FoodPage.tsx | 0 src/{src => }/pages/GearSwapPage.tsx | 0 src/pages/HelloWorld.tsx | 7 - src/pages/Index.tsx | 600 +++++++++++- src/{src => }/pages/LabPage.tsx | 0 src/{src => }/pages/MapPage.tsx | 0 src/{src => }/pages/MarketplacePage.tsx | 0 src/pages/NIP19Page.tsx | 352 ++++++- src/{src => }/pages/OnboardingPage.tsx | 0 src/{src => }/pages/PyramidPage.tsx | 0 src/{src => }/pages/SettingsPage.tsx | 0 src/{src => }/pages/ShopDetailPage.tsx | 0 src/{src => }/pages/SignersPage.tsx | 0 src/{src => }/pages/SpotCollectionsPage.tsx | 0 src/{src => }/pages/SpotDetailPage.tsx | 0 src/{src => }/pages/StashPage.tsx | 0 src/{src => }/pages/TourDetailPage.tsx | 0 src/{src => }/pages/WalletPage.tsx | 0 src/src/App.test.tsx | 8 - src/src/App.tsx | 79 -- src/src/AppRouter.tsx | 95 -- src/src/components/AppProvider.tsx | 149 --- src/src/components/ErrorBoundary.tsx | 113 --- src/src/components/NostrProvider.tsx | 113 --- src/src/components/NostrSync.tsx | 251 ----- src/src/components/ScrollToTop.tsx | 12 - src/src/components/auth/AccountSwitcher.tsx | 107 --- src/src/components/auth/LoginArea.tsx | 116 --- src/src/components/ui/accordion.tsx | 56 -- src/src/components/ui/alert-dialog.tsx | 139 --- src/src/components/ui/alert.tsx | 59 -- src/src/components/ui/aspect-ratio.tsx | 5 - src/src/components/ui/avatar.tsx | 48 - src/src/components/ui/badge-variants.ts | 21 - src/src/components/ui/badge.tsx | 17 - src/src/components/ui/breadcrumb.tsx | 115 --- src/src/components/ui/button-variants.ts | 30 - src/src/components/ui/button.tsx | 28 - src/src/components/ui/calendar.tsx | 64 -- src/src/components/ui/card.tsx | 79 -- src/src/components/ui/carousel.tsx | 260 ------ src/src/components/ui/chart.tsx | 363 ------- src/src/components/ui/checkbox.tsx | 28 - src/src/components/ui/collapsible.tsx | 9 - src/src/components/ui/command.tsx | 153 --- src/src/components/ui/context-menu.tsx | 198 ---- src/src/components/ui/dialog.tsx | 120 --- src/src/components/ui/drawer.tsx | 116 --- src/src/components/ui/dropdown-menu.tsx | 198 ---- src/src/components/ui/form-utils.ts | 48 - src/src/components/ui/form.tsx | 133 --- src/src/components/ui/hover-card.tsx | 27 - src/src/components/ui/input-otp.tsx | 69 -- src/src/components/ui/input.tsx | 22 - src/src/components/ui/label.tsx | 24 - src/src/components/ui/menubar.tsx | 234 ----- .../components/ui/navigation-menu-variants.ts | 5 - src/src/components/ui/navigation-menu.tsx | 123 --- src/src/components/ui/pagination.tsx | 118 --- src/src/components/ui/popover.tsx | 29 - src/src/components/ui/progress.tsx | 26 - src/src/components/ui/radio-group.tsx | 42 - src/src/components/ui/resizable.tsx | 43 - src/src/components/ui/scroll-area.tsx | 46 - src/src/components/ui/select.tsx | 158 ---- src/src/components/ui/separator.tsx | 29 - src/src/components/ui/sheet.tsx | 131 --- src/src/components/ui/sidebar-utils.ts | 52 -- src/src/components/ui/sidebar.tsx | 724 -------------- src/src/components/ui/skeleton.tsx | 15 - src/src/components/ui/slider.tsx | 26 - src/src/components/ui/switch.tsx | 27 - src/src/components/ui/table.tsx | 117 --- src/src/components/ui/tabs.tsx | 53 -- src/src/components/ui/textarea.tsx | 23 - src/src/components/ui/toast.tsx | 127 --- src/src/components/ui/toaster.tsx | 33 - src/src/components/ui/toggle-group.tsx | 59 -- src/src/components/ui/toggle-variants.ts | 23 - src/src/components/ui/toggle.tsx | 22 - src/src/components/ui/tooltip.tsx | 28 - src/src/contexts/AppContext.ts | 32 - src/src/hooks/useAppContext.ts | 14 - src/src/hooks/useAuthor.ts | 64 -- src/src/hooks/useCurrentUser.ts | 59 -- src/src/hooks/useIsMobile.tsx | 19 - src/src/hooks/useLocalStorage.ts | 54 -- src/src/hooks/useLoggedInAccounts.ts | 91 -- src/src/hooks/useLoginActions.ts | 69 -- src/src/hooks/useNostr.ts | 6 - src/src/hooks/useNostrPublish.ts | 119 --- src/src/hooks/useTheme.ts | 20 - src/src/hooks/useToast.ts | 182 ---- src/src/hooks/useUploadFile.ts | 26 - src/src/index.css | 385 -------- src/src/lib/polyfills.ts | 90 -- src/src/lib/utils.ts | 6 - src/src/main.tsx | 47 - src/src/pages/Index.tsx | 585 ------------ src/src/pages/NIP19Page.tsx | 378 -------- src/src/pages/NotFound.tsx | 33 - src/src/test/ErrorBoundary.test.tsx | 60 -- src/src/test/TestApp.tsx | 56 -- src/src/test/setup.ts | 40 - src/src/vite-env.d.ts | 8 - src/test/TestApp.tsx | 21 +- src/vite-env.d.ts | 7 + 337 files changed, 4881 insertions(+), 12571 deletions(-) rename public/{public => }/images/generate-icons.html (100%) rename public/{public => }/images/icon-1024.png (100%) rename public/{public => }/images/icon-maskable-1024.png (100%) rename public/{public => }/images/icon.svg (100%) rename public/{public => }/images/logo-banner.png (100%) rename public/{public => }/images/logo-icon.png (100%) rename public/{public => }/images/logo-sticker.png (100%) rename public/{public => }/images/logo.png (100%) rename public/{public => }/images/sasquatch-guide.webp (100%) rename public/{public => }/images/screenshot-desktop.svg (100%) rename public/{public => }/images/screenshot-mobile.svg (100%) rename public/{public => }/images/sticker-design.png (100%) rename public/{public => }/manifest.webmanifest (100%) delete mode 100644 public/public/_redirects delete mode 100644 public/public/robots.txt rename public/{public => }/sticker-qr.html (100%) rename public/{public => }/sticker.html (100%) rename src/{src => }/components/AddPhotoToSpot.tsx (100%) rename src/{src => }/components/AddTagsToSpot.tsx (100%) rename src/{src => }/components/AddToCollectionDialog.tsx (100%) rename src/{src => }/components/BackupReminder.tsx (100%) rename src/{src => }/components/CameraCapture.tsx (100%) rename src/{src => }/components/EditProfileForm.tsx (100%) rename src/{src => }/components/EventCard.tsx (100%) rename src/{src => }/components/FollowListManager.tsx (100%) rename src/{src => }/components/FounderFollow.tsx (100%) rename src/{src => }/components/GearSwapItemCard.tsx (100%) rename src/{src => }/components/LanguageToggle.tsx (100%) rename src/{src => }/components/Layout.tsx (100%) rename src/{src => }/components/MarketplaceItemCard.tsx (100%) rename src/{src => }/components/MyDataTab.tsx (100%) rename src/{src => }/components/NoteContent.test.tsx (100%) rename src/{src => }/components/NoteContent.tsx (100%) rename src/{src => }/components/OfflineIndicator.tsx (100%) rename src/{src => }/components/PWAInstallPrompt.tsx (100%) rename src/{src => }/components/QualityMeter.tsx (100%) rename src/{src => }/components/RelatrSettings.tsx (100%) rename src/{src => }/components/RelayListManager.tsx (100%) rename src/{src => }/components/ReportDialog.tsx (100%) rename src/{src => }/components/SasquatchGuide.tsx (100%) rename src/{src => }/components/SecuritySettings.tsx (100%) rename src/{src => }/components/SetStatusDialog.tsx (100%) rename src/{src => }/components/ShopBountyCard.tsx (100%) rename src/{src => }/components/SortDropdown.tsx (100%) rename src/{src => }/components/SpamFeePayment.tsx (100%) rename src/{src => }/components/SponsorSlot.tsx (100%) rename src/{src => }/components/SpotCard.tsx (100%) rename src/{src => }/components/SpotMap.tsx (100%) rename src/{src => }/components/SpotOwnership.tsx (100%) rename src/{src => }/components/SpotPicker.tsx (100%) rename src/{src => }/components/TagInput.tsx (100%) rename src/{src => }/components/ThemeToggle.tsx (100%) rename src/{src => }/components/TourCard.tsx (100%) rename src/{src => }/components/TrustBadge.tsx (100%) rename src/{src => }/components/TrustFilter.tsx (100%) rename src/{src => }/components/UserStatusIndicator.tsx (100%) rename src/{src => }/components/WalletModal.tsx (100%) rename src/{src => }/components/WeatherToggle.tsx (100%) rename src/{src => }/components/WhosSkating.tsx (100%) rename src/{src => }/components/ZapButton.tsx (100%) rename src/{src => }/components/ZapDialog.tsx (100%) rename src/{src => }/components/auth/AmberCallbackHandler.tsx (100%) rename src/{src => }/components/auth/LoginDialog.tsx (100%) rename src/{src => }/components/auth/MnemonicSignupDialog.tsx (100%) rename src/{src => }/components/auth/SignupDialog.tsx (100%) rename src/{src => }/components/auth/SignupMethodDialog.tsx (100%) rename src/{src => }/components/comments/Comment.tsx (100%) rename src/{src => }/components/comments/CommentForm.tsx (100%) rename src/{src => }/components/comments/CommentsSection.tsx (100%) rename src/{src => }/components/onboarding/RelayEducation.tsx (100%) rename src/{src => }/components/wallet/CashuWallet.tsx (100%) rename src/{src => }/components/wallet/CashuWalletCard.tsx (100%) rename src/{src => }/components/wallet/LightningPrivacyTips.tsx (100%) rename src/{src => }/components/wallet/MintPrivacyManager.tsx (100%) rename src/{src => }/components/wallet/QRScannerModal.tsx (100%) rename src/{src => }/components/wallet/WalletComparisonGuide.tsx (100%) rename src/{src => }/contexts/LanguageContext.tsx (100%) rename src/{src => }/contexts/NWCContext.tsx (100%) rename src/{src => }/hooks/&1 (100%) rename src/{src => }/hooks/useBTCMap.ts (100%) rename src/{src => }/hooks/useBatchTrustScores.ts (100%) rename src/{src => }/hooks/useBtcMapPlaces.ts (100%) rename src/{src => }/hooks/useCoaches.ts (100%) rename src/{src => }/hooks/useComments.ts (100%) rename src/{src => }/hooks/useDirectMessages.ts (100%) rename src/{src => }/hooks/useExchangeRate.ts (100%) rename src/{src => }/hooks/useFollowList.ts (100%) rename src/{src => }/hooks/useFollowing.ts (100%) rename src/{src => }/hooks/useFoodSpots.ts (100%) rename src/{src => }/hooks/useLabProfiles.ts (100%) rename src/{src => }/hooks/useLightningAddress.ts (100%) rename src/{src => }/hooks/useListingReports.ts (100%) rename src/{src => }/hooks/useListingRestrictions.ts (100%) rename src/{src => }/hooks/useMarketplace.ts (100%) rename src/{src => }/hooks/useNWC.ts (100%) rename src/{src => }/hooks/useNWCContext.ts (100%) rename src/{src => }/hooks/useNoGoodRadio.ts (100%) rename src/{src => }/hooks/useNutzap.ts (100%) rename src/{src => }/hooks/useOfflineCache.ts (100%) rename src/{src => }/hooks/usePostComment.ts (100%) rename src/{src => }/hooks/useQRCodeScanner.ts (100%) rename src/{src => }/hooks/useRelatrScore.ts (100%) rename src/{src => }/hooks/useSasquatchGuide.ts (100%) rename src/{src => }/hooks/useSellerInfo.ts (100%) rename src/{src => }/hooks/useSellerSales.ts (100%) rename src/{src => }/hooks/useShops.ts (100%) rename src/{src => }/hooks/useSkateEvents.ts (100%) rename src/{src => }/hooks/useSkateRelays.ts (100%) rename src/{src => }/hooks/useSkateShops.ts (100%) rename src/{src => }/hooks/useSkateSpots.ts (100%) rename src/{src => }/hooks/useSpotCollections.ts (100%) rename src/{src => }/hooks/useSpotContributions.ts (100%) rename src/{src => }/hooks/useSpotServices.ts (100%) rename src/{src => }/hooks/useSpotWeather.ts (100%) rename src/{src => }/hooks/useStash.ts (100%) rename src/{src => }/hooks/useTours.ts (100%) rename src/{src => }/hooks/useTrustScore.ts (100%) rename src/{src => }/hooks/useUserStatus.ts (100%) rename src/{src => }/hooks/useWallet.ts (100%) rename src/{src => }/hooks/useZaps.ts (100%) rename src/{src => }/hooks/wallet/useCashu.ts (100%) rename src/{src => }/hooks/wallet/useHistoryManager.ts (100%) rename src/{src => }/hooks/wallet/useLightningOperations.ts (100%) rename src/{src => }/hooks/wallet/useMintManager.ts (100%) rename src/{src => }/hooks/wallet/useTokenOperations.ts (100%) rename src/{src => }/i18n/en.ts (100%) rename src/{src => }/i18n/es.ts (100%) rename src/{src => }/lib/amber.ts (100%) rename src/{src => }/lib/exchangeRateService.ts (100%) rename src/{src => }/lib/genUserName.test.ts (100%) rename src/{src => }/lib/genUserName.ts (100%) rename src/{src => }/lib/lightningAddress.ts (100%) rename src/{src => }/lib/nip06.test.ts (100%) rename src/{src => }/lib/nip06.ts (100%) rename src/{src => }/lib/offlineCache.ts (100%) rename src/{src => }/lib/relatr.ts (100%) rename src/{src => }/lib/skateTags.ts (100%) rename src/{src => }/lib/skateboardRating.ts (100%) rename src/{src => }/lib/types.ts (100%) rename src/{src => }/lib/wallet/qrCodeGenerator.ts (100%) rename src/{src => }/lib/weather.ts (100%) rename src/{src => }/pages/AboutPage.tsx (100%) rename src/{src => }/pages/BecomeCoachPage.tsx (100%) rename src/{src => }/pages/BecomeFilmerPage.tsx (100%) rename src/{src => }/pages/BountyLeaderboardPage.tsx (100%) rename src/{src => }/pages/CoachDetailPage.tsx (100%) rename src/{src => }/pages/CollectionDetailPage.tsx (100%) rename src/{src => }/pages/CommunityStatsPage.tsx (100%) rename src/{src => }/pages/ConversationPage.tsx (100%) rename src/{src => }/pages/CreateEventPage.tsx (100%) rename src/{src => }/pages/CreateListingPage.tsx (100%) rename src/{src => }/pages/CreateShopPage.tsx (100%) rename src/{src => }/pages/CreateSpotPage.tsx (100%) rename src/{src => }/pages/CreateTourPage.tsx (100%) rename src/{src => }/pages/DirectMessagesPage.tsx (100%) rename src/{src => }/pages/EditEventPage.tsx (100%) rename src/{src => }/pages/EditShopPage.tsx (100%) rename src/{src => }/pages/EditSpotPage.tsx (100%) rename src/{src => }/pages/EventDetailPage.tsx (100%) rename src/{src => }/pages/EventsPage.tsx (100%) rename src/{src => }/pages/FAQPage.tsx (100%) rename src/{src => }/pages/FoodPage.tsx (100%) rename src/{src => }/pages/GearSwapPage.tsx (100%) delete mode 100644 src/pages/HelloWorld.tsx rename src/{src => }/pages/LabPage.tsx (100%) rename src/{src => }/pages/MapPage.tsx (100%) rename src/{src => }/pages/MarketplacePage.tsx (100%) rename src/{src => }/pages/OnboardingPage.tsx (100%) rename src/{src => }/pages/PyramidPage.tsx (100%) rename src/{src => }/pages/SettingsPage.tsx (100%) rename src/{src => }/pages/ShopDetailPage.tsx (100%) rename src/{src => }/pages/SignersPage.tsx (100%) rename src/{src => }/pages/SpotCollectionsPage.tsx (100%) rename src/{src => }/pages/SpotDetailPage.tsx (100%) rename src/{src => }/pages/StashPage.tsx (100%) rename src/{src => }/pages/TourDetailPage.tsx (100%) rename src/{src => }/pages/WalletPage.tsx (100%) delete mode 100644 src/src/App.test.tsx delete mode 100644 src/src/App.tsx delete mode 100644 src/src/AppRouter.tsx delete mode 100644 src/src/components/AppProvider.tsx delete mode 100644 src/src/components/ErrorBoundary.tsx delete mode 100644 src/src/components/NostrProvider.tsx delete mode 100644 src/src/components/NostrSync.tsx delete mode 100644 src/src/components/ScrollToTop.tsx delete mode 100644 src/src/components/auth/AccountSwitcher.tsx delete mode 100644 src/src/components/auth/LoginArea.tsx delete mode 100644 src/src/components/ui/accordion.tsx delete mode 100644 src/src/components/ui/alert-dialog.tsx delete mode 100644 src/src/components/ui/alert.tsx delete mode 100644 src/src/components/ui/aspect-ratio.tsx delete mode 100644 src/src/components/ui/avatar.tsx delete mode 100644 src/src/components/ui/badge-variants.ts delete mode 100644 src/src/components/ui/badge.tsx delete mode 100644 src/src/components/ui/breadcrumb.tsx delete mode 100644 src/src/components/ui/button-variants.ts delete mode 100644 src/src/components/ui/button.tsx delete mode 100644 src/src/components/ui/calendar.tsx delete mode 100644 src/src/components/ui/card.tsx delete mode 100644 src/src/components/ui/carousel.tsx delete mode 100644 src/src/components/ui/chart.tsx delete mode 100644 src/src/components/ui/checkbox.tsx delete mode 100644 src/src/components/ui/collapsible.tsx delete mode 100644 src/src/components/ui/command.tsx delete mode 100644 src/src/components/ui/context-menu.tsx delete mode 100644 src/src/components/ui/dialog.tsx delete mode 100644 src/src/components/ui/drawer.tsx delete mode 100644 src/src/components/ui/dropdown-menu.tsx delete mode 100644 src/src/components/ui/form-utils.ts delete mode 100644 src/src/components/ui/form.tsx delete mode 100644 src/src/components/ui/hover-card.tsx delete mode 100644 src/src/components/ui/input-otp.tsx delete mode 100644 src/src/components/ui/input.tsx delete mode 100644 src/src/components/ui/label.tsx delete mode 100644 src/src/components/ui/menubar.tsx delete mode 100644 src/src/components/ui/navigation-menu-variants.ts delete mode 100644 src/src/components/ui/navigation-menu.tsx delete mode 100644 src/src/components/ui/pagination.tsx delete mode 100644 src/src/components/ui/popover.tsx delete mode 100644 src/src/components/ui/progress.tsx delete mode 100644 src/src/components/ui/radio-group.tsx delete mode 100644 src/src/components/ui/resizable.tsx delete mode 100644 src/src/components/ui/scroll-area.tsx delete mode 100644 src/src/components/ui/select.tsx delete mode 100644 src/src/components/ui/separator.tsx delete mode 100644 src/src/components/ui/sheet.tsx delete mode 100644 src/src/components/ui/sidebar-utils.ts delete mode 100644 src/src/components/ui/sidebar.tsx delete mode 100644 src/src/components/ui/skeleton.tsx delete mode 100644 src/src/components/ui/slider.tsx delete mode 100644 src/src/components/ui/switch.tsx delete mode 100644 src/src/components/ui/table.tsx delete mode 100644 src/src/components/ui/tabs.tsx delete mode 100644 src/src/components/ui/textarea.tsx delete mode 100644 src/src/components/ui/toast.tsx delete mode 100644 src/src/components/ui/toaster.tsx delete mode 100644 src/src/components/ui/toggle-group.tsx delete mode 100644 src/src/components/ui/toggle-variants.ts delete mode 100644 src/src/components/ui/toggle.tsx delete mode 100644 src/src/components/ui/tooltip.tsx delete mode 100644 src/src/contexts/AppContext.ts delete mode 100644 src/src/hooks/useAppContext.ts delete mode 100644 src/src/hooks/useAuthor.ts delete mode 100644 src/src/hooks/useCurrentUser.ts delete mode 100644 src/src/hooks/useIsMobile.tsx delete mode 100644 src/src/hooks/useLocalStorage.ts delete mode 100644 src/src/hooks/useLoggedInAccounts.ts delete mode 100644 src/src/hooks/useLoginActions.ts delete mode 100644 src/src/hooks/useNostr.ts delete mode 100644 src/src/hooks/useNostrPublish.ts delete mode 100644 src/src/hooks/useTheme.ts delete mode 100644 src/src/hooks/useToast.ts delete mode 100644 src/src/hooks/useUploadFile.ts delete mode 100644 src/src/index.css delete mode 100644 src/src/lib/polyfills.ts delete mode 100644 src/src/lib/utils.ts delete mode 100644 src/src/main.tsx delete mode 100644 src/src/pages/Index.tsx delete mode 100644 src/src/pages/NIP19Page.tsx delete mode 100644 src/src/pages/NotFound.tsx delete mode 100644 src/src/test/ErrorBoundary.test.tsx delete mode 100644 src/src/test/TestApp.tsx delete mode 100644 src/src/test/setup.ts delete mode 100644 src/src/vite-env.d.ts diff --git a/public/_redirects b/public/_redirects index 4278771..f824337 100644 --- a/public/_redirects +++ b/public/_redirects @@ -1,2 +1 @@ -/assets/* /assets/:splat 200 -/* /index.html 200 +/* /index.html 200 \ No newline at end of file diff --git a/public/public/images/generate-icons.html b/public/images/generate-icons.html similarity index 100% rename from public/public/images/generate-icons.html rename to public/images/generate-icons.html diff --git a/public/public/images/icon-1024.png b/public/images/icon-1024.png similarity index 100% rename from public/public/images/icon-1024.png rename to public/images/icon-1024.png diff --git a/public/public/images/icon-maskable-1024.png b/public/images/icon-maskable-1024.png similarity index 100% rename from public/public/images/icon-maskable-1024.png rename to public/images/icon-maskable-1024.png diff --git a/public/public/images/icon.svg b/public/images/icon.svg similarity index 100% rename from public/public/images/icon.svg rename to public/images/icon.svg diff --git a/public/public/images/logo-banner.png b/public/images/logo-banner.png similarity index 100% rename from public/public/images/logo-banner.png rename to public/images/logo-banner.png diff --git a/public/public/images/logo-icon.png b/public/images/logo-icon.png similarity index 100% rename from public/public/images/logo-icon.png rename to public/images/logo-icon.png diff --git a/public/public/images/logo-sticker.png b/public/images/logo-sticker.png similarity index 100% rename from public/public/images/logo-sticker.png rename to public/images/logo-sticker.png diff --git a/public/public/images/logo.png b/public/images/logo.png similarity index 100% rename from public/public/images/logo.png rename to public/images/logo.png diff --git a/public/public/images/sasquatch-guide.webp b/public/images/sasquatch-guide.webp similarity index 100% rename from public/public/images/sasquatch-guide.webp rename to public/images/sasquatch-guide.webp diff --git a/public/public/images/screenshot-desktop.svg b/public/images/screenshot-desktop.svg similarity index 100% rename from public/public/images/screenshot-desktop.svg rename to public/images/screenshot-desktop.svg diff --git a/public/public/images/screenshot-mobile.svg b/public/images/screenshot-mobile.svg similarity index 100% rename from public/public/images/screenshot-mobile.svg rename to public/images/screenshot-mobile.svg diff --git a/public/public/images/sticker-design.png b/public/images/sticker-design.png similarity index 100% rename from public/public/images/sticker-design.png rename to public/images/sticker-design.png diff --git a/public/public/manifest.webmanifest b/public/manifest.webmanifest similarity index 100% rename from public/public/manifest.webmanifest rename to public/manifest.webmanifest diff --git a/public/public/_redirects b/public/public/_redirects deleted file mode 100644 index f824337..0000000 --- a/public/public/_redirects +++ /dev/null @@ -1 +0,0 @@ -/* /index.html 200 \ No newline at end of file diff --git a/public/public/robots.txt b/public/public/robots.txt deleted file mode 100644 index 14267e9..0000000 --- a/public/public/robots.txt +++ /dev/null @@ -1,2 +0,0 @@ -User-agent: * -Allow: / \ No newline at end of file diff --git a/public/public/sticker-qr.html b/public/sticker-qr.html similarity index 100% rename from public/public/sticker-qr.html rename to public/sticker-qr.html diff --git a/public/public/sticker.html b/public/sticker.html similarity index 100% rename from public/public/sticker.html rename to public/sticker.html diff --git a/src/App.tsx b/src/App.tsx index e94d65c..76cc557 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,7 +3,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { createHead, UnheadProvider } from '@unhead/react/client'; -import { InferSeoMetaPlugin } from 'unhead/plugins'; +import { InferSeoMetaPlugin } from '@unhead/addons'; import { Suspense } from 'react'; import NostrProvider from '@/components/NostrProvider'; import { NostrSync } from '@/components/NostrSync'; @@ -11,8 +11,11 @@ import { Toaster } from "@/components/ui/toaster"; import { TooltipProvider } from "@/components/ui/tooltip"; import { NostrLoginProvider } from '@nostrify/react/login'; import { AppProvider } from '@/components/AppProvider'; +import { NWCProvider } from '@/contexts/NWCContext'; +import { LanguageProvider } from '@/contexts/LanguageContext'; import { AppConfig } from '@/contexts/AppContext'; -import { APP_RELAYS } from '@/lib/appRelays'; +import { PWAInstallPrompt } from '@/components/PWAInstallPrompt'; +import { AmberCallbackHandler } from '@/components/auth/AmberCallbackHandler'; import AppRouter from './AppRouter'; const head = createHead({ @@ -32,37 +35,43 @@ const queryClient = new QueryClient({ }); const defaultConfig: AppConfig = { - theme: "light", - relayMetadata: APP_RELAYS, - blossomServerMetadata: { - servers: [ - 'https://blossom.ditto.pub/', - 'https://blossom.dreamith.to/', - 'https://blossom.primal.net/', + theme: "dark", + relayMetadata: { + relays: [ + { url: 'wss://relay.ditto.pub', read: true, write: true }, + { url: 'wss://goskatespots.nostr1.com', read: true, write: true }, + { url: 'wss://wotr.relatr.xyz', read: true, write: true }, + { url: 'wss://purplepag.es', read: true, write: false }, // Profile aggregator - read only ], updatedAt: 0, }, - useAppBlossomServers: true, + useSkateRelay: true, // Use community relay by default for skate content }; export function App() { return ( - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + ); } diff --git a/src/AppRouter.tsx b/src/AppRouter.tsx index 91ee9df..9ffc49a 100644 --- a/src/AppRouter.tsx +++ b/src/AppRouter.tsx @@ -1,8 +1,41 @@ import { BrowserRouter, Route, Routes } from "react-router-dom"; import { ScrollToTop } from "./components/ScrollToTop"; -import HelloWorld from "./pages/HelloWorld"; import Index from "./pages/Index"; +import MapPage from "./pages/MapPage"; +import EventsPage from "./pages/EventsPage"; +import FoodPage from "./pages/FoodPage"; +import WalletPage from "./pages/WalletPage"; +import SettingsPage from "./pages/SettingsPage"; +import CreateSpotPage from "./pages/CreateSpotPage"; +import CreateShopPage from "./pages/CreateShopPage"; +import CreateEventPage from "./pages/CreateEventPage"; +import CreateListingPage from "./pages/CreateListingPage"; +import CreateTourPage from "./pages/CreateTourPage"; +import EditSpotPage from "./pages/EditSpotPage"; +import EditShopPage from "./pages/EditShopPage"; +import EditEventPage from "./pages/EditEventPage"; +import GearSwapPage from "./pages/GearSwapPage"; +import SpotDetailPage from "./pages/SpotDetailPage"; +import ShopDetailPage from "./pages/ShopDetailPage"; +import EventDetailPage from "./pages/EventDetailPage"; +import OnboardingPage from "./pages/OnboardingPage"; +import PyramidPage from "./pages/PyramidPage"; +import LabPage from "./pages/LabPage"; +import BecomeCoachPage from "./pages/BecomeCoachPage"; +import BecomeFilmerPage from "./pages/BecomeFilmerPage"; +import CoachDetailPage from "./pages/CoachDetailPage"; +import TourDetailPage from "./pages/TourDetailPage"; +import AboutPage from "./pages/AboutPage"; +import FAQPage from "./pages/FAQPage"; +import CommunityStatsPage from "./pages/CommunityStatsPage"; +import SpotCollectionsPage from "./pages/SpotCollectionsPage"; +import CollectionDetailPage from "./pages/CollectionDetailPage"; +import DirectMessagesPage from "./pages/DirectMessagesPage"; +import ConversationPage from "./pages/ConversationPage"; +import BountyLeaderboardPage from "./pages/BountyLeaderboardPage"; +import SignersPage from "./pages/SignersPage"; +import StashPage from "./pages/StashPage"; import { NIP19Page } from "./pages/NIP19Page"; import NotFound from "./pages/NotFound"; @@ -12,7 +45,45 @@ export function AppRouter() { } /> - } /> + } /> + } /> + } /> + {/* Legacy route redirect */} + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + {/* Bounty System */} + } /> + {/* Security & Signers */} + } /> + {/* Stash - Private Inventory */} + } /> {/* NIP-19 route for npub1, note1, naddr1, nevent1, nprofile1 */} } /> {/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */} @@ -21,4 +92,4 @@ export function AppRouter() { ); } -export default AppRouter; \ No newline at end of file +export default AppRouter; diff --git a/src/src/components/AddPhotoToSpot.tsx b/src/components/AddPhotoToSpot.tsx similarity index 100% rename from src/src/components/AddPhotoToSpot.tsx rename to src/components/AddPhotoToSpot.tsx diff --git a/src/src/components/AddTagsToSpot.tsx b/src/components/AddTagsToSpot.tsx similarity index 100% rename from src/src/components/AddTagsToSpot.tsx rename to src/components/AddTagsToSpot.tsx diff --git a/src/src/components/AddToCollectionDialog.tsx b/src/components/AddToCollectionDialog.tsx similarity index 100% rename from src/src/components/AddToCollectionDialog.tsx rename to src/components/AddToCollectionDialog.tsx diff --git a/src/components/AppProvider.tsx b/src/components/AppProvider.tsx index ea86a74..0358861 100644 --- a/src/components/AppProvider.tsx +++ b/src/components/AppProvider.tsx @@ -1,7 +1,7 @@ import { ReactNode, useEffect } from 'react'; import { z } from 'zod'; import { useLocalStorage } from '@/hooks/useLocalStorage'; -import { AppContext, type AppConfig, type AppContextType, type Theme, type RelayMetadata, type BlossomServerMetadata } from '@/contexts/AppContext'; +import { AppContext, type AppConfig, type AppContextType, type Theme, type RelayMetadata } from '@/contexts/AppContext'; interface AppProviderProps { children: ReactNode; @@ -21,18 +21,10 @@ const RelayMetadataSchema = z.object({ updatedAt: z.number(), }) satisfies z.ZodType; -// Zod schema for BlossomServerMetadata validation -const BlossomServerMetadataSchema = z.object({ - servers: z.array(z.string().url()), - updatedAt: z.number(), -}) satisfies z.ZodType; - // Zod schema for AppConfig validation const AppConfigSchema = z.object({ theme: z.enum(['dark', 'light', 'system']), relayMetadata: RelayMetadataSchema, - blossomServerMetadata: BlossomServerMetadataSchema, - useAppBlossomServers: z.boolean(), }) satisfies z.ZodType; export function AppProvider(props: AppProviderProps) { @@ -60,8 +52,46 @@ export function AppProvider(props: AppProviderProps) { setConfig(updater); }; + // Merge configs with special handling for relay metadata const config = { ...defaultConfig, ...rawConfig }; + // Force migration to correct relays and dark theme + useEffect(() => { + let needsUpdate = false; + const updates: Partial = {}; + + // Check relay configuration + if (config.relayMetadata.relays) { + const currentRelayUrls = config.relayMetadata.relays.map(r => r.url).sort().join(','); + const expectedRelayUrls = defaultConfig.relayMetadata.relays.map(r => r.url).sort().join(','); + + if (currentRelayUrls !== expectedRelayUrls) { + console.log('Migrating relays to:', defaultConfig.relayMetadata.relays.map(r => r.url)); + updates.relayMetadata = defaultConfig.relayMetadata; + needsUpdate = true; + } + } + + // Force dark theme (app is dark-only now) + if (config.theme !== 'dark') { + console.log('Forcing dark theme'); + updates.theme = 'dark'; + needsUpdate = true; + } + + if (needsUpdate) { + updateConfig((current) => ({ + ...current, + ...updates, + })); + } + }, []); + + // Ensure relay list is never empty - fallback to default if no relays configured + if (!config.relayMetadata.relays || config.relayMetadata.relays.length === 0) { + config.relayMetadata = defaultConfig.relayMetadata; + } + const appContextValue: AppContextType = { config, updateConfig, diff --git a/src/src/components/BackupReminder.tsx b/src/components/BackupReminder.tsx similarity index 100% rename from src/src/components/BackupReminder.tsx rename to src/components/BackupReminder.tsx diff --git a/src/src/components/CameraCapture.tsx b/src/components/CameraCapture.tsx similarity index 100% rename from src/src/components/CameraCapture.tsx rename to src/components/CameraCapture.tsx diff --git a/src/src/components/EditProfileForm.tsx b/src/components/EditProfileForm.tsx similarity index 100% rename from src/src/components/EditProfileForm.tsx rename to src/components/EditProfileForm.tsx diff --git a/src/src/components/EventCard.tsx b/src/components/EventCard.tsx similarity index 100% rename from src/src/components/EventCard.tsx rename to src/components/EventCard.tsx diff --git a/src/src/components/FollowListManager.tsx b/src/components/FollowListManager.tsx similarity index 100% rename from src/src/components/FollowListManager.tsx rename to src/components/FollowListManager.tsx diff --git a/src/src/components/FounderFollow.tsx b/src/components/FounderFollow.tsx similarity index 100% rename from src/src/components/FounderFollow.tsx rename to src/components/FounderFollow.tsx diff --git a/src/src/components/GearSwapItemCard.tsx b/src/components/GearSwapItemCard.tsx similarity index 100% rename from src/src/components/GearSwapItemCard.tsx rename to src/components/GearSwapItemCard.tsx diff --git a/src/src/components/LanguageToggle.tsx b/src/components/LanguageToggle.tsx similarity index 100% rename from src/src/components/LanguageToggle.tsx rename to src/components/LanguageToggle.tsx diff --git a/src/src/components/Layout.tsx b/src/components/Layout.tsx similarity index 100% rename from src/src/components/Layout.tsx rename to src/components/Layout.tsx diff --git a/src/src/components/MarketplaceItemCard.tsx b/src/components/MarketplaceItemCard.tsx similarity index 100% rename from src/src/components/MarketplaceItemCard.tsx rename to src/components/MarketplaceItemCard.tsx diff --git a/src/src/components/MyDataTab.tsx b/src/components/MyDataTab.tsx similarity index 100% rename from src/src/components/MyDataTab.tsx rename to src/components/MyDataTab.tsx diff --git a/src/components/NostrProvider.tsx b/src/components/NostrProvider.tsx index b057cf7..091703a 100644 --- a/src/components/NostrProvider.tsx +++ b/src/components/NostrProvider.tsx @@ -1,126 +1,113 @@ -import React, { useEffect, useMemo, useRef, useState } from 'react'; -import { type NostrSigner, NostrEvent, NostrFilter, NPool, NRelay1 } from '@nostrify/nostrify'; +import React, { useEffect, useRef } from 'react'; +import { NostrEvent, NostrFilter, NPool, NRelay1 } from '@nostrify/nostrify'; import { NostrContext } from '@nostrify/react'; -import { NUser, useNostrLogin } from '@nostrify/react/login'; import { useQueryClient } from '@tanstack/react-query'; import { useAppContext } from '@/hooks/useAppContext'; +import { SKATE_RELAY } from '@/lib/types'; interface NostrProviderProps { children: React.ReactNode; } +// Default fallback relays - used if no relays are configured +const DEFAULT_RELAYS = [ + { url: 'wss://relay.ditto.pub', read: true, write: true }, + { url: SKATE_RELAY, read: true, write: true }, + { url: 'wss://wotr.relatr.xyz', read: true, write: true }, + { url: 'wss://purplepag.es', read: true, write: false }, // Profile aggregator +]; + const NostrProvider: React.FC = (props) => { const { children } = props; const { config } = useAppContext(); - const { logins } = useNostrLogin(); const queryClient = useQueryClient(); - // Use refs so the pool callbacks always see the latest data without - // recreating the pool. The refs are written from effects (never during - // render) to satisfy React's purity rules. - const relayMetadataRef = useRef(config.relayMetadata); + // Create NPool instance only once + const pool = useRef(undefined); - // Stable ref to the current user's signer for NIP-42 AUTH. - // The `open()` callback reads from this ref when a relay sends an AUTH - // challenge, so it always uses the latest signer without recreating the pool. - const signerRef = useRef(undefined); + // Use refs so the pool always has the latest data + const relayMetadata = useRef(config.relayMetadata); + const useSkateRelay = useRef(config.useSkateRelay ?? true); - // Lazily create the pool once, via useState's initializer. This keeps - // the render body pure (no ref writes) while guaranteeing a single - // NPool instance per provider. The initializer runs exactly once at - // mount, so reading refs inside its closures is safe. - // eslint-disable-next-line react-hooks/refs - const [pool] = useState(() => new NPool({ - open(url: string) { - return new NRelay1(url, { - // NIP-42: Respond to relay AUTH challenges by signing a kind - // 22242 ephemeral event with the current user's signer. - auth: async (challenge: string) => { - const signer = signerRef.current; - if (!signer) { - throw new Error('AUTH failed: no signer available (user not logged in)'); - } - return signer.signEvent({ - kind: 22242, - content: '', - tags: [ - ['relay', url], - ['challenge', challenge], - ], - created_at: Math.floor(Date.now() / 1000), - }); - }, - }); - }, - reqRouter(filters: NostrFilter[]) { - const routes = new Map(); - - // Route to all read relays - const readRelays = relayMetadataRef.current.relays - .filter(r => r.read) - .map(r => r.url); - - for (const url of readRelays) { - routes.set(url, filters); - } - - return routes; - }, - eventRouter(_event: NostrEvent) { - // Get write relays from metadata - const writeRelays = relayMetadataRef.current.relays - .filter(r => r.write) - .map(r => r.url); - - const allRelays = new Set(writeRelays); - - return [...allRelays]; - }, - eoseTimeout: 200, - })); - - // Derive the current signer from the active login. This mirrors the - // logic in useCurrentUser but avoids a circular dependency (useCurrentUser - // depends on NostrContext which we are providing here). - const currentLogin = logins[0]; - const currentSigner = useMemo(() => { - if (!currentLogin) return undefined; - try { - switch (currentLogin.type) { - case 'nsec': - return NUser.fromNsecLogin(currentLogin).signer; - case 'bunker': - return NUser.fromBunkerLogin(currentLogin, pool).signer; - case 'extension': - return NUser.fromExtensionLogin(currentLogin).signer; - default: - return undefined; - } - } catch { - return undefined; - } - }, [currentLogin, pool]); - - // Keep the ref in sync so the AUTH callback always sees the latest signer. - // Writing refs from an effect (not during render) satisfies purity rules. + // Invalidate Nostr queries when relay metadata changes useEffect(() => { - signerRef.current = currentSigner; - }, [currentSigner]); - - // Invalidate Nostr queries when relay metadata changes. - useEffect(() => { - relayMetadataRef.current = config.relayMetadata; + relayMetadata.current = config.relayMetadata; + useSkateRelay.current = config.useSkateRelay ?? true; queryClient.invalidateQueries({ queryKey: ['nostr'] }); - }, [config.relayMetadata, queryClient]); + }, [config.relayMetadata, config.useSkateRelay, queryClient]); - const contextValue = useMemo(() => ({ nostr: pool }), [pool]); + // Initialize NPool only once + if (!pool.current) { + pool.current = new NPool({ + open(url: string) { + return new NRelay1(url); + }, + reqRouter(filters: NostrFilter[]) { + const routes = new Map(); + + // Get relays from metadata, fallback to defaults if empty + const configuredRelays = relayMetadata.current.relays.length > 0 + ? relayMetadata.current.relays + : DEFAULT_RELAYS; + + // Route to all read relays + const readRelays = configuredRelays + .filter(r => r.read) + .map(r => r.url); + + // Ensure we always have at least one relay + if (readRelays.length === 0) { + console.warn('[NostrProvider] No read relays configured, using defaults'); + for (const relay of DEFAULT_RELAYS) { + routes.set(relay.url, filters); + } + } else { + console.log('[NostrProvider] Routing query to read relays:', readRelays); + for (const url of readRelays) { + routes.set(url, filters); + } + } + + return routes; + }, + eventRouter(event: NostrEvent) { + // Get relays from metadata, fallback to defaults if empty + const configuredRelays = relayMetadata.current.relays.length > 0 + ? relayMetadata.current.relays + : DEFAULT_RELAYS; + + // Get write relays from metadata + const writeRelays = configuredRelays + .filter(r => r.write) + .map(r => r.url); + + const allRelays = new Set(writeRelays); + + // Include community relay if setting is enabled (default: true) + // This ensures skate content is published to the community relay for discoverability + if (useSkateRelay.current) { + allRelays.add(SKATE_RELAY); + } + + // Ensure we always have at least one relay + if (allRelays.size === 0) { + console.warn('No write relays configured, using defaults'); + DEFAULT_RELAYS.forEach(r => allRelays.add(r.url)); + } + + console.log(`[NostrProvider] Publishing kind ${event.kind} to relays:`, [...allRelays]); + + return [...allRelays]; + }, + }); + } return ( - + {children} ); }; -export default NostrProvider; +export default NostrProvider; \ No newline at end of file diff --git a/src/components/NostrSync.tsx b/src/components/NostrSync.tsx index d9bbefd..9296e87 100644 --- a/src/components/NostrSync.tsx +++ b/src/components/NostrSync.tsx @@ -1,30 +1,97 @@ -import { useEffect } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { useNostr } from '@nostrify/react'; +import { useNostrLogin } from '@nostrify/react/login'; +import { useQueryClient } from '@tanstack/react-query'; +import { NSchema as n } from '@nostrify/nostrify'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { useAppContext } from '@/hooks/useAppContext'; -import { parseBlossomServerList } from '@/lib/appBlossom'; +import { useToast } from '@/hooks/useToast'; + +// Default relays for new users +const NEW_USER_RELAYS = [ + { url: 'wss://relay.ditto.pub', read: true, write: true }, + { url: 'wss://goskatespots.nostr1.com', read: true, write: true }, + { url: 'wss://wotr.relatr.xyz', read: true, write: true }, + { url: 'wss://purplepag.es', read: true, write: false }, // Profile aggregator +]; + +// Fallback relays for fetching profile data (aggregators that index many profiles) +const FALLBACK_RELAYS = [ + 'wss://purplepag.es', + 'wss://relay.nostr.band', + 'wss://relay.damus.io', + 'wss://nos.lol', +]; /** - * NostrSync - Syncs user's Nostr data + * NostrSync - Syncs user's Nostr data on login * * This component runs globally to sync various Nostr data when the user logs in. + * It fetches data and populates TanStack Query cache so other hooks don't need to refetch. + * * Currently syncs: * - NIP-65 relay list (kind 10002) - * - BUD-03 Blossom server list (kind 10063) + * - Profile metadata (kind 0) - cached for useAuthor hook + * + * New users (without a published relay list) get default relays instead of + * inheriting from previous browser sessions. */ export function NostrSync() { const { nostr } = useNostr(); const { user } = useCurrentUser(); + const { logins } = useNostrLogin(); const { config, updateConfig } = useAppContext(); + const { toast } = useToast(); + const queryClient = useQueryClient(); + const syncedUsers = useRef>(new Set()); + const [syncStatus, setSyncStatus] = useState<'idle' | 'syncing' | 'success' | 'error'>('idle'); useEffect(() => { if (!user) return; - const syncRelaysFromNostr = async () => { - try { + // Only sync once per user per session + if (syncedUsers.current.has(user.pubkey)) return; + + const syncWithRetry = async ( + queryFn: () => Promise, + maxRetries: number = 5, + initialDelayMs: number = 1000 + ): Promise => { + for (let attempt = 1; attempt <= maxRetries; attempt++) { + try { + return await queryFn(); + } catch (error) { + console.warn(`[NostrSync] Attempt ${attempt}/${maxRetries} failed:`, error); + if (attempt < maxRetries) { + // Exponential backoff: 1s, 2s, 4s, 8s, 16s + const delay = initialDelayMs * Math.pow(2, attempt - 1); + await new Promise(resolve => setTimeout(resolve, delay)); + } + } + } + return null; + }; + + const syncUserData = async () => { + setSyncStatus('syncing'); + console.log('[NostrSync] Starting sync for user:', user.pubkey.slice(0, 8) + '...'); + + // FIX B: Invalidate any stale cached data for this user BEFORE fetching + // This ensures we don't use old cached empty results + queryClient.invalidateQueries({ queryKey: ['author', user.pubkey] }); + + // Also invalidate the logins cache so useLoggedInAccounts refetches + const loginsKey = logins.map((l) => l.id).join(';'); + queryClient.invalidateQueries({ queryKey: ['nostr', 'logins', loginsKey] }); + + let relaysSynced = false; + let profileSynced = false; + + // Sync relay list (NIP-65) + const syncRelays = async (): Promise => { const events = await nostr.query( [{ kinds: [10002], authors: [user.pubkey], limit: 1 }], - { signal: AbortSignal.timeout(5000) } + { signal: AbortSignal.timeout(10000) } ); if (events.length > 0) { @@ -34,14 +101,14 @@ export function NostrSync() { if (event.created_at > config.relayMetadata.updatedAt) { const fetchedRelays = event.tags .filter(([name]) => name === 'r') - .map(([_, url, marker]) => ({ + .map(([, url, marker]) => ({ url, read: !marker || marker === 'read', write: !marker || marker === 'write', })); if (fetchedRelays.length > 0) { - console.log('Syncing relay list from Nostr:', fetchedRelays); + console.log('[NostrSync] Synced relay list from NIP-65:', fetchedRelays.length, 'relays'); updateConfig((current) => ({ ...current, relayMetadata: { @@ -49,53 +116,136 @@ export function NostrSync() { updatedAt: event.created_at, }, })); + return true; } + } else { + return true; // Already have latest } + } else { + // No NIP-65 relay list found - this is a new user! + console.log('[NostrSync] New user - setting default relays'); + updateConfig((current) => ({ + ...current, + relayMetadata: { + relays: NEW_USER_RELAYS, + updatedAt: 0, + }, + })); + return true; } - } catch (error) { - console.error('Failed to sync relays from Nostr:', error); - } - }; + return false; + }; - syncRelaysFromNostr(); - }, [user, config.relayMetadata.updatedAt, nostr, updateConfig]); - - useEffect(() => { - if (!user) return; - - const syncBlossomServersFromNostr = async () => { - try { - const events = await nostr.query( - [{ kinds: [10063], authors: [user.pubkey], limit: 1 }], - { signal: AbortSignal.timeout(5000) } + // Sync profile (kind 0) and populate useAuthor cache + const syncProfile = async (relayGroup?: ReturnType): Promise => { + const querySource = relayGroup || nostr; + console.log('[NostrSync] Querying profile for pubkey:', user.pubkey.slice(0, 8) + '...'); + const events = await querySource.query( + [{ kinds: [0], authors: [user.pubkey], limit: 1 }], + { signal: AbortSignal.timeout(10000) } ); - + console.log('[NostrSync] Profile query returned', events.length, 'events'); + if (events.length > 0) { const event = events[0]; - - // Only update if the event is newer than our stored data - if (event.created_at > config.blossomServerMetadata.updatedAt) { - const fetchedServers = parseBlossomServerList(event); - - if (fetchedServers.length > 0) { - console.log('Syncing Blossom server list from Nostr:', fetchedServers); - updateConfig((current) => ({ - ...current, - blossomServerMetadata: { - servers: fetchedServers, - updatedAt: event.created_at, - }, - })); - } + + // FIX A: Parse metadata and populate useAuthor's TanStack Query cache + try { + const metadata = n.json().pipe(n.metadata()).parse(event.content); + + // Set the cache data that useAuthor expects + queryClient.setQueryData(['author', user.pubkey], { + metadata, + event + }); + + // FIX A+: Also update useLoggedInAccounts cache so profile shows everywhere + const loginsKey = logins.map((l) => l.id).join(';'); + queryClient.setQueryData(['nostr', 'logins', loginsKey], (oldData: Array<{ id: string; pubkey: string; metadata: typeof metadata; event?: typeof event }> | undefined) => { + if (!oldData) { + // Create initial data from logins + return logins.map((login) => { + if (login.pubkey === user.pubkey) { + return { id: login.id, pubkey: login.pubkey, metadata, event }; + } + return { id: login.id, pubkey: login.pubkey, metadata: {} }; + }); + } + // Update existing data + return oldData.map((account) => { + if (account.pubkey === user.pubkey) { + return { ...account, metadata, event }; + } + return account; + }); + }); + + console.log('[NostrSync] Profile synced and cached:', metadata.name || 'unnamed'); + return true; + } catch (parseError) { + // Event exists but content couldn't be parsed - still cache the event + queryClient.setQueryData(['author', user.pubkey], { event }); + console.warn('[NostrSync] Profile event found but metadata parse failed'); + return true; } } - } catch (error) { - console.error('Failed to sync Blossom servers from Nostr:', error); + return false; + }; + + // Try main relays first with retry + const mainResult = await syncWithRetry(async () => { + const [relayResult, profileResult] = await Promise.all([ + syncRelays(), + syncProfile() + ]); + return { relays: relayResult, profile: profileResult }; + }, 3, 1500); + + if (mainResult) { + relaysSynced = mainResult.relays; + profileSynced = mainResult.profile; + } + + // If profile not found on main relays, try fallback relays + if (!profileSynced) { + console.log('[NostrSync] Profile not found on main relays, trying fallbacks...'); + + const fallbackResult = await syncWithRetry(async () => { + const fallbackGroup = nostr.group(FALLBACK_RELAYS); + return await syncProfile(fallbackGroup); + }, 3, 2000); + + if (fallbackResult) { + profileSynced = true; + } + } + + // Mark sync complete for this user + syncedUsers.current.add(user.pubkey); + + if (relaysSynced || profileSynced) { + setSyncStatus('success'); + console.log('[NostrSync] Sync completed - relays:', relaysSynced, 'profile:', profileSynced); + } else { + setSyncStatus('error'); + console.error('[NostrSync] Sync failed - could not reach relays'); + toast({ + title: 'Connection Issue', + description: 'Could not sync your profile. Check your internet connection.', + variant: 'destructive', + }); } }; - syncBlossomServersFromNostr(); - }, [user, config.blossomServerMetadata.updatedAt, nostr, updateConfig]); + // FIX C: Longer initial delay to allow relay connections to establish + // This helps with external signers like Amber where PWA context may need time + // to re-establish WebSocket connections after returning from the signer app + const timeoutId = setTimeout(() => { + syncUserData(); + }, 800); + + return () => clearTimeout(timeoutId); + }, [user, config.relayMetadata.updatedAt, nostr, updateConfig, toast, queryClient, logins]); return null; } diff --git a/src/src/components/NoteContent.test.tsx b/src/components/NoteContent.test.tsx similarity index 100% rename from src/src/components/NoteContent.test.tsx rename to src/components/NoteContent.test.tsx diff --git a/src/src/components/NoteContent.tsx b/src/components/NoteContent.tsx similarity index 100% rename from src/src/components/NoteContent.tsx rename to src/components/NoteContent.tsx diff --git a/src/src/components/OfflineIndicator.tsx b/src/components/OfflineIndicator.tsx similarity index 100% rename from src/src/components/OfflineIndicator.tsx rename to src/components/OfflineIndicator.tsx diff --git a/src/src/components/PWAInstallPrompt.tsx b/src/components/PWAInstallPrompt.tsx similarity index 100% rename from src/src/components/PWAInstallPrompt.tsx rename to src/components/PWAInstallPrompt.tsx diff --git a/src/src/components/QualityMeter.tsx b/src/components/QualityMeter.tsx similarity index 100% rename from src/src/components/QualityMeter.tsx rename to src/components/QualityMeter.tsx diff --git a/src/src/components/RelatrSettings.tsx b/src/components/RelatrSettings.tsx similarity index 100% rename from src/src/components/RelatrSettings.tsx rename to src/components/RelatrSettings.tsx diff --git a/src/src/components/RelayListManager.tsx b/src/components/RelayListManager.tsx similarity index 100% rename from src/src/components/RelayListManager.tsx rename to src/components/RelayListManager.tsx diff --git a/src/src/components/ReportDialog.tsx b/src/components/ReportDialog.tsx similarity index 100% rename from src/src/components/ReportDialog.tsx rename to src/components/ReportDialog.tsx diff --git a/src/src/components/SasquatchGuide.tsx b/src/components/SasquatchGuide.tsx similarity index 100% rename from src/src/components/SasquatchGuide.tsx rename to src/components/SasquatchGuide.tsx diff --git a/src/src/components/SecuritySettings.tsx b/src/components/SecuritySettings.tsx similarity index 100% rename from src/src/components/SecuritySettings.tsx rename to src/components/SecuritySettings.tsx diff --git a/src/src/components/SetStatusDialog.tsx b/src/components/SetStatusDialog.tsx similarity index 100% rename from src/src/components/SetStatusDialog.tsx rename to src/components/SetStatusDialog.tsx diff --git a/src/src/components/ShopBountyCard.tsx b/src/components/ShopBountyCard.tsx similarity index 100% rename from src/src/components/ShopBountyCard.tsx rename to src/components/ShopBountyCard.tsx diff --git a/src/src/components/SortDropdown.tsx b/src/components/SortDropdown.tsx similarity index 100% rename from src/src/components/SortDropdown.tsx rename to src/components/SortDropdown.tsx diff --git a/src/src/components/SpamFeePayment.tsx b/src/components/SpamFeePayment.tsx similarity index 100% rename from src/src/components/SpamFeePayment.tsx rename to src/components/SpamFeePayment.tsx diff --git a/src/src/components/SponsorSlot.tsx b/src/components/SponsorSlot.tsx similarity index 100% rename from src/src/components/SponsorSlot.tsx rename to src/components/SponsorSlot.tsx diff --git a/src/src/components/SpotCard.tsx b/src/components/SpotCard.tsx similarity index 100% rename from src/src/components/SpotCard.tsx rename to src/components/SpotCard.tsx diff --git a/src/src/components/SpotMap.tsx b/src/components/SpotMap.tsx similarity index 100% rename from src/src/components/SpotMap.tsx rename to src/components/SpotMap.tsx diff --git a/src/src/components/SpotOwnership.tsx b/src/components/SpotOwnership.tsx similarity index 100% rename from src/src/components/SpotOwnership.tsx rename to src/components/SpotOwnership.tsx diff --git a/src/src/components/SpotPicker.tsx b/src/components/SpotPicker.tsx similarity index 100% rename from src/src/components/SpotPicker.tsx rename to src/components/SpotPicker.tsx diff --git a/src/src/components/TagInput.tsx b/src/components/TagInput.tsx similarity index 100% rename from src/src/components/TagInput.tsx rename to src/components/TagInput.tsx diff --git a/src/src/components/ThemeToggle.tsx b/src/components/ThemeToggle.tsx similarity index 100% rename from src/src/components/ThemeToggle.tsx rename to src/components/ThemeToggle.tsx diff --git a/src/src/components/TourCard.tsx b/src/components/TourCard.tsx similarity index 100% rename from src/src/components/TourCard.tsx rename to src/components/TourCard.tsx diff --git a/src/src/components/TrustBadge.tsx b/src/components/TrustBadge.tsx similarity index 100% rename from src/src/components/TrustBadge.tsx rename to src/components/TrustBadge.tsx diff --git a/src/src/components/TrustFilter.tsx b/src/components/TrustFilter.tsx similarity index 100% rename from src/src/components/TrustFilter.tsx rename to src/components/TrustFilter.tsx diff --git a/src/src/components/UserStatusIndicator.tsx b/src/components/UserStatusIndicator.tsx similarity index 100% rename from src/src/components/UserStatusIndicator.tsx rename to src/components/UserStatusIndicator.tsx diff --git a/src/src/components/WalletModal.tsx b/src/components/WalletModal.tsx similarity index 100% rename from src/src/components/WalletModal.tsx rename to src/components/WalletModal.tsx diff --git a/src/src/components/WeatherToggle.tsx b/src/components/WeatherToggle.tsx similarity index 100% rename from src/src/components/WeatherToggle.tsx rename to src/components/WeatherToggle.tsx diff --git a/src/src/components/WhosSkating.tsx b/src/components/WhosSkating.tsx similarity index 100% rename from src/src/components/WhosSkating.tsx rename to src/components/WhosSkating.tsx diff --git a/src/src/components/ZapButton.tsx b/src/components/ZapButton.tsx similarity index 100% rename from src/src/components/ZapButton.tsx rename to src/components/ZapButton.tsx diff --git a/src/src/components/ZapDialog.tsx b/src/components/ZapDialog.tsx similarity index 100% rename from src/src/components/ZapDialog.tsx rename to src/components/ZapDialog.tsx diff --git a/src/components/auth/AccountSwitcher.tsx b/src/components/auth/AccountSwitcher.tsx index 9309d42..a3d7b0c 100644 --- a/src/components/auth/AccountSwitcher.tsx +++ b/src/components/auth/AccountSwitcher.tsx @@ -1,4 +1,7 @@ -import { ChevronDown, LogOut, UserIcon, UserPlus } from 'lucide-react'; +// NOTE: This file is stable and usually should not be modified. +// It is important that all functionality in this file is preserved, and should only be modified if explicitly requested. + +import { ChevronDown, LogOut, UserIcon, UserPlus, Wallet } from 'lucide-react'; import { DropdownMenu, DropdownMenuContent, @@ -7,81 +10,83 @@ import { DropdownMenuTrigger, } from '@/components/ui/dropdown-menu.tsx'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar.tsx'; -import { Skeleton } from '@/components/ui/skeleton.tsx'; +import { WalletModal } from '@/components/WalletModal'; import { useLoggedInAccounts, type Account } from '@/hooks/useLoggedInAccounts'; +import { genUserName } from '@/lib/genUserName'; interface AccountSwitcherProps { onAddAccountClick: () => void; } export function AccountSwitcher({ onAddAccountClick }: AccountSwitcherProps) { - const { currentUser, otherUsers, isLoading, setLogin, removeLogin } = useLoggedInAccounts(); + const { currentUser, otherUsers, setLogin, removeLogin } = useLoggedInAccounts(); if (!currentUser) return null; const getDisplayName = (account: Account): string => { - return account.metadata.name ?? 'Anonymous'; + return account.metadata.name ?? genUserName(account.pubkey); } - // While the metadata query is in-flight and we don't yet have a name, - // we don't want to flash a generated animal name / its first letter. - const isCurrentUserPending = isLoading && !currentUser.metadata.name; + const getNip05 = (account: Account): string | undefined => { + return account.metadata.nip05; + } return ( -
Switch Account
- {otherUsers.map((user) => { - const isPending = isLoading && !user.metadata.name; - return ( - setLogin(user.id)} - className='flex items-center gap-2 cursor-pointer p-2 rounded-md' - > - - - - {isPending ? ( - - ) : ( - getDisplayName(user)?.charAt(0) || - )} - - -
- {isPending ? ( - - ) : ( -

{getDisplayName(user)}

- )} -
- {user.id === currentUser.id &&
} -
- ); - })} + {otherUsers.map((user) => ( + setLogin(user.id)} + className='flex items-center gap-2 cursor-pointer p-2 rounded-md' + > + + + {getDisplayName(user)?.charAt(0) || } + +
+

{getDisplayName(user)}

+ {getNip05(user) && ( +

+ {getNip05(user)} +

+ )} +
+ {user.id === currentUser.id &&
} +
+ ))} + + e.preventDefault()} + > + + Wallet Settings + +
); -} +} \ No newline at end of file diff --git a/src/src/components/auth/AmberCallbackHandler.tsx b/src/components/auth/AmberCallbackHandler.tsx similarity index 100% rename from src/src/components/auth/AmberCallbackHandler.tsx rename to src/components/auth/AmberCallbackHandler.tsx diff --git a/src/components/auth/LoginArea.tsx b/src/components/auth/LoginArea.tsx index cb401a9..be48e02 100644 --- a/src/components/auth/LoginArea.tsx +++ b/src/components/auth/LoginArea.tsx @@ -1,35 +1,116 @@ +// NOTE: This file is stable and usually should not be modified. +// It is important that all functionality in this file is preserved, and should only be modified if explicitly requested. + import { useState } from 'react'; +import { User, UserPlus } from 'lucide-react'; import { Button } from '@/components/ui/button.tsx'; -import AuthDialog from './AuthDialog'; +import LoginDialog from './LoginDialog'; +import SignupDialog from './SignupDialog'; +import SignupMethodDialog from './SignupMethodDialog'; +import MnemonicSignupDialog from './MnemonicSignupDialog'; import { useLoggedInAccounts } from '@/hooks/useLoggedInAccounts'; import { AccountSwitcher } from './AccountSwitcher'; import { cn } from '@/lib/utils'; export interface LoginAreaProps { className?: string; + /** Compact mode for mobile headers - smaller buttons without text */ + compact?: boolean; } -export function LoginArea({ className }: LoginAreaProps) { +export function LoginArea({ className, compact }: LoginAreaProps) { const { currentUser } = useLoggedInAccounts(); - const [authDialogOpen, setAuthDialogOpen] = useState(false); + const [loginDialogOpen, setLoginDialogOpen] = useState(false); + const [signupMethodDialogOpen, setSignupMethodDialogOpen] = useState(false); + const [mnemonicSignupDialogOpen, setMnemonicSignupDialogOpen] = useState(false); + const [nsecSignupDialogOpen, setNsecSignupDialogOpen] = useState(false); + + const handleLogin = () => { + setLoginDialogOpen(false); + setSignupMethodDialogOpen(false); + setMnemonicSignupDialogOpen(false); + setNsecSignupDialogOpen(false); + }; return ( -
+
{currentUser ? ( - setAuthDialogOpen(true)} /> + setLoginDialogOpen(true)} /> + ) : compact ? ( + // Compact mode for mobile headers - icon only +
+ + +
) : ( - + // Default full-size buttons +
+ + +
)} - setAuthDialogOpen(false)} + setLoginDialogOpen(false)} + onLogin={handleLogin} + onSignup={() => setSignupMethodDialogOpen(true)} + /> + + setSignupMethodDialogOpen(false)} + onSelectMnemonic={() => { + setSignupMethodDialogOpen(false); + setMnemonicSignupDialogOpen(true); + }} + onSelectNsec={() => { + setSignupMethodDialogOpen(false); + setNsecSignupDialogOpen(true); + }} + /> + + setMnemonicSignupDialogOpen(false)} + onSwitchToNsec={() => { + setMnemonicSignupDialogOpen(false); + setNsecSignupDialogOpen(true); + }} + /> + + setNsecSignupDialogOpen(false)} />
); -} +} \ No newline at end of file diff --git a/src/src/components/auth/LoginDialog.tsx b/src/components/auth/LoginDialog.tsx similarity index 100% rename from src/src/components/auth/LoginDialog.tsx rename to src/components/auth/LoginDialog.tsx diff --git a/src/src/components/auth/MnemonicSignupDialog.tsx b/src/components/auth/MnemonicSignupDialog.tsx similarity index 100% rename from src/src/components/auth/MnemonicSignupDialog.tsx rename to src/components/auth/MnemonicSignupDialog.tsx diff --git a/src/src/components/auth/SignupDialog.tsx b/src/components/auth/SignupDialog.tsx similarity index 100% rename from src/src/components/auth/SignupDialog.tsx rename to src/components/auth/SignupDialog.tsx diff --git a/src/src/components/auth/SignupMethodDialog.tsx b/src/components/auth/SignupMethodDialog.tsx similarity index 100% rename from src/src/components/auth/SignupMethodDialog.tsx rename to src/components/auth/SignupMethodDialog.tsx diff --git a/src/src/components/comments/Comment.tsx b/src/components/comments/Comment.tsx similarity index 100% rename from src/src/components/comments/Comment.tsx rename to src/components/comments/Comment.tsx diff --git a/src/src/components/comments/CommentForm.tsx b/src/components/comments/CommentForm.tsx similarity index 100% rename from src/src/components/comments/CommentForm.tsx rename to src/components/comments/CommentForm.tsx diff --git a/src/src/components/comments/CommentsSection.tsx b/src/components/comments/CommentsSection.tsx similarity index 100% rename from src/src/components/comments/CommentsSection.tsx rename to src/components/comments/CommentsSection.tsx diff --git a/src/src/components/onboarding/RelayEducation.tsx b/src/components/onboarding/RelayEducation.tsx similarity index 100% rename from src/src/components/onboarding/RelayEducation.tsx rename to src/components/onboarding/RelayEducation.tsx diff --git a/src/components/ui/accordion.tsx b/src/components/ui/accordion.tsx index 70c31c4..e6a723d 100644 --- a/src/components/ui/accordion.tsx +++ b/src/components/ui/accordion.tsx @@ -1,64 +1,56 @@ import * as React from "react" -import { ChevronDownIcon } from "lucide-react" import * as AccordionPrimitive from "@radix-ui/react-accordion" +import { ChevronDown } from "lucide-react" import { cn } from "@/lib/utils" -function Accordion({ - ...props -}: React.ComponentProps) { - return -} +const Accordion = AccordionPrimitive.Root -function AccordionItem({ - className, - ...props -}: React.ComponentProps) { - return ( - - ) -} +const AccordionItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AccordionItem.displayName = "AccordionItem" -function AccordionTrigger({ - className, - children, - ...props -}: React.ComponentProps) { - return ( - - svg]:rotate-180", - className - )} - {...props} - > - {children} - - - - ) -} - -function AccordionContent({ - className, - children, - ...props -}: React.ComponentProps) { - return ( - , + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + svg]:rotate-180", + className + )} {...props} > -
{children}
-
- ) -} + {children} + + + +)) +AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName + +const AccordionContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + +
{children}
+
+)) + +AccordionContent.displayName = AccordionPrimitive.Content.displayName export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } diff --git a/src/components/ui/alert-dialog.tsx b/src/components/ui/alert-dialog.tsx index f9684af..4bd7937 100644 --- a/src/components/ui/alert-dialog.tsx +++ b/src/components/ui/alert-dialog.tsx @@ -1,196 +1,139 @@ -"use client" - import * as React from "react" import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" import { cn } from "@/lib/utils" -import { Button } from "@/components/ui/button" +import { buttonVariants } from "@/components/ui/button-variants" -function AlertDialog({ - ...props -}: React.ComponentProps) { - return -} +const AlertDialog = AlertDialogPrimitive.Root -function AlertDialogTrigger({ - ...props -}: React.ComponentProps) { - return ( - - ) -} +const AlertDialogTrigger = AlertDialogPrimitive.Trigger -function AlertDialogPortal({ - ...props -}: React.ComponentProps) { - return ( - - ) -} +const AlertDialogPortal = AlertDialogPrimitive.Portal -function AlertDialogOverlay({ - className, - ...props -}: React.ComponentProps) { - return ( - , + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName + +const AlertDialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + - ) -} + +)) +AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName -function AlertDialogContent({ - className, - size = "default", - ...props -}: React.ComponentProps & { - size?: "default" | "sm" -}) { - return ( - - - - - ) -} - -function AlertDialogHeader({ +const AlertDialogHeader = ({ className, ...props -}: React.ComponentProps<"div">) { - return ( -
- ) -} +}: React.HTMLAttributes) => ( +
+) +AlertDialogHeader.displayName = "AlertDialogHeader" -function AlertDialogFooter({ +const AlertDialogFooter = ({ className, ...props -}: React.ComponentProps<"div">) { - return ( -
- ) -} +}: React.HTMLAttributes) => ( +
+) +AlertDialogFooter.displayName = "AlertDialogFooter" -function AlertDialogTitle({ - className, - ...props -}: React.ComponentProps) { - return ( - - ) -} +const AlertDialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName -function AlertDialogDescription({ - className, - ...props -}: React.ComponentProps) { - return ( - - ) -} +const AlertDialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogDescription.displayName = + AlertDialogPrimitive.Description.displayName -function AlertDialogMedia({ - className, - ...props -}: React.ComponentProps<"div">) { - return ( -
- ) -} +const AlertDialogAction = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName -function AlertDialogAction({ - className, - variant = "default", - size = "default", - ...props -}: React.ComponentProps & - Pick, "variant" | "size">) { - return ( - - ) -} - -function AlertDialogCancel({ - className, - variant = "outline", - size = "default", - ...props -}: React.ComponentProps & - Pick, "variant" | "size">) { - return ( - - ) -} +const AlertDialogCancel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName export { AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, AlertDialogAction, AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogMedia, - AlertDialogOverlay, - AlertDialogPortal, - AlertDialogTitle, - AlertDialogTrigger, } diff --git a/src/components/ui/alert.tsx b/src/components/ui/alert.tsx index 0d4ddaf..41fa7e0 100644 --- a/src/components/ui/alert.tsx +++ b/src/components/ui/alert.tsx @@ -4,13 +4,13 @@ import { cva, type VariantProps } from "class-variance-authority" import { cn } from "@/lib/utils" const alertVariants = cva( - "relative grid w-full grid-cols-[0_1fr] items-start gap-y-0.5 rounded-lg border px-4 py-3 text-sm has-[>svg]:grid-cols-[--spacing(4)_1fr] has-[>svg]:gap-x-3 [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current", + "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground", { variants: { variant: { - default: "bg-card text-card-foreground", + default: "bg-background text-foreground", destructive: - "bg-card text-destructive *:data-[slot=alert-description]:text-destructive/90 [&>svg]:text-current", + "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", }, }, defaultVariants: { @@ -19,48 +19,41 @@ const alertVariants = cva( } ) -function Alert({ - className, - variant, - ...props -}: React.ComponentProps<"div"> & VariantProps) { - return ( -
- ) -} +const Alert = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & VariantProps +>(({ className, variant, ...props }, ref) => ( +
+)) +Alert.displayName = "Alert" -function AlertTitle({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ) -} +const AlertTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +AlertTitle.displayName = "AlertTitle" -function AlertDescription({ - className, - ...props -}: React.ComponentProps<"div">) { - return ( -
- ) -} +const AlertDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +AlertDescription.displayName = "AlertDescription" export { Alert, AlertTitle, AlertDescription } diff --git a/src/components/ui/aspect-ratio.tsx b/src/components/ui/aspect-ratio.tsx index 3df3fd0..c4abbf3 100644 --- a/src/components/ui/aspect-ratio.tsx +++ b/src/components/ui/aspect-ratio.tsx @@ -1,11 +1,5 @@ -"use client" - import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio" -function AspectRatio({ - ...props -}: React.ComponentProps) { - return -} +const AspectRatio = AspectRatioPrimitive.Root export { AspectRatio } diff --git a/src/components/ui/avatar.tsx b/src/components/ui/avatar.tsx index 2c8c55b..2a948cb 100644 --- a/src/components/ui/avatar.tsx +++ b/src/components/ui/avatar.tsx @@ -3,105 +3,46 @@ import * as AvatarPrimitive from "@radix-ui/react-avatar" import { cn } from "@/lib/utils" -function Avatar({ - className, - size = "default", - ...props -}: React.ComponentProps & { - size?: "default" | "sm" | "lg" -}) { - return ( - - ) -} +const Avatar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +Avatar.displayName = AvatarPrimitive.Root.displayName -function AvatarImage({ - className, - ...props -}: React.ComponentProps) { - return ( - - ) -} +const AvatarImage = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarImage.displayName = AvatarPrimitive.Image.displayName -function AvatarFallback({ - className, - ...props -}: React.ComponentProps) { - return ( - - ) -} +const AvatarFallback = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName -function AvatarBadge({ className, ...props }: React.ComponentProps<"span">) { - return ( - svg]:hidden", - "group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2", - "group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2", - className - )} - {...props} - /> - ) -} - -function AvatarGroup({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ) -} - -function AvatarGroupCount({ - className, - ...props -}: React.ComponentProps<"div">) { - return ( -
svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3", - className - )} - {...props} - /> - ) -} - -export { - Avatar, - AvatarImage, - AvatarFallback, - AvatarBadge, - AvatarGroup, - AvatarGroupCount, -} +export { Avatar, AvatarImage, AvatarFallback } diff --git a/src/components/ui/badge-variants.ts b/src/components/ui/badge-variants.ts index a03395d..f6b20a6 100644 --- a/src/components/ui/badge-variants.ts +++ b/src/components/ui/badge-variants.ts @@ -1,23 +1,21 @@ import { cva } from "class-variance-authority" export const badgeVariants = cva( - "inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3", + "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", { variants: { variant: { - default: "bg-primary text-primary-foreground [a&]:hover:bg-primary/90", + default: + "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", secondary: - "bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", + "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", destructive: - "bg-destructive text-white focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40 [a&]:hover:bg-destructive/90", - outline: - "border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", - ghost: "[a&]:hover:bg-accent [a&]:hover:text-accent-foreground", - link: "text-primary underline-offset-4 [a&]:hover:underline", + "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", + outline: "text-foreground", }, }, defaultVariants: { variant: "default", }, } -) +) \ No newline at end of file diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx index 0405843..0abefc2 100644 --- a/src/components/ui/badge.tsx +++ b/src/components/ui/badge.tsx @@ -1,26 +1,16 @@ import * as React from "react" import { type VariantProps } from "class-variance-authority" -import { Slot } from "@radix-ui/react-slot" import { cn } from "@/lib/utils" import { badgeVariants } from "./badge-variants" -function Badge({ - className, - variant = "default", - asChild = false, - ...props -}: React.ComponentProps<"span"> & - VariantProps & { asChild?: boolean }) { - const Comp = asChild ? Slot.Root : "span" +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} +function Badge({ className, variant, ...props }: BadgeProps) { return ( - +
) } diff --git a/src/components/ui/breadcrumb.tsx b/src/components/ui/breadcrumb.tsx index ac16e0b..71a5c32 100644 --- a/src/components/ui/breadcrumb.tsx +++ b/src/components/ui/breadcrumb.tsx @@ -1,102 +1,108 @@ import * as React from "react" -import { ChevronRight, MoreHorizontal } from "lucide-react" import { Slot } from "@radix-ui/react-slot" +import { ChevronRight, MoreHorizontal } from "lucide-react" import { cn } from "@/lib/utils" -function Breadcrumb({ ...props }: React.ComponentProps<"nav">) { - return